feat: P11(#1) 시상/신인시상 환수 — award_ledger 환수 처리 (PL 자율진행)

incar 유지율시상환수/신인도입유치자시상환수 대응. award_ledger(V92)는 지급만 있고
환수가 없던 갭. 정착지원금/생보운영지원/지점장수당의 clawback_amount+net_amount 패턴으로
통일(신인시상=award_type NEW_RECRUIT로 기존재 → 별도 테이블 불필요).

- DB(V114): award_ledger에 clawback_amount/net_amount 컬럼 추가 + net=award 백필 +
  CHECK(clawback>=0) + status CHECK에 CLAWED_BACK 추가(pg_constraint 동적 탐색 DROP/재생성).
- core: AwardLedgerVO/Resp/SaveReq에 clawbackAmount/netAmount. AwardLedgerMapper
  aggregateByAgent SUM(award_amount)→SUM(net_amount), insert에 두 컬럼, applyClawback UPDATE.
- api: AwardService.createLedger가 net=award−clawback 계산(clawback≤award 검증). 신규
  applyClawback(ledgerId, {clawbackAmount,reason}) — 초과/취소상태 E411, status=CLAWED_BACK.
  PUT /api/awards/ledgers/{id}/clawback.
- frontend: Award 원장 그리드에 환수액/실지급 컬럼 + 합계 + 환수처리 모달(PermissionButton UPDATE).
- AggregateStep 무변경: aggregateByAgent가 net 반환 → 환수가 other_commission 자동 차감,
  net<0이면 P9 환수채권 이월에 자연 연계.

검증: build+test SUCCESSFUL, Flyway V114(schema v114). 라이브: award 100000 원장 →
clawback 30000 → net 70000·CLAWED_BACK, 초과환수 200000 → E411, aggregate net=70000 반영
확인. 테스트 데이터 정리. 스펙 docs/DOMAIN_GAP_P11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-06-05 00:59:48 +09:00
parent 284ecc4203
commit 934e45d295
12 changed files with 307 additions and 18 deletions
+62
View File
@@ -0,0 +1,62 @@
# 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=supportclawback, 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=awardclawback). 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>
@@ -0,0 +1,20 @@
package com.ga.api.controller.commission;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* 시상 원장 환수 처리 요청 (PUT /api/awards/ledgers/{id}/clawback).
*/
@Data
public class AwardClawbackReq {
@NotNull
@DecimalMin("0")
private BigDecimal clawbackAmount;
private String reason;
}
@@ -79,4 +79,13 @@ public class AwardController {
public ApiResponse<Long> createLedger(@Valid @RequestBody AwardLedgerSaveReq req) {
return ApiResponse.ok(service.createLedger(req));
}
@PutMapping("/ledgers/{ledgerId}/clawback")
@RequirePermission(menu = "AWARD", perm = "UPDATE")
@DataChangeLog(menu = "AWARD", table = "award_ledger")
public ApiResponse<Void> applyClawback(@PathVariable Long ledgerId,
@Valid @RequestBody AwardClawbackReq req) {
service.applyClawback(ledgerId, req);
return ApiResponse.ok();
}
}
@@ -112,14 +112,38 @@ public class AwardService {
.setScale(2, RoundingMode.HALF_UP);
}
BigDecimal clawback = req.getClawbackAmount() != null ? req.getClawbackAmount() : BigDecimal.ZERO;
if (clawback.compareTo(awardAmount) > 0) {
throw new BizException(ErrorCode.INVALID_PARAMETER, "환수액이 시상액을 초과합니다");
}
BigDecimal netAmount = awardAmount.subtract(clawback).setScale(2, RoundingMode.HALF_UP);
AwardLedgerVO vo = new AwardLedgerVO();
vo.setAgentId(req.getAgentId());
vo.setSettleMonth(req.getSettleMonth());
vo.setAwardType(req.getAwardType());
vo.setBaseAmount(req.getBaseAmount());
vo.setAwardAmount(awardAmount);
vo.setClawbackAmount(clawback);
vo.setNetAmount(netAmount);
vo.setStatus("CALCULATED");
ledgerMapper.insert(vo);
return vo.getLedgerId();
}
@Transactional
public void applyClawback(Long ledgerId, com.ga.api.controller.commission.AwardClawbackReq req) {
AwardLedgerResp ledger = ledgerMapper.selectById(ledgerId);
if (ledger == null) throw new BizException(ErrorCode.NOT_FOUND);
if ("CANCELLED".equals(ledger.getStatus())) {
throw new BizException(ErrorCode.INVALID_PARAMETER, "취소된 원장에는 환수를 적용할 수 없습니다");
}
if (req.getClawbackAmount().compareTo(ledger.getAwardAmount()) > 0) {
throw new BizException(ErrorCode.INVALID_PARAMETER, "환수액이 시상액을 초과합니다");
}
BigDecimal netAmount = ledger.getAwardAmount()
.subtract(req.getClawbackAmount())
.setScale(2, RoundingMode.HALF_UP);
ledgerMapper.applyClawback(ledgerId, req.getClawbackAmount(), netAmount, "CLAWED_BACK");
}
}
@@ -0,0 +1,56 @@
-- V114: 시상 환수 (award_ledger 컬럼 추가 + status 확장)
-- settling_support_ledger(V90) 패턴 미러:
-- clawback_amount NUMERIC(15,2) NOT NULL DEFAULT 0
-- net_amount NUMERIC(15,2) NOT NULL DEFAULT 0
-- 기존 행 백필: 환수 없던 과거 데이터는 net = award
-- status CHECK에 'CLAWED_BACK' 추가 (기존 제약은 DO 블록으로 동적 탐색 후 DROP)
-- 공통코드: AWARD_STATUS 그룹이 V92/V96/V105 어디에도 없으므로 INSERT 생략
-- ============================================================
-- 1. 컬럼 추가
-- ============================================================
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;
-- ============================================================
-- 2. 기존 행 백필: 환수 없던 과거 = net = award
-- ============================================================
UPDATE award_ledger
SET net_amount = award_amount
WHERE net_amount = 0
AND clawback_amount = 0;
-- ============================================================
-- 3. clawback 음수 방지 제약
-- ============================================================
ALTER TABLE award_ledger
ADD CONSTRAINT chk_award_clawback CHECK (clawback_amount >= 0);
-- ============================================================
-- 4. status CHECK 재생성 — CLAWED_BACK 추가
-- 기존 인라인 CHECK 이름을 추정하지 않고 pg_constraint에서 동적 조회 후 DROP
-- ============================================================
DO $$
DECLARE
c text;
BEGIN
SELECT conname INTO c
FROM pg_constraint
WHERE conrelid = 'award_ledger'::regclass
AND contype = 'c'
AND pg_get_constraintdef(oid) ILIKE '%status%';
IF c IS NOT NULL THEN
EXECUTE 'ALTER TABLE award_ledger DROP CONSTRAINT ' || quote_ident(c);
END IF;
END $$;
ALTER TABLE award_ledger
ADD CONSTRAINT chk_award_status
CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED','CLAWED_BACK'));
-- ============================================================
-- 5. 컬럼 코멘트
-- ============================================================
COMMENT ON COLUMN award_ledger.clawback_amount IS '시상 환수액';
COMMENT ON COLUMN award_ledger.net_amount IS '실지급 시상액 = award_amount clawback_amount';
@@ -31,5 +31,14 @@ public interface AwardLedgerMapper {
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
/**
* 환수 처리 — clawback_amount/net_amount/status/updated_at 갱신.
* status 는 통상 'CLAWED_BACK'. Service에서 clawback ≤ award_amount 검증 후 호출.
*/
int applyClawback(@Param("ledgerId") Long ledgerId,
@Param("clawbackAmount") BigDecimal clawbackAmount,
@Param("netAmount") BigDecimal netAmount,
@Param("status") String status);
int delete(@Param("ledgerId") Long ledgerId);
}
@@ -21,6 +21,10 @@ public class AwardLedgerResp {
private String awardTypeName;
private BigDecimal baseAmount;
private BigDecimal awardAmount;
/** 환수액 (V114) */
private BigDecimal clawbackAmount;
/** 실지급액 = award_amount - clawback_amount (V114) */
private BigDecimal netAmount;
private String status;
/** 공통코드 INCOME_STATUS */
private String statusName;
@@ -1,5 +1,6 @@
package com.ga.core.vo.commission;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
@@ -10,6 +11,7 @@ import java.math.BigDecimal;
/**
* award_ledger 등록 요청.
* award_amount 는 Service가 계산 후 VO에 set.
* net_amount 는 Service가 award_amount - clawbackAmount 계산 후 VO에 set.
*/
@Data
public class AwardLedgerSaveReq {
@@ -26,4 +28,8 @@ public class AwardLedgerSaveReq {
@NotNull
@Positive
private BigDecimal baseAmount;
/** 등록 시 환수액 (선택, 기본 0). clawback ≤ award_amount 검증은 Service에서 수행. */
@DecimalMin("0")
private BigDecimal clawbackAmount;
}
@@ -6,8 +6,9 @@ import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* award_ledger — 시상 원장 (V92)
* award_ledger — 시상 원장 (V92, clawback 컬럼 V114 추가)
* award_amount = base_amount × award_rate (RATE) or award_fixed (FIXED)
* net_amount = award_amount - clawback_amount
*/
@Data
public class AwardLedgerVO {
@@ -21,7 +22,11 @@ public class AwardLedgerVO {
private BigDecimal baseAmount;
/** 시상금 */
private BigDecimal awardAmount;
/** CALCULATED / CONFIRMED / PAID / CANCELLED */
/** 환수액 (V114) */
private BigDecimal clawbackAmount;
/** 실지급액 = award_amount - clawback_amount (V114) */
private BigDecimal netAmount;
/** CALCULATED / CONFIRMED / PAID / CANCELLED / CLAWED_BACK */
private String status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@@ -6,7 +6,7 @@
<resultMap id="VOMap" type="com.ga.core.vo.commission.AwardLedgerVO"/>
<resultMap id="RespMap" type="com.ga.core.vo.commission.AwardLedgerResp"/>
<!-- 목록/단건 조회 컬럼 — 설계사명·조직명·공통코드명 조인 포함 -->
<!-- 목록/단건 조회 컬럼 — 설계사명·조직명·공통코드명 조인 포함 (V114: clawback_amount, net_amount 추가) -->
<sql id="ledgerJoinCols">
l.ledger_id,
l.agent_id, a.agent_name, o.org_name,
@@ -15,6 +15,7 @@
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'AWARD_TYPE' AND cc.code = l.award_type) AS award_type_name,
l.base_amount, l.award_amount,
l.clawback_amount, l.net_amount,
l.status,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'INCOME_STATUS' AND cc.code = l.status) AS status_name
@@ -54,12 +55,14 @@
AND status != 'CANCELLED'
</select>
<!-- award_amount 는 Service가 계산 후 VO에 set, status 기본 CALCULATED -->
<!-- award_amount/net_amount 는 Service가 계산 후 VO에 set, status 기본 CALCULATED (V114: clawback_amount, net_amount 포함) -->
<insert id="insert" useGeneratedKeys="true" keyProperty="ledgerId">
INSERT INTO award_ledger
(agent_id, settle_month, award_type, base_amount, award_amount, status)
(agent_id, settle_month, award_type, base_amount, award_amount,
clawback_amount, net_amount, status)
VALUES
(#{agentId}, #{settleMonth}, #{awardType}, #{baseAmount}, #{awardAmount},
COALESCE(#{clawbackAmount}, 0), #{netAmount},
COALESCE(#{status}, 'CALCULATED'))
</insert>
@@ -73,14 +76,24 @@
DELETE FROM award_ledger WHERE ledger_id = #{ledgerId}
</delete>
<!-- 정산월 기준 설계사별 시상금 합계 -->
<!-- 정산월 기준 설계사별 시상금 합계 — net_amount 기준(V114: 환수 자동 차감). CANCELLED 제외. -->
<select id="aggregateByAgent" resultType="map">
SELECT agent_id AS "agentId",
SUM(award_amount) AS "total"
SELECT agent_id AS "agentId",
SUM(net_amount) AS "total"
FROM award_ledger
WHERE settle_month = #{settleMonth}
AND status != 'CANCELLED'
GROUP BY agent_id
</select>
<!-- 환수 처리 — clawback_amount/net_amount/status/updated_at 갱신 -->
<update id="applyClawback">
UPDATE award_ledger
SET clawback_amount = #{clawbackAmount},
net_amount = #{netAmount},
status = #{status},
updated_at = NOW()
WHERE ledger_id = #{ledgerId}
</update>
</mapper>
+9
View File
@@ -262,9 +262,16 @@ export interface AwardLedgerRow extends Record<string, unknown> {
awardType: string;
baseAmount: number;
awardAmount: number;
clawbackAmount: number;
netAmount: number;
status: string;
}
export interface AwardClawbackReq {
clawbackAmount: number;
reason: string;
}
// ── P7: LifeSupport ───────────────────────────────
export interface LifeSupportRuleRow extends Record<string, unknown> {
ruleId: number;
@@ -472,6 +479,8 @@ export const commissionApi = {
unwrap<PageResponse<AwardLedgerRow>>(
api.get('/api/awards/ledgers', { params: p }),
),
awardLedgerClawback: (ledgerId: number, body: AwardClawbackReq) =>
unwrap<void>(api.put(`/api/awards/ledgers/${ledgerId}/clawback`, body)),
// ── P7: LifeSupport ───────────────────────────────
lifeSupportRules: (p?: Partial<CommissionSearchParam>) =>
+82 -10
View File
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Alert, Form, Input, InputNumber, Modal, Select, Space, Tabs, message } from 'antd';
import { ProCard } from '@ant-design/pro-components';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import type { ColDef } from '@ag-grid-community/core';
import dayjs from 'dayjs';
import PageContainer from '@/components/common/PageContainer';
@@ -12,6 +12,7 @@ import {
commissionApi,
AwardRuleRow,
AwardLedgerRow,
AwardClawbackReq,
CommissionSearchParam,
} from '@/api/commission';
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
@@ -36,12 +37,14 @@ const RULE_COLS: ColDef<AwardRuleRow>[] = [
];
const LEDGER_COLS: ColDef<AwardLedgerRow>[] = [
{ field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' },
{ field: 'agentName', headerName: '설계사', width: 130 },
{ field: 'awardType', headerName: '시상유형', width: 120 },
{ field: 'baseAmount', headerName: '기준실적', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
{ field: 'awardAmount', headerName: '시상금', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
{ field: 'status', headerName: '상태', width: 100 },
{ field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' },
{ field: 'agentName', headerName: '설계사', width: 130 },
{ field: 'awardType', headerName: '시상유형', width: 120 },
{ field: 'baseAmount', headerName: '기준실적', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
{ field: 'awardAmount', headerName: '시상금', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
{ field: 'clawbackAmount', headerName: '환수액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
{ field: 'netAmount', headerName: '실지급', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
{ field: 'status', headerName: '상태', width: 110 },
];
export default function Award() {
@@ -53,6 +56,25 @@ export default function Award() {
const [editingRule, setEditingRule] = useState<AwardRuleRow | null>(null);
const [form] = Form.useForm();
const [clawbackTarget, setClawbackTarget] = useState<AwardLedgerRow | null>(null);
const [clawbackForm] = Form.useForm();
const clawbackMutation = useMutation({
mutationFn: ({ ledgerId, body }: { ledgerId: number; body: AwardClawbackReq }) =>
commissionApi.awardLedgerClawback(ledgerId, body),
onSuccess: () => {
message.success('환수처리 완료');
qc.invalidateQueries({ queryKey: ['award', 'ledgers'] });
setClawbackTarget(null);
clawbackForm.resetFields();
},
});
const handleClawbackOk = async () => {
const values = await clawbackForm.validateFields();
clawbackMutation.mutate({ ledgerId: clawbackTarget!.ledgerId, body: values });
};
const { data: rulesData, isLoading: rulesLoading, isError: rulesError } = useQuery({
queryKey: ['award', 'rules'],
queryFn: () => commissionApi.awardRules(),
@@ -97,8 +119,10 @@ export default function Award() {
const pinnedLedger = ledgers.length > 0 ? {
agentName: '합계',
baseAmount: ledgers.reduce((s, r) => s + (r.baseAmount ?? 0), 0),
awardAmount: ledgers.reduce((s, r) => s + (r.awardAmount ?? 0), 0),
baseAmount: ledgers.reduce((s, r) => s + (r.baseAmount ?? 0), 0),
awardAmount: ledgers.reduce((s, r) => s + (r.awardAmount ?? 0), 0),
clawbackAmount: ledgers.reduce((s, r) => s + (r.clawbackAmount ?? 0), 0),
netAmount: ledgers.reduce((s, r) => s + (r.netAmount ?? 0), 0),
} as Partial<AwardLedgerRow> : undefined;
const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm };
@@ -159,7 +183,23 @@ export default function Award() {
<ProCard style={cardStyle}>
<DataGrid<AwardLedgerRow>
rows={ledgers}
columns={LEDGER_COLS}
columns={[
...LEDGER_COLS,
{
headerName: '액션', width: 110, pinned: 'right',
cellRenderer: (p: { data: AwardLedgerRow }) => (
<PermissionButton
menuCode="AWARD"
permCode="UPDATE"
size="small"
disabled={p.data?.status === 'CANCELLED'}
onClick={() => { setClawbackTarget(p.data); clawbackForm.resetFields(); }}
>
</PermissionButton>
),
},
]}
loading={ledgersLoading}
height={520}
rowKey="ledgerId"
@@ -204,6 +244,38 @@ export default function Award() {
</Form.Item>
</Form>
</Modal>
<Modal
title="환수처리"
open={!!clawbackTarget}
onOk={handleClawbackOk}
onCancel={() => { setClawbackTarget(null); clawbackForm.resetFields(); }}
okText="처리"
cancelText="취소"
confirmLoading={clawbackMutation.isPending}
>
{clawbackTarget && (
<div style={{ marginBottom: 12, color: '#666', fontSize: 13 }}>
{clawbackTarget.agentName} / {clawbackTarget.settleMonth} / {fmt(clawbackTarget.awardAmount)}
</div>
)}
<Form form={clawbackForm} layout="vertical">
<Form.Item
name="clawbackAmount"
label="환수액"
rules={[{ required: true, message: '환수액을 입력하세요' }]}
>
<InputNumber style={{ width: '100%' }} min={0} placeholder="환수할 금액" />
</Form.Item>
<Form.Item
name="reason"
label="사유"
rules={[{ required: true, message: '사유를 입력하세요' }]}
>
<Input.TextArea rows={3} placeholder="환수 사유를 입력하세요" />
</Form.Item>
</Form>
</Modal>
</PageContainer>
);
}