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:
@@ -35,6 +35,8 @@ export const userApi = {
|
||||
update: (id: number, req: UserSaveReq) => unwrap<void>(api.put(`/api/system/users/${id}`, req)),
|
||||
resetPassword: (id: number) => unwrap<void>(api.put(`/api/system/users/${id}/reset-password`)),
|
||||
unlock: (id: number) => unwrap<void>(api.put(`/api/system/users/${id}/unlock`)),
|
||||
updatePhone: (id: number, phone: string) =>
|
||||
unwrap<void>(api.put(`/api/system/users/${id}/phone`, { phone })),
|
||||
};
|
||||
|
||||
export interface RoleVO {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef } from 'react';
|
||||
import { Modal, Space, message } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Input, Modal, Space, message } from 'antd';
|
||||
import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
@@ -31,6 +31,15 @@ export default function UserList() {
|
||||
onOk: async () => { await userApi.unlock(id); message.success('해제 완료'); refresh(); },
|
||||
});
|
||||
|
||||
const [phoneEdit, setPhoneEdit] = useState<{ id: number; loginId: string; phone: string } | null>(null);
|
||||
const submitPhone = async () => {
|
||||
if (!phoneEdit) return;
|
||||
await userApi.updatePhone(phoneEdit.id, phoneEdit.phone);
|
||||
message.success('전화번호 저장 완료');
|
||||
setPhoneEdit(null);
|
||||
refresh();
|
||||
};
|
||||
|
||||
const columns: ProColumns<UserResp>[] = [
|
||||
{
|
||||
title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
|
||||
@@ -44,14 +53,20 @@ export default function UserList() {
|
||||
{ title: '로그인 ID', dataIndex: 'loginId', width: 120, search: false },
|
||||
{ title: '이름', dataIndex: 'userName', width: 120, search: false },
|
||||
{ title: '이메일', dataIndex: 'email', width: 200, search: false },
|
||||
{ title: '전화번호', dataIndex: 'phone', width: 140, search: false,
|
||||
render: (_, r) => r.phone ?? <span style={{ color: '#bbb' }}>미등록</span> },
|
||||
{ title: '소속', dataIndex: 'orgName', width: 160, search: false },
|
||||
{ title: '연결 설계사', dataIndex: 'agentName', width: 100, search: false },
|
||||
{ title: '실패횟수', dataIndex: 'failCount', width: 90, align: 'right', search: false },
|
||||
{ title: '마지막 로그인', dataIndex: 'lastLoginAt', width: 160, search: false },
|
||||
{
|
||||
title: '액션', width: 220, fixed: 'right', search: false,
|
||||
title: '액션', width: 300, fixed: 'right', search: false,
|
||||
render: (_, r) => (
|
||||
<Space>
|
||||
<PermissionButton menuCode="SYSTEM_USER" permCode="UPDATE" size="small"
|
||||
onClick={() => setPhoneEdit({ id: r.userId, loginId: r.loginId, phone: r.phone ?? '' })}>
|
||||
전화번호
|
||||
</PermissionButton>
|
||||
<PermissionButton menuCode="SYSTEM_USER" permCode="UPDATE" size="small"
|
||||
onClick={() => onReset(r.userId)}>비번초기화</PermissionButton>
|
||||
{r.status === 'LOCKED' && (
|
||||
@@ -83,6 +98,27 @@ export default function UserList() {
|
||||
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||
dateFormatter="string"
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={`전화번호 등록${phoneEdit ? ` — ${phoneEdit.loginId}` : ''}`}
|
||||
open={!!phoneEdit}
|
||||
onCancel={() => setPhoneEdit(null)}
|
||||
onOk={submitPhone}
|
||||
okText="저장"
|
||||
cancelText="취소"
|
||||
destroyOnClose
|
||||
>
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="010-1234-5678"
|
||||
value={phoneEdit?.phone ?? ''}
|
||||
onChange={(e) => setPhoneEdit((p) => (p ? { ...p, phone: e.target.value } : p))}
|
||||
onPressEnter={submitPhone}
|
||||
/>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: '#999' }}>
|
||||
SMS 결재 알림 발송 시 사용됩니다. 비워두면 SMS 발송이 스킵됩니다.
|
||||
</div>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user