feat: P6 후속 4 — 출금 폼 잔여 한도 미리 표시

- 백엔드 GET /api/mobile/me/settle-masters/{id}/withdrawable-balance
  · 응답 {settleId, netAmount, usedAmount, remainingAmount}
  · 본인 settle_master 검증 후 net - sumActiveAmountByMaster 반환
  · createWithdrawRequest 가 동일 조회 로직 재사용 (중복 제거)
- PWA WithdrawCreate: settleMaster 선택 시 잔액 카드 표시 + amount 클라이언트 사전 검증

검증: ga-api compileJava BUILD SUCCESSFUL / ga-mobile-pwa tsc --noEmit 0 오류
(라이브 e2e 는 DB/docker 미가동으로 차단 — 환경 복구 후 스모크 예정)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-29 20:51:31 +09:00
parent 366f49c3f0
commit d8d64625a9
4 changed files with 88 additions and 13 deletions
+13
View File
@@ -41,6 +41,13 @@ export interface WithdrawCreateReq {
accountNo: string;
}
export interface WithdrawableBalance {
settleId: number;
netAmount: number;
usedAmount: number;
remainingAmount: number;
}
export const withdrawApi = {
/** 본인 출금 신청 이력 — 서버측이 토큰 → agentId 강제 필터. */
list: (params: WithdrawSearchParam = {}) =>
@@ -50,6 +57,12 @@ export const withdrawApi = {
settleMasters: () =>
unwrap<SettleMasterResp[]>(api.get('/api/mobile/me/settle-masters')),
/** 선택한 정산 마스터의 출금 가능 잔액 (net - 활성 출금합계). */
withdrawableBalance: (settleMasterId: number) =>
unwrap<WithdrawableBalance>(
api.get(`/api/mobile/me/settle-masters/${settleMasterId}/withdrawable-balance`),
),
/** 본인 출금 신청 등록. */
create: (req: WithdrawCreateReq) =>
unwrap<number>(api.post('/api/mobile/me/withdraw-requests', req)),
@@ -18,6 +18,7 @@ export default function WithdrawCreate() {
const [form] = Form.useForm<FormValues>();
const [pickerVisible, setPickerVisible] = useState(false);
const [pickedLabel, setPickedLabel] = useState<string>('');
const [selectedId, setSelectedId] = useState<number | undefined>(undefined);
const { data: settleMasters = [], isLoading } = useQuery({
queryKey: ['mobile.me.settle-masters'],
@@ -25,6 +26,13 @@ export default function WithdrawCreate() {
retry: false,
});
const { data: balance } = useQuery({
queryKey: ['mobile.me.withdrawable-balance', selectedId],
queryFn: () => withdrawApi.withdrawableBalance(selectedId as number),
enabled: selectedId != null,
retry: false,
});
const pickerColumns: PickerColumn[] = useMemo(
() => [
settleMasters.map((s: SettleMasterResp) => ({
@@ -56,6 +64,10 @@ export default function WithdrawCreate() {
Toast.show({ icon: 'fail', content: '금액을 입력하세요' });
return;
}
if (balance && amount > balance.remainingAmount) {
Toast.show({ icon: 'fail', content: `출금 가능 잔액(${fmt(balance.remainingAmount)}원)을 초과합니다` });
return;
}
create.mutate({
settleMasterId: Number(values.settleMasterId),
amount,
@@ -67,6 +79,25 @@ export default function WithdrawCreate() {
return (
<div className="page">
<NavBar onBack={() => navigate(-1)}> </NavBar>
{balance && (
<div
style={{
margin: '12px',
padding: '12px 16px',
background: '#e6f4ff',
borderRadius: 8,
fontSize: 14,
}}
>
<div style={{ color: '#666' }}> </div>
<div style={{ fontSize: 22, fontWeight: 700, color: '#1677ff' }}>
{fmt(balance.remainingAmount)}
</div>
<div style={{ color: '#999', fontSize: 12, marginTop: 4 }}>
{fmt(balance.netAmount)} · {fmt(balance.usedAmount)}
</div>
</div>
)}
<Form
form={form}
layout="horizontal"
@@ -107,6 +138,7 @@ export default function WithdrawCreate() {
onConfirm={(val) => {
const id = val[0] != null ? Number(val[0]) : undefined;
form.setFieldValue('settleMasterId', id);
setSelectedId(id);
const picked = settleMasters.find((s) => String(s.settleId) === val[0]);
if (picked) setPickedLabel(`${picked.settleMonth} · 실수령 ${fmt(picked.netAmount)}`);
}}