feat: UI 시각 강화 + 메뉴 트리 구조 + Redis 폴백
UI 시각 강화 (사용자 피드백 — flat 미니멀이 빈약하게 보임):
- MainLayout: 사이드바 로고 영역(그라디언트 #1677FF→#0C447C),
메뉴 아이콘(Tabler), 푸터, 헤더 breadcrumb + 정산월 라벨,
사용자 아바타 그라디언트
- PageContainer: 흰 카드 + 좌측 4px 컬러 바 + 그림자, 본문 카드 분리
- Dashboard: KPI 카드 4종(아이콘+컬러+델타), 월별 추이 BarChart(recharts),
배치 8 Step 진행 상태(pulse), 알림 3종 (대사/파싱/승인)
- CodeBadge: antd preset → 직접 색상 (#0F6E56/#185FA5/#BA7517/#E24B4A
+ 옅은 배경 + 진한 보더, 진한 톤)
- SearchForm: 그라디언트 배경 + 보더 + 패딩 조정
- MenuIcon 신규: 메뉴 코드/경로 → Tabler 아이콘 매핑
메뉴 트리 구조:
- V18 마이그레이션: 25개 평면 PAGE → 9 roots (1 PAGE + 8 DIRECTORY)
· 조직/인사, 상품/계약, 수수료규정, 데이터수신,
원장관리, 정산/지급, 리포트, 시스템관리
- 자식 메뉴 sort_order 1~6 정리, DASHBOARD sort_order 10
- 모든 역할에 GRP_* READ 권한 자동 부여
Redis 폴백 (로컬 검증용):
- RedisConfig @ConditionalOnBean(RedisConnectionFactory.class)
- SimpleCacheConfig 신규: ConcurrentMapCacheManager 폴백
(RedisAutoConfiguration 제외 시 활성)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,188 @@
|
||||
import { Card, Col, Row, Statistic, Typography } from 'antd';
|
||||
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';
|
||||
|
||||
const { Title } = Typography;
|
||||
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' },
|
||||
];
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<>
|
||||
<Title level={3}>대시보드</Title>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Card><Statistic title="이번 달 정산 건수" value={0} suffix="건" /></Card>
|
||||
<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>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card><Statistic title="확정 대기" value={0} suffix="건" /></Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card><Statistic title="지급 완료" value={0} suffix="건" /></Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card><Statistic title="대사 차이" value={0} suffix="건" valueStyle={{ color: '#cf1322' }} /></Card>
|
||||
<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>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
|
||||
{/* 알림 */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user