Files
ga-commission-system/ga-frontend/src/pages/rule/ExceptionCodeList.tsx
T
GA Pro 87e4e3d1da feat: 추천 작업 5종 일괄 진행 (4 agents 병렬)
[A] 등록 모달 7페이지 (frontend)
- CompanyList / ProductList / Commission / Payout / Override / Chargeback / ExceptionCode
- ModalForm 580px, Switch Y/N 변환, 날짜 dayjs, PermissionButton
- api/company/product/rule.ts 에 SaveReq + create/update/remove 추가

[B] BatchRun + RoleList 보강 (frontend)
- BatchRun: 정산월/보험사 선택 → 실행, antd Steps 8단계 + 5초 폴링
  (refetchInterval 자동 정지), KPI 4 카드, 최근 이력
- RoleList: 좌(역할 목록) 우(메뉴×6권한 매트릭스 체크박스),
  DIRECTORY 들여쓰기, 전체 토글, Modal.confirm 후 저장

[C] 이체파일 + 은행 어댑터 (api)
- BankTransferFileGenerator 인터페이스 + GenericCsvTransferGenerator
  (UTF-8 BOM, 표준 CSV) + KbBankTransferGenerator (088, 고정폭 + 합계)
- BankTransferFactory (Spring Map 빈)
- TransferFileService: PENDING 조회 → 파일 생성 → file_storage 적재 →
  pay_file_ref 갱신 → status=SENT
- POST /api/payments/transfer-file (DataChangeLog + EXPORT 권한)
- account_no EncryptTypeHandler 자동 복호화 후 파일에 사용
- FileService.saveBytes(byte[]) 오버로드 추가
- PaymentList: 이체파일 생성 ModalForm + 다운로드 컬럼

[D] MockMvc Controller 테스트 (api)
- AbstractControllerTest (SpringBootTest + MockMvc, JwtFilter doAnswer
  로 chain 통과 처리), TestSecurityConfig (filter 빈 override)
- 5 클래스 36 테스트: Agent/Contract/Ledger/Settle/User Controller
- happy path + validation 400 + 권한 403 + 인증 401

검증:
- ./gradlew clean build 성공
- 백엔드 단위테스트 94건 (common 33 + batch 19 + api 36 + 기타 6) 통과
- 프론트 tsc -b / vite build 통과

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:08:32 +09:00

172 lines
7.2 KiB
TypeScript

import { useRef, useState } from 'react';
import { Radio, Switch, Tag, message } from 'antd';
import {
ModalForm, ProForm, ProFormSelect, ProFormText, ProFormTextArea,
ProTable, type ActionType, type ProColumns,
} from '@ant-design/pro-components';
import PageContainer from '@/components/common/PageContainer';
import PermissionButton from '@/components/common/PermissionButton';
import { ruleApi, ExceptionCodeResp, ExceptionCodeSaveReq } from '@/api/rule';
const DIRECTION_COLOR = { PLUS: 'success', MINUS: 'error' } as const;
const CATEGORY_OPTIONS = [
{ value: 'BONUS', label: '보너스' },
{ value: 'PENALTY', label: '패널티' },
{ value: 'ADJUSTMENT', label: '조정' },
{ value: 'OTHER', label: '기타' },
];
export default function ExceptionCodeList() {
const actionRef = useRef<ActionType>();
const [modal, setModal] = useState<{ open: boolean; editCode?: string }>({ open: false });
const [editRecord, setEditRecord] = useState<ExceptionCodeResp | null>(null);
const openCreate = () => {
setEditRecord(null);
setModal({ open: true });
};
const openEdit = (record: ExceptionCodeResp) => {
setEditRecord(record);
setModal({ open: true, editCode: record.exceptionCode });
};
const onSubmit = async (v: any) => {
const req: ExceptionCodeSaveReq = {
...v,
autoCalcYn: v.autoCalcYn ? 'Y' : 'N',
approvalRequired: v.approvalRequired ? 'Y' : 'N',
taxIncluded: v.taxIncluded ? 'Y' : 'N',
isActive: v.isActive ? 'Y' : 'N',
};
if (modal.editCode) {
await ruleApi.updateExceptionCode(modal.editCode, req);
message.success('수정 완료');
} else {
await ruleApi.createExceptionCode(req);
message.success('등록 완료');
}
setModal({ open: false });
actionRef.current?.reload();
return true;
};
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>,
},
{
title: '액션', valueType: 'option', width: 100, fixed: 'right',
render: (_, r) => [
<PermissionButton key="edit" menuCode="RULE_EXCEPTION_CODE" permCode="UPDATE" size="small" type="link"
onClick={() => openEdit(r)}></PermissionButton>,
],
},
];
return (
<PageContainer title="예외 코드" description="가산/차감/승인/세금 정책 마스터 (예외금액 원장에서 사용)">
<ProTable<ExceptionCodeResp>
actionRef={actionRef}
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 }}
toolBarRender={() => [
<PermissionButton key="create" menuCode="RULE_EXCEPTION_CODE" permCode="CREATE" type="primary"
onClick={openCreate}>+ </PermissionButton>,
]}
dateFormatter="string"
/>
<ModalForm
title={modal.editCode ? '예외코드 수정' : '예외코드 등록'}
open={modal.open}
onOpenChange={(o) => !o && setModal({ open: false })}
width={580}
layout="vertical"
key={modal.editCode ?? 'new'}
initialValues={editRecord ? {
...editRecord,
autoCalcYn: editRecord.autoCalcYn === 'Y',
approvalRequired: editRecord.approvalRequired === 'Y',
taxIncluded: editRecord.taxIncluded === 'Y',
isActive: editRecord.isActive === 'Y',
} : { direction: 'PLUS', autoCalcYn: false, approvalRequired: false, taxIncluded: false, isActive: true }}
onFinish={onSubmit}
submitter={{ searchConfig: { submitText: modal.editCode ? '수정' : '등록', resetText: '취소' } }}
modalProps={{ destroyOnClose: true, maskClosable: false }}
>
<ProForm.Group>
<ProFormText
name="exceptionCode" label="예외코드" rules={[{ required: true }]}
width="md" placeholder="EX001"
disabled={!!modal.editCode}
/>
<ProFormText
name="exceptionName" label="예외명" rules={[{ required: true }]}
width="md" placeholder="특별 가산금"
/>
</ProForm.Group>
<ProForm.Group>
<ProFormSelect
name="category" label="카테고리" width="md"
options={CATEGORY_OPTIONS}
/>
<ProForm.Item name="direction" label="방향" rules={[{ required: true }]}>
<Radio.Group>
<Radio value="PLUS"> (+)</Radio>
<Radio value="MINUS"> (-)</Radio>
</Radio.Group>
</ProForm.Item>
</ProForm.Group>
<ProForm.Group>
<ProForm.Item name="autoCalcYn" label="자동계산" valuePropName="checked">
<Switch checkedChildren="자동" unCheckedChildren="수동" />
</ProForm.Item>
<ProForm.Item name="approvalRequired" label="승인 필요" valuePropName="checked" style={{ marginLeft: 32 }}>
<Switch checkedChildren="필요" unCheckedChildren="불요" />
</ProForm.Item>
<ProForm.Item name="taxIncluded" label="과세" valuePropName="checked" style={{ marginLeft: 32 }}>
<Switch checkedChildren="과세" unCheckedChildren="비과세" />
</ProForm.Item>
<ProForm.Item name="isActive" label="활성" valuePropName="checked" style={{ marginLeft: 32 }}>
<Switch checkedChildren="활성" unCheckedChildren="비활성" />
</ProForm.Item>
</ProForm.Group>
<ProFormTextArea name="description" label="설명" placeholder="예외 코드 상세 설명" />
</ModalForm>
</PageContainer>
);
}