Files
ga-commission-system/docs/DOMAIN_GAP_MOD1-8.md
T
GA Pro ff6d399dd3 feat: 수수료 공식 갭 보완 + 8종 신규모듈 풀스택 (V116~V123)
수수료 공식 갭분석(5 병렬감사) 후 확정 버그 3건 수정 + 개념적으로 빠진 모듈 8종 풀스택 구현.

[확정 버그 3건]
- 1200% 누적한도(checkContractTotal)를 배치 CalcRecruitStep에 연결(기존 1차년 한도만 적용
  → 두 한도 중 더 제한적인 값으로 clip+이연). +CalcRecruitStepTest
- BatchInstallmentPlanGenerator planMonth 반영(하드코딩 1 제거, API판과 동일). +단위테스트
- PersistencyBonus 임계유지율 게이트 + FIXED 정액 룰 도출. +PersistencyBonusServiceTest

[신규 모듈 8종]
- MOD-1 도입수수료(recruiter/development) / MOD-2 선지급·차익정산 / MOD-3 환수 분할상환
- MOD-4 익월부활 재지급 / MOD-5 정산대사 입수 / MOD-6 환수 시효 / MOD-7 환수 감면
- MOD-8 채널별 수수료차등(sales_channel)
각 DB(테이블+메뉴+권한+공통코드)→VO/Mapper/Enum→Service/Controller→화면 풀스택.
배치 정산연계: AggregateStep에 도입/익월부활 가산(11종), 선지급 FIFO 상계(멱등 reverse),
MOD-8 채널 우선 요율조회(NULL 폴백 하위호환).

[검증]
- 전체 ./gradlew build(전 모듈+전 테스트) GREEN
- Flyway V116~V123 운영DB 적용(schema v115→v123)
- 라이브 스모크 8 GET=200, MOD-1 RATE공식 50000 실측, 액션 POST 5xx 0건
- 적대적 코드리뷰 APPROVED(MOD-8 계산기 미연결 버그 발견·수정)

스펙: docs/DOMAIN_GAP_MOD1-8.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 17:33:13 +09:00

16 KiB
Raw Blame History

DOMAIN_GAP_MOD1-8 — 수수료 갭 8종 신규모듈 권위 스펙

2026-06-06 갭분석 결과 "개념적으로 빠진" 수수료 모듈 8종. 업무 수치는 데모 기본값(운영 시 룰테이블 조정). 모든 레이어 에이전트는 이 문서를 단일 진실원으로 따른다. 테이블/컬럼/Enum/경로/메뉴코드명을 그대로 사용.

공통 규약 (기존 패턴 — V92/V95/V96 미러)

  • PK BIGSERIAL, 금액 NUMERIC(15,2), 율 NUMERIC(7,4)(0~1 소수), 정산월 CHAR(6)(YYYYMM).
  • 감사: created_at TIMESTAMP NOT NULL DEFAULT NOW(), created_by BIGINT, updated_at TIMESTAMP.
  • 모든 테이블 COMMENT ON TABLE/COLUMN, 인덱스 idx_<약어>_....
  • 메뉴: menu(parent=GRP_*, menu_type='PAGE', menu_level=2) + menu_permission(READ/CREATE/UPDATE/DELETE/EXPORT) + role_menu_permission(SUPER_ADMIN/ADMIN 전체, MANAGER READ/CREATE/UPDATE/EXPORT, AGENT READ). NOT EXISTS/ON CONFLICT 가드.
  • 공통코드: common_code_group + common_code.
  • core: XxxVO(전컬럼)/XxxResp(+조인명)/XxxSaveReq(입력)/XxxSearchParam(extends PageParam) + Mapper(.java/.xml, selectList/selectById/insert/update/delete + 도메인쿼리) + Enum(상태) + MapStruct(필요시). Mapper는 BaseMapper 상속 안 함(개별 정의, 기존 commission Mapper 미러).
  • api: XxxService(@Service @Transactional(readOnly=true), 쓰기 @Transactional) + XxxController(@RestController @RequestMapping("/api/...") @RequirePermission, @DataChangeLog 쓰기, ApiResponse/PageResponse 반환). 기존 commission Controller 미러.
  • frontend: ga-frontend/src/pages/<area>/Xxx.tsx + api/<area>.ts 함수 + App.tsx 라우트. 100건↑/셀편집/합계는 AG Grid, 단순 CRUD는 Ant Design DataTable.
  • 마이그레이션 번호: MOD-1=V116, MOD-2=V117, MOD-3=V118, MOD-4=V119, MOD-5=V120, MOD-6=V121, MOD-7=V122, MOD-8=V123. (필요 시 모듈당 2파일 가능 — 예 V116_a 테이블 / 단일 파일에 메뉴·코드 포함 권장)

MOD-1 — 슈퍼바이저/도입수수료 (recruiter/development commission) [V116]

설계사를 GA에 **도입(리크루팅)**한 상위 설계사에게 지급하는 별도 트랙(오버라이드와 무관, recruiter 관계 기반).

DB V116

  • ALTER TABLE agent ADD COLUMN recruiter_agent_id BIGINT REFERENCES agent(agent_id); (도입자, NULL 허용)
  • recruiter_commission_rule(rule_id PK, recruiter_type VARCHAR(20) CHECK IN('NEW_RECRUIT','DEVELOPMENT','TRAINING'), calc_type VARCHAR(10) CHECK IN('RATE','FIXED'), commission_rate NUMERIC(7,4), fixed_amount NUMERIC(15,2), effective_from DATE NOT NULL, effective_to DATE, 감사컬럼). CHECK rate 0~1.
  • recruiter_commission_ledger(ledger_id PK, recruiter_agent_id BIGINT NOT NULL REFERENCES agent, recruit_agent_id BIGINT NOT NULL REFERENCES agent, settle_month CHAR(6) NOT NULL, recruiter_type VARCHAR(20) NOT NULL, base_amount NUMERIC(15,2) NOT NULL, commission_amount NUMERIC(15,2) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED' CHECK IN('CALCULATED','CONFIRMED','PAID','CANCELLED'), 감사). idx (recruiter_agent_id, settle_month).
  • 메뉴 RECRUITER_COMM('도입수수료', '/commission/recruiter', 'commission/RecruiterCommission', GRP_COMMISSION, sort 150) + 권한 + 공통코드 RECRUITER_TYPE(NEW_RECRUIT 신인도입/DEVELOPMENT 육성/TRAINING 교육).
  • 공식(데모): commission = base_amount × rule.commission_rate (RATE) / fixed_amount (FIXED). base_amount=도입 설계사의 당월 모집수수료 합(데모 입력 허용).

core/api

  • RecruiterCommissionRule* + RecruiterCommissionLedger* VO/Resp/SaveReq/SearchParam, Mapper(.java/.xml: selectList/selectById/insert/update/delete + aggregateByAgent(settleMonth) = SUM(commission_amount) GROUP BY recruiter_agent_id). Enum RecruiterType.
  • RecruiterCommissionService: rule CRUD + ledger list/detail/createLedger(룰 calc_type 권위, ReferralCommissionService 미러) + aggregate. Controller /api/recruiter-commissions (rules/ledgers).
  • 정산 반영: AggregateStep에 recruiterCommissionMapper.aggregateByAgent 추가 → other_commission_total 합산(기존 9종 패턴 동일, recruiter_agent_id 기준 agentId). (지급성이므로 gross 가산.)

frontend

  • pages/commission/RecruiterCommission.tsx(룰 탭 + 원장 탭, 등록 모달), api/commission.ts에 함수 추가, App.tsx 라우트.

MOD-2 — 선지급/차익정산 (advance payment recovery + delta settlement) [V117]

신입/램프업 설계사에 미리 지급한 선지급금을 확정수수료로 우선 회수, 차액만 실지급.

DB V117 (GRP_SETTLE)

  • advance_payment(advance_id PK, agent_id BIGINT NOT NULL REFERENCES agent, advance_month CHAR(6) NOT NULL, principal_amount NUMERIC(15,2) NOT NULL CHECK>0, recovered_amount NUMERIC(15,2) NOT NULL DEFAULT 0, balance NUMERIC(15,2) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'OUTSTANDING' CHECK IN('OUTSTANDING','PARTIAL','CLEARED','WAIVED'), memo VARCHAR(200), 감사). CHECK balance>=0. idx(agent_id, status).
  • advance_recovery_ledger(recovery_id PK, advance_id BIGINT NOT NULL REFERENCES advance_payment, settle_month CHAR(6) NOT NULL, recovery_amount NUMERIC(15,2) NOT NULL CHECK>0, 감사). idx(advance_id), idx(settle_month).
  • 메뉴 ADVANCE_PAYMENT('선지급정산','/settle/advance-payment','settle/AdvancePayment', GRP_SETTLE, sort 적절) + 권한 + 공통코드 ADVANCE_STATUS(OUTSTANDING 미상환/PARTIAL 부분/CLEARED 완료/WAIVED 면제).

core/api/batch

  • AdvancePayment* + AdvanceRecoveryLedger* VO/Resp/SaveReq/SearchParam, Mapper. 도메인쿼리: outstandingByAgent(agentId)=SUM(balance) WHERE status IN('OUTSTANDING','PARTIAL'); aging은 advance_month 기준.
  • AdvancePaymentService: 선지급 등록(balance=principal)/조회/aging 리포트/면제(waive). Controller /api/advance-payments.
  • 정산 연계(AggregateStep): net(=tax후) 계산 직후, P9 환수상계와 동일 위치/유사 패턴으로 선지급 잔액 FIFO 우선상계. recovered_advance = min(payable, outstanding). 차익정산 delta = payable recovered_advance. payable 최종 = delta. advance_recovery_ledger 기록 + advance_payment.recovered/balance/status 갱신. 멱등: 당월 recovery reverse 후 재적용(P9 reverseClawbackRecovery 패턴). 환수상계와 선지급상계 순서: 환수(채권) → 선지급. 둘 다 payable 차감.
  • 주의: 기존 settle_master payable_amount 의미에 선지급 차감 반영. 새 컬럼 불필요(payable에 통합) — 단 advance_recovery_ledger로 추적.

frontend

  • pages/settle/AdvancePayment.tsx(선지급 목록+잔액/상태, 등록·면제 모달, 상계내역 Drawer), api/settle.ts 또는 api/operation.ts 함수.

MOD-3 — 환수 분할상환 (installment clawback) [V118]

대액 환수채권을 N개월 분할 납부 계획으로.

DB V118 (GRP_COMMISSION)

  • clawback_installment_plan(plan_id PK, receivable_id BIGINT NOT NULL REFERENCES clawback_receivable(receivable_id), installment_no INT NOT NULL, installment_amount NUMERIC(15,2) NOT NULL CHECK>0, due_month CHAR(6) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'SCHEDULED' CHECK IN('SCHEDULED','RECOVERED','CANCELLED'), recovered_at TIMESTAMP, 감사). UNIQUE(receivable_id, installment_no). idx(due_month, status).
  • 메뉴 CLAWBACK_INSTALLMENT('환수분할상환','/commission/clawback-installment','commission/ClawbackInstallment', GRP_COMMISSION, sort) + 권한. 공통코드 CB_INSTALLMENT_STATUS(SCHEDULED 예정/RECOVERED 회수/CANCELLED 취소).

core/api

  • ClawbackInstallmentPlan* VO/Resp/SaveReq/SearchParam + Mapper(selectByReceivable, insertBatch, updateStatus).
  • ClawbackInstallmentService: receivable 1건 + 회차수 N → 균등분할(마지막 회차 잔차흡수, InstallmentPlanGenerator 잔차패턴) 계획 생성. due_month=기준월+회차. list/detail/generate/cancel. Controller /api/clawback-installments.
  • 월별 상계 연계는 데모 수준(계획 생성·조회·상태전이). (AggregateStep 통합은 선택 — 미연계 시 문서화.)

frontend

  • pages/commission/ClawbackInstallment.tsx(채권 선택→분할 생성, 회차 목록), api/commission.ts.

MOD-4 — 익월 부활 재지급 (lapsed reinstatement repayment) [V119]

실효(보류)된 계약이 익월 부활 시 보류했던 수수료를 make-good 재지급.

DB V119 (GRP_COMMISSION)

  • lapsed_reinstatement_recovery(recovery_id PK, contract_id BIGINT NOT NULL REFERENCES contract(contract_id), agent_id BIGINT NOT NULL REFERENCES agent, lapsed_month CHAR(6) NOT NULL, reinstated_month CHAR(6) NOT NULL, withheld_amount NUMERIC(15,2) NOT NULL CHECK>=0, repaid_amount NUMERIC(15,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'PENDING' CHECK IN('PENDING','REPAID','CANCELLED'), 감사). idx(agent_id, reinstated_month), idx(contract_id).
  • 메뉴 LAPSED_REINSTATEMENT('익월부활재지급','/commission/lapsed-reinstatement','commission/LapsedReinstatement', GRP_COMMISSION, sort) + 권한. 공통코드 LAPSE_RECOVERY_STATUS(PENDING 대기/REPAID 재지급완료/CANCELLED 취소).

core/api

  • LapsedReinstatementRecovery* VO/Resp/SaveReq/SearchParam + Mapper(selectList/selectById/insert/updateStatus + aggregateByAgent=SUM(repaid_amount) WHERE status='REPAID' GROUP BY agent_id).
  • LapsedReinstatementService: 부활 등록(withheld_amount=보류액, 데모 입력)/repay(상태 REPAID, repaid=withheld)/cancel. Controller /api/lapsed-reinstatements.
  • 정산 반영(선택): aggregateByAgent를 AggregateStep other_commission_total에 가산(REPAID만). 미연계 시 문서화.

frontend

  • pages/commission/LapsedReinstatement.tsx, api/commission.ts.

MOD-5 — 정산대사 입수 (carrier reconciliation IN) [V120]

보험사 통보 정산자료 vs GA 원장 대사 차이 산출.

DB V120 (GRP_SETTLE)

  • carrier_reconciliation_in(recon_id PK, company_id BIGINT NOT NULL REFERENCES insurance_company(company_id), settle_month CHAR(6) NOT NULL, file_name VARCHAR(200), record_count INT NOT NULL DEFAULT 0, reported_total NUMERIC(15,2) NOT NULL DEFAULT 0, system_total NUMERIC(15,2) NOT NULL DEFAULT 0, variance_amount NUMERIC(15,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'RECEIVED' CHECK IN('RECEIVED','MATCHED','VARIANCE','APPROVED'), 감사). UNIQUE(company_id, settle_month). idx(settle_month).
  • reconciliation_variance(variance_id PK, recon_id BIGINT NOT NULL REFERENCES carrier_reconciliation_in, policy_no VARCHAR(50), expected_amount NUMERIC(15,2), reported_amount NUMERIC(15,2), variance_reason VARCHAR(100), 감사). idx(recon_id).
  • 메뉴 CARRIER_RECON_IN('정산대사입수','/settle/carrier-recon-in','settle/CarrierReconIn', GRP_SETTLE, sort) + 권한. 공통코드 RECON_IN_STATUS(RECEIVED 입수/MATCHED 일치/VARIANCE 차이/APPROVED 승인).
  • insurance_company PK 컬럼명 확인 후 사용(company_id 가정, 다르면 실제명).

core/api

  • CarrierReconciliationIn* + ReconciliationVariance* VO/Resp/SaveReq/SearchParam + Mapper.
  • CarrierReconciliationInService: 대사 등록(reported_total 입력)/reconcile(system_total=settle_master 또는 income_commission 집계, variance=reportedsystem, status 자동 MATCHED/VARIANCE)/approve. Controller /api/carrier-recon-in.

frontend

  • pages/settle/CarrierReconIn.tsx(대사 목록 + 차이내역 Drawer), api/settle.ts/operation.ts.

MOD-6 — 환수 시효/소멸 (statute of limitations) [V121]

오래된 미결 환수채권을 만료개월 경과 시 자동 대손(WRITTEN_OFF/EXPIRED).

DB V121 (GRP_COMMISSION)

  • statute_config(config_id PK, entity_type VARCHAR(30) NOT NULL UNIQUE CHECK IN('CLAWBACK_RECEIVABLE'), expiration_months INT NOT NULL CHECK>0, is_active CHAR(1) NOT NULL DEFAULT 'Y', 감사). 시드 1행: ('CLAWBACK_RECEIVABLE', 36, 'Y').
  • 메뉴 STATUTE_CONFIG('환수시효관리','/commission/statute','commission/StatuteConfig', GRP_COMMISSION, sort) + 권한.
  • (별도 status 코드 불필요 — 기존 clawback_receivable status에 WRITTEN_OFF 존재 가정. 없으면 사유코드만 추가.)

core/api

  • StatuteConfig* VO/Resp/SaveReq + Mapper. clawback_receivable에 만료대상 조회/대손처리 쿼리(selectExpired(beforeMonth), writeOffExpired) 추가(기존 ClawbackReceivableMapper에).
  • StatuteService: config CRUD + expireOverdue(baseMonth) = origin_settle_month <= baseMonthexpiration_months AND status='OUTSTANDING' → WRITTEN_OFF(reason EXPIRED) + 카운트. Controller /api/statute (config + POST expire).

frontend

  • pages/commission/StatuteConfig.tsx(설정 + 만료실행 버튼 + 결과), api/commission.ts.

MOD-7 — 환수 감면/면제 (chargeback waiver/reduction) [V122]

재직 설계사 환수에 대한 감면 신청·승인 워크플로.

DB V122 (GRP_COMMISSION)

  • chargeback_waiver_request(request_id PK, contract_id BIGINT NOT NULL REFERENCES contract, agent_id BIGINT NOT NULL REFERENCES agent, settle_month CHAR(6) NOT NULL, original_chargeback NUMERIC(15,2) NOT NULL CHECK>=0, waiver_amount NUMERIC(15,2) NOT NULL CHECK>=0, reason VARCHAR(200), approval_status VARCHAR(20) NOT NULL DEFAULT 'REQUESTED' CHECK IN('REQUESTED','APPROVED','REJECTED'), approver_id BIGINT, approved_at TIMESTAMP, 감사). CHECK waiver_amount<=original_chargeback. idx(agent_id, settle_month).
  • 메뉴 CHARGEBACK_WAIVER('환수감면신청','/commission/chargeback-waiver','commission/ChargebackWaiver', GRP_COMMISSION, sort) + 권한. 공통코드 WAIVER_STATUS(REQUESTED 신청/APPROVED 승인/REJECTED 반려).

core/api

  • ChargebackWaiverRequest* VO/Resp/SaveReq/SearchParam + Mapper.
  • ChargebackWaiverService: 신청(create)/approve(승인 시 approval_status, approver/approved_at; 환수액 조정은 데모 수준 — 승인분 추적)/reject. waiver<=original 검증. Controller /api/chargeback-waivers.

frontend

  • pages/commission/ChargebackWaiver.tsx(신청 목록 + 승인/반려 액션), api/commission.ts.

MOD-8 — 채널별 수수료 차등 (channel-specific commission) [V123]

판매채널(직판/방카/홈쇼핑/제휴)별 요율 차등. 하위호환 필수(channel NULL = 기존 동작).

DB V123

  • ALTER TABLE contract ADD COLUMN sales_channel VARCHAR(20); (NULL 허용, NULL=DIRECT 취급)
  • 공통코드 SALES_CHANNEL(DIRECT 직판/BANCASSURANCE 방카/HOME_SHOPPING 홈쇼핑/PARTNERSHIP 제휴).
  • commission_rate / payout_rule 룰 테이블에 sales_channel VARCHAR(20) 컬럼 추가(NULL=모든채널). 실제 룰 테이블명·조회 SQL 확인 필수(CommissionCalculator가 쓰는 rate 조회). 조회 우선순위: channel 일치 룰 우선, 없으면 channel IS NULL 룰 폴백.
  • 메뉴 신규 불필요(계약 화면에서 노출). 단 계약 등록/조회에 sales_channel 추가.

core/api/batch

  • ContractVO/Resp/SaveReq에 salesChannel. ContractMapper.xml insert/update/select에 sales_channel.
  • rate 조회 Mapper SQL에 channel 파라미터 추가(AND (sales_channel = #{channel} OR sales_channel IS NULL) ORDER BY sales_channel NULLS LAST 패턴으로 channel 우선). CommissionCalculator가 contract.salesChannel 전달.
  • 하위호환 회귀 필수 검증: channel NULL 계약은 기존과 동일 rate. 기존 CommissionCalculatorTest 통과 유지.

frontend

  • 계약 페이지(pages/contracts/... 또는 product)에서 sales_channel select 노출(공통코드 SALES_CHANNEL).

실행 순서 (5-Wave, 레이어별 병렬)

  1. DBA: V116~V123 작성 (dba-architect). → SQL 파싱/리뷰.
  2. CORE: 8모듈 VO/Mapper/Enum/MapStruct (core-architect). → :ga-core:compileJava GREEN.
  3. API+BATCH: Service/Controller (api-developer) ‖ AggregateStep/배치 연계 (batch-engineer). → :ga-api:compileJava :ga-batch:compileJava GREEN.
  4. FRONTEND: 화면+api+라우트 (frontend-developer). → tsc GREEN.
  5. INFRA/검증: ./gradlew build(test) GREEN + Flyway 적용 + 스모크 (PL + infra-reviewer).
  • 각 Wave 후 PL이 빌드로 직접 검증. 컬럼/이름 불일치는 PL이 즉시 수정.