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));
|
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")
|
@GetMapping("/notifications")
|
||||||
public ApiResponse<PageResponse<UserNotificationResp>> notifications(
|
public ApiResponse<PageResponse<UserNotificationResp>> notifications(
|
||||||
@ModelAttribute UserNotificationSearchParam param) {
|
@ModelAttribute UserNotificationSearchParam param) {
|
||||||
|
|||||||
@@ -131,6 +131,14 @@ public class MobileMeService {
|
|||||||
return statementService.download(statementId);
|
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 드롭다운 용. */
|
/** 본인 출금 가능 정산 마스터 목록. 출금 신청 폼의 settleMasterId 드롭다운 용. */
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public List<SettleMasterResp> withdrawableSettleMasters() {
|
public List<SettleMasterResp> withdrawableSettleMasters() {
|
||||||
@@ -143,6 +151,16 @@ public class MobileMeService {
|
|||||||
return settleMasterMapper.selectList(param);
|
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 는 토큰에서 강제 결정.
|
* 본인 출금 신청 등록. 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.BankTransferAdapter;
|
||||||
import com.ga.common.adapter.bank.BankTransferResponse;
|
import com.ga.common.adapter.bank.BankTransferResponse;
|
||||||
import com.ga.common.adapter.bank.BankTransferStatus;
|
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.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 com.ga.core.vo.withdraw.WithdrawRequestVO;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -34,6 +37,7 @@ public class WithdrawBankSettleService {
|
|||||||
|
|
||||||
private final WithdrawRequestMapper mapper;
|
private final WithdrawRequestMapper mapper;
|
||||||
private final BankTransferAdapter bankTransferAdapter;
|
private final BankTransferAdapter bankTransferAdapter;
|
||||||
|
private final UserNotificationMapper notificationMapper;
|
||||||
|
|
||||||
@Value("${app.bank-settle.limit:50}")
|
@Value("${app.bank-settle.limit:50}")
|
||||||
private int batchLimit;
|
private int batchLimit;
|
||||||
@@ -56,11 +60,17 @@ public class WithdrawBankSettleService {
|
|||||||
|
|
||||||
if (status == BankTransferStatus.SETTLED) {
|
if (status == BankTransferStatus.SETTLED) {
|
||||||
int n = mapper.markSettled(vo.getRequestId());
|
int n = mapper.markSettled(vo.getRequestId());
|
||||||
if (n > 0) changed++;
|
if (n > 0) {
|
||||||
|
changed++;
|
||||||
|
notify(vo, true, null);
|
||||||
|
}
|
||||||
} else if (status == BankTransferStatus.FAILED) {
|
} else if (status == BankTransferStatus.FAILED) {
|
||||||
String reason = resp.getResultMessage() != null ? resp.getResultMessage() : "settle failed";
|
String reason = resp.getResultMessage() != null ? resp.getResultMessage() : "settle failed";
|
||||||
int n = mapper.markSettleFailed(vo.getRequestId(), reason);
|
int n = mapper.markSettleFailed(vo.getRequestId(), reason);
|
||||||
if (n > 0) changed++;
|
if (n > 0) {
|
||||||
|
changed++;
|
||||||
|
notify(vo, false, reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// ACCEPTED 유지 시 no-op
|
// ACCEPTED 유지 시 no-op
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -72,6 +82,34 @@ public class WithdrawBankSettleService {
|
|||||||
return changed;
|
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 기반 자동 동기화.
|
* cron 기반 자동 동기화.
|
||||||
* yml 에 {@code app.bank-settle.cron} 미설정 시 매년 1월 1일 0시(=사실상 비활성)로 작동.
|
* yml 에 {@code app.bank-settle.cron} 미설정 시 매년 1월 1일 0시(=사실상 비활성)로 작동.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import StatementDetail from '@/pages/StatementDetail';
|
|||||||
import WithdrawList from '@/pages/WithdrawList';
|
import WithdrawList from '@/pages/WithdrawList';
|
||||||
import WithdrawCreate from '@/pages/WithdrawCreate';
|
import WithdrawCreate from '@/pages/WithdrawCreate';
|
||||||
import NoticeList from '@/pages/NoticeList';
|
import NoticeList from '@/pages/NoticeList';
|
||||||
|
import Profile from '@/pages/Profile';
|
||||||
|
|
||||||
function AuthedLayout({ children }: { children: React.ReactNode }) {
|
function AuthedLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@@ -70,6 +71,14 @@ export default function App() {
|
|||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/profile"
|
||||||
|
element={
|
||||||
|
<RequireAuth>
|
||||||
|
<AuthedLayout><Profile /></AuthedLayout>
|
||||||
|
</RequireAuth>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,5 +37,9 @@ export const authApi = {
|
|||||||
me: () => unwrap<MeResp>(api.get('/api/auth/me')),
|
me: () => unwrap<MeResp>(api.get('/api/auth/me')),
|
||||||
refresh: (refreshToken: string) =>
|
refresh: (refreshToken: string) =>
|
||||||
unwrap<LoginResp>(api.post('/api/auth/refresh', { refreshToken })),
|
unwrap<LoginResp>(api.post('/api/auth/refresh', { refreshToken })),
|
||||||
|
changePassword: (oldPassword: string, newPassword: string) =>
|
||||||
|
unwrap<void>(api.post('/api/auth/password', { oldPassword, newPassword })),
|
||||||
|
updateMyPhone: (phone: string) =>
|
||||||
|
unwrap<void>(api.put('/api/mobile/me/profile/phone', { phone })),
|
||||||
logout: () => api.post('/api/auth/logout').catch(() => undefined),
|
logout: () => api.post('/api/auth/logout').catch(() => undefined),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,4 +53,8 @@ export const withdrawApi = {
|
|||||||
/** 본인 출금 신청 등록. */
|
/** 본인 출금 신청 등록. */
|
||||||
create: (req: WithdrawCreateReq) =>
|
create: (req: WithdrawCreateReq) =>
|
||||||
unwrap<number>(api.post('/api/mobile/me/withdraw-requests', req)),
|
unwrap<number>(api.post('/api/mobile/me/withdraw-requests', req)),
|
||||||
|
|
||||||
|
/** 본인 출금 신청 취소 (REQUESTED 상태만). */
|
||||||
|
cancel: (requestId: number) =>
|
||||||
|
unwrap<void>(api.post(`/api/mobile/me/withdraw-requests/${requestId}/cancel`)),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TabBar } from 'antd-mobile';
|
import { TabBar } from 'antd-mobile';
|
||||||
import { AppOutline, FileOutline, PayCircleOutline, SoundOutline } from 'antd-mobile-icons';
|
import { AppOutline, FileOutline, PayCircleOutline, SoundOutline, UserOutline } from 'antd-mobile-icons';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@@ -7,6 +7,7 @@ const tabs = [
|
|||||||
{ key: '/statement', title: '명세서', icon: <FileOutline /> },
|
{ key: '/statement', title: '명세서', icon: <FileOutline /> },
|
||||||
{ key: '/withdraw', title: '출금', icon: <PayCircleOutline /> },
|
{ key: '/withdraw', title: '출금', icon: <PayCircleOutline /> },
|
||||||
{ key: '/notice', title: '공지', icon: <SoundOutline /> },
|
{ key: '/notice', title: '공지', icon: <SoundOutline /> },
|
||||||
|
{ key: '/profile', title: '내정보', icon: <UserOutline /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function MobileTabBar() {
|
export default function MobileTabBar() {
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { Button, Card, Dialog, Form, Input, List, NavBar, Toast } from 'antd-mobile';
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { authApi } from '@/api/auth';
|
||||||
|
import { useAuthStore } from '@/stores/authStore';
|
||||||
|
|
||||||
|
export default function Profile() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const clear = useAuthStore((s) => s.clear);
|
||||||
|
const setProfile = useAuthStore((s) => s.setProfile);
|
||||||
|
const [phoneOpen, setPhoneOpen] = useState(false);
|
||||||
|
const [phoneInput, setPhoneInput] = useState('');
|
||||||
|
const [pwOpen, setPwOpen] = useState(false);
|
||||||
|
const [pwForm] = Form.useForm<{ oldPassword: string; newPassword: string; confirm: string }>();
|
||||||
|
|
||||||
|
const { data: me, refetch } = useQuery({
|
||||||
|
queryKey: ['mobile.me.profile'],
|
||||||
|
queryFn: () => authApi.me(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitPhone = async () => {
|
||||||
|
try {
|
||||||
|
await authApi.updateMyPhone(phoneInput.trim());
|
||||||
|
Toast.show({ icon: 'success', content: '전화번호 저장 완료' });
|
||||||
|
setPhoneOpen(false);
|
||||||
|
await refetch();
|
||||||
|
const fresh = await authApi.me().catch(() => null);
|
||||||
|
if (fresh) setProfile(fresh);
|
||||||
|
} catch {
|
||||||
|
// interceptor 처리
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitPassword = async () => {
|
||||||
|
try {
|
||||||
|
const v = await pwForm.validateFields();
|
||||||
|
if (v.newPassword !== v.confirm) {
|
||||||
|
Toast.show({ icon: 'fail', content: '새 비밀번호 확인이 일치하지 않습니다' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await authApi.changePassword(v.oldPassword, v.newPassword);
|
||||||
|
Toast.show({ icon: 'success', content: '비밀번호 변경 완료' });
|
||||||
|
setPwOpen(false);
|
||||||
|
pwForm.resetFields();
|
||||||
|
} catch {
|
||||||
|
// 검증 실패 또는 서버 오류
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLogout = () => {
|
||||||
|
Dialog.confirm({
|
||||||
|
content: '로그아웃 하시겠습니까?',
|
||||||
|
onConfirm: async () => {
|
||||||
|
await authApi.logout();
|
||||||
|
queryClient.clear();
|
||||||
|
clear();
|
||||||
|
navigate('/login', { replace: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<NavBar back={null}>내 정보</NavBar>
|
||||||
|
<div style={{ padding: 12 }}>
|
||||||
|
<Card title={me?.userName ?? me?.loginId ?? '-'}>
|
||||||
|
<List>
|
||||||
|
<List.Item extra={me?.loginId ?? '-'}>아이디</List.Item>
|
||||||
|
<List.Item extra={me?.userName ?? '-'}>이름</List.Item>
|
||||||
|
<List.Item extra={me?.email ?? '-'}>이메일</List.Item>
|
||||||
|
<List.Item
|
||||||
|
extra={me?.phone ?? <span style={{ color: '#bbb' }}>미등록</span>}
|
||||||
|
clickable
|
||||||
|
onClick={() => { setPhoneInput(me?.phone ?? ''); setPhoneOpen(true); }}
|
||||||
|
>
|
||||||
|
전화번호
|
||||||
|
</List.Item>
|
||||||
|
<List.Item extra={me?.agentName ?? <span style={{ color: '#bbb' }}>미연결</span>}>설계사</List.Item>
|
||||||
|
<List.Item extra={me?.orgName ?? '-'}>소속</List.Item>
|
||||||
|
</List>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card style={{ marginTop: 12 }}>
|
||||||
|
<List>
|
||||||
|
<List.Item clickable onClick={() => setPwOpen(true)}>비밀번호 변경</List.Item>
|
||||||
|
<List.Item clickable onClick={onLogout} style={{ color: '#ff3141' }}>로그아웃</List.Item>
|
||||||
|
</List>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
visible={phoneOpen}
|
||||||
|
title="전화번호 등록"
|
||||||
|
content={
|
||||||
|
<div>
|
||||||
|
<Input placeholder="010-1234-5678" value={phoneInput} onChange={setPhoneInput} autoFocus />
|
||||||
|
<div style={{ marginTop: 8, fontSize: 12, color: '#999' }}>
|
||||||
|
SMS 알림 발송에 사용됩니다.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
actions={[
|
||||||
|
[
|
||||||
|
{ key: 'cancel', text: '취소', onClick: () => setPhoneOpen(false) },
|
||||||
|
{ key: 'save', text: '저장', bold: true, onClick: submitPhone },
|
||||||
|
],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
visible={pwOpen}
|
||||||
|
title="비밀번호 변경"
|
||||||
|
content={
|
||||||
|
<Form form={pwForm} layout="vertical">
|
||||||
|
<Form.Item name="oldPassword" label="현재 비밀번호" rules={[{ required: true, message: '필수' }]}>
|
||||||
|
<Input type="password" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="newPassword" label="새 비밀번호" rules={[{ required: true, min: 8, message: '8자 이상' }]}>
|
||||||
|
<Input type="password" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="confirm" label="새 비밀번호 확인" rules={[{ required: true, message: '필수' }]}>
|
||||||
|
<Input type="password" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
}
|
||||||
|
actions={[
|
||||||
|
[
|
||||||
|
{ key: 'cancel', text: '취소', onClick: () => { setPwOpen(false); pwForm.resetFields(); } },
|
||||||
|
{ key: 'save', text: '변경', bold: true, onClick: submitPassword },
|
||||||
|
],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button block style={{ marginTop: 16 }} onClick={onLogout}>로그아웃</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, ErrorBlock, List, NavBar, PullToRefresh, Tag } from 'antd-mobile';
|
import { Button, Dialog, ErrorBlock, List, NavBar, PullToRefresh, SwipeAction, Tag, Toast } from 'antd-mobile';
|
||||||
import { AddOutline } from 'antd-mobile-icons';
|
import { AddOutline } from 'antd-mobile-icons';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@@ -48,19 +48,40 @@ export default function WithdrawList() {
|
|||||||
)}
|
)}
|
||||||
{items.length > 0 && (
|
{items.length > 0 && (
|
||||||
<List>
|
<List>
|
||||||
{items.map((w: WithdrawResp) => (
|
{items.map((w: WithdrawResp) => {
|
||||||
<List.Item
|
const cancellable = w.status === 'REQUESTED';
|
||||||
key={w.requestId}
|
const row = (
|
||||||
title={<span>#{w.requestId} {w.settleMonth ?? ''}</span>}
|
<List.Item
|
||||||
description={
|
key={w.requestId}
|
||||||
<span style={{ fontSize: 12 }}>
|
title={<span>#{w.requestId} {w.settleMonth ?? ''}</span>}
|
||||||
{w.bankCode ?? '-'} · {w.accountNo ?? '-'} · {w.amount.toLocaleString()}원
|
description={
|
||||||
{w.transferTxId && <> · txId={w.transferTxId}</>}
|
<span style={{ fontSize: 12 }}>
|
||||||
</span>
|
{w.bankCode ?? '-'} · {w.accountNo ?? '-'} · {w.amount.toLocaleString()}원
|
||||||
}
|
{w.transferTxId && <> · txId={w.transferTxId}</>}
|
||||||
extra={<Tag color={STATUS_COLOR[w.status] ?? 'default'}>{w.statusName ?? w.status}</Tag>}
|
</span>
|
||||||
/>
|
}
|
||||||
))}
|
extra={<Tag color={STATUS_COLOR[w.status] ?? 'default'}>{w.statusName ?? w.status}</Tag>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (!cancellable) return row;
|
||||||
|
return (
|
||||||
|
<SwipeAction
|
||||||
|
key={w.requestId}
|
||||||
|
rightActions={[{
|
||||||
|
key: 'cancel', text: '취소', color: 'danger',
|
||||||
|
onClick: async () => {
|
||||||
|
const ok = await Dialog.confirm({ content: `#${w.requestId} 출금 신청을 취소하시겠습니까?` });
|
||||||
|
if (!ok) return;
|
||||||
|
await withdrawApi.cancel(w.requestId);
|
||||||
|
Toast.show({ icon: 'success', content: '취소되었습니다' });
|
||||||
|
await refetch();
|
||||||
|
},
|
||||||
|
}]}
|
||||||
|
>
|
||||||
|
{row}
|
||||||
|
</SwipeAction>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user