Files
ga-commission-system/ga-frontend/src/layouts/MainLayout.tsx
T

225 lines
7.1 KiB
TypeScript
Raw Normal View History

import { useState } from 'react';
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, 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, 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,
}));
}
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());
const { data: menus = [] } = useQuery({
queryKey: ['menu', 'my'],
queryFn: menuApi.myMenus,
});
const handleLogout = async () => {
try { await authApi.logout(); } catch { /* ignore */ }
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
auth.clear();
navigate('/login');
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider
width={220}
collapsedWidth={64}
collapsed={collapsed}
theme="light"
style={{
borderRight: '1px solid #f0f0f0',
background: '#ffffff',
boxShadow: '2px 0 8px rgba(0,0,0,0.04)',
overflow: 'auto',
height: '100vh',
position: 'sticky',
top: 0,
left: 0,
}}
>
{/* 로고 영역 */}
<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',
}}
>
v1.0.0-alpha · trading_ai
</div>
)}
</Sider>
<Layout>
<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,
}}
>
<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] ?? '';
}