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
@@ -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) => (