feat: P6 모바일 셀프 조회 PWA 스캐폴드 (ga-mobile-pwa)

설계사 본인 정산명세서/출금/공지를 모바일에서 조회하는 PWA 신규 모듈.
데스크탑 admin UI(ga-frontend)와 별도. 백엔드 무변경 — 기존 ga-api(8082)
재사용. 토큰 키도 mobile.accessToken 으로 분리.

스택:
- Vite + React 18 + TS + antd-mobile@5
- vite-plugin-pwa (manifest + service worker autoUpdate)
- React Query + Zustand (persist) + react-router-dom
- 포트 3001, /api → 8082 dev 프록시

화면 5종 (NavBar + List + PullToRefresh + TabBar):
- /login         — POST /api/auth/login + GET /api/auth/me
- /              — 홈 (요약 카드 + 메뉴, 로그아웃)
- /statement     — GET /api/statements?agentId=…  (본인 명세서)
- /withdraw      — GET /api/withdraw-requests?agentId=…  (본인 출금이력)
- /notice        — GET /api/notices/active

PWA:
- manifest: name/icon/theme/standalone, ko, /
- /api/** NetworkFirst 5s 타임아웃 / 5분 캐시 (오프라인 시 직전 응답)
- 아이콘은 SVG placeholder (192/512) — 본 배포 시 PNG 권장

검증:
- npm install 407 packages
- tsc --noEmit PASS
- npm run build PASS — manifest.webmanifest + sw.js + workbox-*.js
  자동 생성, precache 8 entries 528KB, 번들 gzip 173KB
- vite dev :3001 LISTENING + ga-api :8082 정상
- /api/statements total=112, /api/withdraw-requests total=3,
  /api/notices/active 0건, /api/auth/me 정상

잔여 (스캐폴드 이후):
- 백엔드 /api/mobile/me/* 전용 endpoint (서버측 본인 필터 강제)
- 출금 신청 작성 폼, 명세서 상세, 본인 알림(user_notification)
- 토큰 만료 자동 refresh, 푸시 알림 (FCM)
- 실 아이콘 PNG, manualChunks 분리

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-24 23:22:18 +09:00
parent 4a7a4eeb7a
commit 23fdd8474b
26 changed files with 7701 additions and 1 deletions
+61
View File
@@ -0,0 +1,61 @@
import { ErrorBlock, List, NavBar, PullToRefresh, Tag } from 'antd-mobile';
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '@/stores/authStore';
import { WithdrawResp, withdrawApi } from '@/api/withdraw';
const STATUS_COLOR: Record<string, string> = {
REQUESTED: 'default',
APPROVED: 'primary',
SENT: 'success',
COMPLETED: 'success',
FAILED: 'danger',
CANCELLED: 'default',
};
export default function WithdrawList() {
const agentId = useAuthStore((s) => s.profile?.agentId);
const { data, isLoading, refetch } = useQuery({
queryKey: ['withdraws', agentId],
enabled: !!agentId,
queryFn: () => withdrawApi.list({ agentId, pageNum: 1, pageSize: 50 }),
});
const items = data?.list ?? [];
return (
<div className="page">
<NavBar back={null}> </NavBar>
<PullToRefresh onRefresh={async () => { await refetch(); }}>
<div style={{ padding: 0, paddingTop: 8 }}>
{!agentId && (
<ErrorBlock status="empty" title="설계사 미연결" description="본 계정에는 설계사가 연결되어 있지 않습니다." />
)}
{agentId && !isLoading && items.length === 0 && (
<ErrorBlock status="empty" title="출금 신청 내역이 없습니다" />
)}
{items.length > 0 && (
<List>
{items.map((w: WithdrawResp) => (
<List.Item
key={w.requestId}
title={
<span>
#{w.requestId} {w.settleMonth ?? ''}
</span>
}
description={
<span style={{ fontSize: 12 }}>
{w.bankCode ?? '-'} · {w.accountNo ?? '-'} · {w.amount.toLocaleString()}
{w.transferTxId && <> · txId={w.transferTxId}</>}
</span>
}
extra={<Tag color={STATUS_COLOR[w.status] ?? 'default'}>{w.statusName ?? w.status}</Tag>}
/>
))}
</List>
)}
</div>
</PullToRefresh>
</div>
);
}