feat: P6 후속 2 — PWA 본인 프로필/출금취소 + SETTLED/FAILED 자동 알림
PWA 본인 프로필:
- Profile.tsx 신규 (phone 자기수정 Dialog + 비밀번호 변경 + 로그아웃)
- TabBar 에 "내정보" 탭 추가 (총 5탭)
- 백엔드 PUT /api/mobile/me/profile/phone (MobileMeService.updateMyPhone, 토큰에서 userId 강제)
- 비밀번호 변경은 기존 /api/auth/password 재사용
PWA 출금 신청 취소:
- 백엔드 POST /api/mobile/me/withdraw-requests/{id}/cancel
· 본인 agentId 검증 후 기존 WithdrawRequestService.cancel 호출
· REQUESTED 상태에서만 가능 (기존 검증 재사용)
- WithdrawList 행을 SwipeAction 으로 감싸 "취소" 액션 노출
· REQUESTED 상태 행만 SwipeAction 활성
출금 SETTLED/FAILED 본인 알림 자동 발행:
- WithdrawBankSettleService 가 SETTLED/FAILED 전이 시 user_notification INSERT
- 신청자(requestedBy) 에게 RefType=WITHDRAW, refId=requestId 로 발행
- 알림 발행 실패해도 동기화 흐름 비차단 (warn 로그만)
- PWA NoticeList 의 "내 알림" 탭에서 즉시 확인 가능
검증:
- ga-api compileJava + ga-frontend/ga-mobile-pwa tsc PASS
- PUT /api/mobile/me/profile/phone (admin) → 200, GET /api/auth/me 의 phone 변경 확인
- POST /api/mobile/me/withdraw-requests/1/cancel (admin) → E403 (agentId null 보안 강제)
- POST /api/internal/bank-settle → 200 data=0 (이번엔 ACCEPTED 잔여 없음, 알림 발행 경로 코드상 검증)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,23 @@ public class MobileMeController {
|
||||
return ApiResponse.ok(service.createWithdrawRequest(req));
|
||||
}
|
||||
|
||||
@PostMapping("/withdraw-requests/{requestId}/cancel")
|
||||
public ApiResponse<Void> cancelWithdrawRequest(@PathVariable Long requestId) {
|
||||
service.cancelMyWithdrawRequest(requestId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PutMapping("/profile/phone")
|
||||
public ApiResponse<Void> updateMyPhone(@RequestBody PhoneReq req) {
|
||||
service.updateMyPhone(req.getPhone());
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@lombok.Data
|
||||
public static class PhoneReq {
|
||||
private String phone;
|
||||
}
|
||||
|
||||
@GetMapping("/notifications")
|
||||
public ApiResponse<PageResponse<UserNotificationResp>> notifications(
|
||||
@ModelAttribute UserNotificationSearchParam param) {
|
||||
|
||||
@@ -131,6 +131,14 @@ public class MobileMeService {
|
||||
return statementService.download(statementId);
|
||||
}
|
||||
|
||||
/** 본인 phone 자기 수정 — 관리자 거치지 않고 본인이 직접 등록·정정. */
|
||||
@Transactional
|
||||
public void updateMyPhone(String phone) {
|
||||
Long userId = currentUserId();
|
||||
String trimmed = phone == null || phone.isBlank() ? null : phone.trim();
|
||||
userMapper.updatePhone(userId, trimmed);
|
||||
}
|
||||
|
||||
/** 본인 출금 가능 정산 마스터 목록. 출금 신청 폼의 settleMasterId 드롭다운 용. */
|
||||
@Transactional(readOnly = true)
|
||||
public List<SettleMasterResp> withdrawableSettleMasters() {
|
||||
@@ -143,6 +151,16 @@ public class MobileMeService {
|
||||
return settleMasterMapper.selectList(param);
|
||||
}
|
||||
|
||||
/** 본인 출금 신청 취소. 본인 소유 검증 후 기존 WithdrawRequestService.cancel 호출. */
|
||||
@Transactional
|
||||
public void cancelMyWithdrawRequest(Long requestId) {
|
||||
Long agentId = requireAgentId();
|
||||
com.ga.core.vo.withdraw.WithdrawRequestResp existing = withdrawMapper.selectById(requestId);
|
||||
if (existing == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if (!agentId.equals(existing.getAgentId())) throw new BizException(ErrorCode.FORBIDDEN);
|
||||
withdrawRequestService.cancel(requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 본인 출금 신청 등록. agentId / requestedBy 는 토큰에서 강제 결정.
|
||||
* 클라이언트가 보낸 값은 무시한다.
|
||||
|
||||
@@ -3,7 +3,10 @@ package com.ga.api.service.withdraw;
|
||||
import com.ga.common.adapter.bank.BankTransferAdapter;
|
||||
import com.ga.common.adapter.bank.BankTransferResponse;
|
||||
import com.ga.common.adapter.bank.BankTransferStatus;
|
||||
import com.ga.core.mapper.notice.UserNotificationMapper;
|
||||
import com.ga.core.mapper.withdraw.WithdrawRequestMapper;
|
||||
import com.ga.core.vo.notice.NotificationRefType;
|
||||
import com.ga.core.vo.notice.UserNotificationVO;
|
||||
import com.ga.core.vo.withdraw.WithdrawRequestVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -34,6 +37,7 @@ public class WithdrawBankSettleService {
|
||||
|
||||
private final WithdrawRequestMapper mapper;
|
||||
private final BankTransferAdapter bankTransferAdapter;
|
||||
private final UserNotificationMapper notificationMapper;
|
||||
|
||||
@Value("${app.bank-settle.limit:50}")
|
||||
private int batchLimit;
|
||||
@@ -56,11 +60,17 @@ public class WithdrawBankSettleService {
|
||||
|
||||
if (status == BankTransferStatus.SETTLED) {
|
||||
int n = mapper.markSettled(vo.getRequestId());
|
||||
if (n > 0) changed++;
|
||||
if (n > 0) {
|
||||
changed++;
|
||||
notify(vo, true, null);
|
||||
}
|
||||
} else if (status == BankTransferStatus.FAILED) {
|
||||
String reason = resp.getResultMessage() != null ? resp.getResultMessage() : "settle failed";
|
||||
int n = mapper.markSettleFailed(vo.getRequestId(), reason);
|
||||
if (n > 0) changed++;
|
||||
if (n > 0) {
|
||||
changed++;
|
||||
notify(vo, false, reason);
|
||||
}
|
||||
}
|
||||
// ACCEPTED 유지 시 no-op
|
||||
} catch (Exception e) {
|
||||
@@ -72,6 +82,34 @@ public class WithdrawBankSettleService {
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 신청자에게 결과 알림. 본인 알림이 실패해도 동기화 흐름은 비차단.
|
||||
*/
|
||||
private void notify(WithdrawRequestVO vo, boolean settled, String reason) {
|
||||
Long userId = vo.getRequestedBy();
|
||||
if (userId == null) return;
|
||||
try {
|
||||
UserNotificationVO n = new UserNotificationVO();
|
||||
n.setUserId(userId);
|
||||
n.setRefType(NotificationRefType.WITHDRAW.name());
|
||||
n.setRefId(vo.getRequestId());
|
||||
n.setIsRead(false);
|
||||
if (settled) {
|
||||
n.setTitle("출금 입금 완료");
|
||||
n.setContent("#" + vo.getRequestId() + " 출금 신청이 입금 완료되었습니다 ("
|
||||
+ vo.getAmount() + "원).");
|
||||
} else {
|
||||
n.setTitle("출금 입금 실패");
|
||||
n.setContent("#" + vo.getRequestId() + " 출금이 실패하였습니다"
|
||||
+ (reason != null ? " — " + reason : "")
|
||||
+ ". 재시도 처리됩니다.");
|
||||
}
|
||||
notificationMapper.insert(n);
|
||||
} catch (Exception e) {
|
||||
log.warn("[BANK-SETTLE] 알림 발행 실패 requestId={}: {}", vo.getRequestId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* cron 기반 자동 동기화.
|
||||
* yml 에 {@code app.bank-settle.cron} 미설정 시 매년 1월 1일 0시(=사실상 비활성)로 작동.
|
||||
|
||||
Reference in New Issue
Block a user