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()}원