feat: MappingEngine + Override 실제 로직 + 프론트 주요 화면

Backend:
- MappingEngine: company_field_mapping 기반 동적 변환 구현
  - raw_content(JSON) → 표준 원장 (TRIM/UPPER/LOWER/DATE/DIVIDE/MULTIPLY/CODE_MAP)
  - policy_no 로 contract 매칭 → contract_id/agent_id 자동
  - 실패 시 parse_error_log 자동 적재
- Recruit/Maintain Mapper.sumByAgentMonth 추가
- OrganizationMapper.selectAncestorIds (CTE 재귀 상향) 추가
- CalcOverrideStep 실제 동작:
  * lower agent 의 모집+유지 합계를 base 로 사용
  * 자기 조직 → 상위 조직 순으로 to_grade 직급 ACTIVE 설계사 탐색
  * (lowerOrgId, toGrade) 캐싱

Frontend (주요 화면):
- 공통 컴포넌트:
  * CodeSelect (공통코드 드롭다운, React Query 캐시)
  * CodeBadge (상태 색상 자동 매핑)
  * MoneyText (천단위 콤마, 음수 빨강, 부호 옵션)
  * PermissionButton (권한 없으면 미렌더)
  * SearchForm (조건 배열 → 자동 폼)
  * ExcelExportButton (blob 다운로드)
  * PageContainer
- 훅: useCommonCodes, usePermission (메뉴별 boolean)
- API 모듈: agent, contract, ledger, settle, code
- 페이지: AgentList, ContractList, RecruitLedger, SettleList
  * SettleList: 월 요약 카드 + 확정/보류 액션 + 권한 체크
- 라우터: /org/agents, /contracts, /ledger/recruit, /settle

V16 fix:
- PAYMENT 메뉴는 V12 에서 level1 directory 로 생성됨 → V16 에서 PAGE 로 UPDATE 후 권한 매트릭스 적용 (자식으로 또 만들면 unique 충돌)
This commit is contained in:
GA Pro
2026-05-09 22:38:10 +09:00
parent d4433a8e46
commit 98eb231950
28 changed files with 1165 additions and 39 deletions
+50
View File
@@ -0,0 +1,50 @@
import api, { PageResponse, unwrap } from './request';
export interface AgentResp {
agentId: number;
agentName: string;
licenseNo?: string;
phone?: string;
email?: string;
status: string;
statusName?: string;
orgId?: number;
orgName?: string;
gradeId?: number;
gradeName?: string;
joinDate?: string;
contractCount?: number;
totalCommission?: number;
}
export interface AgentSearchParam {
pageNum?: number;
pageSize?: number;
searchKeyword?: string;
status?: string;
orgId?: number;
gradeId?: number;
}
export interface AgentSaveReq {
agentName: string;
orgId: number;
gradeId: number;
residentNo?: string;
licenseNo?: string;
phone?: string;
email?: string;
bankCode?: string;
accountNo?: string;
joinDate?: string;
}
export const agentApi = {
list: (p: AgentSearchParam) =>
unwrap<PageResponse<AgentResp>>(api.get('/api/agents', { params: p })),
detail: (id: number) => unwrap<AgentResp>(api.get(`/api/agents/${id}`)),
create: (req: AgentSaveReq) => unwrap<number>(api.post('/api/agents', req)),
update: (id: number, req: AgentSaveReq) => unwrap<void>(api.put(`/api/agents/${id}`, req)),
changeStatus: (id: number, status: string) =>
unwrap<void>(api.put(`/api/agents/${id}/status`, { status })),
};
+14
View File
@@ -0,0 +1,14 @@
import api, { unwrap } from './request';
export interface CommonCode {
codeId: number;
groupCode: string;
code: string;
codeName: string;
sortOrder: number;
}
export const codeApi = {
byGroup: (groupCode: string) =>
unwrap<CommonCode[]>(api.get(`/api/common/codes/${groupCode}`)),
};
+40
View File
@@ -0,0 +1,40 @@
import api, { PageResponse, unwrap } from './request';
export interface ContractResp {
contractId: number;
agentId: number;
agentName: string;
orgName?: string;
productCode?: string;
productName?: string;
companyCode?: string;
companyName?: string;
insuranceType?: string;
policyNo: string;
contractorName?: string;
insuredName?: string;
premium?: number;
payCycle?: string;
payCycleName?: string;
status: string;
statusName?: string;
contractDate: string;
}
export interface ContractSearchParam {
pageNum?: number;
pageSize?: number;
searchKeyword?: string;
agentId?: number;
companyId?: number;
insuranceType?: string;
status?: string;
startDate?: string;
endDate?: string;
}
export const contractApi = {
list: (p: ContractSearchParam) =>
unwrap<PageResponse<ContractResp>>(api.get('/api/contracts', { params: p })),
detail: (id: number) => unwrap<ContractResp>(api.get(`/api/contracts/${id}`)),
};
+47
View File
@@ -0,0 +1,47 @@
import api, { PageResponse, unwrap } from './request';
export interface LedgerResp {
ledgerId: number;
agentId: number;
agentName: string;
orgName?: string;
policyNo?: string;
contractorName?: string;
contractDate?: string;
companyCode?: string;
companyName?: string;
productCode?: string;
productName?: string;
insuranceType?: string;
premium?: number;
companyRate?: number;
companyAmount?: number;
payoutRate?: number;
agentAmount?: number;
taxAmount?: number;
reconcileStatus?: string;
settleMonth: string;
status: string;
statusName?: string;
}
export interface LedgerSearchParam {
pageNum?: number;
pageSize?: number;
settleMonth?: string;
agentId?: number;
orgId?: number;
companyCode?: string;
insuranceType?: string;
policyNo?: string;
searchKeyword?: string;
}
export const ledgerApi = {
recruit: (p: LedgerSearchParam) =>
unwrap<PageResponse<LedgerResp>>(api.get('/api/ledger/recruit', { params: p })),
maintain: (p: LedgerSearchParam) =>
unwrap<PageResponse<LedgerResp>>(api.get('/api/ledger/maintain', { params: p })),
exception: (p: LedgerSearchParam) =>
unwrap<PageResponse<unknown>>(api.get('/api/ledger/exception', { params: p })),
};
+66
View File
@@ -0,0 +1,66 @@
import api, { PageResponse, unwrap } from './request';
export interface SettleResp {
settleId: number;
agentId: number;
agentName: string;
orgName?: string;
gradeName?: string;
settleMonth: string;
recruitTotal?: number;
maintainTotal?: number;
exceptionPlus?: number;
exceptionMinus?: number;
overrideTotal?: number;
chargebackTotal?: number;
grossAmount?: number;
taxAmount?: number;
netAmount?: number;
status: string;
statusName?: string;
holdReason?: string;
}
export interface SettleSearchParam {
pageNum?: number;
pageSize?: number;
settleMonth?: string;
agentId?: number;
orgId?: number;
status?: string;
}
export const settleApi = {
list: (p: SettleSearchParam) =>
unwrap<PageResponse<SettleResp>>(api.get('/api/settle', { params: p })),
summary: (settleMonth: string) =>
unwrap<Record<string, number>>(api.get('/api/settle/summary', { params: { settleMonth } })),
orgSummary: (settleMonth: string) =>
unwrap<Array<Record<string, unknown>>>(api.get('/api/settle/org-summary', { params: { settleMonth } })),
confirm: (settleId: number) =>
unwrap<void>(api.put(`/api/settle/${settleId}/confirm`)),
hold: (settleId: number, reason: string) =>
unwrap<void>(api.put(`/api/settle/${settleId}/hold`, { reason })),
release: (settleId: number) =>
unwrap<void>(api.put(`/api/settle/${settleId}/release`)),
};
export interface BatchJobLog {
jobId: number;
jobName: string;
settleMonth?: string;
status: string;
currentStep?: string;
progressPct?: number;
errorMessage?: string;
startedAt?: string;
finishedAt?: string;
durationSec?: number;
}
export const batchApi = {
run: (settleMonth: string) =>
unwrap<number>(api.post('/api/batch/settlement/run', { settleMonth })),
status: () =>
unwrap<BatchJobLog>(api.get('/api/batch/status')),
};