feat(frontend): 누락된 메뉴 14개 페이지 일괄 구현

신규 페이지 (모두 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) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-10 22:36:51 +09:00
parent 020939e563
commit f176b9702c
22 changed files with 1212 additions and 4 deletions
+42 -4
View File
@@ -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() {
<Route path="/" element={<RequireAuth><MainLayout /></RequireAuth>}>
<Route index element={<Dashboard />} />
{/* 조직 / 설계사 */}
{/* 조직 / 사 */}
<Route path="org/tree" element={<OrgTree />} />
<Route path="org/agents" element={<AgentList />} />
<Route path="org/agents/new" element={<AgentForm />} />
<Route path="org/agents/:id" element={<AgentDetail />} />
<Route path="org/agents/:id/edit" element={<AgentForm />} />
{/* 계약 */}
{/* 상품 / 계약 */}
<Route path="companies" element={<CompanyList />} />
<Route path="products" element={<ProductList />} />
<Route path="contracts" element={<ContractList />} />
{/* 수수료 규정 */}
<Route path="rules/commission" element={<CommissionRateList />} />
<Route path="rules/payout" element={<PayoutRuleList />} />
<Route path="rules/override" element={<OverrideRuleList />} />
<Route path="rules/chargeback" element={<ChargebackRuleList />} />
<Route path="rules/exceptions" element={<ExceptionCodeList />} />
{/* 데이터 수신 */}
<Route path="receive/data" element={<ReceiveData />} />
<Route path="receive/mapping" element={<ReceiveMapping />} />
{/* 원장 */}
<Route path="ledger/recruit" element={<RecruitLedger />} />
<Route path="ledger/maintain" element={<MaintainLedger />} />
<Route path="ledger/exception" element={<ExceptionLedger />} />
{/* 정산 / 지급 / 배치 */}
{/* 정산 / 지급 */}
<Route path="settle" element={<SettleList />} />
<Route path="settle/batch" element={<BatchRun />} />
<Route path="payments" element={<PaymentList />} />
@@ -49,7 +84,10 @@ export default function App() {
{/* 시스템관리 */}
<Route path="system/users" element={<UserList />} />
<Route path="system/roles" element={<RoleList />} />
<Route path="system/menus" element={<MenuManage />} />
<Route path="system/codes" element={<CodeList />} />
<Route path="system/config" element={<SystemConfig />} />
<Route path="system/logs" element={<SystemLog />} />
</Route>
</Routes>
);
+25
View File
@@ -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<PageResponse<CompanyResp>>(api.get('/api/companies', { params: p })),
};
+20
View File
@@ -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<SystemConfigResp[]>(api.get('/api/system/config', { params: { group } })),
update: (key: string, value: string) =>
unwrap<void>(api.put(`/api/system/config/${key}`, { value })),
};
+48
View File
@@ -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<PageResponse<LoginLogResp>>(api.get('/api/system/logs/login', { params: p })),
api: (p: { pageNum?: number; pageSize?: number; username?: string; startDate?: string; endDate?: string }) =>
unwrap<PageResponse<ApiAccessLogResp>>(api.get('/api/system/logs/api', { params: p })),
change: (p: { pageNum?: number; pageSize?: number; tableName?: string; username?: string; startDate?: string; endDate?: string }) =>
unwrap<PageResponse<DataChangeLogResp>>(api.get('/api/system/logs/data-change', { params: p })),
};
+20
View File
@@ -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<OrgNode[]>(api.get('/api/orgs/tree')),
list: () => unwrap<OrgNode[]>(api.get('/api/orgs')),
detail: (id: number) => unwrap<OrgNode>(api.get(`/api/orgs/${id}`)),
};
+29
View File
@@ -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<PageResponse<ProductResp>>(api.get('/api/products', { params: p })),
};
+75
View File
@@ -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<CompanyProfileResp[]>(api.get('/api/receive/profiles')),
fields: (companyCode: string) =>
unwrap<FieldMappingResp[]>(api.get(`/api/receive/mapping/${companyCode}/fields`)),
raw: (params: { companyCode?: string; settleMonth?: string; parseStatus?: string }) =>
unwrap<RawCommissionResp[]>(api.get('/api/receive/raw', { params })),
errors: (params: { companyCode?: string; resolveStatus?: string }) =>
unwrap<ParseErrorResp[]>(api.get('/api/receive/errors', { params })),
};
+79
View File
@@ -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<CommissionRateResp[]>(api.get('/api/rules/commission-rates', { params })),
payoutRules: (params?: { gradeId?: number; insuranceType?: string }) =>
unwrap<PayoutRuleResp[]>(api.get('/api/rules/payout', { params })),
overrideRules: () => unwrap<OverrideRuleResp[]>(api.get('/api/rules/override')),
chargebackRules: (params?: { companyCode?: string }) =>
unwrap<ChargebackRuleResp[]>(api.get('/api/rules/chargeback', { params })),
exceptionCodes: () => unwrap<ExceptionCodeResp[]>(api.get('/api/rules/exception-codes')),
};
@@ -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<LedgerResp>[] = [
{ 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) => <CodeBadge groupCode="INSURANCE_TYPE" value={r.insuranceType} />,
},
{
title: '대사상태', dataIndex: 'reconcileStatus', width: 100, valueType: 'select',
valueEnum: Object.fromEntries(reconCodes.map((c) => [c.code, { text: c.codeName }])),
render: (_, r) => <CodeBadge groupCode="RECON_STATUS" value={r.reconcileStatus} />,
},
{ 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) => <MoneyText value={r.premium} />,
},
{
title: '회사율', dataIndex: 'companyRate', width: 80, align: 'right', search: false,
render: (_, r) => <MoneyText value={r.companyRate} fraction={2} />,
},
{
title: '회사수수료', dataIndex: 'companyAmount', width: 120, align: 'right', search: false,
render: (_, r) => <MoneyText value={r.companyAmount} />,
},
{
title: '지급율', dataIndex: 'payoutRate', width: 80, align: 'right', search: false,
render: (_, r) => <MoneyText value={r.payoutRate} fraction={2} />,
},
{
title: '설계사수수료', dataIndex: 'agentAmount', width: 120, align: 'right', search: false,
render: (_, r) => <MoneyText value={r.agentAmount} />,
},
{
title: '세액', dataIndex: 'taxAmount', width: 100, align: 'right', search: false,
render: (_, r) => <MoneyText value={r.taxAmount} />,
},
];
return (
<PageContainer title="유지수수료 원장" description="2~5년차 유지 계약에 대한 유지수수료">
<ProTable<LedgerResp, LedgerSearchParam>
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={() => [
<ExcelExportButton key="export" url="/api/ledger/maintain/export" fileName="유지수수료원장" />,
]}
dateFormatter="string"
/>
</PageContainer>
);
}
+98
View File
@@ -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: (
<span>
<Tag color={n.orgType === 'HQ' ? 'blue' : n.orgType === 'BR' ? 'cyan' : 'green'} style={{ marginRight: 6 }}>
{n.orgType}
</Tag>
{n.orgName}
</span>
),
children: n.children ? toTree(n.children) : undefined,
}));
}
export default function OrgTree() {
const [selectedOrgId, setSelectedOrgId] = useState<number | undefined>();
const { data: tree = [], isLoading: treeLoading } = useQuery({
queryKey: ['orgs', 'tree'],
queryFn: orgApi.tree,
});
const treeData = useMemo(() => toTree(tree), [tree]);
const columns: ProColumns<AgentResp>[] = [
{ title: '설계사명', dataIndex: 'agentName', width: 110 },
{ title: '직급', dataIndex: 'gradeName', width: 90 },
{
title: '상태', dataIndex: 'status', width: 90,
render: (_, r) => <CodeBadge groupCode="AGENT_STATUS" value={r.status} />,
},
{ title: '사번', dataIndex: 'licenseNo', width: 110 },
{ title: '전화번호', dataIndex: 'phone', width: 130 },
{ title: '계약수', dataIndex: 'contractCount', width: 80, align: 'right',
render: (_, r) => <MoneyText value={r.contractCount} /> },
{ title: '누적수수료', dataIndex: 'totalCommission', align: 'right',
render: (_, r) => <MoneyText value={r.totalCommission} /> },
];
return (
<PageContainer title="조직 트리" description="본부 / 지점 / 팀 트리 + 소속 설계사">
<ProCard split="vertical" ghost>
<ProCard colSpan="280px" bordered title={<><IconBuilding size={16} style={{ verticalAlign: -2 }} /> </>}>
{treeLoading ? <Spin /> : (
treeData.length ? (
<Tree
treeData={treeData as any}
defaultExpandAll
onSelect={(keys, e) => {
const node = e.node as unknown as AntdTreeNode;
setSelectedOrgId(node.orgId);
}}
/>
) : <Empty description="조직 없음" />
)}
</ProCard>
<ProCard ghost>
{selectedOrgId ? (
<Card bordered={false} title={<span style={{ fontWeight: 600 }}> </span>}>
<ProTable<AgentResp>
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}` }}
/>
</Card>
) : (
<Empty description="좌측에서 조직을 선택하세요" />
)}
</ProCard>
</ProCard>
</PageContainer>
);
}
@@ -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<CompanyResp>[] = [
{ title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
fieldProps: { placeholder: '회사명 / 사업자번호' } },
{ title: '회사코드', dataIndex: 'companyCode', width: 100, search: false },
{ title: '회사명', dataIndex: 'companyName', width: 180,
render: (_, r) => <strong>{r.companyName}</strong> },
{ 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) => <Tag color={r.isActive === 'Y' ? 'success' : 'default'}>
{r.isActive === 'Y' ? '활성' : '비활성'}</Tag>,
},
];
return (
<PageContainer title="보험사 관리" description="제휴 보험사 정보 / 데이터 수신 프로파일">
<ProTable<CompanyResp, CompanySearchParam>
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"
/>
</PageContainer>
);
}
@@ -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<ProductResp>[] = [
{ 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) => <CodeBadge groupCode="INSURANCE_TYPE" value={r.insuranceType} />,
},
{ title: '상품코드', dataIndex: 'productCode', width: 120, search: false },
{ title: '상품명', dataIndex: 'productName', width: 240, search: false,
render: (_, r) => <strong>{r.productName}</strong> },
{ 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) => <Tag color={r.isActive === 'Y' ? 'success' : 'default'}>
{r.isActive === 'Y' ? '활성' : '비활성'}</Tag>,
},
];
return (
<PageContainer title="상품 관리" description="보험사별 보험 상품 / 보험종류 / 상품군">
<ProTable<ProductResp, ProductSearchParam>
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"
/>
</PageContainer>
);
}
@@ -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<RawCommissionResp>[] = [
{
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) => <Tag color={STATUS_COLOR[r.parseStatus as 'PENDING' | 'OK' | 'ERROR'] ?? 'default'}>
{r.parseStatus}</Tag>,
},
{ 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 (
<PageContainer title="수신 데이터" description="보험사 수수료 raw 데이터 / 파싱 상태">
<ProTable<RawCommissionResp>
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"
/>
</PageContainer>
);
}
@@ -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<string | undefined>();
const { data: profiles = [] } = useQuery({
queryKey: ['receive', 'profiles'],
queryFn: receiveApi.profiles,
});
const profileColumns: ProColumns<CompanyProfileResp>[] = [
{ title: '보험사', dataIndex: 'companyCode', width: 90,
render: (_, r) => <strong>{r.companyCode}</strong> },
{ title: '데이터 형식', dataIndex: 'dataFormat', width: 110 },
{ title: '수신방법', dataIndex: 'receiveMethod', width: 110 },
{
title: '활성', dataIndex: 'isActive', width: 70, align: 'center',
render: (_, r) => <Tag color={r.isActive === 'Y' ? 'success' : 'default'}>
{r.isActive === 'Y' ? 'Y' : 'N'}</Tag>,
},
];
const fieldColumns: ProColumns<FieldMappingResp>[] = [
{ 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 ? <Tag color="blue">{r.transformRule}</Tag> : '-' },
{ title: '기본값', dataIndex: 'defaultValue', width: 110 },
{ title: '필수', dataIndex: 'isRequired', width: 70, align: 'center',
render: (_, r) => r.isRequired === 'Y' ? <Tag color="error"></Tag> : '' },
{ title: '버전', dataIndex: 'version', width: 60, align: 'center' },
];
return (
<PageContainer title="매핑 관리" description="보험사 raw 데이터 → 표준 원장 매핑 규칙">
<ProCard split="vertical" ghost>
<ProCard colSpan="380px" bordered title={<span style={{ fontWeight: 600 }}> </span>}>
<ProTable<CompanyProfileResp>
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,
},
})}
/>
</ProCard>
<ProCard
ghost
title={selected ? <span style={{ fontWeight: 600 }}> {selected}</span>
: <span> </span>}
>
{selected ? (
<ProTable<FieldMappingResp>
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 };
}}
/>
) : (
<Empty description="좌측에서 보험사를 선택하세요" />
)}
</ProCard>
</ProCard>
</PageContainer>
);
}
@@ -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<ChargebackRuleResp>[] = [
{ title: '보험사', dataIndex: 'companyCode', width: 100, search: false,
render: (_, r) => <strong>{r.companyCode}</strong> },
{
title: '보험종', dataIndex: 'insuranceType', width: 110, search: false,
render: (_, r) => <CodeBadge groupCode="INSURANCE_TYPE" value={r.insuranceType} />,
},
{ 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) => <strong><MoneyText value={r.cbRate} fraction={4} /></strong>,
},
{
title: '오버라이드 포함', dataIndex: 'includeOverride', width: 130, align: 'center', search: false,
render: (_, r) => <Tag color={r.includeOverride === 'Y' ? 'success' : 'default'}>
{r.includeOverride === 'Y' ? '포함' : '제외'}</Tag>,
},
{
title: '분할 가능', dataIndex: 'installmentAllowed', width: 100, align: 'center', search: false,
render: (_, r) => <Tag color={r.installmentAllowed === 'Y' ? 'success' : 'default'}>
{r.installmentAllowed === 'Y' ? '허용' : '불가'}</Tag>,
},
{ title: '최대 분할', dataIndex: 'maxInstallments', width: 90, align: 'right', search: false },
{ title: '적용시작', dataIndex: 'effectiveFrom', width: 110, search: false },
{ title: '적용종료', dataIndex: 'effectiveTo', width: 110, search: false },
];
return (
<PageContainer title="환수 규정" description="실효 계약 환수율 / 분할환수 정책">
<ProTable<ChargebackRuleResp>
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"
/>
</PageContainer>
);
}
@@ -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<CommissionRateResp>[] = [
{ 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) => <strong><MoneyText value={r.ratePct} fraction={4} /></strong>,
},
{ title: '납입조건', dataIndex: 'payMethodCond', width: 110, search: false },
{
title: '최소보험료', dataIndex: 'minPremium', width: 130, align: 'right', search: false,
render: (_, r) => <MoneyText value={r.minPremium} />,
},
{ title: '적용시작', dataIndex: 'effectiveFrom', width: 110, search: false },
{ title: '적용종료', dataIndex: 'effectiveTo', width: 110, search: false },
{ title: '버전', dataIndex: 'version', width: 60, align: 'center', search: false },
];
return (
<PageContainer title="수수료율 (보험사 → 시스템)" description="상품·회차별 보험사 통보 수수료율">
<ProTable<CommissionRateResp>
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"
/>
</PageContainer>
);
}
@@ -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<ExceptionCodeResp>[] = [
{ title: '예외코드', dataIndex: 'exceptionCode', width: 120, search: false,
render: (_, r) => <strong>{r.exceptionCode}</strong> },
{ 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) => <Tag color={DIRECTION_COLOR[r.direction as 'PLUS' | 'MINUS'] ?? 'default'}>
{r.direction === 'PLUS' ? '가산 (+)' : '차감 (-)'}</Tag>,
},
{
title: '자동계산', dataIndex: 'autoCalcYn', width: 90, align: 'center', search: false,
render: (_, r) => r.autoCalcYn === 'Y' ? <Tag color="processing"></Tag> : <Tag></Tag>,
},
{
title: '승인 필요', dataIndex: 'approvalRequired', width: 90, align: 'center', search: false,
render: (_, r) => r.approvalRequired === 'Y' ? <Tag color="warning"></Tag> : <Tag></Tag>,
},
{
title: '과세', dataIndex: 'taxIncluded', width: 80, align: 'center', search: false,
render: (_, r) => r.taxIncluded === 'Y' ? <Tag color="success"></Tag> : <Tag></Tag>,
},
{ title: '설명', dataIndex: 'description', ellipsis: true, search: false },
{
title: '활성', dataIndex: 'isActive', width: 70, align: 'center', search: false,
render: (_, r) => <Tag color={r.isActive === 'Y' ? 'success' : 'default'}>
{r.isActive === 'Y' ? 'Y' : 'N'}</Tag>,
},
];
return (
<PageContainer title="예외 코드" description="가산/차감/승인/세금 정책 마스터 (예외금액 원장에서 사용)">
<ProTable<ExceptionCodeResp>
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"
/>
</PageContainer>
);
}
@@ -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<OverrideRuleResp>[] = [
{ title: '하위 직급', dataIndex: 'fromGradeName', width: 110, search: false,
render: (_, r) => <strong>{r.fromGradeName ?? `#${r.fromGrade}`}</strong> },
{ title: '상위 직급', dataIndex: 'toGradeName', width: 110, search: false,
render: (_, r) => r.toGradeName ?? `#${r.toGrade}` },
{
title: '오버라이드율(%)', dataIndex: 'overridePct', width: 130, align: 'right', search: false,
render: (_, r) => <strong><MoneyText value={r.overridePct} fraction={4} /></strong>,
},
{ 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) => <MoneyText value={r.capAmount} />,
},
{ title: '적용시작', dataIndex: 'effectiveFrom', width: 110, search: false },
{ title: '적용종료', dataIndex: 'effectiveTo', width: 110, search: false },
];
return (
<PageContainer title="오버라이드 규정" description="조직 트리 상향 분배율 (하위 → 상위 직급)">
<ProTable<OverrideRuleResp>
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"
/>
</PageContainer>
);
}
@@ -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<PayoutRuleResp>[] = [
{ title: '직급', dataIndex: 'gradeName', width: 100, search: false,
render: (_, r) => <strong>{r.gradeName}</strong> },
{
title: '보험종류', dataIndex: 'insuranceType', width: 110,
valueType: 'select',
valueEnum: Object.fromEntries(insuranceCodes.map((c) => [c.code, { text: c.codeName }])),
render: (_, r) => <CodeBadge groupCode="INSURANCE_TYPE" value={r.insuranceType} />,
},
{ title: '회차', dataIndex: 'commissionYear', width: 70, align: 'center', search: false },
{
title: '지급율(%)', dataIndex: 'payoutPct', width: 100, align: 'right', search: false,
render: (_, r) => <strong><MoneyText value={r.payoutPct} fraction={4} /></strong>,
},
{ 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 (
<PageContainer title="지급율 (시스템 → 설계사)" description="직급·보험종·회차별 설계사 지급율">
<ProTable<PayoutRuleResp>
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"
/>
</PageContainer>
);
}
@@ -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: (
<span>
<Tag color={m.menuType === 'DIRECTORY' ? 'blue' : 'default'} style={{ marginRight: 6 }}>
{m.menuType}
</Tag>
<strong>{m.menuName}</strong>
<span style={{ color: '#999', marginLeft: 8, fontSize: 12 }}>
{m.menuPath ?? m.menuCode}
</span>
</span>
),
children: m.children?.length ? toTree(m.children) : undefined,
}));
}
export default function MenuManage() {
const { data: tree = [] } = useQuery({
queryKey: ['menu', 'tree'],
queryFn: menuApi.tree,
});
return (
<PageContainer title="메뉴 관리" description="시스템 메뉴 트리 / 표시 / 권한 매핑">
<ProCard
title={<span><IconCategory size={16} style={{ verticalAlign: -3 }} /> </span>}
headerBordered
bordered={false}
extra={<Tag color="processing"> {countMenus(tree)}</Tag>}
>
<Tree
treeData={toTree(tree) as any}
defaultExpandAll
showLine={{ showLeafIcon: false }}
style={{ minHeight: 400 }}
/>
</ProCard>
</PageContainer>
);
}
function countMenus(menus: MenuResp[]): number {
return menus.reduce((sum, m) => sum + 1 + (m.children ? countMenus(m.children) : 0), 0);
}
@@ -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<string, string> = {
GENERAL: 'blue', BATCH: 'cyan', SECURITY: 'red', MAIL: 'green', TAX: 'orange',
};
export default function SystemConfig() {
const columns: ProColumns<SystemConfigResp>[] = [
{
title: '그룹', dataIndex: 'configGroup', width: 110, valueType: 'select',
valueEnum: {
GENERAL: { text: '일반' }, BATCH: { text: '배치' }, SECURITY: { text: '보안' },
MAIL: { text: '메일' }, TAX: { text: '세금' },
},
render: (_, r) => <Tag color={GROUP_COLOR[r.configGroup ?? ''] ?? 'default'}>
{r.configGroup}</Tag>,
},
{ title: '설정 키', dataIndex: 'configKey', width: 220, search: false,
render: (_, r) => <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 4 }}>
{r.configKey}</code> },
{
title: '설정 값', dataIndex: 'configValue', search: false,
render: (_, r) => r.isEncrypted === 'Y' ? <Tag color="red"> (***)</Tag> : <span>{r.configValue}</span>,
},
{ 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' ? <Tag color="success"></Tag> : <Tag></Tag>,
},
{ title: '수정일시', dataIndex: 'updatedAt', width: 160, search: false },
];
return (
<PageContainer title="시스템 설정" description="배치 / 보안 / 세금 / 메일 등 키-값 설정">
<ProTable<SystemConfigResp>
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"
/>
</PageContainer>
);
}
+111
View File
@@ -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 (
<PageContainer title="시스템 로그" description="로그인 / API 호출 / 데이터 변경 이력">
<Tabs
activeKey={tab}
onChange={setTab}
items={[
{ key: 'login', label: '로그인 이력', children: <LoginTable /> },
{ key: 'api', label: 'API 호출', children: <ApiTable /> },
{ key: 'change', label: '변경 이력', children: <ChangeTable /> },
]}
/>
</PageContainer>
);
}
function LoginTable() {
const columns: ProColumns<LoginLogResp>[] = [
{ 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 <Tag color={c}>{r.loginType}</Tag>;
},
},
{ 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 (
<ProTable<LoginLogResp>
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<ApiAccessLogResp>[] = [
{ title: '시각', dataIndex: 'createdAt', width: 170, valueType: 'dateTime' },
{ title: 'Method', dataIndex: 'requestMethod', width: 80, search: false,
render: (_, r) => <Tag color="blue">{r.requestMethod}</Tag> },
{ 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 <Tag color={c}>{s}</Tag>;
} },
{ 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 (
<ProTable<ApiAccessLogResp>
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<DataChangeLogResp>[] = [
{ 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 <Tag color={c}>{r.actionType}</Tag>;
} },
{ 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 (
<ProTable<DataChangeLogResp>
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 }}
/>
);
}