feat(frontend): Ant Design Pro 패턴 전면 적용
@ant-design/pro-components ^2.8.10 + @ant-design/icons ^6.2 추가. ProLayout (MainLayout): - mix 레이아웃 (사이드바 + 헤더 통합), navTheme=light - 자체 메뉴 트리 렌더링 (children 트리), 동적 메뉴 매핑 - 헤더 actionsRender: 정산월 MonthPicker / 알림 뱃지 - avatarProps: 그라디언트 아바타 + Dropdown 로그아웃 - token: 사이드바/헤더 컬러, pageContainer padding - footerRender: 버전 표기 PageContainer (Pro): - @ant-design/pro-layout 의 PageContainer 사용 - 자동 Breadcrumb / Title / extra 슬롯 ProTable 마이그레이션 (6 페이지): - AgentList: search + valueEnum, request → PageHelper 변환 - ContractList, RecruitLedger, ExceptionLedger, SettleList, PaymentList, UserList - 검색 폼 자동 생성 (valueType: select/dateMonth 등) - toolBarRender: 등록/엑셀 버튼 - options: density/fullScreen/reload/setting Dashboard: - ProCard + StatisticCard 로 KPI 4종 그리드 - 차트 + 배치 진행 + 알림을 ProCard 로 묶음 검증: - tsc -b 통과 - vite build 성공 (2.6MB / gzip 811KB) - 백엔드 76건 단위테스트 영향 없음 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,33 +1,33 @@
|
||||
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 { ProLayout, type MenuDataItem } from '@ant-design/pro-components';
|
||||
import { Avatar, Badge, Button, DatePicker, Dropdown, Space } from 'antd';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
IconMenu2, IconBell, IconUser, IconLogout, IconChevronDown,
|
||||
} from '@tabler/icons-react';
|
||||
import { IconBell, IconLogout, IconUser, IconChevronDown } from '@tabler/icons-react';
|
||||
import dayjs from 'dayjs';
|
||||
import { menuApi, MenuResp } from '@/api/menu';
|
||||
import { menuApi, type 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,
|
||||
}));
|
||||
/** 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),
|
||||
};
|
||||
if (m.children?.length) item.children = toProMenu(m.children);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
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({
|
||||
@@ -44,181 +44,100 @@ export default function MainLayout() {
|
||||
};
|
||||
|
||||
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,
|
||||
}}
|
||||
>
|
||||
{/* 로고 영역 */}
|
||||
<ProLayout
|
||||
title="GA 정산시스템"
|
||||
logo={
|
||||
<div
|
||||
style={{
|
||||
height: 56,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
padding: collapsed ? 0 : '0 16px',
|
||||
width: 28, height: 28, borderRadius: 6,
|
||||
background: 'linear-gradient(135deg, #1677FF 0%, #0C447C 100%)',
|
||||
color: '#fff',
|
||||
gap: 10,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.3,
|
||||
color: '#fff', display: 'flex', alignItems: 'center',
|
||||
justifyContent: 'center', fontWeight: 800, fontSize: 13,
|
||||
}}
|
||||
>
|
||||
<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>}
|
||||
>GA</div>
|
||||
}
|
||||
layout="mix"
|
||||
navTheme="light"
|
||||
contentWidth="Fluid"
|
||||
fixedHeader
|
||||
fixSiderbar
|
||||
siderWidth={220}
|
||||
menu={{ locale: false }}
|
||||
menuDataRender={() => toProMenu(menus)}
|
||||
location={{ pathname: location.pathname }}
|
||||
menuItemRender={(item, dom) => (
|
||||
<div onClick={() => item.path && !item.path.startsWith('/_grp/') && navigate(item.path)}>
|
||||
{dom}
|
||||
</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',
|
||||
)}
|
||||
breadcrumbRender={(routers = []) => routers}
|
||||
onMenuHeaderClick={() => navigate('/')}
|
||||
token={{
|
||||
// ProLayout 디자인 토큰 — antd pro 컬러 시스템
|
||||
sider: {
|
||||
colorMenuBackground: '#ffffff',
|
||||
colorTextMenuSelected: '#185FA5',
|
||||
colorBgMenuItemSelected: '#E6F1FB',
|
||||
colorTextMenuItemHover: '#1677FF',
|
||||
colorMenuItemDivider: '#f0f0f0',
|
||||
},
|
||||
header: {
|
||||
colorBgHeader: '#ffffff',
|
||||
colorHeaderTitle: '#185FA5',
|
||||
colorBgRightActionsItemHover: '#f5f7fa',
|
||||
heightLayoutHeader: 48,
|
||||
},
|
||||
bgLayout: '#f5f7fa',
|
||||
pageContainer: {
|
||||
paddingBlockPageContainerContent: 16,
|
||||
paddingInlinePageContainerContent: 20,
|
||||
},
|
||||
}}
|
||||
actionsRender={() => [
|
||||
<Space key="month" size={6}>
|
||||
<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 }}
|
||||
/>
|
||||
</Space>,
|
||||
<Badge key="bell" count={3} size="small" offset={[-2, 4]}>
|
||||
<Button type="text" icon={<IconBell size={18} />} />
|
||||
</Badge>,
|
||||
]}
|
||||
avatarProps={{
|
||||
src: undefined,
|
||||
size: 'small',
|
||||
title: auth.userName ?? auth.loginId ?? '관리자',
|
||||
icon: <IconUser size={14} />,
|
||||
style: { background: 'linear-gradient(135deg, #1677FF 0%, #185FA5 100%)' },
|
||||
render: (_props, dom) => (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'logout', label: '로그아웃', icon: <IconLogout size={14} />, onClick: handleLogout },
|
||||
],
|
||||
}}
|
||||
>
|
||||
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>
|
||||
<Space style={{ cursor: 'pointer' }}>
|
||||
{dom}
|
||||
<IconChevronDown size={12} color="#999" />
|
||||
</Space>
|
||||
</Dropdown>
|
||||
),
|
||||
}}
|
||||
footerRender={() => (
|
||||
<div style={{ textAlign: 'center', padding: '8px 0', fontSize: 11, color: '#bbb' }}>
|
||||
© GA Commission System v1.0.0-alpha · trading_ai
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Outlet />
|
||||
</ProLayout>
|
||||
);
|
||||
}
|
||||
|
||||
/** 경로 → 한글 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