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
+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>
);
}