동기화

This commit is contained in:
GA Pro
2026-05-14 01:21:27 +09:00
parent acea2afce5
commit 958fe6b980
379 changed files with 21786 additions and 423 deletions
@@ -0,0 +1,103 @@
import { Alert } from 'antd';
import { ProTable, type ProColumns } from '@ant-design/pro-components';
import { useQuery } from '@tanstack/react-query';
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 RiskRow {
agentId: number;
agentName: string;
orgName: string;
policyNo: string;
companyName: string;
recruitMonth: string;
monthsElapsed: number;
recruitAmount: number;
chargebackRate: number;
estimatedChargeback: number;
riskLevel: 'HIGH' | 'MEDIUM' | 'LOW';
}
function fetchRisk() {
return unwrap<RiskRow[]>(api.get('/api/report/chargeback-risk'));
}
const riskColor: Record<string, string> = {
HIGH: '#f5222d',
MEDIUM: '#fa8c16',
LOW: '#52c41a',
};
const riskLabel: Record<string, string> = {
HIGH: '고위험',
MEDIUM: '중위험',
LOW: '저위험',
};
const columns: ProColumns<RiskRow>[] = [
{ title: '설계사', dataIndex: 'agentName', width: 100, fixed: 'left' },
{ title: '소속', dataIndex: 'orgName', width: 160 },
{ title: '증권번호', dataIndex: 'policyNo', width: 130 },
{ title: '보험사', dataIndex: 'companyName', width: 110 },
{ title: '모집월', dataIndex: 'recruitMonth', width: 90 },
{ title: '경과월', dataIndex: 'monthsElapsed', width: 80, align: 'right', render: (_, r) => `${r.monthsElapsed}개월` },
{
title: '모집수수료', dataIndex: 'recruitAmount', width: 120, align: 'right',
render: (_, r) => <MoneyText value={r.recruitAmount} />,
},
{
title: '환수율(%)', dataIndex: 'chargebackRate', width: 90, align: 'right',
render: (_, r) => `${r.chargebackRate}%`,
},
{
title: '예상환수액', dataIndex: 'estimatedChargeback', width: 130, align: 'right',
render: (_, r) => <MoneyText value={r.estimatedChargeback} />,
},
{
title: '위험등급', dataIndex: 'riskLevel', width: 90, align: 'center', fixed: 'right',
render: (_, r) => (
<span style={{ color: riskColor[r.riskLevel], fontWeight: 600 }}>
{riskLabel[r.riskLevel] ?? r.riskLevel}
</span>
),
},
];
export default function ChargebackRiskReport() {
const { data, isLoading, isError } = useQuery({
queryKey: ['report', 'chargeback-risk'],
queryFn: fetchRisk,
retry: false,
});
return (
<PageContainer
title="환수위험 리포트"
description="경과월별 환수 가능성 분석"
extra={<ExcelExportButton url="/api/report/chargeback-risk/export" fileName="환수위험리포트" />}
>
{isError && (
<Alert
type="warning"
showIcon
message="백엔드 미구현 — 곧 추가됩니다"
description="/api/report/chargeback-risk API가 아직 준비되지 않았습니다."
style={{ marginBottom: 16 }}
/>
)}
<ProTable<RiskRow>
rowKey={(r) => `${r.agentId}-${r.policyNo}`}
columns={columns}
dataSource={data ?? []}
loading={isLoading}
search={false}
pagination={{ pageSize: 50, showTotal: (t) => `${t}` }}
options={{ density: false, reload: false }}
scroll={{ x: 1100 }}
/>
</PageContainer>
);
}
@@ -0,0 +1,98 @@
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 OrgRow {
orgId: number;
orgName: string;
orgType: string;
agentCount: number;
recruitTotal: number;
maintainTotal: number;
netAmount: number;
}
function fetchOrgReport(settleMonth: string) {
return unwrap<OrgRow[]>(api.get('/api/report/org', { params: { settleMonth } }));
}
const columns: ProColumns<OrgRow>[] = [
{ title: '조직명', dataIndex: 'orgName', width: 180, fixed: 'left' },
{ title: '유형', dataIndex: 'orgType', width: 80 },
{ title: '설계사 수', dataIndex: 'agentCount', width: 90, align: 'right' },
{
title: '모집수수료', dataIndex: 'recruitTotal', width: 130, align: 'right',
render: (_, r) => <MoneyText value={r.recruitTotal} />,
},
{
title: '유지수수료', dataIndex: 'maintainTotal', width: 130, align: 'right',
render: (_, r) => <MoneyText value={r.maintainTotal} />,
},
{
title: '실지급 합계', dataIndex: 'netAmount', width: 140, align: 'right',
render: (_, r) => <strong><MoneyText value={r.netAmount} /></strong>,
},
];
export default function OrgReport() {
const [settleMonth, setSettleMonth] = useState<Dayjs>(dayjs());
const monthStr = settleMonth.format('YYYYMM');
const { data, isLoading, isError } = useQuery({
queryKey: ['report', 'org', monthStr],
queryFn: () => fetchOrgReport(monthStr),
retry: false,
});
return (
<PageContainer
title="조직별 리포트"
description="조직 단위 정산 집계"
extra={
<ExcelExportButton
url="/api/report/org/export"
fileName="조직별리포트"
params={{ settleMonth: monthStr }}
/>
}
>
{isError && (
<Alert
type="warning"
showIcon
message="백엔드 미구현 — 곧 추가됩니다"
description="/api/report/org 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<OrgRow>
rowKey="orgId"
columns={columns}
dataSource={data ?? []}
loading={isLoading}
search={false}
pagination={{ pageSize: 50, showTotal: (t) => `${t}` }}
options={{ density: false, reload: false }}
scroll={{ x: 800 }}
/>
</PageContainer>
);
}
@@ -0,0 +1,96 @@
import { useState } from 'react';
import { Alert, Col, Empty, Row, Spin } from 'antd';
import { ProCard } from '@ant-design/pro-components';
import { useQuery } from '@tanstack/react-query';
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
} from 'recharts';
import dayjs from 'dayjs';
import PageContainer from '@/components/common/PageContainer';
import ExcelExportButton from '@/components/common/ExcelExportButton';
import api, { unwrap } from '@/api/request';
interface PerfRow {
settleMonth: string;
agentCount: number;
recruitTotal: number;
maintainTotal: number;
netAmount: number;
}
function fetchPerf(year: string) {
return unwrap<PerfRow[]>(api.get('/api/report/performance', { params: { year } }));
}
export default function PerformanceReport() {
const [year] = useState(() => dayjs().format('YYYY'));
const { data, isLoading, isError } = useQuery({
queryKey: ['report', 'performance', year],
queryFn: () => fetchPerf(year),
retry: false,
});
return (
<PageContainer
title="실적 리포트"
description="월별 수수료 집계 — 모집/유지/실지급"
extra={<ExcelExportButton url="/api/report/performance/export" fileName="실적리포트" params={{ year }} />}
>
{isError && (
<Alert
type="warning"
showIcon
message="백엔드 미구현 — 곧 추가됩니다"
description="/api/report/performance API가 아직 준비되지 않았습니다."
style={{ marginBottom: 16 }}
/>
)}
<ProCard bordered style={{ marginBottom: 16 }}>
<Spin spinning={isLoading}>
{data && data.length > 0 ? (
<ResponsiveContainer width="100%" height={340}>
<BarChart data={data} margin={{ top: 16, right: 24, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="settleMonth" />
<YAxis tickFormatter={(v: number | string) => `${(Number(v) / 1_000_000).toFixed(0)}백만`} />
<Tooltip formatter={(v: number | string) => Number(v).toLocaleString()} />
<Legend />
<Bar dataKey="recruitTotal" name="모집수수료" fill="#1677ff" />
<Bar dataKey="maintainTotal" name="유지수수료" fill="#52c41a" />
<Bar dataKey="netAmount" name="실지급" fill="#faad14" />
</BarChart>
</ResponsiveContainer>
) : (
<Empty description={isLoading ? '로딩 중...' : '데이터 없음'} style={{ padding: 40 }} />
)}
</Spin>
</ProCard>
{data && data.length > 0 && (
<ProCard bordered>
<Row gutter={16}>
{data.map((row) => (
<Col key={row.settleMonth} xs={24} sm={12} md={8} lg={6} xl={4}>
<ProCard size="small" bordered style={{ marginBottom: 8 }}>
<div style={{ fontWeight: 600, marginBottom: 4 }}>{row.settleMonth}</div>
<div style={{ fontSize: 12, color: '#666' }}>: {row.agentCount}</div>
<div style={{ fontSize: 12, color: '#1677ff' }}>
: {(row.recruitTotal ?? 0).toLocaleString()}
</div>
<div style={{ fontSize: 12, color: '#52c41a' }}>
: {(row.maintainTotal ?? 0).toLocaleString()}
</div>
<div style={{ fontSize: 12, fontWeight: 600 }}>
: {(row.netAmount ?? 0).toLocaleString()}
</div>
</ProCard>
</Col>
))}
</Row>
</ProCard>
)}
</PageContainer>
);
}