diff --git a/ga-frontend/src/App.tsx b/ga-frontend/src/App.tsx
index 22848e5..235b2bf 100644
--- a/ga-frontend/src/App.tsx
+++ b/ga-frontend/src/App.tsx
@@ -3,18 +3,38 @@ import LoginPage from '@/pages/LoginPage';
import MainLayout from '@/layouts/MainLayout';
import Dashboard from '@/pages/Dashboard';
+import OrgTree from '@/pages/org/OrgTree';
import AgentList from '@/pages/org/AgentList';
import AgentDetail from '@/pages/org/AgentDetail';
import AgentForm from '@/pages/org/AgentForm';
+
+import CompanyList from '@/pages/product/CompanyList';
+import ProductList from '@/pages/product/ProductList';
import ContractList from '@/pages/product/ContractList';
+
+import CommissionRateList from '@/pages/rule/CommissionRateList';
+import PayoutRuleList from '@/pages/rule/PayoutRuleList';
+import OverrideRuleList from '@/pages/rule/OverrideRuleList';
+import ChargebackRuleList from '@/pages/rule/ChargebackRuleList';
+import ExceptionCodeList from '@/pages/rule/ExceptionCodeList';
+
+import ReceiveData from '@/pages/receive/ReceiveData';
+import ReceiveMapping from '@/pages/receive/ReceiveMapping';
+
import RecruitLedger from '@/pages/ledger/RecruitLedger';
+import MaintainLedger from '@/pages/ledger/MaintainLedger';
import ExceptionLedger from '@/pages/ledger/ExceptionLedger';
+
import SettleList from '@/pages/settle/SettleList';
-import PaymentList from '@/pages/settle/PaymentList';
import BatchRun from '@/pages/settle/BatchRun';
+import PaymentList from '@/pages/settle/PaymentList';
+
import UserList from '@/pages/system/UserList';
import RoleList from '@/pages/system/RoleList';
+import MenuManage from '@/pages/system/MenuManage';
import CodeList from '@/pages/system/CodeList';
+import SystemConfig from '@/pages/system/SystemConfig';
+import SystemLog from '@/pages/system/SystemLog';
function RequireAuth({ children }: { children: React.ReactNode }) {
const token = localStorage.getItem('accessToken');
@@ -28,20 +48,35 @@ export default function App() {
}>
} />
- {/* 조직 / 설계사 */}
+ {/* 조직 / 인사 */}
+ } />
} />
} />
} />
} />
- {/* 계약 */}
+ {/* 상품 / 계약 */}
+ } />
+ } />
} />
+ {/* 수수료 규정 */}
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ {/* 데이터 수신 */}
+ } />
+ } />
+
{/* 원장 */}
} />
+ } />
} />
- {/* 정산 / 지급 / 배치 */}
+ {/* 정산 / 지급 */}
} />
} />
} />
@@ -49,7 +84,10 @@ export default function App() {
{/* 시스템관리 */}
} />
} />
+ } />
} />
+ } />
+ } />
);
diff --git a/ga-frontend/src/api/company.ts b/ga-frontend/src/api/company.ts
new file mode 100644
index 0000000..0d16213
--- /dev/null
+++ b/ga-frontend/src/api/company.ts
@@ -0,0 +1,25 @@
+import api, { PageResponse, unwrap } from './request';
+
+export interface CompanyResp {
+ companyId: number;
+ companyCode: string;
+ companyName: string;
+ companyType?: string;
+ bizNo?: string;
+ contactName?: string;
+ contactPhone?: string;
+ contactEmail?: string;
+ isActive: string;
+}
+
+export interface CompanySearchParam {
+ pageNum?: number;
+ pageSize?: number;
+ searchKeyword?: string;
+ isActive?: string;
+}
+
+export const companyApi = {
+ list: (p: CompanySearchParam) =>
+ unwrap>(api.get('/api/companies', { params: p })),
+};
diff --git a/ga-frontend/src/api/config.ts b/ga-frontend/src/api/config.ts
new file mode 100644
index 0000000..e882aab
--- /dev/null
+++ b/ga-frontend/src/api/config.ts
@@ -0,0 +1,20 @@
+import api, { unwrap } from './request';
+
+export interface SystemConfigResp {
+ configKey: string;
+ configValue?: string;
+ configDesc?: string;
+ configGroup?: string;
+ valueType?: string;
+ isEncrypted?: string;
+ isEditable?: string;
+ updatedAt?: string;
+ updatedBy?: number;
+}
+
+export const configApi = {
+ list: (group?: string) =>
+ unwrap(api.get('/api/system/config', { params: { group } })),
+ update: (key: string, value: string) =>
+ unwrap(api.put(`/api/system/config/${key}`, { value })),
+};
diff --git a/ga-frontend/src/api/log.ts b/ga-frontend/src/api/log.ts
new file mode 100644
index 0000000..69a0085
--- /dev/null
+++ b/ga-frontend/src/api/log.ts
@@ -0,0 +1,48 @@
+import api, { PageResponse, unwrap } from './request';
+
+export interface LoginLogResp {
+ logId: number;
+ userId?: number;
+ username?: string;
+ loginType: string;
+ ipAddress?: string;
+ userAgent?: string;
+ failReason?: string;
+ loginAt?: string;
+}
+
+export interface ApiAccessLogResp {
+ logId: number;
+ userId?: number;
+ username?: string;
+ requestMethod: string;
+ requestUrl: string;
+ responseStatus?: number;
+ responseTime?: number;
+ ipAddress?: string;
+ errorMessage?: string;
+ createdAt?: string;
+}
+
+export interface DataChangeLogResp {
+ logId: number;
+ tableName: string;
+ recordId?: string;
+ actionType: string;
+ changeData?: any;
+ changeReason?: string;
+ userId?: number;
+ username?: string;
+ ipAddress?: string;
+ menuCode?: string;
+ createdAt?: string;
+}
+
+export const logApi = {
+ login: (p: { pageNum?: number; pageSize?: number; username?: string; startDate?: string; endDate?: string }) =>
+ unwrap>(api.get('/api/system/logs/login', { params: p })),
+ api: (p: { pageNum?: number; pageSize?: number; username?: string; startDate?: string; endDate?: string }) =>
+ unwrap>(api.get('/api/system/logs/api', { params: p })),
+ change: (p: { pageNum?: number; pageSize?: number; tableName?: string; username?: string; startDate?: string; endDate?: string }) =>
+ unwrap>(api.get('/api/system/logs/data-change', { params: p })),
+};
diff --git a/ga-frontend/src/api/org.ts b/ga-frontend/src/api/org.ts
new file mode 100644
index 0000000..da88f7d
--- /dev/null
+++ b/ga-frontend/src/api/org.ts
@@ -0,0 +1,20 @@
+import api, { unwrap } from './request';
+
+export interface OrgNode {
+ orgId: number;
+ parentOrgId?: number;
+ orgName: string;
+ orgType: string; // HQ / BR / TM
+ orgLevel: number;
+ region?: string;
+ effectiveFrom?: string;
+ effectiveTo?: string;
+ isActive: string;
+ children?: OrgNode[];
+}
+
+export const orgApi = {
+ tree: () => unwrap(api.get('/api/orgs/tree')),
+ list: () => unwrap(api.get('/api/orgs')),
+ detail: (id: number) => unwrap(api.get(`/api/orgs/${id}`)),
+};
diff --git a/ga-frontend/src/api/product.ts b/ga-frontend/src/api/product.ts
new file mode 100644
index 0000000..61fb026
--- /dev/null
+++ b/ga-frontend/src/api/product.ts
@@ -0,0 +1,29 @@
+import api, { PageResponse, unwrap } from './request';
+
+export interface ProductResp {
+ productId: number;
+ companyId: number;
+ companyName?: string;
+ productCode: string;
+ productName: string;
+ insuranceType: string;
+ productGroup?: string;
+ payPeriodType?: string;
+ isActive: string;
+ launchDate?: string;
+ endDate?: string;
+}
+
+export interface ProductSearchParam {
+ pageNum?: number;
+ pageSize?: number;
+ searchKeyword?: string;
+ companyId?: number;
+ insuranceType?: string;
+ isActive?: string;
+}
+
+export const productApi = {
+ list: (p: ProductSearchParam) =>
+ unwrap>(api.get('/api/products', { params: p })),
+};
diff --git a/ga-frontend/src/api/receive.ts b/ga-frontend/src/api/receive.ts
new file mode 100644
index 0000000..f605287
--- /dev/null
+++ b/ga-frontend/src/api/receive.ts
@@ -0,0 +1,75 @@
+import api, { unwrap } from './request';
+
+export interface CompanyProfileResp {
+ companyCode: string;
+ dataFormat?: string;
+ receiveMethod?: string;
+ fileEncoding?: string;
+ delimiter?: string;
+ headerRow?: number;
+ dataStartRow?: number;
+ sheetName?: string;
+ dateFormat?: string;
+ amountFormat?: string;
+ apiUrl?: string;
+ ftpHost?: string;
+ ftpPath?: string;
+ receiveSchedule?: string;
+ isActive: string;
+}
+
+export interface FieldMappingResp {
+ mappingId: number;
+ companyCode: string;
+ sourceField: string;
+ sourceColIndex?: number;
+ targetTable: string;
+ targetField: string;
+ dataType?: string;
+ transformRule?: string;
+ defaultValue?: string;
+ isRequired?: string;
+ sortOrder: number;
+ version?: number;
+ effectiveFrom?: string;
+}
+
+export interface RawCommissionResp {
+ rawId: number;
+ companyCode: string;
+ batchId?: string;
+ settleMonth: string;
+ dataType: string;
+ parseStatus: string;
+ mappedLedgerId?: number;
+ mappedTable?: string;
+ fileName?: string;
+ rowNumber?: number;
+ receivedAt?: string;
+ parsedAt?: string;
+}
+
+export interface ParseErrorResp {
+ errorId: number;
+ rawId?: number;
+ companyCode: string;
+ errorType: string;
+ errorField?: string;
+ errorValue?: string;
+ errorMessage?: string;
+ resolveStatus: string;
+ resolvedBy?: number;
+ resolveNote?: string;
+ resolvedAt?: string;
+ createdAt?: string;
+}
+
+export const receiveApi = {
+ profiles: () => unwrap(api.get('/api/receive/profiles')),
+ fields: (companyCode: string) =>
+ unwrap(api.get(`/api/receive/mapping/${companyCode}/fields`)),
+ raw: (params: { companyCode?: string; settleMonth?: string; parseStatus?: string }) =>
+ unwrap(api.get('/api/receive/raw', { params })),
+ errors: (params: { companyCode?: string; resolveStatus?: string }) =>
+ unwrap(api.get('/api/receive/errors', { params })),
+};
diff --git a/ga-frontend/src/api/rule.ts b/ga-frontend/src/api/rule.ts
new file mode 100644
index 0000000..4cc3216
--- /dev/null
+++ b/ga-frontend/src/api/rule.ts
@@ -0,0 +1,79 @@
+import api, { unwrap } from './request';
+
+export interface CommissionRateResp {
+ rateId: number;
+ productId: number;
+ productName?: string;
+ commissionYear: number;
+ ratePct: number;
+ payMethodCond?: string;
+ minPremium?: number;
+ effectiveFrom?: string;
+ effectiveTo?: string;
+ version: number;
+}
+
+export interface PayoutRuleResp {
+ ruleId: number;
+ gradeId: number;
+ gradeName?: string;
+ insuranceType: string;
+ commissionYear: number;
+ payoutPct: number;
+ payMethodCond?: string;
+ performanceGrade?: string;
+ effectiveFrom?: string;
+ effectiveTo?: string;
+ version: number;
+}
+
+export interface OverrideRuleResp {
+ overrideId: number;
+ fromGrade: number;
+ toGrade: number;
+ fromGradeName?: string;
+ toGradeName?: string;
+ overridePct: number;
+ calcType?: string;
+ insuranceTypeCond?: string;
+ capAmount?: number;
+ effectiveFrom?: string;
+ effectiveTo?: string;
+}
+
+export interface ChargebackRuleResp {
+ cbRuleId: number;
+ companyCode: string;
+ insuranceType: string;
+ lapseMonthFrom: number;
+ lapseMonthTo: number;
+ cbRate: number;
+ includeOverride: string;
+ installmentAllowed: string;
+ maxInstallments?: number;
+ effectiveFrom?: string;
+ effectiveTo?: string;
+}
+
+export interface ExceptionCodeResp {
+ exceptionCode: string;
+ exceptionName: string;
+ category?: string;
+ direction: string;
+ autoCalcYn: string;
+ approvalRequired: string;
+ taxIncluded: string;
+ description?: string;
+ isActive: string;
+}
+
+export const ruleApi = {
+ commissionRates: (params?: { productId?: number; year?: number }) =>
+ unwrap(api.get('/api/rules/commission-rates', { params })),
+ payoutRules: (params?: { gradeId?: number; insuranceType?: string }) =>
+ unwrap(api.get('/api/rules/payout', { params })),
+ overrideRules: () => unwrap(api.get('/api/rules/override')),
+ chargebackRules: (params?: { companyCode?: string }) =>
+ unwrap(api.get('/api/rules/chargeback', { params })),
+ exceptionCodes: () => unwrap(api.get('/api/rules/exception-codes')),
+};
diff --git a/ga-frontend/src/pages/ledger/MaintainLedger.tsx b/ga-frontend/src/pages/ledger/MaintainLedger.tsx
new file mode 100644
index 0000000..38b4125
--- /dev/null
+++ b/ga-frontend/src/pages/ledger/MaintainLedger.tsx
@@ -0,0 +1,79 @@
+import { ProTable, type ProColumns } from '@ant-design/pro-components';
+import PageContainer from '@/components/common/PageContainer';
+import CodeBadge from '@/components/common/CodeBadge';
+import MoneyText from '@/components/common/MoneyText';
+import ExcelExportButton from '@/components/common/ExcelExportButton';
+import { useCommonCodes } from '@/hooks/useCommonCodes';
+import { ledgerApi, LedgerResp, LedgerSearchParam } from '@/api/ledger';
+
+export default function MaintainLedger() {
+ const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
+ const { data: reconCodes = [] } = useCommonCodes('RECON_STATUS');
+
+ const columns: ProColumns[] = [
+ { title: '정산월', dataIndex: 'settleMonth', width: 90, fixed: 'left',
+ valueType: 'dateMonth', fieldProps: { format: 'YYYYMM' } },
+ { title: '증권번호', dataIndex: 'policyNo', width: 130, fixed: 'left',
+ fieldProps: { placeholder: '증권번호' } },
+ {
+ title: '보험종류', dataIndex: 'insuranceType', width: 100, valueType: 'select',
+ valueEnum: Object.fromEntries(insuranceCodes.map((c) => [c.code, { text: c.codeName }])),
+ render: (_, r) => ,
+ },
+ {
+ title: '대사상태', dataIndex: 'reconcileStatus', width: 100, valueType: 'select',
+ valueEnum: Object.fromEntries(reconCodes.map((c) => [c.code, { text: c.codeName }])),
+ render: (_, r) => ,
+ },
+ { title: '설계사', dataIndex: 'agentName', width: 100, search: false },
+ { title: '소속', dataIndex: 'orgName', width: 160, search: false },
+ { title: '보험사', dataIndex: 'companyName', width: 120, search: false },
+ { title: '상품', dataIndex: 'productName', width: 200, search: false },
+ {
+ title: '보험료', dataIndex: 'premium', width: 110, align: 'right', search: false,
+ render: (_, r) => ,
+ },
+ {
+ title: '회사율', dataIndex: 'companyRate', width: 80, align: 'right', search: false,
+ render: (_, r) => ,
+ },
+ {
+ title: '회사수수료', dataIndex: 'companyAmount', width: 120, align: 'right', search: false,
+ render: (_, r) => ,
+ },
+ {
+ title: '지급율', dataIndex: 'payoutRate', width: 80, align: 'right', search: false,
+ render: (_, r) => ,
+ },
+ {
+ title: '설계사수수료', dataIndex: 'agentAmount', width: 120, align: 'right', search: false,
+ render: (_, r) => ,
+ },
+ {
+ title: '세액', dataIndex: 'taxAmount', width: 100, align: 'right', search: false,
+ render: (_, r) => ,
+ },
+ ];
+
+ return (
+
+
+ rowKey="ledgerId"
+ columns={columns}
+ scroll={{ x: 1500 }}
+ request={async (params) => {
+ const { current, pageSize, ...rest } = params as any;
+ const res = await ledgerApi.maintain({ ...rest, pageNum: current, pageSize });
+ return { data: res.list, total: res.total, success: true };
+ }}
+ pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
+ search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
+ options={{ density: false, fullScreen: true, reload: true, setting: true }}
+ toolBarRender={() => [
+ ,
+ ]}
+ dateFormatter="string"
+ />
+
+ );
+}
diff --git a/ga-frontend/src/pages/org/OrgTree.tsx b/ga-frontend/src/pages/org/OrgTree.tsx
new file mode 100644
index 0000000..105e0f5
--- /dev/null
+++ b/ga-frontend/src/pages/org/OrgTree.tsx
@@ -0,0 +1,98 @@
+import { useMemo, useState } from 'react';
+import { Tree, Card, Empty, Tag, Spin } from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import { ProCard, ProTable, type ProColumns } from '@ant-design/pro-components';
+import { IconBuilding } from '@tabler/icons-react';
+import PageContainer from '@/components/common/PageContainer';
+import CodeBadge from '@/components/common/CodeBadge';
+import MoneyText from '@/components/common/MoneyText';
+import { orgApi, OrgNode } from '@/api/org';
+import { agentApi, AgentResp } from '@/api/agent';
+
+interface AntdTreeNode { key: string; title: any; children?: AntdTreeNode[]; orgId: number; orgType: string; }
+
+function toTree(nodes: OrgNode[] = []): AntdTreeNode[] {
+ return nodes.map((n) => ({
+ key: String(n.orgId),
+ orgId: n.orgId,
+ orgType: n.orgType,
+ title: (
+
+
+ {n.orgType}
+
+ {n.orgName}
+
+ ),
+ children: n.children ? toTree(n.children) : undefined,
+ }));
+}
+
+export default function OrgTree() {
+ const [selectedOrgId, setSelectedOrgId] = useState();
+
+ const { data: tree = [], isLoading: treeLoading } = useQuery({
+ queryKey: ['orgs', 'tree'],
+ queryFn: orgApi.tree,
+ });
+ const treeData = useMemo(() => toTree(tree), [tree]);
+
+ const columns: ProColumns[] = [
+ { title: '설계사명', dataIndex: 'agentName', width: 110 },
+ { title: '직급', dataIndex: 'gradeName', width: 90 },
+ {
+ title: '상태', dataIndex: 'status', width: 90,
+ render: (_, r) => ,
+ },
+ { title: '사번', dataIndex: 'licenseNo', width: 110 },
+ { title: '전화번호', dataIndex: 'phone', width: 130 },
+ { title: '계약수', dataIndex: 'contractCount', width: 80, align: 'right',
+ render: (_, r) => },
+ { title: '누적수수료', dataIndex: 'totalCommission', align: 'right',
+ render: (_, r) => },
+ ];
+
+ return (
+
+
+ 조직>}>
+ {treeLoading ? : (
+ treeData.length ? (
+ {
+ const node = e.node as unknown as AntdTreeNode;
+ setSelectedOrgId(node.orgId);
+ }}
+ />
+ ) :
+ )}
+
+
+ {selectedOrgId ? (
+ 소속 설계사}>
+
+ rowKey="agentId"
+ columns={columns}
+ search={false}
+ options={false}
+ request={async (params) => {
+ const res = await agentApi.list({
+ orgId: selectedOrgId,
+ pageNum: params.current ?? 1,
+ pageSize: params.pageSize ?? 50,
+ });
+ return { data: res.list, total: res.total, success: true };
+ }}
+ pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}명` }}
+ />
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/ga-frontend/src/pages/product/CompanyList.tsx b/ga-frontend/src/pages/product/CompanyList.tsx
new file mode 100644
index 0000000..dc11397
--- /dev/null
+++ b/ga-frontend/src/pages/product/CompanyList.tsx
@@ -0,0 +1,42 @@
+import { ProTable, type ProColumns } from '@ant-design/pro-components';
+import { Tag } from 'antd';
+import PageContainer from '@/components/common/PageContainer';
+import { companyApi, CompanyResp, CompanySearchParam } from '@/api/company';
+
+export default function CompanyList() {
+ const columns: ProColumns[] = [
+ { title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
+ fieldProps: { placeholder: '회사명 / 사업자번호' } },
+ { title: '회사코드', dataIndex: 'companyCode', width: 100, search: false },
+ { title: '회사명', dataIndex: 'companyName', width: 180,
+ render: (_, r) => {r.companyName} },
+ { title: '구분', dataIndex: 'companyType', width: 100, search: false },
+ { title: '사업자번호', dataIndex: 'bizNo', width: 130, search: false },
+ { title: '담당자', dataIndex: 'contactName', width: 100, search: false },
+ { title: '연락처', dataIndex: 'contactPhone', width: 130, search: false },
+ { title: '이메일', dataIndex: 'contactEmail', width: 200, search: false },
+ {
+ title: '활성', dataIndex: 'isActive', width: 80,
+ render: (_, r) =>
+ {r.isActive === 'Y' ? '활성' : '비활성'},
+ },
+ ];
+
+ return (
+
+
+ rowKey="companyId"
+ columns={columns}
+ request={async (params) => {
+ const { current, pageSize, ...rest } = params as any;
+ const res = await companyApi.list({ ...rest, pageNum: current, pageSize });
+ return { data: res.list, total: res.total, success: true };
+ }}
+ pagination={{ pageSize: 20, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
+ search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
+ options={{ density: false, fullScreen: true, reload: true, setting: true }}
+ dateFormatter="string"
+ />
+
+ );
+}
diff --git a/ga-frontend/src/pages/product/ProductList.tsx b/ga-frontend/src/pages/product/ProductList.tsx
new file mode 100644
index 0000000..55fbb12
--- /dev/null
+++ b/ga-frontend/src/pages/product/ProductList.tsx
@@ -0,0 +1,52 @@
+import { ProTable, type ProColumns } from '@ant-design/pro-components';
+import { Tag } from 'antd';
+import PageContainer from '@/components/common/PageContainer';
+import CodeBadge from '@/components/common/CodeBadge';
+import { useCommonCodes } from '@/hooks/useCommonCodes';
+import { productApi, ProductResp, ProductSearchParam } from '@/api/product';
+
+export default function ProductList() {
+ const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
+
+ const columns: ProColumns[] = [
+ { title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
+ fieldProps: { placeholder: '상품명 / 코드' } },
+ {
+ title: '보험종류', dataIndex: 'insuranceType', width: 110, valueType: 'select',
+ valueEnum: Object.fromEntries(insuranceCodes.map((c) => [c.code, { text: c.codeName }])),
+ render: (_, r) => ,
+ },
+ { title: '상품코드', dataIndex: 'productCode', width: 120, search: false },
+ { title: '상품명', dataIndex: 'productName', width: 240, search: false,
+ render: (_, r) => {r.productName} },
+ { title: '보험사', dataIndex: 'companyName', width: 120, search: false },
+ { title: '상품군', dataIndex: 'productGroup', width: 100, search: false },
+ { title: '납입주기', dataIndex: 'payPeriodType', width: 100, search: false },
+ { title: '판매개시', dataIndex: 'launchDate', width: 110, search: false },
+ { title: '판매종료', dataIndex: 'endDate', width: 110, search: false },
+ {
+ title: '활성', dataIndex: 'isActive', width: 80, search: false,
+ render: (_, r) =>
+ {r.isActive === 'Y' ? '활성' : '비활성'},
+ },
+ ];
+
+ return (
+
+
+ rowKey="productId"
+ columns={columns}
+ scroll={{ x: 1300 }}
+ request={async (params) => {
+ const { current, pageSize, ...rest } = params as any;
+ const res = await productApi.list({ ...rest, pageNum: current, pageSize });
+ return { data: res.list, total: res.total, success: true };
+ }}
+ pagination={{ pageSize: 20, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
+ search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
+ options={{ density: false, fullScreen: true, reload: true, setting: true }}
+ dateFormatter="string"
+ />
+
+ );
+}
diff --git a/ga-frontend/src/pages/receive/ReceiveData.tsx b/ga-frontend/src/pages/receive/ReceiveData.tsx
new file mode 100644
index 0000000..dd13bc0
--- /dev/null
+++ b/ga-frontend/src/pages/receive/ReceiveData.tsx
@@ -0,0 +1,55 @@
+import { ProTable, type ProColumns } from '@ant-design/pro-components';
+import { Tag } from 'antd';
+import PageContainer from '@/components/common/PageContainer';
+import { useCommonCodes } from '@/hooks/useCommonCodes';
+import { receiveApi, RawCommissionResp, ParseErrorResp } from '@/api/receive';
+
+const STATUS_COLOR = { PENDING: 'processing', OK: 'success', ERROR: 'error' } as const;
+
+export default function ReceiveData() {
+ const { data: companies = [] } = useCommonCodes('COMPANY_CODE');
+
+ const rawColumns: ProColumns[] = [
+ {
+ title: '보험사', dataIndex: 'companyCode', width: 110, valueType: 'select',
+ valueEnum: Object.fromEntries(companies.map((c) => [c.code, { text: c.codeName }])),
+ },
+ { title: '정산월', dataIndex: 'settleMonth', width: 90, valueType: 'dateMonth',
+ fieldProps: { format: 'YYYYMM' } },
+ {
+ title: '파싱상태', dataIndex: 'parseStatus', width: 110, valueType: 'select',
+ valueEnum: { PENDING: { text: '대기' }, OK: { text: '성공' }, ERROR: { text: '실패' } },
+ render: (_, r) =>
+ {r.parseStatus},
+ },
+ { title: '데이터 유형', dataIndex: 'dataType', width: 110, search: false },
+ { title: '배치 ID', dataIndex: 'batchId', width: 140, search: false },
+ { title: '파일명', dataIndex: 'fileName', width: 200, search: false, ellipsis: true },
+ { title: '행 번호', dataIndex: 'rowNumber', width: 80, align: 'right', search: false },
+ { title: '연결 원장', dataIndex: 'mappedLedgerId', width: 100, align: 'right', search: false },
+ { title: '수신일시', dataIndex: 'receivedAt', width: 160, search: false },
+ { title: '파싱일시', dataIndex: 'parsedAt', width: 160, search: false },
+ ];
+
+ return (
+
+
+ rowKey="rawId"
+ columns={rawColumns}
+ scroll={{ x: 1300 }}
+ request={async (params) => {
+ const list = await receiveApi.raw({
+ companyCode: (params as any).companyCode,
+ settleMonth: (params as any).settleMonth,
+ parseStatus: (params as any).parseStatus,
+ });
+ return { data: list, total: list.length, success: true };
+ }}
+ pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
+ search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
+ options={{ density: false, fullScreen: true, reload: true, setting: true }}
+ dateFormatter="string"
+ />
+
+ );
+}
diff --git a/ga-frontend/src/pages/receive/ReceiveMapping.tsx b/ga-frontend/src/pages/receive/ReceiveMapping.tsx
new file mode 100644
index 0000000..d820e54
--- /dev/null
+++ b/ga-frontend/src/pages/receive/ReceiveMapping.tsx
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import { Empty, Tag } from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import { ProCard, ProTable, type ProColumns } from '@ant-design/pro-components';
+import PageContainer from '@/components/common/PageContainer';
+import { receiveApi, CompanyProfileResp, FieldMappingResp } from '@/api/receive';
+
+export default function ReceiveMapping() {
+ const [selected, setSelected] = useState();
+
+ const { data: profiles = [] } = useQuery({
+ queryKey: ['receive', 'profiles'],
+ queryFn: receiveApi.profiles,
+ });
+
+ const profileColumns: ProColumns[] = [
+ { title: '보험사', dataIndex: 'companyCode', width: 90,
+ render: (_, r) => {r.companyCode} },
+ { title: '데이터 형식', dataIndex: 'dataFormat', width: 110 },
+ { title: '수신방법', dataIndex: 'receiveMethod', width: 110 },
+ {
+ title: '활성', dataIndex: 'isActive', width: 70, align: 'center',
+ render: (_, r) =>
+ {r.isActive === 'Y' ? 'Y' : 'N'},
+ },
+ ];
+
+ const fieldColumns: ProColumns[] = [
+ { title: '#', dataIndex: 'sortOrder', width: 50, align: 'center' },
+ { title: 'Source 필드', dataIndex: 'sourceField', width: 130 },
+ { title: 'Source 컬럼', dataIndex: 'sourceColIndex', width: 90, align: 'right' },
+ { title: 'Target 테이블', dataIndex: 'targetTable', width: 120 },
+ { title: 'Target 필드', dataIndex: 'targetField', width: 130 },
+ { title: 'Type', dataIndex: 'dataType', width: 100 },
+ { title: '변환규칙', dataIndex: 'transformRule', width: 110,
+ render: (_, r) => r.transformRule ? {r.transformRule} : '-' },
+ { title: '기본값', dataIndex: 'defaultValue', width: 110 },
+ { title: '필수', dataIndex: 'isRequired', width: 70, align: 'center',
+ render: (_, r) => r.isRequired === 'Y' ? 필수 : '' },
+ { title: '버전', dataIndex: 'version', width: 60, align: 'center' },
+ ];
+
+ return (
+
+
+ 보험사 프로파일}>
+
+ rowKey="companyCode"
+ columns={profileColumns}
+ dataSource={profiles}
+ search={false}
+ options={false}
+ pagination={false}
+ size="small"
+ onRow={(r) => ({
+ onClick: () => setSelected(r.companyCode),
+ style: {
+ cursor: 'pointer',
+ background: selected === r.companyCode ? '#E6F1FB' : undefined,
+ },
+ })}
+ />
+
+ 필드 매핑 — {selected}
+ : 좌측에서 보험사 선택}
+ >
+ {selected ? (
+
+ rowKey="mappingId"
+ columns={fieldColumns}
+ search={false}
+ options={false}
+ pagination={false}
+ size="small"
+ request={async () => {
+ const list = await receiveApi.fields(selected);
+ return { data: list, total: list.length, success: true };
+ }}
+ />
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/ga-frontend/src/pages/rule/ChargebackRuleList.tsx b/ga-frontend/src/pages/rule/ChargebackRuleList.tsx
new file mode 100644
index 0000000..c8ecf9b
--- /dev/null
+++ b/ga-frontend/src/pages/rule/ChargebackRuleList.tsx
@@ -0,0 +1,53 @@
+import { Tag } from 'antd';
+import { ProTable, type ProColumns } from '@ant-design/pro-components';
+import PageContainer from '@/components/common/PageContainer';
+import CodeBadge from '@/components/common/CodeBadge';
+import MoneyText from '@/components/common/MoneyText';
+import { ruleApi, ChargebackRuleResp } from '@/api/rule';
+
+export default function ChargebackRuleList() {
+ const columns: ProColumns[] = [
+ { title: '보험사', dataIndex: 'companyCode', width: 100, search: false,
+ render: (_, r) => {r.companyCode} },
+ {
+ title: '보험종', dataIndex: 'insuranceType', width: 110, search: false,
+ render: (_, r) => ,
+ },
+ { title: '실효 시작월', dataIndex: 'lapseMonthFrom', width: 100, align: 'center', search: false },
+ { title: '실효 종료월', dataIndex: 'lapseMonthTo', width: 100, align: 'center', search: false },
+ {
+ title: '환수율(%)', dataIndex: 'cbRate', width: 110, align: 'right', search: false,
+ render: (_, r) => ,
+ },
+ {
+ title: '오버라이드 포함', dataIndex: 'includeOverride', width: 130, align: 'center', search: false,
+ render: (_, r) =>
+ {r.includeOverride === 'Y' ? '포함' : '제외'},
+ },
+ {
+ title: '분할 가능', dataIndex: 'installmentAllowed', width: 100, align: 'center', search: false,
+ render: (_, r) =>
+ {r.installmentAllowed === 'Y' ? '허용' : '불가'},
+ },
+ { title: '최대 분할', dataIndex: 'maxInstallments', width: 90, align: 'right', search: false },
+ { title: '적용시작', dataIndex: 'effectiveFrom', width: 110, search: false },
+ { title: '적용종료', dataIndex: 'effectiveTo', width: 110, search: false },
+ ];
+
+ return (
+
+
+ rowKey="cbRuleId"
+ columns={columns}
+ search={false}
+ request={async () => {
+ const list = await ruleApi.chargebackRules();
+ return { data: list, total: list.length, success: true };
+ }}
+ pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}건` }}
+ options={{ density: false, fullScreen: true, reload: true, setting: true }}
+ dateFormatter="string"
+ />
+
+ );
+}
diff --git a/ga-frontend/src/pages/rule/CommissionRateList.tsx b/ga-frontend/src/pages/rule/CommissionRateList.tsx
new file mode 100644
index 0000000..c652ba3
--- /dev/null
+++ b/ga-frontend/src/pages/rule/CommissionRateList.tsx
@@ -0,0 +1,41 @@
+import { ProTable, type ProColumns } from '@ant-design/pro-components';
+import PageContainer from '@/components/common/PageContainer';
+import MoneyText from '@/components/common/MoneyText';
+import { ruleApi, CommissionRateResp } from '@/api/rule';
+
+export default function CommissionRateList() {
+ const columns: ProColumns[] = [
+ { title: '상품ID', dataIndex: 'productId', width: 90 },
+ { title: '상품명', dataIndex: 'productName', width: 200, search: false },
+ { title: '회차', dataIndex: 'commissionYear', width: 70, align: 'center', search: false },
+ {
+ title: '수수료율(%)', dataIndex: 'ratePct', width: 110, align: 'right', search: false,
+ render: (_, r) => ,
+ },
+ { title: '납입조건', dataIndex: 'payMethodCond', width: 110, search: false },
+ {
+ title: '최소보험료', dataIndex: 'minPremium', width: 130, align: 'right', search: false,
+ render: (_, r) => ,
+ },
+ { title: '적용시작', dataIndex: 'effectiveFrom', width: 110, search: false },
+ { title: '적용종료', dataIndex: 'effectiveTo', width: 110, search: false },
+ { title: '버전', dataIndex: 'version', width: 60, align: 'center', search: false },
+ ];
+
+ return (
+
+
+ rowKey="rateId"
+ columns={columns}
+ search={false}
+ request={async () => {
+ const list = await ruleApi.commissionRates();
+ return { data: list, total: list.length, success: true };
+ }}
+ pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
+ options={{ density: false, fullScreen: true, reload: true, setting: true }}
+ dateFormatter="string"
+ />
+
+ );
+}
diff --git a/ga-frontend/src/pages/rule/ExceptionCodeList.tsx b/ga-frontend/src/pages/rule/ExceptionCodeList.tsx
new file mode 100644
index 0000000..e75b0cf
--- /dev/null
+++ b/ga-frontend/src/pages/rule/ExceptionCodeList.tsx
@@ -0,0 +1,55 @@
+import { Tag } from 'antd';
+import { ProTable, type ProColumns } from '@ant-design/pro-components';
+import PageContainer from '@/components/common/PageContainer';
+import { ruleApi, ExceptionCodeResp } from '@/api/rule';
+
+const DIRECTION_COLOR = { PLUS: 'success', MINUS: 'error' } as const;
+
+export default function ExceptionCodeList() {
+ const columns: ProColumns[] = [
+ { title: '예외코드', dataIndex: 'exceptionCode', width: 120, search: false,
+ render: (_, r) => {r.exceptionCode} },
+ { title: '예외명', dataIndex: 'exceptionName', width: 160, search: false },
+ { title: '카테고리', dataIndex: 'category', width: 110, search: false },
+ {
+ title: '방향', dataIndex: 'direction', width: 80, align: 'center', search: false,
+ render: (_, r) =>
+ {r.direction === 'PLUS' ? '가산 (+)' : '차감 (-)'},
+ },
+ {
+ title: '자동계산', dataIndex: 'autoCalcYn', width: 90, align: 'center', search: false,
+ render: (_, r) => r.autoCalcYn === 'Y' ? 자동 : 수동,
+ },
+ {
+ title: '승인 필요', dataIndex: 'approvalRequired', width: 90, align: 'center', search: false,
+ render: (_, r) => r.approvalRequired === 'Y' ? 필요 : 불요,
+ },
+ {
+ title: '과세', dataIndex: 'taxIncluded', width: 80, align: 'center', search: false,
+ render: (_, r) => r.taxIncluded === 'Y' ? 과세 : 비과세,
+ },
+ { title: '설명', dataIndex: 'description', ellipsis: true, search: false },
+ {
+ title: '활성', dataIndex: 'isActive', width: 70, align: 'center', search: false,
+ render: (_, r) =>
+ {r.isActive === 'Y' ? 'Y' : 'N'},
+ },
+ ];
+
+ return (
+
+
+ rowKey="exceptionCode"
+ columns={columns}
+ search={false}
+ request={async () => {
+ const list = await ruleApi.exceptionCodes();
+ return { data: list, total: list.length, success: true };
+ }}
+ pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}건` }}
+ options={{ density: false, fullScreen: true, reload: true, setting: true }}
+ dateFormatter="string"
+ />
+
+ );
+}
diff --git a/ga-frontend/src/pages/rule/OverrideRuleList.tsx b/ga-frontend/src/pages/rule/OverrideRuleList.tsx
new file mode 100644
index 0000000..c1b9f4c
--- /dev/null
+++ b/ga-frontend/src/pages/rule/OverrideRuleList.tsx
@@ -0,0 +1,42 @@
+import { ProTable, type ProColumns } from '@ant-design/pro-components';
+import PageContainer from '@/components/common/PageContainer';
+import MoneyText from '@/components/common/MoneyText';
+import { ruleApi, OverrideRuleResp } from '@/api/rule';
+
+export default function OverrideRuleList() {
+ const columns: ProColumns[] = [
+ { title: '하위 직급', dataIndex: 'fromGradeName', width: 110, search: false,
+ render: (_, r) => {r.fromGradeName ?? `#${r.fromGrade}`} },
+ { title: '상위 직급', dataIndex: 'toGradeName', width: 110, search: false,
+ render: (_, r) => r.toGradeName ?? `#${r.toGrade}` },
+ {
+ title: '오버라이드율(%)', dataIndex: 'overridePct', width: 130, align: 'right', search: false,
+ render: (_, r) => ,
+ },
+ { title: '계산방식', dataIndex: 'calcType', width: 110, search: false },
+ { title: '보험종조건', dataIndex: 'insuranceTypeCond', width: 120, search: false },
+ {
+ title: '월 상한', dataIndex: 'capAmount', width: 130, align: 'right', search: false,
+ render: (_, r) => ,
+ },
+ { title: '적용시작', dataIndex: 'effectiveFrom', width: 110, search: false },
+ { title: '적용종료', dataIndex: 'effectiveTo', width: 110, search: false },
+ ];
+
+ return (
+
+
+ rowKey="overrideId"
+ columns={columns}
+ search={false}
+ request={async () => {
+ const list = await ruleApi.overrideRules();
+ return { data: list, total: list.length, success: true };
+ }}
+ pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}건` }}
+ options={{ density: false, fullScreen: true, reload: true, setting: true }}
+ dateFormatter="string"
+ />
+
+ );
+}
diff --git a/ga-frontend/src/pages/rule/PayoutRuleList.tsx b/ga-frontend/src/pages/rule/PayoutRuleList.tsx
new file mode 100644
index 0000000..7904268
--- /dev/null
+++ b/ga-frontend/src/pages/rule/PayoutRuleList.tsx
@@ -0,0 +1,48 @@
+import { ProTable, type ProColumns } from '@ant-design/pro-components';
+import PageContainer from '@/components/common/PageContainer';
+import CodeBadge from '@/components/common/CodeBadge';
+import MoneyText from '@/components/common/MoneyText';
+import { useCommonCodes } from '@/hooks/useCommonCodes';
+import { ruleApi, PayoutRuleResp } from '@/api/rule';
+
+export default function PayoutRuleList() {
+ const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
+
+ const columns: ProColumns[] = [
+ { title: '직급', dataIndex: 'gradeName', width: 100, search: false,
+ render: (_, r) => {r.gradeName} },
+ {
+ title: '보험종류', dataIndex: 'insuranceType', width: 110,
+ valueType: 'select',
+ valueEnum: Object.fromEntries(insuranceCodes.map((c) => [c.code, { text: c.codeName }])),
+ render: (_, r) => ,
+ },
+ { title: '회차', dataIndex: 'commissionYear', width: 70, align: 'center', search: false },
+ {
+ title: '지급율(%)', dataIndex: 'payoutPct', width: 100, align: 'right', search: false,
+ render: (_, r) => ,
+ },
+ { title: '납입조건', dataIndex: 'payMethodCond', width: 110, search: false },
+ { title: '실적등급', dataIndex: 'performanceGrade', width: 100, search: false },
+ { title: '적용시작', dataIndex: 'effectiveFrom', width: 110, search: false },
+ { title: '적용종료', dataIndex: 'effectiveTo', width: 110, search: false },
+ { title: '버전', dataIndex: 'version', width: 60, align: 'center', search: false },
+ ];
+
+ return (
+
+
+ rowKey="ruleId"
+ columns={columns}
+ request={async (params) => {
+ const list = await ruleApi.payoutRules({ insuranceType: (params as any).insuranceType });
+ return { data: list, total: list.length, success: true };
+ }}
+ pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
+ search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
+ options={{ density: false, fullScreen: true, reload: true, setting: true }}
+ dateFormatter="string"
+ />
+
+ );
+}
diff --git a/ga-frontend/src/pages/system/MenuManage.tsx b/ga-frontend/src/pages/system/MenuManage.tsx
new file mode 100644
index 0000000..3e721cc
--- /dev/null
+++ b/ga-frontend/src/pages/system/MenuManage.tsx
@@ -0,0 +1,55 @@
+import { Tag, Tree } from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import { ProCard } from '@ant-design/pro-components';
+import { IconCategory } from '@tabler/icons-react';
+import PageContainer from '@/components/common/PageContainer';
+import { menuApi, MenuResp } from '@/api/menu';
+
+interface TreeNode { key: string; title: any; children?: TreeNode[]; }
+
+function toTree(menus: MenuResp[]): TreeNode[] {
+ return menus.map((m) => ({
+ key: String(m.menuId),
+ title: (
+
+
+ {m.menuType}
+
+ {m.menuName}
+
+ {m.menuPath ?? m.menuCode}
+
+
+ ),
+ children: m.children?.length ? toTree(m.children) : undefined,
+ }));
+}
+
+export default function MenuManage() {
+ const { data: tree = [] } = useQuery({
+ queryKey: ['menu', 'tree'],
+ queryFn: menuApi.tree,
+ });
+
+ return (
+
+ 메뉴 트리}
+ headerBordered
+ bordered={false}
+ extra={전체 {countMenus(tree)}개}
+ >
+
+
+
+ );
+}
+
+function countMenus(menus: MenuResp[]): number {
+ return menus.reduce((sum, m) => sum + 1 + (m.children ? countMenus(m.children) : 0), 0);
+}
diff --git a/ga-frontend/src/pages/system/SystemConfig.tsx b/ga-frontend/src/pages/system/SystemConfig.tsx
new file mode 100644
index 0000000..252f15f
--- /dev/null
+++ b/ga-frontend/src/pages/system/SystemConfig.tsx
@@ -0,0 +1,54 @@
+import { Tag } from 'antd';
+import { ProTable, type ProColumns } from '@ant-design/pro-components';
+import PageContainer from '@/components/common/PageContainer';
+import { configApi, SystemConfigResp } from '@/api/config';
+
+const GROUP_COLOR: Record = {
+ GENERAL: 'blue', BATCH: 'cyan', SECURITY: 'red', MAIL: 'green', TAX: 'orange',
+};
+
+export default function SystemConfig() {
+ const columns: ProColumns[] = [
+ {
+ title: '그룹', dataIndex: 'configGroup', width: 110, valueType: 'select',
+ valueEnum: {
+ GENERAL: { text: '일반' }, BATCH: { text: '배치' }, SECURITY: { text: '보안' },
+ MAIL: { text: '메일' }, TAX: { text: '세금' },
+ },
+ render: (_, r) =>
+ {r.configGroup},
+ },
+ { title: '설정 키', dataIndex: 'configKey', width: 220, search: false,
+ render: (_, r) =>
+ {r.configKey} },
+ {
+ title: '설정 값', dataIndex: 'configValue', search: false,
+ render: (_, r) => r.isEncrypted === 'Y' ? 암호화 (***) : {r.configValue},
+ },
+ { title: '타입', dataIndex: 'valueType', width: 90, search: false },
+ { title: '설명', dataIndex: 'configDesc', ellipsis: true, search: false },
+ {
+ title: '수정 가능', dataIndex: 'isEditable', width: 100, align: 'center', search: false,
+ render: (_, r) => r.isEditable === 'Y' ? 가능 : 잠김,
+ },
+ { title: '수정일시', dataIndex: 'updatedAt', width: 160, search: false },
+ ];
+
+ return (
+
+
+ rowKey="configKey"
+ columns={columns}
+ scroll={{ x: 1200 }}
+ request={async (params) => {
+ const list = await configApi.list((params as any).configGroup);
+ return { data: list, total: list.length, success: true };
+ }}
+ pagination={{ pageSize: 50, showTotal: (t) => `총 ${t}건` }}
+ search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
+ options={{ density: false, fullScreen: true, reload: true, setting: true }}
+ dateFormatter="string"
+ />
+
+ );
+}
diff --git a/ga-frontend/src/pages/system/SystemLog.tsx b/ga-frontend/src/pages/system/SystemLog.tsx
new file mode 100644
index 0000000..dec0aa7
--- /dev/null
+++ b/ga-frontend/src/pages/system/SystemLog.tsx
@@ -0,0 +1,111 @@
+import { useState } from 'react';
+import { Tabs, Tag } from 'antd';
+import { ProTable, type ProColumns } from '@ant-design/pro-components';
+import PageContainer from '@/components/common/PageContainer';
+import { logApi, ApiAccessLogResp, DataChangeLogResp, LoginLogResp } from '@/api/log';
+
+export default function SystemLog() {
+ const [tab, setTab] = useState('login');
+
+ return (
+
+ },
+ { key: 'api', label: 'API 호출', children: },
+ { key: 'change', label: '변경 이력', children: },
+ ]}
+ />
+
+ );
+}
+
+function LoginTable() {
+ const columns: ProColumns[] = [
+ { title: '로그인일시', dataIndex: 'loginAt', width: 170, valueType: 'dateTime' },
+ {
+ title: '구분', dataIndex: 'loginType', width: 90, valueType: 'select',
+ valueEnum: { LOGIN: { text: '로그인' }, LOGOUT: { text: '로그아웃' }, FAIL: { text: '실패' } },
+ render: (_, r) => {
+ const c = r.loginType === 'LOGIN' ? 'success' : r.loginType === 'FAIL' ? 'error' : 'default';
+ return {r.loginType};
+ },
+ },
+ { title: 'ID', dataIndex: 'username', width: 120 },
+ { title: 'IP', dataIndex: 'ipAddress', width: 130, search: false },
+ { title: 'User-Agent', dataIndex: 'userAgent', ellipsis: true, search: false },
+ { title: '실패사유', dataIndex: 'failReason', width: 200, search: false },
+ ];
+ return (
+
+ rowKey="logId" columns={columns} dateFormatter="string" scroll={{ x: 1100 }}
+ request={async (params) => {
+ const { current, pageSize, ...rest } = params as any;
+ const res = await logApi.login({ ...rest, pageNum: current, pageSize });
+ return { data: res.list, total: res.total, success: true };
+ }}
+ pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
+ search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
+ />
+ );
+}
+
+function ApiTable() {
+ const columns: ProColumns[] = [
+ { title: '시각', dataIndex: 'createdAt', width: 170, valueType: 'dateTime' },
+ { title: 'Method', dataIndex: 'requestMethod', width: 80, search: false,
+ render: (_, r) => {r.requestMethod} },
+ { title: 'URL', dataIndex: 'requestUrl', ellipsis: true, search: false },
+ { title: 'Status', dataIndex: 'responseStatus', width: 80, align: 'right', search: false,
+ render: (_, r) => {
+ const s = r.responseStatus ?? 0;
+ const c = s >= 500 ? 'error' : s >= 400 ? 'warning' : s >= 300 ? 'processing' : 'success';
+ return {s};
+ } },
+ { title: 'Time(ms)', dataIndex: 'responseTime', width: 90, align: 'right', search: false },
+ { title: 'User', dataIndex: 'username', width: 110 },
+ { title: 'IP', dataIndex: 'ipAddress', width: 130, search: false },
+ ];
+ return (
+
+ rowKey="logId" columns={columns} dateFormatter="string" scroll={{ x: 1200 }}
+ request={async (params) => {
+ const { current, pageSize, ...rest } = params as any;
+ const res = await logApi.api({ ...rest, pageNum: current, pageSize });
+ return { data: res.list, total: res.total, success: true };
+ }}
+ pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
+ search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
+ />
+ );
+}
+
+function ChangeTable() {
+ const columns: ProColumns[] = [
+ { title: '시각', dataIndex: 'createdAt', width: 170, valueType: 'dateTime' },
+ { title: '테이블', dataIndex: 'tableName', width: 150 },
+ { title: 'Action', dataIndex: 'actionType', width: 90, search: false,
+ render: (_, r) => {
+ const c = r.actionType === 'CREATE' ? 'success' : r.actionType === 'DELETE' ? 'error' : 'processing';
+ return {r.actionType};
+ } },
+ { title: 'Record ID', dataIndex: 'recordId', width: 100, search: false },
+ { title: '메뉴', dataIndex: 'menuCode', width: 130, search: false },
+ { title: '사용자', dataIndex: 'username', width: 110 },
+ { title: '사유', dataIndex: 'changeReason', ellipsis: true, search: false },
+ ];
+ return (
+
+ rowKey="logId" columns={columns} dateFormatter="string" scroll={{ x: 1100 }}
+ request={async (params) => {
+ const { current, pageSize, ...rest } = params as any;
+ const res = await logApi.change({ ...rest, pageNum: current, pageSize });
+ return { data: res.list, total: res.total, success: true };
+ }}
+ pagination={{ pageSize: 50, showTotal: (t) => `총 ${t.toLocaleString()}건` }}
+ search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
+ />
+ );
+}