feat: 테스트 데이터(V17) + 실 DB 프로파일 + 프론트 화면 확장

V17 (192.168.0.60:55432/trading_ai/ga 적용 완료):
- insurance_company 5 (삼성/한화/교보/디비/KB)
- product 20 (보험사별 4개)
- organization 7 (본부 1, 지점 3, 팀 3)
- agent 50 (GM 3 / DM 3 / SMGR 6 / SFC 12 / FC 26)
- contract 200 (ACTIVE 190 + LAPSE 10)
- commission_rate 100 (5상품 × 5년차 × 5보험사 = 일부)
  실제 INSERT는 상품 20개 × 1년차 + 20 × 4년차 = 100건
- payout_rule 120 (직급 6 × 보험종류 4 × 연차 5)
- override_rule 10 (FC/SFC/MGR/SMGR/DM 직급 간 오버라이드)
- chargeback_rule 4 (실효월수 구간별 환수율)

운영 DB 프로파일:
- application-trading_ai.yml (ga-api, ga-batch)
- 192.168.0.60:55432 / trading_ai / currentSchema=ga
- Flyway baseline-version=17 (수동 적용 분 인정)

DB 포트 변경 반영: 5432 → 55432 (운영 변경)

프론트 추가 화면:
- DataGrid (AG Grid Community 래퍼; 합계행/셀편집/로딩 오버레이)
- 예외원장 (ExceptionLedger): 승인/반려 액션
- 지급관리 (PaymentList)
- 정산 배치 (BatchRun): 실행 + 진행률 폴링
- 시스템관리:
  * UserList: 비밀번호 초기화 + 잠금 해제
  * RoleList: 좌측 역할 + 우측 권한 매트릭스 (메뉴 × 6권한)
  * CodeList: 좌측 그룹 + 우측 상세 코드, 시스템그룹 경고

설계사 상세/등록 폼:
- AgentDetail: 통계 카드 + 기본정보 + 수정 버튼
- AgentForm: 등록/수정 통합 (id='new' 또는 :id/edit)

라우터:
- /org/agents/{new|:id|:id/edit}
- /ledger/exception, /payments, /settle/batch
- /system/{users|roles|codes}
This commit is contained in:
GA Pro
2026-05-09 22:47:55 +09:00
parent 98eb231950
commit 92079d2385
16 changed files with 1197 additions and 4 deletions
+64
View File
@@ -0,0 +1,64 @@
import { Button, Card, Col, Descriptions, Row, Space, Statistic } from 'antd';
import { useQuery } from '@tanstack/react-query';
import { useNavigate, useParams } from 'react-router-dom';
import PageContainer from '@/components/common/PageContainer';
import CodeBadge from '@/components/common/CodeBadge';
import MoneyText from '@/components/common/MoneyText';
import PermissionButton from '@/components/common/PermissionButton';
import { agentApi } from '@/api/agent';
export default function AgentDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { data, isLoading } = useQuery({
queryKey: ['agent', 'detail', id],
queryFn: () => agentApi.detail(Number(id)),
enabled: !!id,
});
return (
<PageContainer
title={data ? `설계사 — ${data.agentName}` : '설계사 상세'}
extra={
<Space>
<PermissionButton menuCode="ORG_AGENT" permCode="UPDATE" type="primary"
onClick={() => navigate(`/org/agents/${id}/edit`)}>
</PermissionButton>
<Button onClick={() => navigate(-1)}></Button>
</Space>
}
>
{isLoading || !data ? null : (
<>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}><Card><Statistic title="계약 수" value={data.contractCount ?? 0} suffix="건" /></Card></Col>
<Col span={6}>
<Card>
<Statistic title="누적 수수료" valueRender={() =>
<MoneyText value={data.totalCommission} />
} />
</Card>
</Col>
<Col span={6}><Card><Statistic title="소속" value={data.orgName ?? '-'} /></Card></Col>
<Col span={6}><Card><Statistic title="직급" value={data.gradeName ?? '-'} /></Card></Col>
</Row>
<Card title="기본 정보">
<Descriptions column={2} bordered size="small">
<Descriptions.Item label="설계사명">{data.agentName}</Descriptions.Item>
<Descriptions.Item label="사번">{data.licenseNo ?? '-'}</Descriptions.Item>
<Descriptions.Item label="전화번호">{data.phone ?? '-'}</Descriptions.Item>
<Descriptions.Item label="이메일">{data.email ?? '-'}</Descriptions.Item>
<Descriptions.Item label="입사일">{data.joinDate ?? '-'}</Descriptions.Item>
<Descriptions.Item label="상태">
<CodeBadge groupCode="AGENT_STATUS" value={data.status} />
</Descriptions.Item>
</Descriptions>
</Card>
</>
)}
</PageContainer>
);
}
+115
View File
@@ -0,0 +1,115 @@
import { useEffect } from 'react';
import { Button, Card, Col, DatePicker, Form, Input, message, Row, Space } from 'antd';
import { useNavigate, useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import dayjs from 'dayjs';
import PageContainer from '@/components/common/PageContainer';
import CodeSelect from '@/components/common/CodeSelect';
import { agentApi, AgentSaveReq } from '@/api/agent';
export default function AgentForm() {
const { id } = useParams<{ id: string }>();
const isEdit = id && id !== 'new';
const navigate = useNavigate();
const [form] = Form.useForm();
const { data } = useQuery({
queryKey: ['agent', 'detail', id],
queryFn: () => agentApi.detail(Number(id)),
enabled: !!isEdit,
});
useEffect(() => {
if (data) {
form.setFieldsValue({
...data,
joinDate: data.joinDate ? dayjs(data.joinDate) : null,
});
}
}, [data, form]);
const onFinish = async (values: any) => {
const req: AgentSaveReq = {
...values,
joinDate: values.joinDate ? values.joinDate.format('YYYY-MM-DD') : undefined,
};
try {
if (isEdit) {
await agentApi.update(Number(id), req);
message.success('수정 완료');
} else {
await agentApi.create(req);
message.success('등록 완료');
}
navigate('/org/agents');
} catch {
// 인터셉터에서 처리
}
};
return (
<PageContainer title={isEdit ? '설계사 수정' : '설계사 등록'}>
<Card>
<Form form={form} layout="vertical" onFinish={onFinish}>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="설계사명" name="agentName" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="사번" name="licenseNo">
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="소속 ID" name="orgId" rules={[{ required: true }]}>
<Input type="number" placeholder="조직 ID" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="직급 ID" name="gradeId" rules={[{ required: true }]}>
<Input type="number" placeholder="직급 ID" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="전화번호" name="phone">
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="이메일" name="email" rules={[{ type: 'email' }]}>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="주민번호" name="residentNo" extra="저장 시 자동 암호화">
<Input />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="은행" name="bankCode">
<CodeSelect groupCode="BANK_CODE" />
</Form.Item>
</Col>
<Col span={18}>
<Form.Item label="계좌번호" name="accountNo" extra="저장 시 자동 암호화">
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="입사일" name="joinDate">
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Space>
<Button type="primary" htmlType="submit">{isEdit ? '수정' : '등록'}</Button>
<Button onClick={() => navigate(-1)}></Button>
</Space>
</Form>
</Card>
</PageContainer>
);
}