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:
@@ -8,36 +8,64 @@ interface Props {
|
||||
|
||||
/**
|
||||
* 09_UI_DESIGN.md §5 CodeBadge — 4단계 색상 (success/info/warning/danger/default).
|
||||
* antd Tag 의 preset 컬러 사용: success(초록) / processing(파랑) / warning(주황) / error(빨강) / default(회색).
|
||||
* antd preset 대신 직접 컬러 + 옅은 배경으로 명확한 시각 구분.
|
||||
*/
|
||||
const COLOR_MAP: Record<string, string> = {
|
||||
type Tone = 'success' | 'info' | 'warning' | 'danger' | 'default';
|
||||
|
||||
const TONE_MAP: Record<string, Tone> = {
|
||||
// 초록 — 긍정적 완료
|
||||
CONFIRMED: 'success', APPROVED: 'success', ACTIVE: 'success',
|
||||
PAID: 'success', COMPLETED: 'success', MATCH: 'success',
|
||||
MATCHED: 'success', DONE: 'success', SUCCESS: 'success',
|
||||
|
||||
// 파랑 — 진행 중
|
||||
CALCULATED: 'processing', PENDING: 'processing', PROCESSING: 'processing',
|
||||
REVIEWED: 'processing', NEW: 'processing', SENT: 'processing',
|
||||
CALCULATED: 'info', PENDING: 'info', PROCESSING: 'info',
|
||||
REVIEWED: 'info', NEW: 'info', SENT: 'info',
|
||||
|
||||
// 주황 — 주의
|
||||
HOLD: 'warning', SUSPEND: 'warning', MISMATCH: 'warning',
|
||||
PARTIAL: 'warning', DIFF: 'warning',
|
||||
|
||||
// 빨강 — 부정적
|
||||
REJECTED: 'error', LEAVE: 'error', CANCEL: 'error',
|
||||
FAILED: 'error', FAIL: 'error', LAPSED: 'error',
|
||||
LAPSE: 'error', LOCKED: 'error',
|
||||
REJECTED: 'danger', LEAVE: 'danger', CANCEL: 'danger',
|
||||
FAILED: 'danger', FAIL: 'danger', LAPSED: 'danger',
|
||||
LAPSE: 'danger', LOCKED: 'danger',
|
||||
|
||||
// 회색 — 기본
|
||||
DRAFT: 'default', READY: 'default', RETIRED: 'default',
|
||||
};
|
||||
|
||||
const STYLE_MAP: Record<Tone, { color: string; bg: string; border: string }> = {
|
||||
success: { color: '#0F6E56', bg: '#E1F5EE', border: '#B9E5D5' },
|
||||
info: { color: '#185FA5', bg: '#E6F1FB', border: '#BFDDF6' },
|
||||
warning: { color: '#BA7517', bg: '#FAEEDA', border: '#F1D9A8' },
|
||||
danger: { color: '#E24B4A', bg: '#FCEBEB', border: '#F4C8C8' },
|
||||
default: { color: '#666666', bg: '#F5F5F5', border: '#DDDDDD' },
|
||||
};
|
||||
|
||||
export default function CodeBadge({ groupCode, value }: Props) {
|
||||
const { data = [] } = useCommonCodes(groupCode);
|
||||
if (!value) return null;
|
||||
const code = data.find((c) => c.code === value);
|
||||
const label = code?.codeName ?? value;
|
||||
const color = COLOR_MAP[value] ?? 'default';
|
||||
return <Tag color={color}>{label}</Tag>;
|
||||
const tone = TONE_MAP[value] ?? 'default';
|
||||
const s = STYLE_MAP[tone];
|
||||
return (
|
||||
<Tag
|
||||
style={{
|
||||
color: s.color,
|
||||
background: s.bg,
|
||||
border: `1px solid ${s.border}`,
|
||||
margin: 0,
|
||||
padding: '0 8px',
|
||||
height: 22,
|
||||
lineHeight: '20px',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
IconLayoutDashboard, IconUsersGroup, IconUser, IconBriefcase,
|
||||
IconReceipt, IconFileInvoice, IconCalculator, IconCash,
|
||||
IconReportMoney, IconSettings, IconUsers, IconShieldLock,
|
||||
IconCategory, IconKey, IconClipboardList, IconFolder,
|
||||
IconHome, IconChartBar, IconDatabase, IconBuildingBank,
|
||||
IconFileSpreadsheet, IconTransform, IconAlertTriangle,
|
||||
} from '@tabler/icons-react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
* 메뉴 코드 / 경로 → Tabler 아이콘 매핑.
|
||||
* 09_UI_DESIGN.md 의 "사이드 메뉴 좌측 아이콘 16px" 규칙.
|
||||
*/
|
||||
const ICON_MAP: Record<string, ReactNode> = {
|
||||
// 최상위 카테고리
|
||||
DASHBOARD: <IconLayoutDashboard size={16} />,
|
||||
ORG: <IconUsersGroup size={16} />,
|
||||
ORG_AGENT: <IconUser size={16} />,
|
||||
CONTRACT: <IconBriefcase size={16} />,
|
||||
RULE: <IconReceipt size={16} />,
|
||||
RECEIVE: <IconDatabase size={16} />,
|
||||
LEDGER: <IconFileInvoice size={16} />,
|
||||
LEDGER_RECRUIT: <IconFileInvoice size={16} />,
|
||||
LEDGER_MAINTAIN: <IconFileInvoice size={16} />,
|
||||
LEDGER_EXCEPTION: <IconAlertTriangle size={16} />,
|
||||
SETTLE: <IconCalculator size={16} />,
|
||||
SETTLE_LIST: <IconCalculator size={16} />,
|
||||
SETTLE_BATCH: <IconTransform size={16} />,
|
||||
PAYMENT: <IconCash size={16} />,
|
||||
REPORT: <IconChartBar size={16} />,
|
||||
SYSTEM: <IconSettings size={16} />,
|
||||
SYSTEM_USER: <IconUsers size={16} />,
|
||||
SYSTEM_ROLE: <IconShieldLock size={16} />,
|
||||
SYSTEM_MENU: <IconCategory size={16} />,
|
||||
SYSTEM_CODE: <IconKey size={16} />,
|
||||
SYSTEM_LOG: <IconClipboardList size={16} />,
|
||||
SYSTEM_FILE: <IconFolder size={16} />,
|
||||
SYSTEM_CONFIG: <IconSettings size={16} />,
|
||||
COMPANY: <IconBuildingBank size={16} />,
|
||||
PRODUCT: <IconFileSpreadsheet size={16} />,
|
||||
RECON: <IconReportMoney size={16} />,
|
||||
};
|
||||
|
||||
const PATH_MAP: Record<string, ReactNode> = {
|
||||
'/': <IconHome size={16} />,
|
||||
'/org/agents': <IconUser size={16} />,
|
||||
'/contracts': <IconBriefcase size={16} />,
|
||||
'/ledger/recruit': <IconFileInvoice size={16} />,
|
||||
'/ledger/exception': <IconAlertTriangle size={16} />,
|
||||
'/settle': <IconCalculator size={16} />,
|
||||
'/settle/batch': <IconTransform size={16} />,
|
||||
'/payments': <IconCash size={16} />,
|
||||
'/system/users': <IconUsers size={16} />,
|
||||
'/system/roles': <IconShieldLock size={16} />,
|
||||
'/system/codes': <IconKey size={16} />,
|
||||
};
|
||||
|
||||
export function getMenuIcon(code?: string, path?: string): ReactNode {
|
||||
if (path && PATH_MAP[path]) return PATH_MAP[path];
|
||||
if (code && ICON_MAP[code]) return ICON_MAP[code];
|
||||
return <IconCategory size={16} />;
|
||||
}
|
||||
@@ -5,19 +5,58 @@ const { Title } = Typography;
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
extra?: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function PageContainer({ title, extra, children }: Props) {
|
||||
/**
|
||||
* 페이지 표준 컨테이너.
|
||||
* - 상단: 제목 + 좌측 컬러 바 + 우측 액션
|
||||
* - 본문: 흰 카드 + 그림자
|
||||
* 09_UI_DESIGN.md §4 타입 A 의 시각적 보강 버전.
|
||||
*/
|
||||
export default function PageContainer({ title, description, extra, children }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
marginBottom: 16 }}>
|
||||
<Title level={4} style={{ margin: 0 }}>{title}</Title>
|
||||
{extra}
|
||||
{/* 페이지 제목 영역 */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '16px 20px',
|
||||
background: '#ffffff',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
boxShadow: 'var(--ga-shadow-card)',
|
||||
borderLeft: '4px solid var(--ga-primary)',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Title level={4} style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
|
||||
{title}
|
||||
</Title>
|
||||
{description && (
|
||||
<div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{extra && <div>{extra}</div>}
|
||||
</div>
|
||||
|
||||
{/* 본문 카드 */}
|
||||
<div
|
||||
style={{
|
||||
background: '#ffffff',
|
||||
borderRadius: 8,
|
||||
padding: 20,
|
||||
boxShadow: 'var(--ga-shadow-card)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,13 @@ export default function SearchForm({ conditions, initialValues, onSearch, onRese
|
||||
layout="vertical"
|
||||
initialValues={initialValues}
|
||||
onFinish={handleSearch}
|
||||
style={{ background: '#fafafa', padding: '12px 16px', marginBottom: 16, borderRadius: 8 }}
|
||||
style={{
|
||||
background: 'linear-gradient(180deg, #FAFBFC 0%, #F5F7FA 100%)',
|
||||
padding: '14px 16px 6px',
|
||||
marginBottom: 16,
|
||||
borderRadius: 8,
|
||||
border: '1px solid #EEF1F5',
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
{conditions.map((c) => (
|
||||
|
||||
Reference in New Issue
Block a user