feat(WEB): 입금/출금 관리 계좌 선택 및 데이터 변환 개선
- 입금/출금 상세에 계좌 선택 기능 추가 (bankAccountId 필드) - API 변환 로직 개선 (계좌명 포맷, depositType 처리) - getBankAccounts 액션 추가 - payment_method 기본값 설정
This commit is contained in:
@@ -12,8 +12,8 @@ import {
|
||||
createDeposit,
|
||||
updateDeposit,
|
||||
deleteDeposit,
|
||||
|
||||
getVendors,
|
||||
getBankAccounts,
|
||||
} from './actions';
|
||||
import { useDevFill, generateDepositData } from '@/components/dev';
|
||||
|
||||
@@ -35,11 +35,15 @@ export default function DepositDetailClientV2({
|
||||
// ===== DevFill: 자동 입력 기능 =====
|
||||
useDevFill('deposit', useCallback(async () => {
|
||||
if (initialMode === 'create') {
|
||||
// 거래처 목록 가져오기
|
||||
const vendorResult = await getVendors();
|
||||
// 거래처 및 계좌 목록 가져오기
|
||||
const [vendorResult, bankAccountResult] = await Promise.all([
|
||||
getVendors(),
|
||||
getBankAccounts(),
|
||||
]);
|
||||
const vendors = vendorResult.success ? vendorResult.data : undefined;
|
||||
const bankAccounts = bankAccountResult.success ? bankAccountResult.data : undefined;
|
||||
|
||||
const mockData = generateDepositData({ vendors });
|
||||
const mockData = generateDepositData({ vendors, bankAccounts });
|
||||
setDeposit(mockData as unknown as DepositRecord);
|
||||
toast.success('입금 데이터가 자동 입력되었습니다.');
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ function transformApiToFrontend(apiData: DepositApiData): DepositRecord {
|
||||
depositAmount: typeof apiData.amount === 'string'
|
||||
? parseFloat(apiData.amount)
|
||||
: (apiData.amount ?? 0),
|
||||
accountName: apiData.bank_account?.account_name || '',
|
||||
bankAccountId: apiData.bank_account_id ? String(apiData.bank_account_id) : '',
|
||||
accountName: apiData.bank_account
|
||||
? `${apiData.bank_account.bank_name} ${apiData.bank_account.account_name}`
|
||||
: '',
|
||||
depositorName: apiData.client_name || apiData.client?.name || '',
|
||||
note: apiData.description || '',
|
||||
depositType: (apiData.account_code || 'unset') as DepositType,
|
||||
@@ -61,9 +64,18 @@ function transformFrontendToApi(data: Partial<DepositRecord>): Record<string, un
|
||||
if (data.depositAmount !== undefined) result.amount = data.depositAmount;
|
||||
if (data.depositorName !== undefined) result.client_name = data.depositorName;
|
||||
if (data.note !== undefined) result.description = data.note || null;
|
||||
if (data.depositType !== undefined) result.account_code = data.depositType;
|
||||
// 'unset'은 미설정 상태이므로 API에 null로 전송
|
||||
if (data.depositType !== undefined) {
|
||||
result.account_code = data.depositType === 'unset' ? null : data.depositType;
|
||||
}
|
||||
if (data.vendorId !== undefined) result.client_id = data.vendorId ? parseInt(data.vendorId, 10) : null;
|
||||
// accountName, vendorName은 관계 데이터이므로 직접 저장하지 않음
|
||||
// 계좌 ID
|
||||
if (data.bankAccountId !== undefined) {
|
||||
result.bank_account_id = data.bankAccountId ? parseInt(data.bankAccountId, 10) : null;
|
||||
}
|
||||
// payment_method는 API 필수 필드 - 기본값 'transfer'(계좌이체)
|
||||
// 유효값: cash, transfer, card, check
|
||||
result.payment_method = 'transfer';
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -328,3 +340,37 @@ export async function getVendors(): Promise<{
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 계좌 목록 조회 =====
|
||||
export async function getBankAccounts(): Promise<{
|
||||
success: boolean;
|
||||
data: { id: string; name: string }[];
|
||||
error?: string;
|
||||
}> {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts?per_page=100`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, data: [], error: result.message };
|
||||
}
|
||||
|
||||
const accounts = result.data?.data || result.data || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: accounts.map((a: { id: number; account_name: string; bank_name: string }) => ({
|
||||
id: String(a.id),
|
||||
name: `${a.bank_name} ${a.account_name}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Banknote } from 'lucide-react';
|
||||
import type { DetailConfig, FieldDefinition } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
import type { DepositRecord } from './types';
|
||||
import { DEPOSIT_TYPE_SELECTOR_OPTIONS } from './types';
|
||||
import { getVendors } from './actions';
|
||||
import { getVendors, getBankAccounts } from './actions';
|
||||
|
||||
// ===== 필드 정의 =====
|
||||
const fields: FieldDefinition[] = [
|
||||
@@ -16,10 +16,20 @@ const fields: FieldDefinition[] = [
|
||||
},
|
||||
// 입금계좌
|
||||
{
|
||||
key: 'accountName',
|
||||
key: 'bankAccountId',
|
||||
label: '입금계좌',
|
||||
type: 'text',
|
||||
placeholder: '입금계좌를 입력해주세요',
|
||||
type: 'select',
|
||||
placeholder: '선택',
|
||||
fetchOptions: async () => {
|
||||
const result = await getBankAccounts();
|
||||
if (result.success) {
|
||||
return result.data.map((a) => ({
|
||||
value: a.id,
|
||||
label: a.name,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
disabled: (mode) => mode === 'view',
|
||||
},
|
||||
// 입금자명
|
||||
@@ -105,7 +115,7 @@ export const depositDetailConfig: DetailConfig = {
|
||||
const record = data as unknown as DepositRecord;
|
||||
return {
|
||||
depositDate: record.depositDate || '',
|
||||
accountName: record.accountName || '',
|
||||
bankAccountId: record.bankAccountId || '',
|
||||
depositorName: record.depositorName || '',
|
||||
depositAmount: record.depositAmount || 0,
|
||||
note: record.note || '',
|
||||
@@ -116,7 +126,7 @@ export const depositDetailConfig: DetailConfig = {
|
||||
transformSubmitData: (formData: Record<string, unknown>): Partial<DepositRecord> => {
|
||||
return {
|
||||
depositDate: formData.depositDate as string,
|
||||
accountName: formData.accountName as string,
|
||||
bankAccountId: formData.bankAccountId as string,
|
||||
depositorName: formData.depositorName as string,
|
||||
depositAmount: formData.depositAmount ? Number(formData.depositAmount) : 0,
|
||||
note: formData.note as string,
|
||||
|
||||
@@ -117,6 +117,7 @@ export interface DepositRecord {
|
||||
id: string;
|
||||
depositDate: string; // 입금일
|
||||
depositAmount: number; // 입금액
|
||||
bankAccountId?: string; // 입금계좌 ID
|
||||
accountName: string; // 입금계좌명
|
||||
depositorName: string; // 입금자명
|
||||
note: string; // 적요
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
updateWithdrawal,
|
||||
deleteWithdrawal,
|
||||
getVendors,
|
||||
getBankAccounts,
|
||||
} from './actions';
|
||||
import { useDevFill, generateWithdrawalData } from '@/components/dev';
|
||||
|
||||
@@ -34,11 +35,15 @@ export default function WithdrawalDetailClientV2({
|
||||
// ===== DevFill: 자동 입력 기능 =====
|
||||
useDevFill('withdrawal', useCallback(async () => {
|
||||
if (initialMode === 'create') {
|
||||
// 거래처 목록 가져오기
|
||||
const vendorResult = await getVendors();
|
||||
// 거래처 및 계좌 목록 가져오기
|
||||
const [vendorResult, bankAccountResult] = await Promise.all([
|
||||
getVendors(),
|
||||
getBankAccounts(),
|
||||
]);
|
||||
const vendors = vendorResult.success ? vendorResult.data : undefined;
|
||||
const bankAccounts = bankAccountResult.success ? bankAccountResult.data : undefined;
|
||||
|
||||
const mockData = generateWithdrawalData({ vendors });
|
||||
const mockData = generateWithdrawalData({ vendors, bankAccounts });
|
||||
setWithdrawal(mockData as unknown as WithdrawalRecord);
|
||||
toast.success('출금 데이터가 자동 입력되었습니다.');
|
||||
}
|
||||
|
||||
@@ -45,7 +45,10 @@ function transformApiToFrontend(apiData: WithdrawalApiData): WithdrawalRecord {
|
||||
withdrawalAmount: typeof apiData.amount === 'string'
|
||||
? parseFloat(apiData.amount)
|
||||
: (apiData.amount ?? 0),
|
||||
accountName: apiData.bank_account?.account_name || '',
|
||||
bankAccountId: apiData.bank_account_id ? String(apiData.bank_account_id) : '',
|
||||
accountName: apiData.bank_account
|
||||
? `${apiData.bank_account.bank_name} ${apiData.bank_account.account_name}`
|
||||
: '',
|
||||
recipientName: apiData.merchant_name || apiData.client_name || apiData.client?.name || '',
|
||||
note: apiData.description || '',
|
||||
withdrawalType: (apiData.account_code || 'unset') as WithdrawalType,
|
||||
@@ -64,9 +67,18 @@ function transformFrontendToApi(data: Partial<WithdrawalRecord>): Record<string,
|
||||
if (data.withdrawalAmount !== undefined) result.amount = data.withdrawalAmount;
|
||||
if (data.recipientName !== undefined) result.client_name = data.recipientName;
|
||||
if (data.note !== undefined) result.description = data.note || null;
|
||||
if (data.withdrawalType !== undefined) result.account_code = data.withdrawalType;
|
||||
// 'unset'은 미설정 상태이므로 API에 null로 전송
|
||||
if (data.withdrawalType !== undefined) {
|
||||
result.account_code = data.withdrawalType === 'unset' ? null : data.withdrawalType;
|
||||
}
|
||||
if (data.vendorId !== undefined) result.client_id = data.vendorId ? parseInt(data.vendorId, 10) : null;
|
||||
// accountName, vendorName은 관계 데이터이므로 직접 저장하지 않음
|
||||
// 계좌 ID
|
||||
if (data.bankAccountId !== undefined) {
|
||||
result.bank_account_id = data.bankAccountId ? parseInt(data.bankAccountId, 10) : null;
|
||||
}
|
||||
// payment_method는 API 필수 필드 - 기본값 'transfer'(계좌이체)
|
||||
// 유효값: cash, transfer, card, check
|
||||
result.payment_method = 'transfer';
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -331,3 +343,37 @@ export async function getVendors(): Promise<{
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 계좌 목록 조회 =====
|
||||
export async function getBankAccounts(): Promise<{
|
||||
success: boolean;
|
||||
data: { id: string; name: string }[];
|
||||
error?: string;
|
||||
}> {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts?per_page=100`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error) {
|
||||
return { success: false, data: [], error: error.message };
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
return { success: false, data: [], error: `API 오류: ${response?.status}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, data: [], error: result.message };
|
||||
}
|
||||
|
||||
const accounts = result.data?.data || result.data || [];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: accounts.map((a: { id: number; account_name: string; bank_name: string }) => ({
|
||||
id: String(a.id),
|
||||
name: `${a.bank_name} ${a.account_name}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface WithdrawalRecord {
|
||||
id: string;
|
||||
withdrawalDate: string; // 출금일
|
||||
withdrawalAmount: number; // 출금액
|
||||
bankAccountId?: string; // 출금계좌 ID
|
||||
accountName: string; // 출금계좌명
|
||||
recipientName: string; // 수취인명
|
||||
note: string; // 적요
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Banknote } from 'lucide-react';
|
||||
import type { DetailConfig, FieldDefinition } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
import type { WithdrawalRecord } from './types';
|
||||
import { WITHDRAWAL_TYPE_SELECTOR_OPTIONS } from './types';
|
||||
import { getVendors } from './actions';
|
||||
import { getVendors, getBankAccounts } from './actions';
|
||||
|
||||
// ===== 필드 정의 =====
|
||||
const fields: FieldDefinition[] = [
|
||||
@@ -16,10 +16,20 @@ const fields: FieldDefinition[] = [
|
||||
},
|
||||
// 출금계좌
|
||||
{
|
||||
key: 'accountName',
|
||||
key: 'bankAccountId',
|
||||
label: '출금계좌',
|
||||
type: 'text',
|
||||
placeholder: '출금계좌를 입력해주세요',
|
||||
type: 'select',
|
||||
placeholder: '선택',
|
||||
fetchOptions: async () => {
|
||||
const result = await getBankAccounts();
|
||||
if (result.success) {
|
||||
return result.data.map((a) => ({
|
||||
value: a.id,
|
||||
label: a.name,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
disabled: (mode) => mode === 'view',
|
||||
},
|
||||
// 수취인명
|
||||
@@ -105,7 +115,7 @@ export const withdrawalDetailConfig: DetailConfig = {
|
||||
const record = data as unknown as WithdrawalRecord;
|
||||
return {
|
||||
withdrawalDate: record.withdrawalDate || '',
|
||||
accountName: record.accountName || '',
|
||||
bankAccountId: record.bankAccountId || '',
|
||||
recipientName: record.recipientName || '',
|
||||
withdrawalAmount: record.withdrawalAmount ? record.withdrawalAmount.toLocaleString() : '0',
|
||||
note: record.note || '',
|
||||
@@ -116,7 +126,7 @@ export const withdrawalDetailConfig: DetailConfig = {
|
||||
transformSubmitData: (formData: Record<string, unknown>): Partial<WithdrawalRecord> => {
|
||||
return {
|
||||
withdrawalDate: formData.withdrawalDate as string,
|
||||
accountName: formData.accountName as string,
|
||||
bankAccountId: formData.bankAccountId as string,
|
||||
recipientName: formData.recipientName as string,
|
||||
withdrawalAmount: typeof formData.withdrawalAmount === 'string'
|
||||
? parseInt(formData.withdrawalAmount.replace(/,/g, ''), 10)
|
||||
|
||||
Reference in New Issue
Block a user