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:
@@ -127,6 +127,7 @@ const IncentiveProgram = React.lazy(() => import('@/pages/settle/IncentivePro
|
||||
const PerformanceReport = React.lazy(() => import('@/pages/report/PerformanceReport'));
|
||||
const OrgReport = React.lazy(() => import('@/pages/report/OrgReport'));
|
||||
const ChargebackRiskReport = React.lazy(() => import('@/pages/report/ChargebackRiskReport'));
|
||||
const SimplifiedPaymentReport = React.lazy(() => import('@/pages/report/SimplifiedPaymentReport'));
|
||||
|
||||
// KPI 대시보드
|
||||
const KpiMonthlySummary = React.lazy(() => import('@/pages/kpi/KpiMonthlySummary'));
|
||||
@@ -212,6 +213,7 @@ export default function App() {
|
||||
<Route path="report/performance" element={<PerformanceReport />} />
|
||||
<Route path="report/org" element={<OrgReport />} />
|
||||
<Route path="report/chargeback-risk" element={<ChargebackRiskReport />} />
|
||||
<Route path="report/simplified-payment" element={<SimplifiedPaymentReport />} />
|
||||
|
||||
{/* KPI 대시보드 */}
|
||||
<Route path="kpi/monthly" element={<KpiMonthlySummary />} />
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface ClawbackReceivableRow extends Record<string, unknown> {
|
||||
originalAmount: number;
|
||||
recoveredAmount: number;
|
||||
balance: number;
|
||||
accruedInterest: number;
|
||||
status: ClawbackStatus;
|
||||
statusName: string;
|
||||
lastRecoveryMonth?: string;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user