feat: 수수료 갭 보완 4종 — 지급보류/환수원천세역분개/연체이자/간이지급명세서 (V124~V126)

감사(서브에이전트 3영역)로 발견한 실제 결함/규제 갭만 선별 보완.

- A 지급보류(HOLD) 지급배치 누락 버그: PaymentBatchItemMapper.insertFromSettleMaster
  status NOT IN('PAID','HOLD') — 보류건이 지급배치에 포함되어 실제 이체되던 결함 수정
- B 환수 원천세 역분개: withholding_tax_adjustment(V124) 멱등 적재 +
  분기 원천세신고 집계 netting(payment UNION ALL adjustment). settle_master 금액흐름 불변
- C 환수채권 연체이자: clawback_receivable.accrued_interest + clawback_interest(V125) +
  ClawbackInterestCalculator(연율/1200), config 기본 0 = 안전폴백, 기존 FIFO 상계가 이자 회수
- D 사업소득 간이지급명세서(월별): commission_statement(MONTHLY) 위 읽기전용 리포트 +
  메뉴 REPORT_SIMPLIFIED_PAYMENT(V126) + 프론트 SimplifiedPaymentReport

검증: ./gradlew build 전모듈 GREEN, 신규 단위테스트 6건, ga-frontend tsc -b PASS,
Flyway V124~V126 운영DB 적용(v126), 적대리뷰 APPROVED(LOW 1건 GREATEST 클램프 즉수정),
B/C/D 라이브DB 검증(롤백) + D admin e2e list/export 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-06-08 21:22:13 +09:00
parent ff6d399dd3
commit fbbabf2d04
29 changed files with 832 additions and 19 deletions
@@ -73,9 +73,16 @@ const GRID_COLS: ColDef<ClawbackReceivableRow>[] = [
type: 'numericColumn',
valueFormatter: (p) => fmt(p.value),
},
{
field: 'accruedInterest',
headerName: '연체이자',
flex: 1,
type: 'numericColumn',
valueFormatter: (p) => fmt(p.value),
},
{
field: 'balance',
headerName: '잔액',
headerName: '잔액(이자포함)',
flex: 1,
type: 'numericColumn',
valueFormatter: (p) => fmt(p.value),
@@ -274,7 +281,8 @@ function DrawerContent({ detail }: { detail: ClawbackReceivableDetail }) {
{ label: '발생월', value: detail.originSettleMonth },
{ label: '발생액', value: fmt(detail.originalAmount) },
{ label: '회수액', value: fmt(detail.recoveredAmount) },
{ label: '잔액', value: fmt(detail.balance) },
{ label: '연체이자', value: fmt(detail.accruedInterest) },
{ label: '잔액(이자포함)', value: fmt(detail.balance) },
{ label: '상태', value: <StatusChip status={detail.status} statusName={detail.statusName} /> },
{ label: '최근회수월', value: detail.lastRecoveryMonth ?? '-' },
{ label: '등록일', value: detail.createdAt ? dayjs(detail.createdAt).format('YYYY-MM-DD') : '-' },
@@ -0,0 +1,108 @@
import { useState } from 'react';
import { Alert, DatePicker } from 'antd';
import { ProCard, ProTable, type ProColumns } from '@ant-design/pro-components';
import { useQuery } from '@tanstack/react-query';
import dayjs, { type Dayjs } from 'dayjs';
import PageContainer from '@/components/common/PageContainer';
import MoneyText from '@/components/common/MoneyText';
import ExcelExportButton from '@/components/common/ExcelExportButton';
import api, { unwrap } from '@/api/request';
interface SimplifiedPaymentRow {
agentId: number;
settleMonth: string;
agentName: string;
residentNo?: string;
businessNo?: string;
incomeTypeName: string;
grossAmount: number;
incomeTaxAmount: number;
localTaxAmount: number;
netAmount: number;
}
function fetchReport(settleMonth: string) {
return unwrap<SimplifiedPaymentRow[]>(
api.get('/api/report/simplified-payment', { params: { settleMonth } }),
);
}
const columns: ProColumns<SimplifiedPaymentRow>[] = [
{ title: '소득자', dataIndex: 'agentName', width: 120, fixed: 'left' },
{ title: '주민등록번호', dataIndex: 'residentNo', width: 140 },
{ title: '사업자번호', dataIndex: 'businessNo', width: 140 },
{ title: '소득구분', dataIndex: 'incomeTypeName', width: 90 },
{
title: '지급액', dataIndex: 'grossAmount', width: 130, align: 'right',
render: (_, r) => <MoneyText value={r.grossAmount} />,
},
{
title: '소득세', dataIndex: 'incomeTaxAmount', width: 120, align: 'right',
render: (_, r) => <MoneyText value={r.incomeTaxAmount} />,
},
{
title: '지방소득세', dataIndex: 'localTaxAmount', width: 120, align: 'right',
render: (_, r) => <MoneyText value={r.localTaxAmount} />,
},
{
title: '차인지급액', dataIndex: 'netAmount', width: 140, align: 'right',
render: (_, r) => <strong><MoneyText value={r.netAmount} /></strong>,
},
];
export default function SimplifiedPaymentReport() {
const [settleMonth, setSettleMonth] = useState<Dayjs>(dayjs());
const monthStr = settleMonth.format('YYYYMM');
const { data, isLoading, isError } = useQuery({
queryKey: ['report', 'simplified-payment', monthStr],
queryFn: () => fetchReport(monthStr),
retry: false,
});
return (
<PageContainer
title="사업소득 간이지급명세서"
description="월별 사업소득 지급·원천징수 명세 (국세청 간이지급명세서 제출 대응)"
extra={
<ExcelExportButton
url="/api/report/simplified-payment/export"
fileName={`간이지급명세서_${monthStr}`}
params={{ settleMonth: monthStr }}
/>
}
>
{isError && (
<Alert
type="warning"
showIcon
message="조회 실패"
description="/api/report/simplified-payment API 응답이 없습니다."
style={{ marginBottom: 16 }}
/>
)}
<ProCard bordered style={{ marginBottom: 16 }}>
<DatePicker
picker="month"
value={settleMonth}
onChange={(d) => d && setSettleMonth(d)}
allowClear={false}
format="YYYY-MM"
/>
</ProCard>
<ProTable<SimplifiedPaymentRow>
rowKey="agentId"
columns={columns}
dataSource={data ?? []}
loading={isLoading}
search={false}
pagination={{ pageSize: 50, showTotal: (t) => `${t}` }}
options={{ density: false, reload: false }}
scroll={{ x: 1000 }}
/>
</PageContainer>
);
}