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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user