- 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>
218 lines
10 KiB
TypeScript
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 };
|
|
} |