feat: P6 후속 3 — 출금 잔여한도 검증 + 알림 모두읽음 + HANDOFF 갱신
출금 잔여 한도 검증:
- WithdrawRequestMapper.sumActiveAmountByMaster(settleMasterId) 추가
· CANCELLED 외 모든 활성 출금의 amount SUM (COALESCE 0)
- MobileMeService.createWithdrawRequest 에 잔여 한도 가드 추가
· 1) settle_master 본인 소유 확인 (FORBIDDEN)
· 2) 잔여 = net_amount - 활성 출금합계
· 3) 요청 amount > 잔여 시 INVALID_PARAMETER(E411) 던짐
· 메시지에 잔여 금액 명시 ("출금 가능 잔액(N)을 초과합니다")
PWA 알림 모두읽음:
- POST /api/mobile/me/notifications/read-all (기존 UserNotificationMapper.markAllRead 재사용)
- NoticeList 내알림 탭 헤더에 "모두 읽음 (N)" 버튼 (안읽음 카운트 표시)
- 성공 시 invalidate notifications + summary 쿼리
검증:
- ga-api compileJava + ga-mobile-pwa tsc PASS
- e2e (agent01 / userId=4 / agentId=1):
· settle#51 net=35,079원, 기존 사용 5,000원
· amount=999,999 → E411 "출금 가능 잔액(30079.00)을 초과합니다"
· amount=100 → success, requestId=5
· markAllRead → 200, summary.unread=0
- 이전 e2e 검증 (총 9건) 모두 PASS
HANDOFF.md §3-15 갱신 (2026-05-27 P6 후속 전체 정리 + agent01 테스트 계정 명시).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,5 +32,7 @@ export const notificationApi = {
|
||||
unwrap<PageResponse<NotificationResp>>(api.get('/api/mobile/me/notifications', { params })),
|
||||
markRead: (id: number) =>
|
||||
unwrap<void>(api.post(`/api/mobile/me/notifications/${id}/read`, {})),
|
||||
markAllRead: () =>
|
||||
unwrap<number>(api.post('/api/mobile/me/notifications/read-all', {})),
|
||||
summary: () => unwrap<MeSummary>(api.get('/api/mobile/me/summary')),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Badge, ErrorBlock, List, NavBar, PullToRefresh, Tabs, Tag } from 'antd-mobile';
|
||||
import { Badge, Button, ErrorBlock, List, NavBar, PullToRefresh, Tabs, Tag, Toast } from 'antd-mobile';
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { NoticeResp, noticeApi } from '@/api/notice';
|
||||
@@ -29,6 +29,7 @@ function Notifications() {
|
||||
queryFn: () => notificationApi.list({ pageNum: 1, pageSize: 50 }),
|
||||
});
|
||||
const items = data?.list ?? [];
|
||||
const unreadCount = items.filter((n) => !n.isRead).length;
|
||||
|
||||
const onItemClick = async (n: NotificationResp) => {
|
||||
if (!n.isRead) {
|
||||
@@ -42,9 +43,25 @@ function Notifications() {
|
||||
}
|
||||
};
|
||||
|
||||
const onMarkAllRead = async () => {
|
||||
try {
|
||||
const affected = await notificationApi.markAllRead();
|
||||
Toast.show({ icon: 'success', content: `${affected}건 읽음 처리되었습니다` });
|
||||
await qc.invalidateQueries({ queryKey: ['mobile.me.notifications'] });
|
||||
await qc.invalidateQueries({ queryKey: ['mobile.me.summary'] });
|
||||
} catch {
|
||||
// interceptor 처리
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PullToRefresh onRefresh={async () => { await refetch(); }}>
|
||||
<div style={{ paddingTop: 4 }}>
|
||||
{unreadCount > 0 && (
|
||||
<div style={{ padding: '8px 12px', textAlign: 'right' }}>
|
||||
<Button size="mini" onClick={onMarkAllRead}>모두 읽음 ({unreadCount})</Button>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && items.length === 0 && (
|
||||
<ErrorBlock status="empty" title="알림이 없습니다" />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user