diff --git a/.gitignore b/.gitignore
index c01513c..bc360a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,3 +38,7 @@ target/
# Claude Code 개인 설정 (Agent Teams 등)
.claude/
+
+# vite/tsc 빌드 산출물
+ga-frontend/tsconfig.tsbuildinfo
+ga-frontend/dist/
diff --git a/ga-frontend/index.html b/ga-frontend/index.html
index 3792c08..66d7027 100644
--- a/ga-frontend/index.html
+++ b/ga-frontend/index.html
@@ -4,6 +4,7 @@
+
GA 수수료 정산 솔루션
diff --git a/ga-frontend/package-lock.json b/ga-frontend/package-lock.json
index 633f57a..0849fba 100644
--- a/ga-frontend/package-lock.json
+++ b/ga-frontend/package-lock.json
@@ -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",
diff --git a/ga-frontend/package.json b/ga-frontend/package.json
index dc5828a..975674e 100644
--- a/ga-frontend/package.json
+++ b/ga-frontend/package.json
@@ -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",
diff --git a/ga-frontend/src/components/common/CodeBadge.tsx b/ga-frontend/src/components/common/CodeBadge.tsx
index 5df2249..23aecfe 100644
--- a/ga-frontend/src/components/common/CodeBadge.tsx
+++ b/ga-frontend/src/components/common/CodeBadge.tsx
@@ -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 = {
- 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) {
diff --git a/ga-frontend/src/components/common/DataGrid.tsx b/ga-frontend/src/components/common/DataGrid.tsx
index 8281cf4..535e643 100644
--- a/ga-frontend/src/components/common/DataGrid.tsx
+++ b/ga-frontend/src/components/common/DataGrid.tsx
@@ -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 = {
+ 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 {
rows: T[] | undefined;
columns: ColDef[];
@@ -65,9 +80,18 @@ export default function DataGrid>({
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);
diff --git a/ga-frontend/src/components/common/MoneyText.tsx b/ga-frontend/src/components/common/MoneyText.tsx
index bd1051d..420549b 100644
--- a/ga-frontend/src/components/common/MoneyText.tsx
+++ b/ga-frontend/src/components/common/MoneyText.tsx
@@ -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 -;
const n = typeof value === 'string' ? Number(value) : value;
if (Number.isNaN(n)) return -;
+
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 {sign}{formatted};
+
+ 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 (
+
+ {sign}{formatted}
+
+ );
}
diff --git a/ga-frontend/src/components/common/SearchForm.tsx b/ga-frontend/src/components/common/SearchForm.tsx
index 90e64fa..15c2cf4 100644
--- a/ga-frontend/src/components/common/SearchForm.tsx
+++ b/ga-frontend/src/components/common/SearchForm.tsx
@@ -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 }}
>
{conditions.map((c) => (
diff --git a/ga-frontend/src/index.css b/ga-frontend/src/index.css
index 5bf0e79..96be04b 100644
--- a/ga-frontend/src/index.css
+++ b/ga-frontend/src/index.css
@@ -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;
}
diff --git a/ga-frontend/src/layouts/MainLayout.tsx b/ga-frontend/src/layouts/MainLayout.tsx
index aed83aa..34669c0 100644
--- a/ga-frontend/src/layouts/MainLayout.tsx
+++ b/ga-frontend/src/layouts/MainLayout.tsx
@@ -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 (
-
- GA 수수료 정산 솔루션
-
-
- } />
+
+
+ }
+ onClick={() => setCollapsed((c) => !c)}
+ />
+
+ GA 정산시스템
+
+
+
+ d && setSettleMonth(d)}
+ allowClear={false}
+ format="YYYY-MM"
+ size="small"
+ />
+
+ } />
- , onClick: handleLogout },
- ],
- }}>
-
- } />
- {auth.userName ?? auth.loginId}
+ ,
+ onClick: handleLogout,
+ },
+ ],
+ }}
+ >
+
+ } />
+ {auth.userName ?? auth.loginId}
-
+
-
-
+
+
+
+
diff --git a/ga-frontend/src/main.tsx b/ga-frontend/src/main.tsx
index 2b29369..f0566ee 100644
--- a/ga-frontend/src/main.tsx
+++ b/ga-frontend/src/main.tsx
@@ -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(
-
+
diff --git a/ga-frontend/src/pages/ledger/ExceptionLedger.tsx b/ga-frontend/src/pages/ledger/ExceptionLedger.tsx
index ae228f0..2431d2c 100644
--- a/ga-frontend/src/pages/ledger/ExceptionLedger.tsx
+++ b/ga-frontend/src/pages/ledger/ExceptionLedger.tsx
@@ -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={[
diff --git a/ga-frontend/src/pages/ledger/RecruitLedger.tsx b/ga-frontend/src/pages/ledger/RecruitLedger.tsx
index 5cd1096..d59cdc6 100644
--- a/ga-frontend/src/pages/ledger/RecruitLedger.tsx
+++ b/ga-frontend/src/pages/ledger/RecruitLedger.tsx
@@ -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={[
diff --git a/ga-frontend/src/pages/org/AgentList.tsx b/ga-frontend/src/pages/org/AgentList.tsx
index 8874168..03862f4 100644
--- a/ga-frontend/src/pages/org/AgentList.tsx
+++ b/ga-frontend/src/pages/org/AgentList.tsx
@@ -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}`) })}
diff --git a/ga-frontend/src/pages/product/ContractList.tsx b/ga-frontend/src/pages/product/ContractList.tsx
index b319980..de6e3d0 100644
--- a/ga-frontend/src/pages/product/ContractList.tsx
+++ b/ga-frontend/src/pages/product/ContractList.tsx
@@ -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={[
diff --git a/ga-frontend/src/pages/settle/PaymentList.tsx b/ga-frontend/src/pages/settle/PaymentList.tsx
index 110298a..2ad2ae9 100644
--- a/ga-frontend/src/pages/settle/PaymentList.tsx
+++ b/ga-frontend/src/pages/settle/PaymentList.tsx
@@ -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={[
diff --git a/ga-frontend/src/pages/settle/SettleList.tsx b/ga-frontend/src/pages/settle/SettleList.tsx
index f373ece..3639293 100644
--- a/ga-frontend/src/pages/settle/SettleList.tsx
+++ b/ga-frontend/src/pages/settle/SettleList.tsx
@@ -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={[
diff --git a/ga-frontend/src/pages/system/UserList.tsx b/ga-frontend/src/pages/system/UserList.tsx
index f94cc50..5cd692b 100644
--- a/ga-frontend/src/pages/system/UserList.tsx
+++ b/ga-frontend/src/pages/system/UserList.tsx
@@ -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={[
diff --git a/ga-frontend/src/styles/variables.css b/ga-frontend/src/styles/variables.css
new file mode 100644
index 0000000..978888a
--- /dev/null
+++ b/ga-frontend/src/styles/variables.css
@@ -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);
+}
diff --git a/ga-frontend/src/utils/tablePagination.ts b/ga-frontend/src/utils/tablePagination.ts
new file mode 100644
index 0000000..1b8c185
--- /dev/null
+++ b/ga-frontend/src/utils/tablePagination.ts
@@ -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()}건`,
+};