From f176b9702c7c25852a6cc57dd0630e7f6df806a8 Mon Sep 17 00:00:00 2001 From: GA Pro Date: Sun, 10 May 2026 22:36:51 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=EB=88=84=EB=9D=BD=EB=90=9C?= =?UTF-8?q?=20=EB=A9=94=EB=89=B4=2014=EA=B0=9C=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=9D=BC=EA=B4=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 신규 페이지 (모두 ProTable / ProCard 패턴): - org/OrgTree: 조직 트리 + 선택 시 우측에 소속 설계사 ProTable - product/CompanyList: 보험사 목록 - product/ProductList: 상품 목록 (보험종 필터) - rule/CommissionRateList: 수수료율 (보험사 통보) - rule/PayoutRuleList: 지급율 (직급 × 보험종 × 회차) - rule/OverrideRuleList: 오버라이드 규정 (직급 페어) - rule/ChargebackRuleList: 환수 규정 (실효 월수 구간) - rule/ExceptionCodeList: 예외 코드 마스터 - ledger/MaintainLedger: 유지수수료 원장 - receive/ReceiveData: 보험사 raw 수신 데이터 - receive/ReceiveMapping: 보험사 프로파일 + 필드 매핑 규칙 - system/MenuManage: 메뉴 트리 표시 (Tree) - system/SystemConfig: 키-값 설정 (그룹별 필터) - system/SystemLog: 로그인/API/변경 이력 3 탭 신규 API 모듈: - api/org.ts (조직 트리) - api/company.ts, api/product.ts - api/rule.ts (5종 통합) - api/receive.ts (profile/field/raw/error) - api/config.ts, api/log.ts App.tsx: 14 라우트 추가 (org/tree, companies, products, rules/*, receive/*, ledger/maintain, system/menus, system/config, system/logs) 검증: - tsc -b 통과 - 12개 백엔드 엔드포인트 200 OK 확인 - dev 서버 HMR 자동 반영 Co-Authored-By: Claude Opus 4.7 (1M context) --- ga-frontend/src/App.tsx | 46 +++++++- ga-frontend/src/api/company.ts | 25 ++++ ga-frontend/src/api/config.ts | 20 ++++ ga-frontend/src/api/log.ts | 48 ++++++++ ga-frontend/src/api/org.ts | 20 ++++ ga-frontend/src/api/product.ts | 29 +++++ ga-frontend/src/api/receive.ts | 75 ++++++++++++ ga-frontend/src/api/rule.ts | 79 +++++++++++++ .../src/pages/ledger/MaintainLedger.tsx | 79 +++++++++++++ ga-frontend/src/pages/org/OrgTree.tsx | 98 ++++++++++++++++ ga-frontend/src/pages/product/CompanyList.tsx | 42 +++++++ ga-frontend/src/pages/product/ProductList.tsx | 52 ++++++++ ga-frontend/src/pages/receive/ReceiveData.tsx | 55 +++++++++ .../src/pages/receive/ReceiveMapping.tsx | 89 ++++++++++++++ .../src/pages/rule/ChargebackRuleList.tsx | 53 +++++++++ .../src/pages/rule/CommissionRateList.tsx | 41 +++++++ .../src/pages/rule/ExceptionCodeList.tsx | 55 +++++++++ .../src/pages/rule/OverrideRuleList.tsx | 42 +++++++ ga-frontend/src/pages/rule/PayoutRuleList.tsx | 48 ++++++++ ga-frontend/src/pages/system/MenuManage.tsx | 55 +++++++++ ga-frontend/src/pages/system/SystemConfig.tsx | 54 +++++++++ ga-frontend/src/pages/system/SystemLog.tsx | 111 ++++++++++++++++++ 22 files changed, 1212 insertions(+), 4 deletions(-) create mode 100644 ga-frontend/src/api/company.ts create mode 100644 ga-frontend/src/api/config.ts create mode 100644 ga-frontend/src/api/log.ts create mode 100644 ga-frontend/src/api/org.ts create mode 100644 ga-frontend/src/api/product.ts create mode 100644 ga-frontend/src/api/receive.ts create mode 100644 ga-frontend/src/api/rule.ts create mode 100644 ga-frontend/src/pages/ledger/MaintainLedger.tsx create mode 100644 ga-frontend/src/pages/org/OrgTree.tsx create mode 100644 ga-frontend/src/pages/product/CompanyList.tsx create mode 100644 ga-frontend/src/pages/product/ProductList.tsx create mode 100644 ga-frontend/src/pages/receive/ReceiveData.tsx create mode 100644 ga-frontend/src/pages/receive/ReceiveMapping.tsx create mode 100644 ga-frontend/src/pages/rule/ChargebackRuleList.tsx create mode 100644 ga-frontend/src/pages/rule/CommissionRateList.tsx create mode 100644 ga-frontend/src/pages/rule/ExceptionCodeList.tsx create mode 100644 ga-frontend/src/pages/rule/OverrideRuleList.tsx create mode 100644 ga-frontend/src/pages/rule/PayoutRuleList.tsx create mode 100644 ga-frontend/src/pages/system/MenuManage.tsx create mode 100644 ga-frontend/src/pages/system/SystemConfig.tsx create mode 100644 ga-frontend/src/pages/system/SystemLog.tsx 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 }} + /> + ); +}