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

11 KiB
Raw Permalink Blame History

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:

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } }
  }
});

1. 공통 컴포넌트 / 훅

상세 시그니처는 02_COMMON_SPEC.md §10 참고. 핵심:

// 공통코드 셀렉트
<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" />

훅:

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)

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_pathorg/AgentListpages/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 패턴

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분 컷 패턴

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>
  );
}