Files
sam-react-prod/src/components/accounting/ExpectedExpenseManagement/actions.ts
유병철 437d5f6834 refactor(WEB): SearchableSelectionModal 공통화 및 actions lookup 통합
- SearchableSelectionModal<T> 제네릭 컴포넌트 추출 (organisms)
- 검색 모달 5개 리팩토링: SupplierSearch, QuotationSelect, SalesOrderSelect, OrderSelect, ItemSearch
- shared-lookups API 유틸 추가 (거래처/품목/수주 등 공통 조회)
- create-crud-service 확장 (lookup, search 메서드)
- actions.ts 20+개 파일 lookup 패턴 통일
- 공통 페이지 패턴 가이드 문서 추가
- CLAUDE.md Common Component Usage Rules 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 16:01:23 +09:00

218 lines
10 KiB
TypeScript

'use server';
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
import type { PaginatedApiResponse } from '@/lib/api/types';
import { fetchBankAccountDetailOptions } from '@/lib/api/shared-lookups';
import type { ExpectedExpenseRecord, TransactionType, PaymentStatus, ApprovalStatus } from './types';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ===== API 응답 타입 =====
interface ExpectedExpenseApiData {
id: number;
tenant_id: number;
expected_payment_date: string;
settlement_date: string | null;
transaction_type: string;
amount: number | string;
client_id: number | null;
client_name: string | null;
bank_account_id: number | null;
account_code: string | null;
payment_status: string;
approval_status: string;
description: string | null;
created_at: string;
updated_at: string;
client?: { id: number; name: string } | null;
bank_account?: { id: number; bank_name: string; account_name: string } | null;
}
type ExpensePaginatedResponse = PaginatedApiResponse<ExpectedExpenseApiData>;
interface SummaryData {
total_amount: number;
total_count: number;
by_payment_status: Record<string, { count: number; amount: number }>;
by_transaction_type: Record<string, { count: number; amount: number }>;
by_month: Record<string, { count: number; amount: number }>;
}
interface FrontendPagination { currentPage: number; lastPage: number; perPage: number; total: number }
const DEFAULT_PAGINATION: FrontendPagination = { currentPage: 1, lastPage: 1, perPage: 50, total: 0 };
// ===== API → Frontend 변환 =====
function transformApiToFrontend(apiData: ExpectedExpenseApiData): ExpectedExpenseRecord {
return {
id: String(apiData.id),
expectedPaymentDate: apiData.expected_payment_date,
settlementDate: apiData.settlement_date || '',
transactionType: (apiData.transaction_type || 'other') as TransactionType,
amount: typeof apiData.amount === 'string' ? parseFloat(apiData.amount) : apiData.amount,
vendorId: apiData.client_id ? String(apiData.client_id) : '',
vendorName: apiData.client_name || apiData.client?.name || '',
bankAccount: apiData.bank_account ? `${apiData.bank_account.bank_name} ${apiData.bank_account.account_name}` : '',
accountSubject: apiData.account_code || '',
paymentStatus: (apiData.payment_status || 'pending') as PaymentStatus,
approvalStatus: (apiData.approval_status || 'none') as ApprovalStatus,
note: apiData.description || '',
createdAt: apiData.created_at,
updatedAt: apiData.updated_at,
};
}
// ===== Frontend → API 변환 =====
function transformFrontendToApi(data: Partial<ExpectedExpenseRecord>): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (data.expectedPaymentDate !== undefined) result.expected_payment_date = data.expectedPaymentDate;
if (data.settlementDate !== undefined) result.settlement_date = data.settlementDate || null;
if (data.transactionType !== undefined) result.transaction_type = data.transactionType;
if (data.amount !== undefined) result.amount = data.amount;
if (data.vendorId !== undefined) result.client_id = data.vendorId ? parseInt(data.vendorId, 10) : null;
if (data.vendorName !== undefined) result.client_name = data.vendorName || null;
if (data.accountSubject !== undefined) result.account_code = data.accountSubject || null;
if (data.paymentStatus !== undefined) result.payment_status = data.paymentStatus;
if (data.approvalStatus !== undefined) result.approval_status = data.approvalStatus;
if (data.note !== undefined) result.description = data.note || null;
return result;
}
// ===== 미지급비용 목록 조회 =====
export async function getExpectedExpenses(params?: {
page?: number; perPage?: number; startDate?: string; endDate?: string;
transactionType?: string; paymentStatus?: string; approvalStatus?: string;
clientId?: string; search?: string; sortBy?: string; sortDir?: 'asc' | 'desc';
}): Promise<{ success: boolean; data: ExpectedExpenseRecord[]; pagination: FrontendPagination; error?: string }> {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.transactionType && params.transactionType !== 'all') searchParams.set('transaction_type', params.transactionType);
if (params?.paymentStatus && params.paymentStatus !== 'all') searchParams.set('payment_status', params.paymentStatus);
if (params?.approvalStatus && params.approvalStatus !== 'all') searchParams.set('approval_status', params.approvalStatus);
if (params?.clientId) searchParams.set('client_id', params.clientId);
if (params?.search) searchParams.set('search', params.search);
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
const queryString = searchParams.toString();
const result = await executeServerAction({
url: `${API_URL}/api/v1/expected-expenses${queryString ? `?${queryString}` : ''}`,
transform: (data: ExpensePaginatedResponse) => ({
items: (data?.data || []).map(transformApiToFrontend),
pagination: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 50, total: data?.total || 0 },
}),
errorMessage: '미지급비용 조회에 실패했습니다.',
});
return { success: result.success, data: result.data?.items || [], pagination: result.data?.pagination || DEFAULT_PAGINATION, error: result.error };
}
// ===== 미지급비용 상세 조회 =====
export async function getExpectedExpenseById(id: string): Promise<ActionResult<ExpectedExpenseRecord>> {
return executeServerAction({
url: `${API_URL}/api/v1/expected-expenses/${id}`,
transform: (data: ExpectedExpenseApiData) => transformApiToFrontend(data),
errorMessage: '미지급비용 조회에 실패했습니다.',
});
}
// ===== 미지급비용 등록 =====
export async function createExpectedExpense(data: Partial<ExpectedExpenseRecord>): Promise<ActionResult<ExpectedExpenseRecord>> {
return executeServerAction({
url: `${API_URL}/api/v1/expected-expenses`,
method: 'POST',
body: transformFrontendToApi(data),
transform: (data: ExpectedExpenseApiData) => transformApiToFrontend(data),
errorMessage: '미지급비용 등록에 실패했습니다.',
});
}
// ===== 미지급비용 수정 =====
export async function updateExpectedExpense(id: string, data: Partial<ExpectedExpenseRecord>): Promise<ActionResult<ExpectedExpenseRecord>> {
return executeServerAction({
url: `${API_URL}/api/v1/expected-expenses/${id}`,
method: 'PUT',
body: transformFrontendToApi(data),
transform: (data: ExpectedExpenseApiData) => transformApiToFrontend(data),
errorMessage: '미지급비용 수정에 실패했습니다.',
});
}
// ===== 미지급비용 삭제 =====
export async function deleteExpectedExpense(id: string): Promise<ActionResult> {
return executeServerAction({
url: `${API_URL}/api/v1/expected-expenses/${id}`,
method: 'DELETE',
errorMessage: '미지급비용 삭제에 실패했습니다.',
});
}
// ===== 미지급비용 일괄 삭제 =====
export async function deleteExpectedExpenses(ids: string[]): Promise<{
success: boolean; deletedCount?: number; error?: string;
}> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/expected-expenses`,
method: 'DELETE',
body: { ids: ids.map(id => parseInt(id, 10)) },
transform: (data: { deleted_count?: number }) => ({ deletedCount: data?.deleted_count }),
errorMessage: '미지급비용 일괄 삭제에 실패했습니다.',
});
return { success: result.success, deletedCount: result.data?.deletedCount, error: result.error };
}
// ===== 예상 지급일 일괄 변경 =====
export async function updateExpectedPaymentDate(ids: string[], expectedPaymentDate: string): Promise<{
success: boolean; updatedCount?: number; error?: string;
}> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/expected-expenses/update-payment-date`,
method: 'PUT',
body: { ids: ids.map(id => parseInt(id, 10)), expected_payment_date: expectedPaymentDate },
transform: (data: { updated_count?: number }) => ({ updatedCount: data?.updated_count }),
errorMessage: '예상 지급일 변경에 실패했습니다.',
});
return { success: result.success, updatedCount: result.data?.updatedCount, error: result.error };
}
// ===== 미지급비용 요약 조회 =====
export async function getExpectedExpenseSummary(params?: {
startDate?: string; endDate?: string; paymentStatus?: string;
}): Promise<ActionResult<SummaryData>> {
const searchParams = new URLSearchParams();
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.paymentStatus && params.paymentStatus !== 'all') searchParams.set('payment_status', params.paymentStatus);
const queryString = searchParams.toString();
return executeServerAction<SummaryData>({
url: `${API_URL}/api/v1/expected-expenses/summary${queryString ? `?${queryString}` : ''}`,
errorMessage: '요약 조회에 실패했습니다.',
});
}
// ===== 거래처 목록 조회 =====
export async function getClients(): Promise<{
success: boolean; data: { id: string; name: string }[]; error?: string;
}> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/clients?per_page=100`,
transform: (data: { data?: { id: number; name: string }[] } | { id: number; name: string }[]) => {
type ClientApi = { id: number; name: string };
const clients: ClientApi[] = Array.isArray(data) ? data : (data as { data?: ClientApi[] })?.data || [];
return clients.map(c => ({ id: String(c.id), name: c.name }));
},
errorMessage: '거래처 조회에 실패했습니다.',
});
return { success: result.success, data: result.data || [], error: result.error };
}
// ===== 은행 계좌 목록 조회 =====
export async function getBankAccounts(): Promise<{
success: boolean; data: { id: string; bankName: string; accountName: string; accountNumber: string }[]; error?: string;
}> {
const result = await fetchBankAccountDetailOptions();
return { success: result.success, data: result.data || [], error: result.error };
}