# 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 // 공통코드 셀렉트 // +123,456 (음수 빨강) // 검색폼 — conditions 배열로 자동 렌더링 // 권한 버튼 — perm 없으면 안 보임 navigate('new')}>등록 // 엑셀 다운로드 — 100만건 진행률 표시 // AG Grid 서버사이드 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 ( } /> }> } /> {flat.map(m => { const Page = lazy(() => import(`../pages/${m.componentPath}.tsx`)); return ( }>} /> ); })} } /> ); } ``` - 로그인 → `/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 + ``) - 엑셀 다운로드 (`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` 어노테이션) - **빈 상태**: `` (Ant Design) - **로딩**: `` --- ## 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((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 ( navigate('/org/agents/new')}>등록 {canExport && } }, { title: '계약수', dataIndex: 'contractCount', align: 'right' }, { title: '수수료', dataIndex: 'totalAmt', render: v => , align: 'right' }, ]} onRow={(r) => ({ onClick: () => navigate(`/org/agents/${r.agentId}`) })} /> ); } ```