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:
+42
-4
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 })),
|
||||
};
|
||||
@@ -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 })),
|
||||
};
|
||||
@@ -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 })),
|
||||
};
|
||||
@@ -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}`)),
|
||||
};
|
||||
@@ -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 })),
|
||||
};
|
||||
@@ -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 })),
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user