feat(frontend): 09_UI_DESIGN 디자인 시스템 전체 적용

테마 / 폰트 / 아이콘:
- ConfigProvider theme 토큰 30+ 추가 (브랜드 컬러, 폰트, 라운딩, 메뉴/테이블/버튼)
- Pretendard 폰트 link + index.css fontFamily 적용
- @tabler/icons-react 추가, MainLayout 에 적용

레이아웃:
- styles/variables.css 신규 (--ga-* 변수 30+)
- MainLayout: Header 48px 흰색 + bottom border, Sider 220px + 토글,
  Content 1400px 중앙 정렬, 정산월 MonthPicker, 햄버거 토글

공통 컴포넌트:
- CodeBadge: 4단계 색상 매핑 (success/info/warning/danger), 누락 코드 보강
- MoneyText: 양수 #0F6E56, 음수 #E24B4A, 0 #888 (CSS 변수 사용)
- SearchForm: 배경 #fafafa, borderRadius 8, padding 12px 16px
- DataGrid: rowHeight 36, headerHeight 38, AG_GRID_LOCALE_KO,
  합계행 getRowStyle 회색 배경 + 굵게

페이지 페이지네이션 통일:
- utils/tablePagination.ts 신규 (showTotal '총 N건', pageSizeOptions [20/50/100])
- 7개 페이지 (Agent/Contract/Recruit/Exception/Settle/Payment/User) 적용

검증: tsc -b 통과 / vite build 성공 (10s) / 백엔드 단위테스트 76건 영향 없음

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-10 21:34:17 +09:00
parent 5b024d9b9c
commit 24fc187e2e
20 changed files with 330 additions and 39 deletions
+4
View File
@@ -38,3 +38,7 @@ target/
# Claude Code 개인 설정 (Agent Teams 등)
.claude/
# vite/tsc 빌드 산출물
ga-frontend/tsconfig.tsbuildinfo
ga-frontend/dist/
+1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css" />
<title>GA 수수료 정산 솔루션</title>
</head>
<body>
+27
View File
@@ -12,6 +12,7 @@
"@ag-grid-community/core": "^31.3.2",
"@ag-grid-community/react": "^31.3.2",
"@ag-grid-community/styles": "^31.3.2",
"@tabler/icons-react": "^3.5.0",
"@tanstack/react-query": "^5.40.0",
"antd": "^5.18.0",
"axios": "^1.7.2",
@@ -1462,6 +1463,32 @@
"win32"
]
},
"node_modules/@tabler/icons": {
"version": "3.44.0",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.44.0.tgz",
"integrity": "sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
}
},
"node_modules/@tabler/icons-react": {
"version": "3.44.0",
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.44.0.tgz",
"integrity": "sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg==",
"license": "MIT",
"dependencies": {
"@tabler/icons": "3.44.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
},
"peerDependencies": {
"react": ">= 16"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.100.9",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz",
+1
View File
@@ -14,6 +14,7 @@
"@ag-grid-community/core": "^31.3.2",
"@ag-grid-community/react": "^31.3.2",
"@ag-grid-community/styles": "^31.3.2",
"@tabler/icons-react": "^3.5.0",
"@tanstack/react-query": "^5.40.0",
"antd": "^5.18.0",
"axios": "^1.7.2",
@@ -6,11 +6,31 @@ interface Props {
value?: string;
}
/**
* 09_UI_DESIGN.md §5 CodeBadge — 4단계 색상 (success/info/warning/danger/default).
* antd Tag 의 preset 컬러 사용: success(초록) / processing(파랑) / warning(주황) / error(빨강) / default(회색).
*/
const COLOR_MAP: Record<string, string> = {
ACTIVE: 'green', CONFIRMED: 'green', APPROVED: 'green', PAID: 'green', MATCHED: 'green', DONE: 'green',
PENDING: 'blue', CALCULATED: 'blue', SENT: 'blue',
HOLD: 'red', SUSPEND: 'red', REJECTED: 'red', FAIL: 'red', LAPSE: 'red', DIFF: 'red', LOCKED: 'red',
DRAFT: 'default', NEW: 'default', LEAVE: 'default', RETIRED: 'default',
// 초록 — 긍정적 완료
CONFIRMED: 'success', APPROVED: 'success', ACTIVE: 'success',
PAID: 'success', COMPLETED: 'success', MATCH: 'success',
MATCHED: 'success', DONE: 'success', SUCCESS: 'success',
// 파랑 — 진행 중
CALCULATED: 'processing', PENDING: 'processing', PROCESSING: 'processing',
REVIEWED: 'processing', NEW: 'processing', SENT: 'processing',
// 주황 — 주의
HOLD: 'warning', SUSPEND: 'warning', MISMATCH: 'warning',
PARTIAL: 'warning', DIFF: 'warning',
// 빨강 — 부정적
REJECTED: 'error', LEAVE: 'error', CANCEL: 'error',
FAILED: 'error', FAIL: 'error', LAPSED: 'error',
LAPSE: 'error', LOCKED: 'error',
// 회색 — 기본
DRAFT: 'default', READY: 'default', RETIRED: 'default',
};
export default function CodeBadge({ groupCode, value }: Props) {
@@ -8,6 +8,21 @@ import '@ag-grid-community/styles/ag-theme-quartz.css';
ModuleRegistry.registerModules([ClientSideRowModelModule]);
/**
* AG Grid 한국어 로케일 (자주 쓰이는 항목 위주).
* 전체 키 목록은 AG Grid 공식 문서 참조.
*/
const AG_GRID_LOCALE_KO: Record<string, string> = {
page: '페이지', more: '더보기', to: '~', of: '/', next: '다음', last: '마지막',
first: '처음', previous: '이전', loadingOoo: '불러오는 중...',
selectAll: '전체 선택', searchOoo: '검색...', blanks: '(빈값)',
filterOoo: '필터...', equals: '같음', notEqual: '같지않음',
contains: '포함', notContains: '포함하지않음', startsWith: '시작',
endsWith: '끝', noRowsToShow: '데이터가 없습니다', resetColumns: '컬럼 초기화',
pinColumn: '컬럼 고정', valueAggregation: '집계',
sum: '합계', min: '최소', max: '최대', count: '건수',
};
interface Props<T> {
rows: T[] | undefined;
columns: ColDef<T>[];
@@ -65,9 +80,18 @@ export default function DataGrid<T extends Record<string, unknown>>({
columnDefs={columns}
defaultColDef={defaultColDef}
animateRows
rowHeight={36}
headerHeight={38}
localeText={AG_GRID_LOCALE_KO}
rowSelection="single"
getRowId={rowKey ? (p) => String(p.data[rowKey]) : undefined}
pinnedBottomRowData={pinnedBottomRow ? [pinnedBottomRow as T] : undefined}
getRowStyle={(params) => {
if (params.node.rowPinned) {
return { background: '#fafafa', fontWeight: 500 };
}
return undefined;
}}
onCellValueChanged={(e) => {
if (onCellChanged && e.data) {
onCellChanged(e.data, String(e.colDef.field), e.newValue);
@@ -6,15 +6,30 @@ interface Props {
fraction?: number;
}
/**
* 09_UI_DESIGN.md §5 MoneyText — 양수 #0F6E56(초록), 음수 #E24B4A(빨강), 0 #888(회색).
* 천 단위 콤마, tabular-nums 적용.
*/
export default function MoneyText({ value, showSign, fraction = 0 }: Props) {
if (value === null || value === undefined || value === '') return <span>-</span>;
const n = typeof value === 'string' ? Number(value) : value;
if (Number.isNaN(n)) return <span>-</span>;
const formatted = n.toLocaleString(undefined, {
minimumFractionDigits: fraction,
maximumFractionDigits: fraction,
});
const sign = n < 0 ? '' : showSign ? '+' : '';
const color = n < 0 ? '#cf1322' : n > 0 && showSign ? '#1677ff' : 'inherit';
return <span style={{ color, fontVariantNumeric: 'tabular-nums' }}>{sign}{formatted}</span>;
let color: string;
if (n > 0) color = 'var(--ga-money-plus)';
else if (n < 0) color = 'var(--ga-money-minus)';
else color = 'var(--ga-money-zero)';
const sign = n < 0 ? '' : showSign && n > 0 ? '+' : '';
return (
<span style={{ color, fontVariantNumeric: 'tabular-nums' }}>
{sign}{formatted}
</span>
);
}
@@ -55,7 +55,7 @@ export default function SearchForm({ conditions, initialValues, onSearch, onRese
layout="vertical"
initialValues={initialValues}
onFinish={handleSearch}
style={{ background: '#fff', padding: 16, marginBottom: 16, borderRadius: 4 }}
style={{ background: '#fafafa', padding: '12px 16px', marginBottom: 16, borderRadius: 8 }}
>
<Row gutter={16}>
{conditions.map((c) => (
+20 -1
View File
@@ -1,5 +1,24 @@
@import './styles/variables.css';
html, body, #root { height: 100%; margin: 0; padding: 0; }
body {
font-family: -apple-system, "Apple SD Gothic Neo", "Malgun Gothic", "맑은 고딕", sans-serif;
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo",
"Malgun Gothic", "맑은 고딕", system-ui, sans-serif;
background: #f5f5f5;
font-size: 13px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 숫자 셀에 등폭 숫자 (정렬 일관) */
.ga-tabular { font-variant-numeric: tabular-nums; }
/* 레이아웃 콘테이너 */
.ga-page-container { max-width: var(--ga-page-max-width); margin: 0 auto; }
/* AG Grid 합계행 강조 */
.ag-theme-quartz .ag-row-pinned {
background: #fafafa !important;
font-weight: 500;
}
+75 -21
View File
@@ -1,7 +1,14 @@
import { Layout, Menu, Dropdown, Badge, Space, Avatar, Button } from 'antd';
import { useNavigate, Outlet } from 'react-router-dom';
import { useState } from 'react';
import { Layout, Menu, Dropdown, Badge, Space, Avatar, Button, DatePicker } from 'antd';
import { useNavigate, Outlet, useLocation } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { BellOutlined, UserOutlined, LogoutOutlined } from '@ant-design/icons';
import {
IconMenu2,
IconBell,
IconUser,
IconLogout,
} from '@tabler/icons-react';
import dayjs from 'dayjs';
import { menuApi, MenuResp } from '@/api/menu';
import { authApi } from '@/api/auth';
import { useAuthStore } from '@/stores/auth';
@@ -18,7 +25,10 @@ function toMenuItems(menus: MenuResp[]): any[] {
export default function MainLayout() {
const navigate = useNavigate();
const location = useLocation();
const auth = useAuthStore();
const [collapsed, setCollapsed] = useState(false);
const [settleMonth, setSettleMonth] = useState(dayjs());
const { data: menus = [] } = useQuery({
queryKey: ['menu', 'my'],
@@ -35,36 +45,80 @@ export default function MainLayout() {
return (
<Layout style={{ minHeight: '100vh' }}>
<Header style={{ display: 'flex', justifyContent: 'space-between',
background: '#001529', color: '#fff' }}>
<div style={{ fontSize: 18, fontWeight: 600 }}>GA </div>
<Space>
<Badge count={0}>
<Button type="text" icon={<BellOutlined style={{ color: '#fff' }} />} />
<Header
style={{
height: 48,
lineHeight: '48px',
background: '#ffffff',
borderBottom: '1px solid #f0f0f0',
padding: '0 16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
boxShadow: 'var(--ga-shadow-card)',
}}
>
<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: <LogoutOutlined />, onClick: handleLogout },
],
}}>
<Space style={{ color: '#fff', cursor: 'pointer' }}>
<Avatar size="small" icon={<UserOutlined />} />
{auth.userName ?? auth.loginId}
<Dropdown
menu={{
items: [
{
key: 'logout',
label: '로그아웃',
icon: <IconLogout size={14} />,
onClick: handleLogout,
},
],
}}
>
<Space style={{ cursor: 'pointer' }}>
<Avatar size="small" icon={<IconUser size={14} />} />
<span style={{ fontSize: 13 }}>{auth.userName ?? auth.loginId}</span>
</Space>
</Dropdown>
</Space>
</Header>
<Layout>
<Sider width={240} theme="light">
<Sider
width={220}
collapsedWidth={64}
collapsed={collapsed}
theme="light"
style={{ borderRight: '1px solid #f0f0f0' }}
>
<Menu
mode="inline"
items={toMenuItems(menus)}
selectedKeys={[location.pathname]}
onClick={({ key }) => navigate(String(key))}
style={{ borderRight: 0, height: '100%' }}
style={{ borderRight: 0, height: '100%', paddingTop: 8 }}
/>
</Sider>
<Content style={{ padding: 24 }}>
<Outlet />
<Content style={{ padding: 'var(--ga-content-padding)', overflow: 'auto' }}>
<div className="ga-page-container">
<Outlet />
</div>
</Content>
</Layout>
</Layout>
+66 -2
View File
@@ -2,7 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ConfigProvider } from 'antd';
import { ConfigProvider, ThemeConfig } from 'antd';
import koKR from 'antd/locale/ko_KR';
import 'dayjs/locale/ko';
import App from './App';
@@ -12,9 +12,73 @@ const queryClient = new QueryClient({
defaultOptions: { queries: { retry: 1, staleTime: 30_000 } },
});
const theme: ThemeConfig = {
token: {
// 브랜드
colorPrimary: '#1677FF',
colorSuccess: '#0F6E56',
colorWarning: '#BA7517',
colorError: '#E24B4A',
colorInfo: '#1677FF',
// 폰트
fontFamily: "'Pretendard', -apple-system, BlinkMacSystemFont, sans-serif",
fontSize: 13,
fontSizeHeading1: 22,
fontSizeHeading2: 18,
fontSizeHeading3: 16,
// 라운딩
borderRadius: 6,
borderRadiusLG: 8,
// 간격
padding: 16,
paddingLG: 24,
margin: 16,
// 배경
colorBgContainer: '#ffffff',
colorBgLayout: '#f5f5f5',
colorBgElevated: '#ffffff',
},
components: {
Menu: {
itemHeight: 40,
itemMarginInline: 8,
itemBorderRadius: 6,
subMenuItemBg: 'transparent',
itemSelectedBg: '#e6f1fb',
itemSelectedColor: '#185fa5',
},
Table: {
headerBg: '#fafafa',
rowHoverBg: '#f5f7fa',
cellPaddingBlock: 10,
cellPaddingInline: 12,
headerSplitColor: '#f0f0f0',
},
Button: {
controlHeight: 32,
controlHeightSM: 28,
paddingInline: 16,
},
Input: { controlHeight: 32, paddingInline: 10 },
Select: { controlHeight: 32 },
Card: { paddingLG: 16 },
Layout: {
headerHeight: 48,
headerBg: '#ffffff',
headerPadding: '0 16px',
siderBg: '#ffffff',
bodyBg: '#f5f5f5',
},
},
};
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ConfigProvider locale={koKR}>
<ConfigProvider locale={koKR} theme={theme}>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
@@ -6,6 +6,7 @@ import SearchForm from '@/components/common/SearchForm';
import CodeBadge from '@/components/common/CodeBadge';
import MoneyText from '@/components/common/MoneyText';
import PermissionButton from '@/components/common/PermissionButton';
import { defaultPagination } from '@/utils/tablePagination';
import { exceptionApi, ExceptionLedgerResp } from '@/api/exceptionLedger';
export default function ExceptionLedger() {
@@ -49,8 +50,8 @@ export default function ExceptionLedger() {
size="small"
scroll={{ x: 1400 }}
pagination={{
...defaultPagination,
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
showSizeChanger: true,
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
}}
columns={[
@@ -6,6 +6,7 @@ import SearchForm from '@/components/common/SearchForm';
import MoneyText from '@/components/common/MoneyText';
import CodeBadge from '@/components/common/CodeBadge';
import ExcelExportButton from '@/components/common/ExcelExportButton';
import { defaultPagination } from '@/utils/tablePagination';
import { ledgerApi, LedgerResp, LedgerSearchParam } from '@/api/ledger';
export default function RecruitLedger() {
@@ -41,7 +42,8 @@ export default function RecruitLedger() {
size="small"
scroll={{ x: 1500 }}
pagination={{
current: param.pageNum, pageSize: param.pageSize, total: data?.total, showSizeChanger: true,
...defaultPagination,
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
}}
columns={[
+2 -1
View File
@@ -8,6 +8,7 @@ import CodeBadge from '@/components/common/CodeBadge';
import MoneyText from '@/components/common/MoneyText';
import PermissionButton from '@/components/common/PermissionButton';
import ExcelExportButton from '@/components/common/ExcelExportButton';
import { defaultPagination } from '@/utils/tablePagination';
import { agentApi, AgentResp, AgentSearchParam } from '@/api/agent';
export default function AgentList() {
@@ -46,10 +47,10 @@ export default function AgentList() {
loading={isLoading}
dataSource={data?.list}
pagination={{
...defaultPagination,
current: param.pageNum,
pageSize: param.pageSize,
total: data?.total,
showSizeChanger: true,
onChange: (page, size) => setParam({ ...param, pageNum: page, pageSize: size }),
}}
onRow={(r) => ({ onClick: () => navigate(`/org/agents/${r.agentId}`) })}
@@ -5,6 +5,7 @@ import PageContainer from '@/components/common/PageContainer';
import SearchForm from '@/components/common/SearchForm';
import CodeBadge from '@/components/common/CodeBadge';
import MoneyText from '@/components/common/MoneyText';
import { defaultPagination } from '@/utils/tablePagination';
import { contractApi, ContractResp, ContractSearchParam } from '@/api/contract';
export default function ContractList() {
@@ -33,7 +34,8 @@ export default function ContractList() {
loading={isLoading}
dataSource={data?.list}
pagination={{
current: param.pageNum, pageSize: param.pageSize, total: data?.total, showSizeChanger: true,
...defaultPagination,
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
}}
columns={[
+2 -1
View File
@@ -5,6 +5,7 @@ import PageContainer from '@/components/common/PageContainer';
import SearchForm from '@/components/common/SearchForm';
import CodeBadge from '@/components/common/CodeBadge';
import MoneyText from '@/components/common/MoneyText';
import { defaultPagination } from '@/utils/tablePagination';
import { paymentApi, PaymentResp } from '@/api/payment';
export default function PaymentList() {
@@ -34,8 +35,8 @@ export default function PaymentList() {
dataSource={data?.list}
size="small"
pagination={{
...defaultPagination,
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
showSizeChanger: true,
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
}}
columns={[
+3 -1
View File
@@ -6,6 +6,7 @@ import SearchForm from '@/components/common/SearchForm';
import CodeBadge from '@/components/common/CodeBadge';
import MoneyText from '@/components/common/MoneyText';
import PermissionButton from '@/components/common/PermissionButton';
import { defaultPagination } from '@/utils/tablePagination';
import { settleApi, SettleResp, SettleSearchParam } from '@/api/settle';
export default function SettleList() {
@@ -67,7 +68,8 @@ export default function SettleList() {
size="small"
scroll={{ x: 1600 }}
pagination={{
current: param.pageNum, pageSize: param.pageSize, total: data?.total, showSizeChanger: true,
...defaultPagination,
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
}}
columns={[
+3 -1
View File
@@ -5,6 +5,7 @@ import PageContainer from '@/components/common/PageContainer';
import SearchForm from '@/components/common/SearchForm';
import CodeBadge from '@/components/common/CodeBadge';
import PermissionButton from '@/components/common/PermissionButton';
import { defaultPagination } from '@/utils/tablePagination';
import { userApi, UserResp } from '@/api/user';
export default function UserList() {
@@ -49,7 +50,8 @@ export default function UserList() {
loading={isLoading}
dataSource={data?.list}
pagination={{
current: param.pageNum, pageSize: param.pageSize, total: data?.total, showSizeChanger: true,
...defaultPagination,
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
}}
columns={[
+39
View File
@@ -0,0 +1,39 @@
:root {
/* 브랜드 */
--ga-primary: #1677FF;
--ga-primary-light: #E6F1FB;
--ga-primary-dark: #0C447C;
/* 상태 색상 (CodeBadge 등) */
--ga-success: #0F6E56;
--ga-success-bg: #E1F5EE;
--ga-warning: #BA7517;
--ga-warning-bg: #FAEEDA;
--ga-danger: #E24B4A;
--ga-danger-bg: #FCEBEB;
--ga-info: #185FA5;
--ga-info-bg: #E6F1FB;
/* 금액 */
--ga-money-plus: #0F6E56;
--ga-money-minus: #E24B4A;
--ga-money-zero: #888888;
/* 레이아웃 */
--ga-sider-width: 220px;
--ga-sider-collapsed-width: 64px;
--ga-header-height: 48px;
--ga-content-padding: 20px;
--ga-page-max-width: 1400px;
/* 간격 단위 */
--ga-spacing-xs: 4px;
--ga-spacing-sm: 8px;
--ga-spacing-md: 12px;
--ga-spacing-lg: 16px;
--ga-spacing-xl: 24px;
/* 그림자 (최소한만 사용) */
--ga-shadow-card: 0 1px 2px rgba(0, 0, 0, 0.03);
--ga-shadow-dropdown: 0 4px 12px rgba(0, 0, 0, 0.08);
}
+12
View File
@@ -0,0 +1,12 @@
import type { TablePaginationConfig } from 'antd';
/**
* 09_UI_DESIGN.md §5 DataTable — 표준 페이지네이션 설정.
* 사용: pagination={{ ...defaultPagination, current, pageSize, total, onChange }}
*/
export const defaultPagination: TablePaginationConfig = {
showSizeChanger: true,
pageSizeOptions: ['20', '50', '100'],
defaultPageSize: 20,
showTotal: (total) => `${total.toLocaleString()}`,
};