2026-05-10 22:09:17 +09:00
|
|
|
import { Card, Col, Row, Typography } from 'antd';
|
|
|
|
|
import {
|
|
|
|
|
IconCash, IconCheck, IconClock, IconAlertTriangle,
|
|
|
|
|
IconTrendingUp, IconTrendingDown,
|
|
|
|
|
} from '@tabler/icons-react';
|
|
|
|
|
import {
|
|
|
|
|
ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, Legend,
|
|
|
|
|
} from 'recharts';
|
|
|
|
|
import PageContainer from '@/components/common/PageContainer';
|
|
|
|
|
import MoneyText from '@/components/common/MoneyText';
|
2026-05-09 22:00:53 +09:00
|
|
|
|
2026-05-10 22:09:17 +09:00
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
|
|
|
|
const KPI_CARDS = [
|
|
|
|
|
{ key: 'recruit', label: '모집수수료', value: 124_500_000, delta: +8.2,
|
|
|
|
|
icon: <IconCash size={20} />, color: '#1677FF', bg: '#E6F1FB' },
|
|
|
|
|
{ key: 'maintain', label: '유지수수료', value: 82_300_000, delta: +3.1,
|
|
|
|
|
icon: <IconCheck size={20} />, color: '#0F6E56', bg: '#E1F5EE' },
|
|
|
|
|
{ key: 'pending', label: '확정 대기', value: 12, delta: -2.0, suffix: '건',
|
|
|
|
|
icon: <IconClock size={20} />, color: '#BA7517', bg: '#FAEEDA' },
|
|
|
|
|
{ key: 'alert', label: '대사 차이', value: 3, delta: +50.0, suffix: '건',
|
|
|
|
|
icon: <IconAlertTriangle size={20} />, color: '#E24B4A', bg: '#FCEBEB' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const MONTHLY_TREND = [
|
|
|
|
|
{ month: '12월', recruit: 95, maintain: 70, chargeback: 5 },
|
|
|
|
|
{ month: '1월', recruit: 105, maintain: 72, chargeback: 8 },
|
|
|
|
|
{ month: '2월', recruit: 110, maintain: 75, chargeback: 6 },
|
|
|
|
|
{ month: '3월', recruit: 118, maintain: 78, chargeback: 7 },
|
|
|
|
|
{ month: '4월', recruit: 122, maintain: 80, chargeback: 9 },
|
|
|
|
|
{ month: '5월', recruit: 125, maintain: 82, chargeback: 4 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const BATCH_STEPS = [
|
|
|
|
|
{ name: '데이터 수신', status: 'done', time: '02:14' },
|
|
|
|
|
{ name: '데이터 변환', status: 'done', time: '00:52' },
|
|
|
|
|
{ name: '대사 검증', status: 'done', time: '00:21' },
|
|
|
|
|
{ name: '모집수수료', status: 'done', time: '01:08' },
|
|
|
|
|
{ name: '유지수수료', status: 'done', time: '00:46' },
|
|
|
|
|
{ name: '환수/예외', status: 'progress', time: '00:33' },
|
|
|
|
|
{ name: '오버라이드', status: 'pending' },
|
|
|
|
|
{ name: '집계', status: 'pending' },
|
|
|
|
|
];
|
2026-05-09 22:00:53 +09:00
|
|
|
|
|
|
|
|
export default function Dashboard() {
|
|
|
|
|
return (
|
2026-05-10 22:09:17 +09:00
|
|
|
<PageContainer
|
|
|
|
|
title="대시보드"
|
|
|
|
|
description="이번 달 (2026-05) 정산 요약 / 실시간 배치 진행 / 알림"
|
|
|
|
|
>
|
|
|
|
|
{/* KPI 카드 4개 */}
|
|
|
|
|
<Row gutter={16} style={{ marginBottom: 16 }}>
|
|
|
|
|
{KPI_CARDS.map((c) => {
|
|
|
|
|
const isUp = c.delta >= 0;
|
|
|
|
|
return (
|
|
|
|
|
<Col span={6} key={c.key}>
|
|
|
|
|
<Card
|
|
|
|
|
bordered={false}
|
|
|
|
|
style={{
|
|
|
|
|
boxShadow: 'var(--ga-shadow-card)',
|
|
|
|
|
borderTop: `3px solid ${c.color}`,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
width: 44, height: 44, borderRadius: 10,
|
|
|
|
|
background: c.bg, color: c.color,
|
|
|
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{c.icon}
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ flex: 1 }}>
|
|
|
|
|
<div style={{ fontSize: 12, color: '#888' }}>{c.label}</div>
|
|
|
|
|
<div style={{ fontSize: 20, fontWeight: 600, color: '#222', marginTop: 2 }}>
|
|
|
|
|
{c.suffix === '건' ? c.value.toLocaleString() : (
|
|
|
|
|
<MoneyText value={c.value} />
|
|
|
|
|
)}
|
|
|
|
|
<span style={{ fontSize: 12, color: '#888', marginLeft: 4, fontWeight: 400 }}>
|
|
|
|
|
{c.suffix ?? '원'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: 11, marginTop: 4,
|
|
|
|
|
color: isUp ? '#0F6E56' : '#E24B4A',
|
|
|
|
|
display: 'flex', alignItems: 'center', gap: 2,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{isUp ? <IconTrendingUp size={12} /> : <IconTrendingDown size={12} />}
|
|
|
|
|
{Math.abs(c.delta).toFixed(1)}% 전월 대비
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
{/* 차트 + 배치 현황 */}
|
|
|
|
|
<Row gutter={16} style={{ marginBottom: 16 }}>
|
|
|
|
|
<Col span={16}>
|
|
|
|
|
<Card
|
|
|
|
|
title={<span style={{ fontWeight: 600 }}>월별 추이 (최근 6개월)</span>}
|
|
|
|
|
bordered={false}
|
|
|
|
|
style={{ boxShadow: 'var(--ga-shadow-card)' }}
|
|
|
|
|
styles={{ body: { padding: 16, height: 280 } }}
|
|
|
|
|
>
|
|
|
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
|
|
|
<BarChart data={MONTHLY_TREND}>
|
|
|
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" vertical={false} />
|
|
|
|
|
<XAxis dataKey="month" tick={{ fontSize: 12, fill: '#666' }} />
|
|
|
|
|
<YAxis tick={{ fontSize: 12, fill: '#666' }}
|
|
|
|
|
label={{ value: '백만원', angle: 0, position: 'top', fontSize: 11, fill: '#999' }} />
|
|
|
|
|
<Tooltip />
|
|
|
|
|
<Legend wrapperStyle={{ fontSize: 12 }} />
|
|
|
|
|
<Bar dataKey="recruit" name="모집" fill="#1677FF" radius={[4, 4, 0, 0]} />
|
|
|
|
|
<Bar dataKey="maintain" name="유지" fill="#0F6E56" radius={[4, 4, 0, 0]} />
|
|
|
|
|
<Bar dataKey="chargeback" name="환수" fill="#E24B4A" radius={[4, 4, 0, 0]} />
|
|
|
|
|
</BarChart>
|
|
|
|
|
</ResponsiveContainer>
|
|
|
|
|
</Card>
|
2026-05-09 22:00:53 +09:00
|
|
|
</Col>
|
2026-05-10 22:09:17 +09:00
|
|
|
<Col span={8}>
|
|
|
|
|
<Card
|
|
|
|
|
title={<span style={{ fontWeight: 600 }}>배치 진행 (5월)</span>}
|
|
|
|
|
bordered={false}
|
|
|
|
|
style={{ boxShadow: 'var(--ga-shadow-card)' }}
|
|
|
|
|
styles={{ body: { padding: 16 } }}
|
|
|
|
|
>
|
|
|
|
|
{BATCH_STEPS.map((s, i) => {
|
|
|
|
|
const dotColor = s.status === 'done' ? '#0F6E56' :
|
|
|
|
|
s.status === 'progress' ? '#1677FF' : '#ddd';
|
|
|
|
|
return (
|
|
|
|
|
<div key={i} style={{
|
|
|
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
|
|
|
padding: '6px 0', borderBottom: i < BATCH_STEPS.length - 1 ? '1px dashed #f5f5f5' : 'none',
|
|
|
|
|
}}>
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
|
|
|
<div style={{
|
|
|
|
|
width: 8, height: 8, borderRadius: '50%', background: dotColor,
|
|
|
|
|
boxShadow: s.status === 'progress' ? '0 0 0 4px rgba(22,119,255,0.15)' : 'none',
|
|
|
|
|
}} />
|
|
|
|
|
<span style={{ fontSize: 13, color: s.status === 'pending' ? '#bbb' : '#333' }}>
|
|
|
|
|
Step {i + 1}. {s.name}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span style={{ fontSize: 11, color: '#999' }}>{s.time ?? '-'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</Card>
|
2026-05-09 22:00:53 +09:00
|
|
|
</Col>
|
|
|
|
|
</Row>
|
2026-05-10 22:09:17 +09:00
|
|
|
|
|
|
|
|
{/* 알림 */}
|
|
|
|
|
<Card
|
|
|
|
|
title={<span style={{ fontWeight: 600 }}>최근 알림</span>}
|
|
|
|
|
bordered={false}
|
|
|
|
|
style={{ boxShadow: 'var(--ga-shadow-card)' }}
|
|
|
|
|
styles={{ body: { padding: 0 } }}
|
|
|
|
|
>
|
|
|
|
|
{[
|
|
|
|
|
{ tag: '대사불일치', tagColor: '#E24B4A', tagBg: '#FCEBEB',
|
|
|
|
|
text: '삼성생명 5월 통보액 vs 시스템 계산액 3건 차이', time: '10분 전' },
|
|
|
|
|
{ tag: '파싱에러', tagColor: '#BA7517', tagBg: '#FAEEDA',
|
|
|
|
|
text: '한화생명 raw 파일 12행 날짜 형식 오류 (parse_error_log #4521)', time: '1시간 전' },
|
|
|
|
|
{ tag: '승인대기', tagColor: '#1677FF', tagBg: '#E6F1FB',
|
|
|
|
|
text: '예외금액 원장 승인 대기 5건 (정산월 202605)', time: '3시간 전' },
|
|
|
|
|
].map((n, i) => (
|
|
|
|
|
<div key={i} style={{
|
|
|
|
|
display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px',
|
|
|
|
|
borderBottom: i < 2 ? '1px solid #f5f5f5' : 'none',
|
|
|
|
|
}}>
|
|
|
|
|
<span style={{
|
|
|
|
|
fontSize: 11, fontWeight: 500, padding: '2px 8px', borderRadius: 4,
|
|
|
|
|
color: n.tagColor, background: n.tagBg,
|
|
|
|
|
}}>{n.tag}</span>
|
|
|
|
|
<Text style={{ flex: 1, fontSize: 13 }}>{n.text}</Text>
|
|
|
|
|
<span style={{ fontSize: 11, color: '#999' }}>{n.time}</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</Card>
|
|
|
|
|
</PageContainer>
|
2026-05-09 22:00:53 +09:00
|
|
|
);
|
|
|
|
|
}
|