feat(frontend): Ant Design Pro 패턴 전면 적용

@ant-design/pro-components ^2.8.10 + @ant-design/icons ^6.2 추가.

ProLayout (MainLayout):
- mix 레이아웃 (사이드바 + 헤더 통합), navTheme=light
- 자체 메뉴 트리 렌더링 (children 트리), 동적 메뉴 매핑
- 헤더 actionsRender: 정산월 MonthPicker / 알림 뱃지
- avatarProps: 그라디언트 아바타 + Dropdown 로그아웃
- token: 사이드바/헤더 컬러, pageContainer padding
- footerRender: 버전 표기

PageContainer (Pro):
- @ant-design/pro-layout 의 PageContainer 사용
- 자동 Breadcrumb / Title / extra 슬롯

ProTable 마이그레이션 (6 페이지):
- AgentList: search + valueEnum, request → PageHelper 변환
- ContractList, RecruitLedger, ExceptionLedger, SettleList, PaymentList, UserList
- 검색 폼 자동 생성 (valueType: select/dateMonth 등)
- toolBarRender: 등록/엑셀 버튼
- options: density/fullScreen/reload/setting

Dashboard:
- ProCard + StatisticCard 로 KPI 4종 그리드
- 차트 + 배치 진행 + 알림을 ProCard 로 묶음

검증:
- tsc -b 통과
- vite build 성공 (2.6MB / gzip 811KB)
- 백엔드 76건 단위테스트 영향 없음

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-10 22:20:33 +09:00
parent e70589fda8
commit 020939e563
12 changed files with 1387 additions and 797 deletions
+51 -53
View File
@@ -1,67 +1,65 @@
import { useState } from 'react';
import { Table } from 'antd';
import { useQuery } from '@tanstack/react-query';
import { ProTable, type ProColumns } from '@ant-design/pro-components';
import PageContainer from '@/components/common/PageContainer';
import SearchForm from '@/components/common/SearchForm';
import CodeBadge from '@/components/common/CodeBadge';
import MoneyText from '@/components/common/MoneyText';
import { defaultPagination } from '@/utils/tablePagination';
import { useCommonCodes } from '@/hooks/useCommonCodes';
import { contractApi, ContractResp, ContractSearchParam } from '@/api/contract';
export default function ContractList() {
const [param, setParam] = useState<ContractSearchParam>({ pageNum: 1, pageSize: 20 });
const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
const { data: statusCodes = [] } = useCommonCodes('CONTRACT_STATUS');
const { data, isLoading } = useQuery({
queryKey: ['contract', 'list', param],
queryFn: () => contractApi.list(param),
});
const columns: ProColumns<ContractResp>[] = [
{
title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
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: 'status', width: 90, valueType: 'select',
valueEnum: Object.fromEntries(statusCodes.map((c) => [c.code, { text: c.codeName }])),
render: (_, r) => <CodeBadge groupCode="CONTRACT_STATUS" value={r.status} />,
},
{ title: '증권번호', dataIndex: 'policyNo', width: 140, search: false },
{ title: '계약자', dataIndex: 'contractorName', width: 100, search: false },
{ title: '설계사', dataIndex: 'agentName', width: 100, search: false },
{ title: '보험사', dataIndex: 'companyName', width: 120, search: false },
{ title: '상품', dataIndex: 'productName', width: 220, search: false },
{
title: '보험료', dataIndex: 'premium', width: 120, align: 'right', search: false,
render: (_, r) => <MoneyText value={r.premium} />,
},
{
title: '납입주기', dataIndex: 'payCycle', width: 90, search: false,
render: (_, r) => <CodeBadge groupCode="PAY_CYCLE" value={r.payCycle} />,
},
{ title: '계약일', dataIndex: 'contractDate', width: 110, search: false },
];
return (
<PageContainer title="계약 관리">
<SearchForm
conditions={[
{ type: 'text', name: 'searchKeyword', label: '계약자/증권번호' },
{ type: 'code', name: 'insuranceType', label: '보험종류', groupCode: 'INSURANCE_TYPE' },
{ type: 'code', name: 'status', label: '상태', groupCode: 'CONTRACT_STATUS' },
{ type: 'dateRange', name: 'dateRange', label: '계약일' },
]}
onSearch={(v) => setParam({ ...param, ...v, pageNum: 1 })}
onReset={() => setParam({ pageNum: 1, pageSize: 20 })}
/>
<Table<ContractResp>
<PageContainer title="계약 관리" description="보험사·상품·계약 통합 관리">
<ProTable<ContractResp, ContractSearchParam>
rowKey="contractId"
loading={isLoading}
dataSource={data?.list}
pagination={{
...defaultPagination,
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
columns={columns}
scroll={{ x: 1300 }}
request={async (params) => {
const { current, pageSize, ...rest } = params as any;
const res = await contractApi.list({ ...rest, pageNum: current, pageSize });
return { data: res.list, total: res.total, success: true };
}}
columns={[
{ title: '증권번호', dataIndex: 'policyNo', width: 140 },
{ title: '계약자', dataIndex: 'contractorName', width: 100 },
{ title: '설계사', dataIndex: 'agentName', width: 100 },
{ title: '보험사', dataIndex: 'companyName', width: 120 },
{ title: '상품', dataIndex: 'productName', width: 220 },
{
title: '보험종류', dataIndex: 'insuranceType', width: 100,
render: (v) => <CodeBadge groupCode="INSURANCE_TYPE" value={v} />,
},
{
title: '보험료', dataIndex: 'premium', width: 120, align: 'right',
render: (v) => <MoneyText value={v} />,
},
{
title: '납입주기', dataIndex: 'payCycle', width: 90,
render: (v) => <CodeBadge groupCode="PAY_CYCLE" value={v} />,
},
{
title: '상태', dataIndex: 'status', width: 90,
render: (v) => <CodeBadge groupCode="CONTRACT_STATUS" value={v} />,
},
{ title: '계약일', dataIndex: 'contractDate', width: 110 },
]}
pagination={{
pageSize: 20,
showSizeChanger: true,
pageSizeOptions: ['20', '50', '100'],
showTotal: (total) => `${total.toLocaleString()}`,
}}
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
options={{ density: false, fullScreen: true, reload: true, setting: true }}
dateFormatter="string"
/>
</PageContainer>
);