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:
GA Pro
2026-05-10 22:20:33 +09:00
parent e70589fda8
commit 020939e563
12 changed files with 1387 additions and 797 deletions
+105 -186
View File
@@ -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] ?? '';
}