# 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 확인. 테스트 데이터 정리.
</content>
