-);
+ );
+};
const ReviewComplete = ({ onEnterMain, onCheckReview = onEnterMain }) => (
diff --git a/src/web-merchant/styles.css b/src/web-merchant/styles.css
index fb081a7..2845939 100644
--- a/src/web-merchant/styles.css
+++ b/src/web-merchant/styles.css
@@ -1883,11 +1883,26 @@ table.tbl {
background: #FBFCFD;
color: var(--text-secondary);
text-align: center;
+ cursor: pointer;
+ transition: border-color 0.2s ease, background 0.2s ease;
+}
+
+.upload-box:hover,
+.upload-box:focus-within {
+ border-color: var(--c-main);
+ background: #F2FBF8;
+}
+
+.upload-box.selected {
+ border-color: var(--c-main);
+ background: #F2FBF8;
+ color: var(--c-main);
}
.upload-box strong {
color: var(--text-secondary);
font-size: 12px;
+ overflow-wrap: anywhere;
}
.upload-box small {
@@ -1895,6 +1910,23 @@ table.tbl {
color: var(--text-tertiary);
}
+.upload-input {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+.upload-error {
+ color: var(--c-danger);
+ font-size: 10px;
+}
+
.review-complete-copy {
text-align: center;
}
diff --git a/src/web-pg/api/backend-api.jsx b/src/web-pg/api/backend-api.jsx
new file mode 100644
index 0000000..d71d866
--- /dev/null
+++ b/src/web-pg/api/backend-api.jsx
@@ -0,0 +1,130 @@
+/* Backend API clients for PG admin web */
+
+const PG_AUTH_API_BASE_URL = import.meta.env.VITE_AUTH_API_BASE_URL || "/backend/auth";
+const PG_MERCHANT_API_BASE_URL = import.meta.env.VITE_MERCHANT_API_BASE_URL || "/backend/merchant";
+const PG_SESSION_KEY = "erumpay.pg.session";
+
+class PgApiError extends Error {
+ constructor(message, status, code) {
+ super(message);
+ this.name = "PgApiError";
+ this.status = status;
+ this.code = code;
+ }
+}
+
+const pgReadBody = async response => {
+ if (response.status === 204) return null;
+ const text = await response.text();
+ if (!text) return null;
+ try {
+ return JSON.parse(text);
+ } catch {
+ return response.ok ? text : { message: text };
+ }
+};
+
+const pgRequest = async (baseUrl, path, options = {}) => {
+ const response = await fetch(`${baseUrl}${path}`, {
+ ...options,
+ headers: {
+ "Content-Type": "application/json",
+ ...options.headers,
+ },
+ });
+ const body = await pgReadBody(response);
+ if (!response.ok) {
+ throw new PgApiError(
+ body?.message || "요청 처리 중 오류가 발생했습니다.",
+ response.status,
+ body?.code
+ );
+ }
+ return body;
+};
+
+const PgSession = {
+ get: () => {
+ try {
+ return JSON.parse(sessionStorage.getItem(PG_SESSION_KEY)) || {};
+ } catch {
+ return {};
+ }
+ },
+ set: value => sessionStorage.setItem(PG_SESSION_KEY, JSON.stringify(value)),
+ clear: () => sessionStorage.removeItem(PG_SESSION_KEY),
+ isAuthenticated: () => Boolean(PgSession.get().accessToken),
+ authHeaders: () => {
+ const { accessToken } = PgSession.get();
+ return accessToken ? { Authorization: `Bearer ${accessToken}` } : {};
+ },
+};
+
+const PgAdminApi = {
+ login: async ({ loginId, password, totpCode }) => {
+ const response = await pgRequest(PG_AUTH_API_BASE_URL, "/api/v1/auth/admin/login", {
+ method: "POST",
+ body: JSON.stringify({
+ login_id: loginId,
+ password,
+ totp_code: totpCode,
+ }),
+ });
+ PgSession.set({
+ loginId,
+ accessToken: response.access_token,
+ refreshToken: response.refresh_token,
+ role: response.role,
+ });
+ return response;
+ },
+ logout: async () => {
+ const { refreshToken } = PgSession.get();
+ try {
+ if (refreshToken) {
+ await pgRequest(PG_AUTH_API_BASE_URL, "/api/v1/auth/admin/logout", {
+ method: "POST",
+ headers: { Authorization: `Bearer ${refreshToken}` },
+ });
+ }
+ } finally {
+ PgSession.clear();
+ }
+ },
+ getPendingMerchants: async () => {
+ const size = 100;
+ const pendingMerchants = [];
+ let page = 0;
+ let totalPages = 1;
+
+ do {
+ const response = await pgRequest(
+ PG_MERCHANT_API_BASE_URL,
+ `/api/v1/pg-admin/merchants?page=${page}&size=${size}&sort=createdAt,desc`,
+ { headers: PgSession.authHeaders() }
+ );
+ pendingMerchants.push(
+ ...(response?.content || []).filter(merchant => merchant.status === "PENDING")
+ );
+ totalPages = response?.totalPages || 0;
+ page += 1;
+ } while (page < totalPages);
+
+ return pendingMerchants;
+ },
+ approveMerchant: merchantId => pgRequest(
+ PG_AUTH_API_BASE_URL,
+ `/api/v1/pg-admin/merchants/${merchantId}/approve`,
+ {
+ method: "PATCH",
+ headers: PgSession.authHeaders(),
+ }
+ ),
+ getAuditLogs: async (page = 0, size = 20) => pgRequest(
+ PG_AUTH_API_BASE_URL,
+ `/api/v1/pg-admin/audit-logs?page=${page}&size=${size}`,
+ { headers: PgSession.authHeaders() }
+ ),
+};
+
+Object.assign(window, { PgApiError, PgSession, PgAdminApi });
diff --git a/src/web-pg/app.jsx b/src/web-pg/app.jsx
index 0a1f20c..3b7c773 100644
--- a/src/web-pg/app.jsx
+++ b/src/web-pg/app.jsx
@@ -2,6 +2,7 @@
const ROUTES = {
dashboard: { title: "대시보드", crumbs: ["대시보드"] },
+ approvals: { title: "가맹점 가입 대기", crumbs: ["가맹점 가입 대기"] },
merchants: { title: "가맹점 관리", crumbs: ["가맹점 관리"] },
transactions: { title: "결제관리", crumbs: ["결제관리"] },
settlements: { title: "정산관리", crumbs: ["정산관리"] },
@@ -15,7 +16,7 @@ const ROUTES = {
const App = () => {
const [page, setPage] = React.useState("dashboard");
const [collapsed, setCollapsed] = React.useState(false);
- const [authed, setAuthed] = React.useState(true);
+ const [authed, setAuthed] = React.useState(() => window.PgSession.isAuthenticated());
const [merchant, setMerchant] = React.useState(null);
const [tx, setTx] = React.useState(null);
@@ -30,15 +31,29 @@ const App = () => {
return () => window.removeEventListener("hashchange", onHash);
}, []);
- const nav = (id) => {
- if (id === "login") { setAuthed(false); window.location.hash = "login"; return; }
+ const nav = async (id) => {
+ if (id === "login") {
+ try {
+ await window.PgAdminApi.logout();
+ } catch (error) {
+ console.error("PG logout failed", error);
+ }
+ setAuthed(false);
+ window.location.hash = "login";
+ return;
+ }
setPage(id);
setMerchant(null); setTx(null);
window.location.hash = id;
};
if (!authed) {
- return
{ setAuthed(true); window.location.hash = "dashboard"; }}/>;
+ return {
+ await window.PgAdminApi.login(form);
+ setAuthed(true);
+ setPage("approvals");
+ window.location.hash = "approvals";
+ }}/>;
}
const route = ROUTES[page] || ROUTES.dashboard;
@@ -55,6 +70,7 @@ const App = () => {
unread={3}
/>
{page === "dashboard" && }
+ {page === "approvals" && }
{page === "merchants" && }
{page === "transactions" && }
{page === "settlements" && }
diff --git a/src/web-pg/components.jsx b/src/web-pg/components.jsx
index d61ba03..58044ba 100644
--- a/src/web-pg/components.jsx
+++ b/src/web-pg/components.jsx
@@ -4,8 +4,13 @@ const Tag = ({ kind = "neutral", dot = false, children }) => (
{children}
);
-const Button = ({ kind = "ghost", size, icon, children, onClick, type = "button" }) => (
-