From ee3e0d40c51a8bbd2ccb7ae9d783ffc23e2d4e24 Mon Sep 17 00:00:00 2001 From: GA Pro Date: Sun, 24 May 2026 23:32:19 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20P6=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20PWA?= =?UTF-8?q?=20=EB=B3=B4=EC=95=88=20=EA=B0=95=ED=99=94=20=E2=80=94=20/api/m?= =?UTF-8?q?obile/me/*=20=EC=84=9C=EB=B2=84=EC=B8=A1=20=EB=B3=B8=EC=9D=B8?= =?UTF-8?q?=20=ED=95=84=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 스캐폴드의 클라이언트 agentId 쿼리 필터(변조 가능)를 서버측 강제 필터로 교체. 토큰 → user → agentId 자동 추출. 백엔드 (ga-api): - MobileMeService — 클라이언트 param 의 agentId/userId 는 무시·덮어쓰기. agentId 미연결 시 BizException(FORBIDDEN). PageHelper 로 정확한 total 산출. - MobileMeController — GET /api/mobile/me/{summary,statements, withdraw-requests,notifications} + POST /notifications/{id}/read. 별도 메뉴 권한 없이 인증만 통과 (PWA 사용자 전체 대상). ga-mobile-pwa: - api/statement.ts, api/withdraw.ts → /api/mobile/me/* 호출, agentId 제거 - api/notification.ts 신규 — list/markRead/summary - Home.tsx — summary() 단일 호출로 3카운트 + 안 읽음 Badge - NoticeList.tsx — Tabs(내 알림 / 공지). 알림 클릭 markRead + summary 무효화 - Statement/WithdrawList — E403(설계사 미연결) 시 ErrorBlock 안내 라이브 검증: - ga-api 컴파일 BUILD SUCCESSFUL · 재기동 OK - PWA tsc PASS · vite build PASS (precache 538KB, gzip 176KB) - admin(agentId=null): /summary → 200 OK 0/0/0, /statements → E403 차단 정상, /notifications → 200 OK total=0 Co-Authored-By: Claude Opus 4.7 (1M context) --- HANDOFF.md | 29 +++- .../controller/mobile/MobileMeController.java | 59 ++++++++ .../api/service/mobile/MobileMeService.java | 132 +++++++++++++++++ ga-mobile-pwa/src/api/notification.ts | 36 +++++ ga-mobile-pwa/src/api/statement.ts | 6 +- ga-mobile-pwa/src/api/withdraw.ts | 19 +-- ga-mobile-pwa/src/pages/Home.tsx | 45 +++--- ga-mobile-pwa/src/pages/NoticeList.tsx | 134 ++++++++++++++---- ga-mobile-pwa/src/pages/StatementList.tsx | 17 ++- ga-mobile-pwa/src/pages/WithdrawList.tsx | 23 ++- 10 files changed, 399 insertions(+), 101 deletions(-) create mode 100644 ga-api/src/main/java/com/ga/api/controller/mobile/MobileMeController.java create mode 100644 ga-api/src/main/java/com/ga/api/service/mobile/MobileMeService.java create mode 100644 ga-mobile-pwa/src/api/notification.ts diff --git a/HANDOFF.md b/HANDOFF.md index bf4b91b..4bd652c 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -339,14 +339,39 @@ npm run preview # 3001 에서 production 번들 서빙 - API 응답 확인: `/api/statements` total=112, `/api/withdraw-requests` total=3, `/api/notices/active` 0건(정상), `/api/auth/me` 정상 ### 잔여 (스캐폴드 이후) -- 백엔드 `/api/mobile/me/*` 전용 endpoint (서버측 본인 필터 강제) +- ~~백엔드 `/api/mobile/me/*` 전용 endpoint~~ → §3-14 에서 완료 +- ~~본인 알림(`user_notification`) 표시~~ → §3-14 에서 완료 - 출금 신청 작성 폼 (현재 이력 조회만) - 정산 명세서 상세 + PDF 다운로드 -- 본인 알림(`user_notification`) 표시 + 토큰 만료 자동 refresh +- 토큰 만료 자동 refresh - 푸시 알림 (FCM 등) — 외부 채널 결정 후 - 실 아이콘 PNG (192/512/512 maskable) - 번들 코드 스플리팅 (manualChunks) +## 3-14. P6 — 모바일 PWA 보안 강화: `/api/mobile/me/*` 전용 endpoint (2026-05-24) + +PWA 스캐폴드에서 클라이언트가 `agentId` 를 쿼리 파라미터로 보내 본인 데이터를 필터하던 변조 가능 구조를 **서버측 강제 필터**로 교체. 토큰 → user → agentId 자동 추출. + +### 백엔드 신규 +| 위치 | 내용 | +|---|---| +| `ga-api/service/mobile/MobileMeService` | 토큰에서 userId 추출, `userMapper.selectById` 로 agentId 결정. 클라이언트 param 의 agentId/userId 는 **무시하고 덮어쓰기**. agentId 미연결 시 `BizException(FORBIDDEN)` | +| `ga-api/controller/mobile/MobileMeController` | `GET /api/mobile/me/{summary,statements,withdraw-requests,notifications}` + `POST /api/mobile/me/notifications/{id}/read`. 별도 메뉴 권한 없이 인증만 통과 | + +### PWA 측 전환 +- `src/api/statement.ts` / `withdraw.ts` → `/api/mobile/me/*` 호출, `agentId` 인터페이스에서 제거 +- `src/api/notification.ts` 신규 — list/markRead/summary +- `Home.tsx` — `notificationApi.summary()` 한 번으로 3 카운트 동시 표시, 안 읽음 뱃지 +- `NoticeList.tsx` — `Tabs` 로 `내 알림` / `공지사항` 두 탭. 알림 클릭 시 markRead + summary 무효화 +- `StatementList.tsx` / `WithdrawList.tsx` — agentId 의존 제거, E403(설계사 미연결) 시 ErrorBlock 안내 + +### 라이브 검증 (2026-05-24) +- ga-api 컴파일 BUILD SUCCESSFUL · 재기동 OK +- PWA `tsc --noEmit` PASS · `npm run build` PASS (precache 8 entries 538 KiB, gzip 176KB) +- `GET /api/mobile/me/summary` (admin, agentId=null) → 200 OK, 세 카운트 모두 0 +- `GET /api/mobile/me/statements` (admin) → **E403** 차단 (보안 강제 동작 확인) +- `GET /api/mobile/me/notifications` (admin) → 200 OK total=0 + ## 4. 잔여 작업 (P6 실연동) P5 전체 + P6 PoC(어댑터 모듈 + Mock + SDK 분기 + 재시도 + 추적 컬럼 + 호출 감사 이력 + **모바일 PWA 스캐폴드**) 완료. diff --git a/ga-api/src/main/java/com/ga/api/controller/mobile/MobileMeController.java b/ga-api/src/main/java/com/ga/api/controller/mobile/MobileMeController.java new file mode 100644 index 0000000..d2fecb8 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/mobile/MobileMeController.java @@ -0,0 +1,59 @@ +package com.ga.api.controller.mobile; + +import com.ga.api.service.mobile.MobileMeService; +import com.ga.api.service.mobile.MobileMeService.MeSummary; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.notice.UserNotificationResp; +import com.ga.core.vo.notice.UserNotificationSearchParam; +import com.ga.core.vo.settle.CommissionStatementResp; +import com.ga.core.vo.settle.CommissionStatementSearchParam; +import com.ga.core.vo.withdraw.WithdrawRequestResp; +import com.ga.core.vo.withdraw.WithdrawRequestSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +/** + * 모바일 PWA 전용 — 토큰의 사용자 본인 데이터만 반환. + * + *

모든 endpoint 는 인증만 통과하면 사용 가능 (별도 메뉴 권한 없음). + * agentId / userId 는 서버측에서 토큰 → user 조회로 강제 결정. 클라이언트 파라미터는 무시한다. + */ +@Tag(name = "모바일 셀프 조회") +@RestController +@RequestMapping("/api/mobile/me") +@RequiredArgsConstructor +public class MobileMeController { + + private final MobileMeService service; + + @GetMapping("/summary") + public ApiResponse summary() { + return ApiResponse.ok(service.summary()); + } + + @GetMapping("/statements") + public ApiResponse> statements( + @ModelAttribute CommissionStatementSearchParam param) { + return ApiResponse.ok(service.statements(param)); + } + + @GetMapping("/withdraw-requests") + public ApiResponse> withdrawRequests( + @ModelAttribute WithdrawRequestSearchParam param) { + return ApiResponse.ok(service.withdrawRequests(param)); + } + + @GetMapping("/notifications") + public ApiResponse> notifications( + @ModelAttribute UserNotificationSearchParam param) { + return ApiResponse.ok(service.notifications(param)); + } + + @PostMapping("/notifications/{notificationId}/read") + public ApiResponse markRead(@PathVariable Long notificationId) { + service.markNotificationRead(notificationId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/mobile/MobileMeService.java b/ga-api/src/main/java/com/ga/api/service/mobile/MobileMeService.java new file mode 100644 index 0000000..48a3bad --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/mobile/MobileMeService.java @@ -0,0 +1,132 @@ +package com.ga.api.service.mobile; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.common.util.SecurityUtil; +import com.ga.core.mapper.notice.UserNotificationMapper; +import com.ga.core.mapper.settle.CommissionStatementMapper; +import com.ga.core.mapper.user.UserMapper; +import com.ga.core.mapper.withdraw.WithdrawRequestMapper; +import com.ga.core.vo.notice.UserNotificationResp; +import com.ga.core.vo.notice.UserNotificationSearchParam; +import com.ga.core.vo.settle.CommissionStatementResp; +import com.ga.core.vo.settle.CommissionStatementSearchParam; +import com.ga.core.vo.user.UserVO; +import com.ga.core.vo.withdraw.WithdrawRequestResp; +import com.ga.core.vo.withdraw.WithdrawRequestSearchParam; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 모바일 PWA 전용 — 본인 데이터만 조회. agentId/userId 는 토큰에서 강제 추출. + * + *

클라이언트가 보낸 search param 의 agentId/userId 는 무시하고 SecurityContext 의 사용자로 덮어쓴다. + * 본 서비스 외부에서는 본인 데이터 임의 조회가 절대 불가능하도록 보장. + */ +@Service +@RequiredArgsConstructor +public class MobileMeService { + + private final UserMapper userMapper; + private final CommissionStatementMapper statementMapper; + private final WithdrawRequestMapper withdrawMapper; + private final UserNotificationMapper notificationMapper; + + @Transactional(readOnly = true) + public MeSummary summary() { + Long userId = currentUserId(); + Long agentId = resolveAgentId(userId); + + long stTotal = 0L; + long wdTotal = 0L; + if (agentId != null) { + CommissionStatementSearchParam sp = new CommissionStatementSearchParam(); + sp.setAgentId(agentId); + sp.setPageNum(1); + sp.setPageSize(1); + sp.startPage(); + stTotal = PageResponse.of(statementMapper.selectList(sp)).getTotal(); + + WithdrawRequestSearchParam wp = new WithdrawRequestSearchParam(); + wp.setAgentId(agentId); + wp.setPageNum(1); + wp.setPageSize(1); + wp.startPage(); + wdTotal = PageResponse.of(withdrawMapper.selectList(wp)).getTotal(); + } + int unreadCount = notificationMapper.selectUnreadCount(userId); + + return MeSummary.builder() + .userId(userId) + .agentId(agentId) + .statementCount(stTotal) + .withdrawRequestCount(wdTotal) + .unreadNotificationCount(unreadCount) + .build(); + } + + @Transactional(readOnly = true) + public PageResponse statements(CommissionStatementSearchParam param) { + Long agentId = requireAgentId(); + param.setAgentId(agentId); // 강제 덮어쓰기 + param.startPage(); + return PageResponse.of(statementMapper.selectList(param)); + } + + @Transactional(readOnly = true) + public PageResponse withdrawRequests(WithdrawRequestSearchParam param) { + Long agentId = requireAgentId(); + param.setAgentId(agentId); // 강제 덮어쓰기 + param.startPage(); + return PageResponse.of(withdrawMapper.selectList(param)); + } + + @Transactional(readOnly = true) + public PageResponse notifications(UserNotificationSearchParam param) { + Long userId = currentUserId(); + param.setUserId(userId); // 강제 덮어쓰기 + param.startPage(); + return PageResponse.of(notificationMapper.selectList(param)); + } + + @Transactional + public void markNotificationRead(Long notificationId) { + Long userId = currentUserId(); + notificationMapper.markRead(notificationId, userId); + } + + // ── private ─────────────────────────────────────────────────────────────── + + private Long currentUserId() { + Long uid = SecurityUtil.getCurrentUserId(); + if (uid == null) throw new BizException(ErrorCode.UNAUTHORIZED); + return uid; + } + + private Long resolveAgentId(Long userId) { + UserVO user = userMapper.selectById(userId); + return user != null ? user.getAgentId() : null; + } + + /** 본인 agent 가 연결돼 있어야만 통과. 미연결이면 FORBIDDEN. */ + private Long requireAgentId() { + Long uid = currentUserId(); + Long agentId = resolveAgentId(uid); + if (agentId == null) throw new BizException(ErrorCode.FORBIDDEN); + return agentId; + } + + @Data + @Builder + public static class MeSummary { + private Long userId; + private Long agentId; + private long statementCount; + private long withdrawRequestCount; + private int unreadNotificationCount; + } +} diff --git a/ga-mobile-pwa/src/api/notification.ts b/ga-mobile-pwa/src/api/notification.ts new file mode 100644 index 0000000..6acca31 --- /dev/null +++ b/ga-mobile-pwa/src/api/notification.ts @@ -0,0 +1,36 @@ +import api, { unwrap, PageResponse } from './request'; + +export interface NotificationResp extends Record { + notificationId: number; + userId: number; + title: string; + content?: string; + refType?: string; + refId?: number; + isRead: boolean; + readAt?: string; + createdAt?: string; +} + +export interface NotificationSearchParam { + isRead?: boolean; + refType?: string; + pageNum?: number; + pageSize?: number; +} + +export interface MeSummary { + userId: number; + agentId?: number; + statementCount: number; + withdrawRequestCount: number; + unreadNotificationCount: number; +} + +export const notificationApi = { + list: (params: NotificationSearchParam = {}) => + unwrap>(api.get('/api/mobile/me/notifications', { params })), + markRead: (id: number) => + unwrap(api.post(`/api/mobile/me/notifications/${id}/read`, {})), + summary: () => unwrap(api.get('/api/mobile/me/summary')), +}; diff --git a/ga-mobile-pwa/src/api/statement.ts b/ga-mobile-pwa/src/api/statement.ts index 7929c5b..fe03921 100644 --- a/ga-mobile-pwa/src/api/statement.ts +++ b/ga-mobile-pwa/src/api/statement.ts @@ -16,7 +16,6 @@ export interface StatementResp extends Record { } export interface StatementSearchParam { - agentId?: number; settleMonth?: string; statementType?: string; issueStatus?: string; @@ -25,8 +24,7 @@ export interface StatementSearchParam { } export const statementApi = { + /** 본인 명세서 — 서버측이 토큰 → agentId 강제 필터. */ list: (params: StatementSearchParam = {}) => - unwrap>(api.get('/api/statements', { params })), - detail: (id: number) => - unwrap(api.get(`/api/statements/${id}`)), + unwrap>(api.get('/api/mobile/me/statements', { params })), }; diff --git a/ga-mobile-pwa/src/api/withdraw.ts b/ga-mobile-pwa/src/api/withdraw.ts index 3275e2c..0985802 100644 --- a/ga-mobile-pwa/src/api/withdraw.ts +++ b/ga-mobile-pwa/src/api/withdraw.ts @@ -20,28 +20,13 @@ export interface WithdrawResp extends Record { } export interface WithdrawSearchParam { - agentId?: number; settleMasterId?: number; pageNum?: number; pageSize?: number; } -export interface WithdrawSaveReq { - settleMasterId: number; - agentId: number; - amount: number; - requestedBy: number; - bankCode: string; - accountNo: string; -} - export const withdrawApi = { + /** 본인 출금 신청 이력 — 서버측이 토큰 → agentId 강제 필터. */ list: (params: WithdrawSearchParam = {}) => - unwrap>(api.get('/api/withdraw-requests', { params })), - detail: (id: number) => - unwrap(api.get(`/api/withdraw-requests/${id}`)), - create: (req: WithdrawSaveReq) => - unwrap(api.post('/api/withdraw-requests', req)), - cancel: (id: number) => - unwrap(api.post(`/api/withdraw-requests/${id}/cancel`, {})), + unwrap>(api.get('/api/mobile/me/withdraw-requests', { params })), }; diff --git a/ga-mobile-pwa/src/pages/Home.tsx b/ga-mobile-pwa/src/pages/Home.tsx index c5b9949..f317dd6 100644 --- a/ga-mobile-pwa/src/pages/Home.tsx +++ b/ga-mobile-pwa/src/pages/Home.tsx @@ -2,31 +2,22 @@ import { Card, List, NavBar, Space, Tag } from 'antd-mobile'; import { useNavigate } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { useAuthStore } from '@/stores/authStore'; -import { statementApi } from '@/api/statement'; -import { withdrawApi } from '@/api/withdraw'; -import { noticeApi } from '@/api/notice'; +import { notificationApi } from '@/api/notification'; export default function Home() { const navigate = useNavigate(); const profile = useAuthStore((s) => s.profile); const clear = useAuthStore((s) => s.clear); - const agentId = profile?.agentId; - const statements = useQuery({ - queryKey: ['home.statements', agentId], - enabled: !!agentId, - queryFn: () => statementApi.list({ agentId, pageNum: 1, pageSize: 1 }), - }); - const withdraws = useQuery({ - queryKey: ['home.withdraws', agentId], - enabled: !!agentId, - queryFn: () => withdrawApi.list({ agentId, pageNum: 1, pageSize: 1 }), - }); - const notices = useQuery({ - queryKey: ['home.notices'], - queryFn: () => noticeApi.active(), + const summary = useQuery({ + queryKey: ['mobile.me.summary'], + queryFn: () => notificationApi.summary(), + staleTime: 10_000, }); + const s = summary.data; + const loading = summary.isLoading; + return (

{ clear(); navigate('/login'); }} style={{ fontSize: 14 }}>로그아웃}> @@ -40,7 +31,7 @@ export default function Home() {
{profile?.agentName ? `${profile.agentName} · ${profile.orgName ?? ''}` : '설계사 정보 없음'}
- {profile?.roleCodes && ( + {profile?.roleCodes && profile.roleCodes.length > 0 && (
{profile.roleCodes.map((r) => {r})} @@ -52,32 +43,38 @@ export default function Home() {
navigate('/statement')} clickable > 정산 명세서 navigate('/withdraw')} clickable > 출금 신청 0 + ? {s?.unreadNotificationCount}건 안 읽음 + : '모두 확인' + } onClick={() => navigate('/notice')} clickable > - 공지사항 + 알림 / 공지
- {!agentId && ( + {s && !s.agentId && (
- 본 계정에 연결된 설계사(agent)가 없습니다. 일부 기능이 제한될 수 있습니다. + 본 계정에 연결된 설계사(agent)가 없습니다. 명세서·출금 조회가 제한됩니다.
)}
diff --git a/ga-mobile-pwa/src/pages/NoticeList.tsx b/ga-mobile-pwa/src/pages/NoticeList.tsx index ec978c7..cda286a 100644 --- a/ga-mobile-pwa/src/pages/NoticeList.tsx +++ b/ga-mobile-pwa/src/pages/NoticeList.tsx @@ -1,40 +1,112 @@ -import { ErrorBlock, List, NavBar, PullToRefresh, Tag } from 'antd-mobile'; -import { useQuery } from '@tanstack/react-query'; +import { Badge, ErrorBlock, List, NavBar, PullToRefresh, Tabs, Tag } from 'antd-mobile'; +import { useState } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { NoticeResp, noticeApi } from '@/api/notice'; +import { NotificationResp, notificationApi } from '@/api/notification'; export default function NoticeList() { - const { data, isLoading, refetch } = useQuery({ - queryKey: ['notices.active'], - queryFn: () => noticeApi.active(), - }); - - const items = data ?? []; + const [tab, setTab] = useState('notifications'); return (
- 공지사항 - { await refetch(); }}> -
- {!isLoading && items.length === 0 && ( - - )} - {items.length > 0 && ( - - {items.map((n: NoticeResp) => ( - - {n.startAt ?? ''} {n.noticeType && {n.noticeType}} - - } - /> - ))} - - )} -
-
+ 알림 / 공지 + + + + + + + +
); } + +function Notifications() { + const qc = useQueryClient(); + const { data, isLoading, refetch } = useQuery({ + queryKey: ['mobile.me.notifications'], + queryFn: () => notificationApi.list({ pageNum: 1, pageSize: 50 }), + }); + const items = data?.list ?? []; + + const onItemClick = async (n: NotificationResp) => { + if (!n.isRead) { + try { + await notificationApi.markRead(n.notificationId); + await qc.invalidateQueries({ queryKey: ['mobile.me.notifications'] }); + await qc.invalidateQueries({ queryKey: ['mobile.me.summary'] }); + } catch { + // interceptor 가 토스트 처리 + } + } + }; + + return ( + { await refetch(); }}> +
+ {!isLoading && items.length === 0 && ( + + )} + {items.length > 0 && ( + + {items.map((n: NotificationResp) => ( + onItemClick(n)} + clickable + title={ + + {!n.isRead && } + {n.title} + + } + description={ + + {n.refType && {n.refType}} + {n.content ?? ''} + + } + extra={{n.createdAt ?? ''}} + /> + ))} + + )} +
+
+ ); +} + +function Notices() { + const { data, isLoading, refetch } = useQuery({ + queryKey: ['mobile.notices.active'], + queryFn: () => noticeApi.active(), + }); + const items = data ?? []; + + return ( + { await refetch(); }}> +
+ {!isLoading && items.length === 0 && ( + + )} + {items.length > 0 && ( + + {items.map((n: NoticeResp) => ( + + {n.startAt ?? ''}{' '} + {n.noticeType && {n.noticeType}} + + } + /> + ))} + + )} +
+
+ ); +} diff --git a/ga-mobile-pwa/src/pages/StatementList.tsx b/ga-mobile-pwa/src/pages/StatementList.tsx index bfec408..6d67823 100644 --- a/ga-mobile-pwa/src/pages/StatementList.tsx +++ b/ga-mobile-pwa/src/pages/StatementList.tsx @@ -1,27 +1,26 @@ import { ErrorBlock, List, NavBar, PullToRefresh, Tag } from 'antd-mobile'; import { useQuery } from '@tanstack/react-query'; -import { useAuthStore } from '@/stores/authStore'; import { StatementResp, statementApi } from '@/api/statement'; export default function StatementList() { - const agentId = useAuthStore((s) => s.profile?.agentId); - const { data, isLoading, refetch } = useQuery({ - queryKey: ['statements', agentId], - enabled: !!agentId, - queryFn: () => statementApi.list({ agentId, pageNum: 1, pageSize: 50 }), + const { data, isLoading, refetch, error } = useQuery({ + queryKey: ['mobile.me.statements'], + queryFn: () => statementApi.list({ pageNum: 1, pageSize: 50 }), + retry: false, }); const items = data?.list ?? []; + const forbidden = (error as { code?: string } | undefined)?.code === 'E403'; return (
정산 명세서 { await refetch(); }}> -
- {!agentId && ( +
+ {forbidden && ( )} - {agentId && !isLoading && items.length === 0 && ( + {!forbidden && !isLoading && items.length === 0 && ( )} {items.length > 0 && ( diff --git a/ga-mobile-pwa/src/pages/WithdrawList.tsx b/ga-mobile-pwa/src/pages/WithdrawList.tsx index f04ad31..3d265fb 100644 --- a/ga-mobile-pwa/src/pages/WithdrawList.tsx +++ b/ga-mobile-pwa/src/pages/WithdrawList.tsx @@ -1,6 +1,5 @@ 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 = { @@ -13,24 +12,24 @@ const STATUS_COLOR: Record = { }; 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 { data, isLoading, refetch, error } = useQuery({ + queryKey: ['mobile.me.withdraws'], + queryFn: () => withdrawApi.list({ pageNum: 1, pageSize: 50 }), + retry: false, }); const items = data?.list ?? []; + const forbidden = (error as { code?: string } | undefined)?.code === 'E403'; return (
출금 신청 { await refetch(); }}> -
- {!agentId && ( +
+ {forbidden && ( )} - {agentId && !isLoading && items.length === 0 && ( + {!forbidden && !isLoading && items.length === 0 && ( )} {items.length > 0 && ( @@ -38,11 +37,7 @@ export default function WithdrawList() { {items.map((w: WithdrawResp) => ( - #{w.requestId} {w.settleMonth ?? ''} - - } + title={#{w.requestId} {w.settleMonth ?? ''}} description={ {w.bankCode ?? '-'} · {w.accountNo ?? '-'} · {w.amount.toLocaleString()}원