Files
GA Pro 5b024d9b9c docs: 09_UI_DESIGN.md 추가 (디자인 시스템) + 참조 갱신
- 09_UI_DESIGN.md 신규: Ant Design 테마 토큰, 글로벌 CSS 변수,
  레이아웃 규격(Header 48px / Sider 220px), 페이지 표준 3종(목록/상세/대시보드),
  공통 컴포넌트 스타일(CodeBadge/MoneyText/SearchForm/DataTable/DataGrid),
  등록·수정 폼 규칙(Drawer), 화면별 와이어프레임 6종, 신입 체크리스트
- 00_PROMPTS.md: 프롬프트 3 frontend-developer 가 09_UI_DESIGN.md 도 함께 참조
- 06_FRONTEND_SPEC.md: 상단에 디자인 시스템 참조 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:21:13 +09:00

308 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 06_FRONTEND_SPEC — ga-frontend (React 18)
> Vite + React 18 + TypeScript + Ant Design 5 + AG Grid Community + React Query + Zustand.
> ga-common 에서 작성한 공통 컴포넌트(02_COMMON_SPEC §10)를 반드시 재사용.
> **디자인 시스템**: `docs/09_UI_DESIGN.md` 함께 참조 (테마 토큰, 레이아웃 규격, 화면별 와이어프레임)
## 패키지 구조
```
ga-frontend/
├── package.json # AG Grid Community ^31.3, antd ^5.18, react-query ^5.40
├── vite.config.ts # /api → http://localhost:8080 proxy
├── tsconfig.json # strict: true
├── index.html
└── src/
├── main.tsx # ReactDOM.createRoot, QueryClientProvider, ConfigProvider(ko)
├── App.tsx # 라우터 + 전역 메시지/모달
├── api/
│ ├── request.ts # axios + 인터셉터 + ApiResponse 타입
│ ├── auth.ts, agent.ts, contract.ts, ledger.ts, settle.ts, payment.ts,
│ ├── system.ts, menu.ts, code.ts, file.ts, batch.ts
├── components/
│ └── common/
│ ├── CodeSelect.tsx, CodeBadge.tsx, MoneyText.tsx
│ ├── SearchForm.tsx, DataGrid.tsx
│ ├── PermissionButton.tsx, ExcelExportButton.tsx, PageContainer.tsx
├── hooks/
│ ├── useCommonCodes.ts, useMenuTree.ts, usePermission.ts, useSearchParam.ts
├── layouts/
│ └── MainLayout.tsx # Sider + Header + Content + Breadcrumb
├── pages/
│ ├── Dashboard.tsx
│ ├── LoginPage.tsx
│ ├── org/ # AgentList / AgentDetail / AgentForm
│ ├── product/ # ContractList
│ ├── ledger/ # RecruitLedger / ExceptionLedger
│ ├── settle/ # SettleList / BatchRun / PaymentList
│ └── system/ # UserList / RoleList / CodeList
├── router/
│ └── DynamicRouter.tsx # /api/menus/my → React.lazy 매핑
├── stores/
│ ├── authStore.ts (Zustand) # accessToken, user, login/logout
│ ├── menuStore.ts # 메뉴 트리 캐시
│ └── notificationStore.ts # 미읽음 카운트
├── types/
│ └── api.ts, domain.ts
└── utils/
└── formatMoney.ts, formatDate.ts, downloadFile.ts
```
`vite.config.ts`:
```typescript
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } }
}
});
```
---
## 1. 공통 컴포넌트 / 훅
상세 시그니처는 `02_COMMON_SPEC.md §10` 참고. 핵심:
```tsx
// 공통코드 셀렉트
<CodeSelect groupCode="AGENT_STATUS" allowClear />
<CodeBadge groupCode="SETTLE_STATUS" value="CONFIRMED" />
<MoneyText value={123456} sign /> // +123,456 (음수 빨강)
<MonthPicker value={month} onChange={setMonth} />
// 검색폼 — conditions 배열로 자동 렌더링
<SearchForm
conditions={[
{ type: 'text', name: 'searchKeyword', label: '검색어' },
{ type: 'code', name: 'status', label: '상태', groupCode: 'AGENT_STATUS' },
{ type: 'dateRange', name: 'dateRange', label: '기간' },
{ type: 'month', name: 'settleMonth', label: '정산월' },
]}
onSearch={setParam} onReset={resetParam}
/>
// 권한 버튼 — perm 없으면 안 보임
<PermissionButton menuCode="ORG_AGENT" permCode="CREATE" type="primary"
onClick={() => navigate('new')}></PermissionButton>
// 엑셀 다운로드 — 100만건 진행률 표시
<ExcelExportButton url="/api/agents/export" params={searchParam}
fileName="설계사목록" showProgress />
// AG Grid 서버사이드
<DataGrid columnDefs={cols} dataSource={(p) => agentApi.list(p)}
totalRow rowSelection="multiple" />
```
훅:
```tsx
const { data: codes } = useCommonCodes('AGENT_STATUS'); // React Query staleTime 10분
const { canCreate, canExport, canApprove } = usePermission('ORG_AGENT');
const [param, setParam] = useSearchParam({ status: '' }); // URL 동기화
```
---
## 2. AG Grid vs DataTable 사용 기준
| 화면 | 컴포넌트 | 이유 |
|---|---|---|
| 설계사 목록, 사용자 목록 | Ant DataTable | 단순 CRUD, < 100 행 |
| 계약 목록 | Ant DataTable | 검색 위주 |
| **모집/유지/예외 원장** | **AG Grid** | 1만~100만 행, 합계행, 셀 편집 |
| **정산 결과** | **AG Grid** | 설계사 × 항목, 합계행 |
| 수수료율 편집 | AG Grid | 인라인 셀 편집 |
| 지급 관리 | Ant DataTable | 단순 상태 추적 |
판단 규칙:
- 100 건 이상 → AG Grid
- 셀 편집 필요 → AG Grid
- 합계행 (`pinnedBottomRowData`) → AG Grid
- 그 외 단순 목록 → Ant DataTable
---
## 3. 동적 라우터 (`router/DynamicRouter.tsx`)
```tsx
export function DynamicRouter() {
const { menuTree } = useMenuTree(); // /api/menus/my
const flat = flatten(menuTree).filter(m => m.menuType === 'PAGE' && m.componentPath);
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<MainLayout />}>
<Route path="/" element={<Dashboard />} />
{flat.map(m => {
const Page = lazy(() => import(`../pages/${m.componentPath}.tsx`));
return (
<Route key={m.menuId} path={m.menuPath}
element={<Suspense fallback={<Spin />}><Page /></Suspense>} />
);
})}
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
);
}
```
- 로그인 → `/api/menus/my` 호출 → Sider 메뉴 + 라우터 동적 등록
- `menu.component_path``org/AgentList``pages/org/AgentList.tsx` 로 lazy import
- 권한 없는 URL 진입 시 403
---
## 4. 화면 13개 (현재 구현됨)
### 1) `Dashboard.tsx` (`/`)
- 정산 요약 카드 4개: 모집 / 유지 / 예외 / 환수 (월간 합계)
- 배치 진행 상황 (최근 monthlySettlementJob)
- 미해결 대사 오류 알림
- 월별 추이 (recharts `LineChart`)
### 2) `LoginPage.tsx` (`/login`)
- 로그인 폼 (`loginId`, `password`)
- 성공 시 토큰 저장 + 메뉴 트리 fetch + `/` 리다이렉트
### 3~5) `org/AgentList.tsx`, `AgentDetail.tsx`, `AgentForm.tsx` (`/org/agents`)
- 목록 (DataTable, 50명 더미 표시)
- SearchForm: 이름/상태/조직
- 상세 모달 또는 페이지 (탭: 기본정보/조직이력/계약/정산이력)
- 등록/수정 폼 (`AgentSaveReq`, Ant Form + `<Form.Item rules>`)
- 엑셀 다운로드 (`ExcelExportButton`)
### 6) `product/ContractList.tsx` (`/contracts`)
- 200건 더미 표시
- 필터: 상태(ACTIVE/LAPSE), 보험사, 보험종류, 기간
### 7) `ledger/RecruitLedger.tsx` (`/ledger/recruit`)
- AG Grid (서버사이드 페이징/정렬/필터)
- 합계행 (`pinnedBottomRowData`)
- 컬럼: 정산월, 증권번호, 설계사, 보험사, 보험료, 수수료율, 수수료, 세액
- 엑셀 다운로드
### 8) `ledger/ExceptionLedger.tsx` (`/ledger/exception`)
- AG Grid + 등록 모달 (수동 등록)
- 행 액션: 승인 / 반려 (`@RequirePermission perm=APPROVE`)
### 9) `settle/SettleList.tsx` (`/settle`)
- 월별 정산 요약
- 설계사별 카드 / AG Grid (모집/유지/예외/오버라이드/환수/총액/세금/실지급)
- 행 액션: 확정 / 보류
### 10) `settle/BatchRun.tsx` (`/settle/batch`)
- 정산월 선택 → 배치 실행 버튼
- 진행률 폴링 (5초 간격, `GET /api/batch/settlement/status`)
- 8 Step 별 진행 카운트 표시
### 11) `settle/PaymentList.tsx` (`/payments`)
- 지급 상태 (PENDING / SENT / SUCCESS / FAIL)
- 이체파일 생성 / 결과 등록
### 12) `system/UserList.tsx` (`/system/users`)
- 사용자 CRUD, 역할 할당 체크박스
- 비밀번호 초기화 / 잠금 해제 액션
### 13) `system/RoleList.tsx` (`/system/roles`)
- 역할 CRUD + 권한 매트릭스 (메뉴 × 6 권한 체크박스 그리드)
### `system/CodeList.tsx` (`/system/codes`)
- 좌측: 그룹 리스트 (DataTable)
- 우측: 선택한 그룹의 상세 코드 (DataTable)
- `is_system='Y'` 인 그룹은 수정/삭제 disable
---
## 5. 색상 / 디자인 규칙
- **상태 뱃지 색** (`CodeBadge` 자동):
- 초록: CONFIRMED / APPROVED / ACTIVE / PAID / SUCCESS
- 파랑: PENDING / CALCULATED / PROCESSING / SENT
- 빨강: HOLD / SUSPEND / REJECTED / FAIL / LAPSE / ERROR
- 회색: DRAFT / NEW / OPEN / IGNORED
- **금액**: `MoneyText` 사용 (양수 검정 / 음수 빨강 / null `-`)
- **마스킹**: 주민번호/계좌번호는 응답에서 마스킹돼 옴 (`@Mask` 어노테이션)
- **빈 상태**: `<Empty />` (Ant Design)
- **로딩**: `<Spin />`
---
## 6. Zustand Store 패턴
```typescript
interface AuthState {
accessToken: string | null;
user: { userId: number; loginId: string; userName: string; roleCodes: string[] } | null;
login: (token: string, user: any) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>((set) => ({
accessToken: localStorage.getItem('accessToken'),
user: JSON.parse(localStorage.getItem('user') ?? 'null'),
login: (token, user) => {
localStorage.setItem('accessToken', token);
localStorage.setItem('user', JSON.stringify(user));
set({ accessToken: token, user });
},
logout: () => {
localStorage.removeItem('accessToken');
localStorage.removeItem('user');
set({ accessToken: null, user: null });
location.href = '/login';
},
}));
```
---
## 7. 신입 개발자 5분 컷 패턴
```tsx
export default function AgentList() {
const { canCreate, canExport } = usePermission('ORG_AGENT');
const [param, setParam] = useSearchParam({ status: '' });
const { data, isLoading } = useQuery({
queryKey: ['agents', param],
queryFn: () => agentApi.list(param),
});
return (
<PageContainer title="설계사 관리">
<SearchForm conditions={[
{ type: 'text', name: 'searchKeyword', label: '설계사명' },
{ type: 'code', name: 'status', label: '상태', groupCode: 'AGENT_STATUS' },
]} onSearch={setParam} />
<Space style={{ marginBottom: 16, justifyContent: 'flex-end' }}>
<PermissionButton menuCode="ORG_AGENT" permCode="CREATE" type="primary"
onClick={() => navigate('/org/agents/new')}></PermissionButton>
{canExport && <ExcelExportButton url="/api/agents/export" params={param}
fileName="설계사목록" showProgress />}
</Space>
<DataTable
loading={isLoading}
dataSource={data?.list ?? []}
rowKey="agentId"
pagination={{ total: data?.total ?? 0, current: param.pageNum, pageSize: param.pageSize }}
columns={[
{ title: '설계사명', dataIndex: 'agentName' },
{ title: '소속', dataIndex: 'orgName' },
{ title: '직급', dataIndex: 'gradeName' },
{ title: '상태', dataIndex: 'status',
render: v => <CodeBadge groupCode="AGENT_STATUS" value={v} /> },
{ title: '계약수', dataIndex: 'contractCount', align: 'right' },
{ title: '수수료', dataIndex: 'totalAmt',
render: v => <MoneyText value={v} />, align: 'right' },
]}
onRow={(r) => ({ onClick: () => navigate(`/org/agents/${r.agentId}`) })}
/>
</PageContainer>
);
}
```