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:
GA Pro
2026-05-28 00:00:17 +09:00
parent dfce9e5c02
commit 366f49c3f0
7 changed files with 109 additions and 19 deletions
@@ -108,4 +108,9 @@ public class MobileMeController {
service.markNotificationRead(notificationId);
return ApiResponse.ok();
}
@PostMapping("/notifications/read-all")
public ApiResponse<Integer> markAllRead() {
return ApiResponse.ok(service.markAllNotificationsRead());
}
}
@@ -114,6 +114,13 @@ public class MobileMeService {
notificationMapper.markRead(notificationId, userId);
}
/** 본인 알림 전체 읽음 처리. */
@Transactional
public int markAllNotificationsRead() {
Long userId = currentUserId();
return notificationMapper.markAllRead(userId);
}
/** 본인 명세서 단건. statementId 가 본인 agentId 의 것이 아니면 FORBIDDEN. */
@Transactional(readOnly = true)
public CommissionStatementResp statementDetail(Long statementId) {
@@ -170,6 +177,21 @@ public class MobileMeService {
Long userId = currentUserId();
Long agentId = requireAgentId();
// 1) settle_master 본인 소유 + net_amount 확보
var master = settleMasterMapper.selectById(req.getSettleMasterId());
if (master == null) throw new BizException(ErrorCode.NOT_FOUND, "정산 마스터를 찾을 수 없습니다");
if (!agentId.equals(master.getAgentId())) throw new BizException(ErrorCode.FORBIDDEN);
// 2) 잔여 한도 = net_amount - 활성 출금합계
BigDecimal netAmount = master.getNetAmount() != null ? master.getNetAmount() : BigDecimal.ZERO;
BigDecimal used = withdrawMapper.sumActiveAmountByMaster(req.getSettleMasterId());
if (used == null) used = BigDecimal.ZERO;
BigDecimal remaining = netAmount.subtract(used);
if (req.getAmount().compareTo(remaining) > 0) {
throw new BizException(ErrorCode.INVALID_PARAMETER,
"출금 가능 잔액(" + remaining + ")을 초과합니다");
}
WithdrawRequestSaveReq inner = new WithdrawRequestSaveReq();
inner.setSettleMasterId(req.getSettleMasterId());
inner.setAgentId(agentId); // 강제