동기화
This commit is contained in:
@@ -1,38 +1,58 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ProLayout, type MenuDataItem } from '@ant-design/pro-components';
|
||||
import { Badge, Button, DatePicker, Dropdown, Space } from 'antd';
|
||||
import { Avatar, Button, DatePicker, Dropdown, Space, Typography } from 'antd';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { IconBell, IconLogout, IconUser } from '@tabler/icons-react';
|
||||
import { IconChevronDown, IconLogout, IconSettings, IconUser } from '@tabler/icons-react';
|
||||
import dayjs from 'dayjs';
|
||||
import { menuApi, type MenuResp } from '@/api/menu';
|
||||
import { authApi } from '@/api/auth';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { getMenuIcon } from '@/components/common/MenuIcon';
|
||||
import ErrorBoundary from '@/components/common/ErrorBoundary';
|
||||
import NotificationBell from '@/components/biz/NotificationBell';
|
||||
import {
|
||||
COLOR_PRIMARY, COLOR_PRIMARY_LIGHT, GRAY, LAYOUT,
|
||||
} from '@/theme/tokens';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const COLLAPSE_KEY = 'ga-sider-collapsed';
|
||||
|
||||
/** API 메뉴 트리 → ProLayout MenuDataItem */
|
||||
function toProMenu(menus: MenuResp[]): MenuDataItem[] {
|
||||
return menus.map((m) => {
|
||||
const item: MenuDataItem = {
|
||||
name: m.menuName,
|
||||
path: m.menuPath ?? `/_grp/${m.menuCode}`,
|
||||
key: m.menuCode,
|
||||
icon: getMenuIcon(m.menuCode, m.menuPath),
|
||||
name: m.menuName,
|
||||
path: m.menuPath ?? `/_grp/${m.menuCode}`,
|
||||
key: m.menuCode,
|
||||
icon: getMenuIcon(m.menuCode, m.menuPath),
|
||||
};
|
||||
if (m.children?.length) item.children = toProMenu(m.children);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/** 로컬스토리지에서 collapse 상태 읽기 */
|
||||
function readCollapsed(): boolean {
|
||||
try { return localStorage.getItem(COLLAPSE_KEY) === 'true'; } catch { return false; }
|
||||
}
|
||||
|
||||
export default function MainLayout() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const auth = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const auth = useAuthStore();
|
||||
const [settleMonth, setSettleMonth] = useState(dayjs());
|
||||
const [collapsed, setCollapsed] = useState(readCollapsed);
|
||||
|
||||
// collapse 상태 localStorage 저장
|
||||
useEffect(() => {
|
||||
try { localStorage.setItem(COLLAPSE_KEY, String(collapsed)); } catch { /* ignore */ }
|
||||
}, [collapsed]);
|
||||
|
||||
const { data: menus = [] } = useQuery({
|
||||
queryKey: ['menu', 'my'],
|
||||
queryFn: menuApi.myMenus,
|
||||
queryFn: menuApi.myMenus,
|
||||
});
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -43,21 +63,62 @@ export default function MainLayout() {
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const displayName = auth.userName ?? auth.loginId ?? '관리자';
|
||||
const roleLabel = auth.roles?.[0] ?? 'USER';
|
||||
|
||||
const userMenuItems = [
|
||||
{
|
||||
key: 'profile',
|
||||
label: '내 프로필',
|
||||
icon: <IconUser size={14} />,
|
||||
onClick: () => navigate('/system/users'),
|
||||
},
|
||||
{
|
||||
key: 'config',
|
||||
label: '시스템 설정',
|
||||
icon: <IconSettings size={14} />,
|
||||
onClick: () => navigate('/system/config'),
|
||||
},
|
||||
{ type: 'divider' as const },
|
||||
{
|
||||
key: 'logout',
|
||||
label: '로그아웃',
|
||||
icon: <IconLogout size={14} />,
|
||||
danger: true,
|
||||
onClick: handleLogout,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ProLayout
|
||||
title="GA 정산시스템"
|
||||
logo="/vite.svg"
|
||||
logo={
|
||||
<div style={{
|
||||
width: 32, height: 32, borderRadius: 8,
|
||||
background: COLOR_PRIMARY,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: '#fff', fontWeight: 700, fontSize: 14, letterSpacing: -0.5,
|
||||
}}>
|
||||
GA
|
||||
</div>
|
||||
}
|
||||
layout="mix"
|
||||
navTheme="light"
|
||||
contentWidth="Fluid"
|
||||
fixedHeader
|
||||
fixSiderbar
|
||||
siderWidth={220}
|
||||
siderWidth={LAYOUT.siderWidth}
|
||||
collapsed={collapsed}
|
||||
onCollapse={setCollapsed}
|
||||
menu={{ locale: false }}
|
||||
menuDataRender={() => toProMenu(menus)}
|
||||
location={{ pathname: location.pathname }}
|
||||
menuItemRender={(item, dom) => (
|
||||
<div onClick={() => item.path && !item.path.startsWith('/_grp/') && navigate(item.path)}>
|
||||
<div
|
||||
onClick={() =>
|
||||
item.path && !item.path.startsWith('/_grp/') && navigate(item.path)
|
||||
}
|
||||
>
|
||||
{dom}
|
||||
</div>
|
||||
)}
|
||||
@@ -65,21 +126,27 @@ export default function MainLayout() {
|
||||
onMenuHeaderClick={() => navigate('/')}
|
||||
token={{
|
||||
sider: {
|
||||
colorMenuBackground: '#ffffff',
|
||||
colorTextMenuSelected: '#185FA5',
|
||||
colorBgMenuItemSelected: '#E6F1FB',
|
||||
colorTextMenuItemHover: '#1677FF',
|
||||
colorMenuBackground: '#ffffff',
|
||||
colorTextMenuSelected: COLOR_PRIMARY,
|
||||
colorBgMenuItemSelected: COLOR_PRIMARY_LIGHT,
|
||||
colorTextMenuItemHover: GRAY[800],
|
||||
colorTextMenu: GRAY[600],
|
||||
colorTextMenuSecondary: GRAY[500],
|
||||
colorTextSubMenuSelected: COLOR_PRIMARY,
|
||||
},
|
||||
header: {
|
||||
colorBgHeader: '#ffffff',
|
||||
colorHeaderTitle: '#185FA5',
|
||||
heightLayoutHeader: 56,
|
||||
colorBgHeader: '#ffffff',
|
||||
colorHeaderTitle: GRAY[800],
|
||||
heightLayoutHeader: LAYOUT.headerHeight,
|
||||
colorBgRightActionsItemHover: GRAY[100],
|
||||
},
|
||||
bgLayout: '#f5f7fa',
|
||||
bgLayout: GRAY[50],
|
||||
colorTextAppListIconHover: COLOR_PRIMARY,
|
||||
}}
|
||||
actionsRender={() => [
|
||||
<Space key="month" size={4} align="center">
|
||||
<span style={{ fontSize: 12, color: '#666' }}>정산월</span>
|
||||
// 정산월 선택
|
||||
<Space key="month" size={6} align="center" style={{ marginRight: 4 }}>
|
||||
<Text style={{ fontSize: 12, color: GRAY[500], whiteSpace: 'nowrap' }}>정산월</Text>
|
||||
<DatePicker
|
||||
picker="month"
|
||||
value={settleMonth}
|
||||
@@ -87,36 +154,66 @@ export default function MainLayout() {
|
||||
allowClear={false}
|
||||
format="YYYY-MM"
|
||||
size="small"
|
||||
style={{ width: 100 }}
|
||||
style={{ width: 96 }}
|
||||
/>
|
||||
</Space>,
|
||||
<Badge key="bell" count={3} size="small" offset={[-2, 4]}>
|
||||
<Button type="text" icon={<IconBell size={18} />} />
|
||||
</Badge>,
|
||||
|
||||
// 알림 벨 (NotificationBell — unread-count API 연동)
|
||||
<NotificationBell key="bell" />,
|
||||
]}
|
||||
avatarProps={{
|
||||
size: 'small',
|
||||
title: auth.userName ?? auth.loginId ?? '관리자',
|
||||
icon: <IconUser size={14} />,
|
||||
size: 'small',
|
||||
style: {
|
||||
background: COLOR_PRIMARY,
|
||||
color: '#fff',
|
||||
fontWeight: 600,
|
||||
fontSize: 12,
|
||||
},
|
||||
src: undefined,
|
||||
children: displayName.charAt(0),
|
||||
render: (_props, dom) => (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'logout', label: '로그아웃', icon: <IconLogout size={14} />, onClick: handleLogout },
|
||||
],
|
||||
}}
|
||||
menu={{ items: userMenuItems }}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
>
|
||||
{dom}
|
||||
<Space
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '4px 8px',
|
||||
borderRadius: 6,
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.background = GRAY[100];
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.background = 'transparent';
|
||||
}}
|
||||
>
|
||||
{dom}
|
||||
<div style={{ lineHeight: 1.2 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: GRAY[900] }}>{displayName}</div>
|
||||
<div style={{ fontSize: 11, color: GRAY[500] }}>{roleLabel}</div>
|
||||
</div>
|
||||
<IconChevronDown size={12} style={{ color: GRAY[400] }} />
|
||||
</Space>
|
||||
</Dropdown>
|
||||
),
|
||||
}}
|
||||
footerRender={() => (
|
||||
<div style={{ textAlign: 'center', padding: '8px 0', fontSize: 11, color: '#bbb' }}>
|
||||
© GA Commission System v1.0.0-alpha · trading_ai
|
||||
<div style={{
|
||||
textAlign: 'center', padding: '8px 0',
|
||||
fontSize: 11, color: GRAY[400],
|
||||
borderTop: `1px solid ${GRAY[200]}`,
|
||||
}}>
|
||||
GA Commission System v1.0.0-alpha
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Outlet />
|
||||
<ErrorBoundary>
|
||||
<Outlet />
|
||||
</ErrorBoundary>
|
||||
</ProLayout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user