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,24 +1,23 @@
|
||||
import { useState } from 'react';
|
||||
import { Layout, Menu, Dropdown, Badge, Space, Avatar, Button, DatePicker } from 'antd';
|
||||
import { Layout, Menu, Dropdown, Badge, Space, Avatar, Button, DatePicker, theme } from 'antd';
|
||||
import { useNavigate, Outlet, useLocation } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
IconMenu2,
|
||||
IconBell,
|
||||
IconUser,
|
||||
IconLogout,
|
||||
IconMenu2, IconBell, IconUser, IconLogout, IconChevronDown,
|
||||
} from '@tabler/icons-react';
|
||||
import dayjs from 'dayjs';
|
||||
import { menuApi, MenuResp } from '@/api/menu';
|
||||
import { authApi } from '@/api/auth';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { getMenuIcon } from '@/components/common/MenuIcon';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
const { Header, Sider, Content, Footer } = Layout;
|
||||
|
||||
function toMenuItems(menus: MenuResp[]): any[] {
|
||||
return menus.map((m) => ({
|
||||
key: m.menuPath ?? m.menuCode,
|
||||
label: m.menuName,
|
||||
icon: getMenuIcon(m.menuCode, m.menuPath),
|
||||
children: m.children?.length ? toMenuItems(m.children) : undefined,
|
||||
}));
|
||||
}
|
||||
@@ -27,6 +26,7 @@ export default function MainLayout() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const auth = useAuthStore();
|
||||
const { token } = theme.useToken();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [settleMonth, setSettleMonth] = useState(dayjs());
|
||||
|
||||
@@ -45,82 +45,180 @@ export default function MainLayout() {
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Header
|
||||
<Sider
|
||||
width={220}
|
||||
collapsedWidth={64}
|
||||
collapsed={collapsed}
|
||||
theme="light"
|
||||
style={{
|
||||
height: 48,
|
||||
lineHeight: '48px',
|
||||
borderRight: '1px solid #f0f0f0',
|
||||
background: '#ffffff',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
padding: '0 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: 'var(--ga-shadow-card)',
|
||||
boxShadow: '2px 0 8px rgba(0,0,0,0.04)',
|
||||
overflow: 'auto',
|
||||
height: '100vh',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
>
|
||||
<Space size={12}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<IconMenu2 size={18} />}
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
/>
|
||||
<span style={{ fontSize: 15, fontWeight: 600, color: '#185FA5' }}>
|
||||
GA 정산시스템
|
||||
</span>
|
||||
</Space>
|
||||
<Space size={12}>
|
||||
<DatePicker
|
||||
picker="month"
|
||||
value={settleMonth}
|
||||
onChange={(d) => d && setSettleMonth(d)}
|
||||
allowClear={false}
|
||||
format="YYYY-MM"
|
||||
size="small"
|
||||
/>
|
||||
<Badge count={0} size="small">
|
||||
<Button type="text" icon={<IconBell size={18} />} />
|
||||
</Badge>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'logout',
|
||||
label: '로그아웃',
|
||||
icon: <IconLogout size={14} />,
|
||||
onClick: handleLogout,
|
||||
},
|
||||
],
|
||||
{/* 로고 영역 */}
|
||||
<div
|
||||
style={{
|
||||
height: 56,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
padding: collapsed ? 0 : '0 16px',
|
||||
background: 'linear-gradient(135deg, #1677FF 0%, #0C447C 100%)',
|
||||
color: '#fff',
|
||||
gap: 10,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.3,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 32, height: 32, borderRadius: 8,
|
||||
background: 'rgba(255,255,255,0.18)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 16, fontWeight: 800,
|
||||
}}
|
||||
>GA</div>
|
||||
{!collapsed && <span style={{ fontSize: 14 }}>정산 시스템</span>}
|
||||
</div>
|
||||
|
||||
<Menu
|
||||
mode="inline"
|
||||
items={toMenuItems(menus)}
|
||||
selectedKeys={[location.pathname]}
|
||||
onClick={({ key }) => navigate(String(key))}
|
||||
style={{ borderRight: 0, paddingTop: 8, paddingBottom: 8 }}
|
||||
/>
|
||||
|
||||
{!collapsed && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute', bottom: 0, left: 0, right: 0,
|
||||
padding: '12px 16px',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
background: '#fafafa',
|
||||
fontSize: 11, color: '#999',
|
||||
}}
|
||||
>
|
||||
<Space style={{ cursor: 'pointer' }}>
|
||||
<Avatar size="small" icon={<IconUser size={14} />} />
|
||||
<span style={{ fontSize: 13 }}>{auth.userName ?? auth.loginId}</span>
|
||||
</Space>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Header>
|
||||
v1.0.0-alpha · trading_ai
|
||||
</div>
|
||||
)}
|
||||
</Sider>
|
||||
|
||||
<Layout>
|
||||
<Sider
|
||||
width={220}
|
||||
collapsedWidth={64}
|
||||
collapsed={collapsed}
|
||||
theme="light"
|
||||
style={{ borderRight: '1px solid #f0f0f0' }}
|
||||
<Header
|
||||
style={{
|
||||
height: 48,
|
||||
lineHeight: '48px',
|
||||
background: '#ffffff',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
padding: '0 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
mode="inline"
|
||||
items={toMenuItems(menus)}
|
||||
selectedKeys={[location.pathname]}
|
||||
onClick={({ key }) => navigate(String(key))}
|
||||
style={{ borderRight: 0, height: '100%', paddingTop: 8 }}
|
||||
/>
|
||||
</Sider>
|
||||
<Space size={8}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<IconMenu2 size={18} />}
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13, fontWeight: 500, color: '#666',
|
||||
paddingLeft: 8, borderLeft: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
{breadcrumbOf(location.pathname)}
|
||||
</span>
|
||||
</Space>
|
||||
<Space size={12}>
|
||||
<span style={{ fontSize: 12, color: '#999' }}>정산월</span>
|
||||
<DatePicker
|
||||
picker="month"
|
||||
value={settleMonth}
|
||||
onChange={(d) => d && setSettleMonth(d)}
|
||||
allowClear={false}
|
||||
format="YYYY-MM"
|
||||
size="small"
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
<Badge count={3} size="small" offset={[-2, 4]}>
|
||||
<Button type="text" icon={<IconBell size={18} />} />
|
||||
</Badge>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'logout',
|
||||
label: '로그아웃',
|
||||
icon: <IconLogout size={14} />,
|
||||
onClick: handleLogout,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Space
|
||||
style={{
|
||||
cursor: 'pointer', padding: '4px 8px', borderRadius: 6,
|
||||
background: token.colorBgLayout,
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
size={26}
|
||||
style={{ background: 'linear-gradient(135deg, #1677FF 0%, #185FA5 100%)' }}
|
||||
icon={<IconUser size={14} />}
|
||||
/>
|
||||
<span style={{ fontSize: 13, fontWeight: 500 }}>
|
||||
{auth.userName ?? auth.loginId ?? '관리자'}
|
||||
</span>
|
||||
<IconChevronDown size={12} color="#999" />
|
||||
</Space>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Header>
|
||||
<Content style={{ padding: 'var(--ga-content-padding)', overflow: 'auto' }}>
|
||||
<div className="ga-page-container">
|
||||
<Outlet />
|
||||
</div>
|
||||
</Content>
|
||||
<Footer
|
||||
style={{
|
||||
textAlign: 'center', fontSize: 11, color: '#bbb',
|
||||
padding: '8px 0', background: 'transparent',
|
||||
}}
|
||||
>
|
||||
© GA Commission System · trading_ai · admin@local
|
||||
</Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
/** 경로 → 한글 breadcrumb 라벨 */
|
||||
function breadcrumbOf(pathname: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'/': '대시보드',
|
||||
'/org/agents': '조직 / 설계사',
|
||||
'/contracts': '계약 관리',
|
||||
'/ledger/recruit': '원장 / 모집수수료',
|
||||
'/ledger/exception': '원장 / 예외금액',
|
||||
'/settle': '정산 / 결과',
|
||||
'/settle/batch': '정산 / 배치 실행',
|
||||
'/payments': '지급 관리',
|
||||
'/system/users': '시스템 / 사용자',
|
||||
'/system/roles': '시스템 / 역할 권한',
|
||||
'/system/codes': '시스템 / 공통 코드',
|
||||
};
|
||||
return map[pathname] ?? '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user