feat: P6 후속 — PWA 출금폼/명세상세/토큰refresh + 펌뱅킹 SETTLED 동기화 + user phone UI

외부 SDK 차단 항목 외 P6 잔여 운영성 작업을 일괄 완료. DB 변경 없음(v85 유지).

PWA:
- 출금 신청 작성 폼 (WithdrawCreate.tsx + POST /api/mobile/me/withdraw-requests + GET /settle-masters)
  · 클라이언트가 보낸 agentId/userId 무시, 토큰에서 강제 결정
- 명세서 상세 + 엑셀 다운로드 (StatementDetail.tsx + GET /api/mobile/me/statements/{id}[/download])
  · 본인 agentId 검증 후 기존 CommissionStatementService.download() 재사용
- 토큰 자동 refresh (request.ts interceptor + authStore.refreshToken)
  · 401 시 단일 refresh 호출로 합치고 broadcast, login/refresh 자체 401 은 무한루프 차단

펌뱅킹 SETTLED 동기화:
- WithdrawBankSettleService + POST /api/internal/bank-settle (BankSettleController)
- Mapper: selectAcceptedTransfers / markSettled / markSettleFailed
- 어댑터 queryStatus 폴링 결과로 SETTLED→COMPLETED, FAILED→APPROVED(재시도풀) 전이
- app.bank-settle.cron 옵션 (기본 비활성)

사용자 phone 등록 UI:
- PUT /api/system/users/{id}/phone — NotBlank 검증 없는 단일 패치 endpoint
- UserList 에 phone 컬럼 + "전화번호" 액션 → Antd Modal 인라인 편집
- 결재 SMS 발신 전제 충족

검증 (9/9 PASS, 라이브):
- ga-common/ga-core/ga-external-adapter/ga-api compileJava BUILD SUCCESSFUL
- ga-frontend / ga-mobile-pwa tsc --noEmit PASS
- POST /api/internal/bank-settle → data=3 (3건 COMPLETED 전이 확인)
- PUT /api/system/users/1/phone → 200, 이후 GET 으로 phone 노출 확인
- 보안 강제: admin(agentId=null)으로 /api/mobile/me/{statements,settle-masters,withdraw-requests} → E403
- POST /api/auth/refresh → 200, 새 accessToken 발급
- 3개 서비스 라이브: ga-api :8082 / ga-frontend :3000 / ga-mobile-pwa :3001

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-27 22:53:59 +09:00
parent ee3e0d40c5
commit 2c9bf710bc
24 changed files with 727 additions and 18 deletions
@@ -26,6 +26,7 @@ public interface UserMapper {
int incrementFailCount(@Param("userId") Long userId);
int resetFailCount(@Param("userId") Long userId);
int updateStatus(@Param("userId") Long userId, @Param("status") String status);
int updatePhone(@Param("userId") Long userId, @Param("phone") String phone);
List<String> selectRoleCodes(@Param("userId") Long userId);
@@ -33,4 +33,17 @@ public interface WithdrawRequestMapper {
* account_no 는 복호화되어 반환.
*/
List<WithdrawRequestVO> selectFailedTransfers(@Param("limit") int limit);
/**
* SETTLED 동기화 대상 — transfer_status='ACCEPTED' 이고 status='SENT'.
* queryStatus 폴링 결과로 입금 확정/실패를 판정한다.
*/
List<WithdrawRequestVO> selectAcceptedTransfers(@Param("limit") int limit);
/** queryStatus 가 SETTLED 응답: status='COMPLETED', transfer_status='SETTLED', completed_at=NOW(). */
int markSettled(@Param("requestId") Long requestId);
/** queryStatus 가 FAILED 응답: status='APPROVED'(재시도 풀로), transfer_status='FAILED', fail_reason. */
int markSettleFailed(@Param("requestId") Long requestId,
@Param("failReason") String failReason);
}
@@ -110,6 +110,10 @@
UPDATE users SET status = #{status} WHERE user_id = #{userId}
</update>
<update id="updatePhone">
UPDATE users SET phone = #{phone,jdbcType=VARCHAR} WHERE user_id = #{userId}
</update>
<select id="selectRoleCodes" resultType="string">
SELECT r.role_code FROM role r
JOIN user_role ur ON ur.role_id = r.role_id
@@ -176,4 +176,43 @@
WHERE request_id = #{requestId}
</update>
<!-- SETTLED 동기화 대상 — 펌뱅킹 어댑터로 보냈고(SENT) 외부 응답이 접수만(ACCEPTED) 된 건 -->
<select id="selectAcceptedTransfers" resultMap="VOMap">
SELECT wr.request_id, wr.settle_master_id, wr.agent_id, wr.amount,
wr.requested_by, wr.requested_at, wr.approval_request_id,
wr.bank_code, wr.account_no, wr.status,
wr.sent_at, wr.completed_at, wr.fail_reason,
wr.transfer_tx_id, wr.transfer_status, wr.transfer_at,
wr.created_at, wr.updated_at
FROM withdraw_request wr
WHERE wr.transfer_status = 'ACCEPTED'
AND wr.status = 'SENT'
AND wr.transfer_tx_id IS NOT NULL
ORDER BY wr.transfer_at NULLS FIRST, wr.request_id
LIMIT #{limit}
</select>
<!-- 입금 확정 — SETTLED -->
<update id="markSettled">
UPDATE withdraw_request
SET transfer_status = 'SETTLED',
status = 'COMPLETED',
completed_at = NOW(),
fail_reason = NULL,
updated_at = NOW()
WHERE request_id = #{requestId}
AND status = 'SENT'
</update>
<!-- 입금 실패 동기화 — 재시도 풀(APPROVED + FAILED) 로 회귀 -->
<update id="markSettleFailed">
UPDATE withdraw_request
SET transfer_status = 'FAILED',
status = 'APPROVED',
fail_reason = #{failReason,jdbcType=VARCHAR},
updated_at = NOW()
WHERE request_id = #{requestId}
AND status = 'SENT'
</update>
</mapper>