feat(frontend): 헤더 단순화 + 등록/수정 모달 도입

헤더 깨짐 수정 (MainLayout):
- logo 를 div 박스 대신 단순 img(/vite.svg) 로 (ProLayout 기본 슬롯 호환)
- avatarProps.render 의 chevron 제거 → ProLayout 기본 dom 만 Dropdown 트리거
- token.header 의 잘못된 키(colorBgRightActionsItemHover) 제거,
  heightLayoutHeader 56 으로 통일
- actionsRender 의 정산월 영역을 Space + 라벨 조합으로 정리

등록/수정 모달 (DrawerForm / ModalForm):
- AgentList: DrawerForm (680px) — 설계사명 / 사번 / 소속(Select) / 직급 /
  전화 / 이메일 / 주민번호(암호화) / 은행 / 계좌(암호화) / 입사일,
  수정 시 detail API 자동 fetch + initialValues 주입,
  액션 컬럼: 수정/퇴사 (Modal.confirm)
- ContractList: ModalForm (680px) — 증권번호 / 설계사(Select) / 상품(Select) /
  계약자 / 피보험자 / 보험료 / 납입주기 / 계약일 / 효력발생일
  toolBarRender 등록 버튼 추가
- ExceptionLedger: ModalForm (620px) — 설계사 / 정산월 / 예외코드 /
  금액 / 증권번호 / 보험사 / 내용
  toolBarRender 등록 버튼 추가, 예외코드 옵션은 ruleApi 에서 동적 로드

api/contract.ts: ContractSaveReq + create/update 추가

검증: tsc -b 통과 / 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:43:57 +09:00
parent f176b9702c
commit 4e008527b9
5 changed files with 301 additions and 61 deletions
+123 -20
View File
@@ -1,19 +1,72 @@
import { useRef } from 'react';
import { Space } from 'antd';
import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components';
import { useRef, useState } from 'react';
import { Modal, Space, message } from 'antd';
import {
DrawerForm, ProForm, ProFormDatePicker, ProFormSelect, ProFormText,
ProTable, type ActionType, type ProColumns,
} from '@ant-design/pro-components';
import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import dayjs from 'dayjs';
import CodeBadge from '@/components/common/CodeBadge';
import MoneyText from '@/components/common/MoneyText';
import PermissionButton from '@/components/common/PermissionButton';
import ExcelExportButton from '@/components/common/ExcelExportButton';
import PageContainer from '@/components/common/PageContainer';
import { useCommonCodes } from '@/hooks/useCommonCodes';
import { agentApi, AgentResp, AgentSearchParam } from '@/api/agent';
import { agentApi, AgentResp, AgentSaveReq, AgentSearchParam } from '@/api/agent';
import { orgApi } from '@/api/org';
export default function AgentList() {
const navigate = useNavigate();
const actionRef = useRef<ActionType>();
const { data: statusCodes = [] } = useCommonCodes('AGENT_STATUS');
const { data: bankCodes = [] } = useCommonCodes('BANK_CODE');
const [drawer, setDrawer] = useState<{ open: boolean; editId?: number }>({ open: false });
const { data: orgList = [] } = useQuery({
queryKey: ['orgs', 'list'],
queryFn: orgApi.list,
});
const orgOptions = orgList.map((o) => ({ value: o.orgId, label: `${o.orgName}` }));
const gradeOptions = [1, 2, 3, 4, 5, 6].map((id) => ({ value: id, label: `직급 #${id}` }));
// 수정 시 상세 조회
const { data: editing } = useQuery({
queryKey: ['agent', 'detail', drawer.editId],
queryFn: () => agentApi.detail(drawer.editId!),
enabled: !!drawer.editId,
});
const onSubmit = async (values: any) => {
const req: AgentSaveReq = {
...values,
joinDate: values.joinDate ? dayjs(values.joinDate).format('YYYY-MM-DD') : undefined,
};
if (drawer.editId) {
await agentApi.update(drawer.editId, req);
message.success('수정 완료');
} else {
await agentApi.create(req);
message.success('등록 완료');
}
setDrawer({ open: false });
actionRef.current?.reload();
return true;
};
const onDelete = (id: number) => {
Modal.confirm({
title: '설계사 삭제',
content: '관련 계약/원장이 있으면 실패합니다. 진행하시겠습니까?',
okType: 'danger',
onOk: async () => {
await agentApi.changeStatus(id, 'LEAVE');
message.success('퇴사 처리 완료');
actionRef.current?.reload();
},
});
};
const columns: ProColumns<AgentResp>[] = [
{
@@ -42,6 +95,15 @@ export default function AgentList() {
render: (_, r) => <MoneyText value={r.totalCommission} />,
},
{ title: '입사일', dataIndex: 'joinDate', width: 110, search: false },
{
title: '액션', valueType: 'option', width: 140, fixed: 'right',
render: (_, r) => [
<PermissionButton key="edit" menuCode="ORG_AGENT" permCode="UPDATE" size="small" type="link"
onClick={() => setDrawer({ open: true, editId: r.agentId })}></PermissionButton>,
<PermissionButton key="del" menuCode="ORG_AGENT" permCode="DELETE" size="small" type="link" danger
onClick={() => onDelete(r.agentId)}></PermissionButton>,
],
},
{
title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
fieldProps: { placeholder: '설계사명' },
@@ -57,37 +119,78 @@ export default function AgentList() {
actionRef={actionRef}
rowKey="agentId"
columns={columns}
scroll={{ x: 1200 }}
scroll={{ x: 1300 }}
request={async (params) => {
const { current, pageSize, ...rest } = params as any;
const res = await agentApi.list({ ...rest, pageNum: current, pageSize });
return { data: res.list, total: res.total, success: true };
}}
pagination={{
pageSize: 20,
showSizeChanger: true,
pageSizeOptions: ['20', '50', '100'],
pageSize: 20, showSizeChanger: true, pageSizeOptions: ['20', '50', '100'],
showTotal: (total) => `${total.toLocaleString()}`,
}}
search={{
labelWidth: 'auto',
searchText: '검색',
resetText: '초기화',
collapsed: false,
collapseRender: false,
}}
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
options={{ density: false, fullScreen: true, reload: true, setting: true }}
toolBarRender={() => [
<ExcelExportButton key="export" url="/api/agents/export" fileName="설계사목록" />,
<PermissionButton
key="create"
menuCode="ORG_AGENT" permCode="CREATE" type="primary"
onClick={() => navigate('/org/agents/new')}
>+ </PermissionButton>,
<PermissionButton key="create" menuCode="ORG_AGENT" permCode="CREATE" type="primary"
onClick={() => setDrawer({ open: true })}>+ </PermissionButton>,
]}
dateFormatter="string"
headerTitle={<Space size={6}><span style={{ fontWeight: 600 }}> </span></Space>}
/>
{/* 등록/수정 Drawer */}
<DrawerForm
title={drawer.editId ? '설계사 수정' : '설계사 등록'}
open={drawer.open}
onOpenChange={(o) => !o && setDrawer({ open: false })}
width={680}
layout="vertical"
initialValues={drawer.editId && editing ? {
...editing,
joinDate: editing.joinDate ? dayjs(editing.joinDate) : null,
} : undefined}
// 수정으로 전환 시 form 강제 갱신
key={drawer.editId ?? 'new'}
onFinish={onSubmit}
submitter={{ searchConfig: { submitText: drawer.editId ? '수정' : '등록', resetText: '취소' } }}
drawerProps={{ destroyOnClose: true, maskClosable: false }}
>
<ProForm.Group>
<ProFormText
name="agentName" label="설계사명" rules={[{ required: true, message: '필수입니다' }]}
width="md" placeholder="홍길동"
/>
<ProFormText name="licenseNo" label="사번" width="md" placeholder="L00123" />
</ProForm.Group>
<ProForm.Group>
<ProFormSelect
name="orgId" label="소속" rules={[{ required: true }]}
width="md" options={orgOptions} showSearch
/>
<ProFormSelect
name="gradeId" label="직급" rules={[{ required: true }]}
width="md" options={gradeOptions}
/>
</ProForm.Group>
<ProForm.Group>
<ProFormText name="phone" label="전화번호" width="md" placeholder="010-1234-5678" />
<ProFormText
name="email" label="이메일" width="md" placeholder="x@ga.com"
rules={[{ type: 'email', message: '이메일 형식이 아닙니다' }]}
/>
</ProForm.Group>
<ProFormText name="residentNo" label="주민번호 (저장 시 자동 암호화)" placeholder="900101-1234567" />
<ProForm.Group>
<ProFormSelect
name="bankCode" label="은행" width="md"
options={bankCodes.map((c) => ({ value: c.code, label: c.codeName }))}
/>
<ProFormText name="accountNo" label="계좌번호 (저장 시 자동 암호화)" width="md" />
</ProForm.Group>
<ProFormDatePicker name="joinDate" label="입사일" />
</DrawerForm>
</PageContainer>
);
}