# P11 — 시상/신인시상 환수 (#1) 작성: PL, 2026-06-04. 후속 갭 3종 중 두 번째. 근거: incar `유지율시상환수`/`신인도입유치자시상환수`/`시상내역 조회`. GA award_ledger(V92)는 지급만 있고 환수(clawback) 처리가 없음. 정착지원금/생보운영지원/지점장수당은 이미 `clawback_amount`+`net_amount` 패턴 보유 → **award_ledger도 동일 패턴으로 통일**. 신인시상은 award_type='NEW_RECRUIT'로 이미 존재 → 별도 테이블 불필요. 모든 award_type에 환수 적용. Flyway: **V113 최신 → V114**. 금액 NUMERIC(15,2), HALF_UP. ## 1. 현재 award_ledger (V92) ledger_id / agent_id / settle_month / award_type(NEW_RECRUIT/CORPORATE/RETENTION/PERFORMANCE) / base_amount / award_amount / status(CALCULATED/CONFIRMED/PAID/CANCELLED) / created_at / updated_at. aggregateByAgent = SUM(award_amount) WHERE status!=CANCELLED. **문제**: clawback_amount/net_amount 없음. settling/life/branch는 net=support−clawback, aggregate=SUM(net). ## 2. 스키마 변경 (V114) — settling_support_ledger 패턴 미러 ```sql ALTER TABLE award_ledger ADD COLUMN clawback_amount NUMERIC(15,2) NOT NULL DEFAULT 0, ADD COLUMN net_amount NUMERIC(15,2) NOT NULL DEFAULT 0; -- 기존 행 백필: 환수 없던 과거 = net = award UPDATE award_ledger SET net_amount = award_amount WHERE net_amount = 0 AND clawback_amount = 0; -- clawback 음수 방지 + 상태에 CLAWED_BACK 추가(settling 패턴과 통일) ALTER TABLE award_ledger ADD CONSTRAINT chk_award_clawback CHECK (clawback_amount >= 0); ALTER TABLE award_ledger DROP CONSTRAINT <기존 status CHECK 이름>; -- \d 로 확인 (예: award_ledger_status_check) ALTER TABLE award_ledger ADD CONSTRAINT chk_award_status CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED','CLAWED_BACK')); ``` > 기존 status CHECK 제약명은 `\d award_ledger` 또는 information_schema 로 정확히 확인 후 DROP. 추정명 사용 금지. > net_amount는 음수 허용(환수>시상 시 음수 가능)이나, **§4 검증에서 clawback ≤ award_amount 강제**하므로 실제 net≥0. (지급성이므로 음수 차단 — settling 동일) 메뉴 변경 없음(award 화면 기존). 공통코드 AWARD_STATUS에 CLAWED_BACK 있으면 추가(없으면 생략). ## 3. Core - AwardLedgerVO/Resp/SaveReq에 `clawbackAmount`, `netAmount` 추가. - `AwardLedgerMapper.aggregateByAgent` XML: `SUM(award_amount)` → **`SUM(net_amount)`** (net=award−clawback). status!=CANCELLED 유지. - 신규 Mapper 메서드 `applyClawback(@Param ledgerId, @Param clawbackAmount, @Param netAmount, @Param status)` (+XML): clawback_amount/net_amount/status='CLAWED_BACK'/updated_at 갱신. - createLedger 시 net_amount = award_amount − clawback_amount 저장(insert XML에 두 컬럼 포함). ## 4. API (AwardService 확장) - `createLedger`: clawback_amount(SaveReq, 기본0) 받아 **clawback ≤ award_amount 검증(초과 시 E411)**, net = award − clawback 계산해 저장. - **신규 액션** `applyClawback(ledgerId, AwardClawbackReq{clawbackAmount, reason})` @Transactional: - 원장 조회(없으면 NOT_FOUND). 이미 CANCELLED면 E411. - clawbackAmount > award_amount 면 E411("환수액이 시상액 초과"). - net = award_amount − clawbackAmount. mapper.applyClawback(ledgerId, clawback, net, 'CLAWED_BACK'). - @DataChangeLog. - Controller: `PUT /api/awards/ledgers/{id}/clawback` (@RequirePermission(menu=AWARD, action=UPDATE)). 기존 award 컨트롤러 경로 패턴 확인. > **AggregateStep 무변경**: awardMapper.aggregateByAgent가 SUM(net_amount) 반환하므로 환수가 자동으로 other_commission 차감에 반영. net<0이면 P9 환수채권 이월에 자연 연계. ## 5. Frontend (기존 Award 화면 확장) - award 원장 그리드에 환수액/실지급(net) 컬럼 추가. - 행 액션 "환수처리"(PermissionButton UPDATE) → 모달(환수액 입력 + 사유) → PUT /clawback. 성공 시 invalidate. - api 모듈에 applyClawback 추가. 기존 award api/페이지 파일명 확인 후 확장. ## 6. 검증(PL) build+test, Flyway V114(award_ledger 컬럼/제약/백필), 라이브: award 원장 1건 생성(net=award) → applyClawback(부분환수) → net 감소·status CLAWED_BACK 확인 → clawback>award E411 → aggregateByAgent 가 net 반영하는지 SQL 확인. 테스트 데이터 정리.