/** * 매입 관리 서버 액션 * * API Endpoints: * - GET /api/v1/purchases - 목록 조회 * - GET /api/v1/purchases/{id} - 상세 조회 * - POST /api/v1/purchases - 등록 * - PUT /api/v1/purchases/{id} - 수정 * - DELETE /api/v1/purchases/{id} - 삭제 * - PUT /api/v1/purchases/{id}/confirm - 확정 */ 'use server'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action'; import type { PaginatedApiResponse } from '@/lib/api/types'; import { fetchVendorOptions, fetchBankAccountDetailOptions } from '@/lib/api/shared-lookups'; import type { PurchaseRecord, PurchaseType } from './types'; const API_URL = process.env.NEXT_PUBLIC_API_URL; // ===== API 데이터 타입 ===== interface PurchaseApiData { id: number; purchase_number: string; purchase_date: string; client_id: number; client?: { id: number; name: string }; supply_amount: string; tax_amount: string; total_amount: string; description?: string; status: string; purchase_type?: string; withdrawal_id?: number; approval_id?: number; approval?: { id: number; document_number: string; title: string; content?: Record; form?: { id: number; name: string; category: string }; }; tax_invoice_received: boolean; created_at?: string; updated_at?: string; } type PurchaseApiPaginatedResponse = PaginatedApiResponse; // ===== 변환 함수 ===== const VALID_PURCHASE_TYPES: PurchaseType[] = [ 'unset', 'raw_material', 'subsidiary_material', 'product', 'outsourcing', 'consumables', 'repair', 'transportation', 'office_supplies', 'rent', 'utilities', 'communication', 'vehicle', 'entertainment', 'insurance', 'other_service' ]; function transformApiToFrontend(data: PurchaseApiData): PurchaseRecord { const purchaseType: PurchaseType = data.purchase_type && VALID_PURCHASE_TYPES.includes(data.purchase_type as PurchaseType) ? (data.purchase_type as PurchaseType) : 'unset'; let sourceDocument: PurchaseRecord['sourceDocument'] = undefined; if (data.approval) { const docType = data.approval.form?.category === 'expense_report' ? 'expense_report' : 'proposal'; const expectedCost = (data.approval.content?.expected_cost as number) || (data.approval.content?.total_amount as number) || 0; sourceDocument = { type: docType, documentNo: data.approval.document_number, title: data.approval.title, expectedCost }; } return { id: String(data.id), purchaseNo: data.purchase_number, purchaseDate: data.purchase_date, vendorId: String(data.client_id), vendorName: data.client?.name || '', supplyAmount: parseFloat(data.supply_amount) || 0, vat: parseFloat(data.tax_amount) || 0, totalAmount: parseFloat(data.total_amount) || 0, purchaseType, evidenceType: 'tax_invoice', status: data.status === 'confirmed' ? 'completed' : 'pending', approvalId: data.approval_id ? String(data.approval_id) : undefined, sourceDocument, items: [], taxInvoiceReceived: data.tax_invoice_received ?? false, createdAt: data.created_at || '', updatedAt: data.updated_at || '', }; } function transformFrontendToApi(data: Partial): Record { const result: Record = {}; if (data.purchaseDate !== undefined) result.purchase_date = data.purchaseDate; if (data.vendorId !== undefined) result.client_id = parseInt(data.vendorId, 10); if (data.supplyAmount !== undefined) result.supply_amount = data.supplyAmount; if (data.vat !== undefined) result.tax_amount = data.vat; if (data.totalAmount !== undefined) result.total_amount = data.totalAmount; if (data.purchaseType !== undefined) result.purchase_type = data.purchaseType; if (data.taxInvoiceReceived !== undefined) result.tax_invoice_received = data.taxInvoiceReceived; if (data.approvalId !== undefined) result.approval_id = data.approvalId ? parseInt(data.approvalId, 10) : null; return result; } interface PaginationMeta { currentPage: number; lastPage: number; perPage: number; total: number } const DEFAULT_PAGINATION: PaginationMeta = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 }; // ===== 매입 목록 조회 ===== export async function getPurchases(params?: { page?: number; perPage?: number; startDate?: string; endDate?: string; clientId?: string; status?: string; search?: string; }): Promise<{ success: boolean; data: PurchaseRecord[]; pagination: PaginationMeta; 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?.clientId) searchParams.set('client_id', params.clientId); if (params?.status && params.status !== 'all') searchParams.set('status', params.status); if (params?.search) searchParams.set('search', params.search); const queryString = searchParams.toString(); const result = await executeServerAction({ url: `${API_URL}/api/v1/purchases${queryString ? `?${queryString}` : ''}`, transform: (data: PurchaseApiPaginatedResponse) => ({ items: (data?.data || []).map(transformApiToFrontend), pagination: { currentPage: data?.current_page || 1, lastPage: data?.last_page || 1, perPage: data?.per_page || 20, 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 getPurchaseById(id: string): Promise> { return executeServerAction({ url: `${API_URL}/api/v1/purchases/${id}`, transform: (data: PurchaseApiData) => transformApiToFrontend(data), errorMessage: '매입 조회에 실패했습니다.', }); } // ===== 매입 등록 ===== export async function createPurchase(data: Partial): Promise> { return executeServerAction({ url: `${API_URL}/api/v1/purchases`, method: 'POST', body: transformFrontendToApi(data), transform: (data: PurchaseApiData) => transformApiToFrontend(data), errorMessage: '매입 등록에 실패했습니다.', }); } // ===== 매입 수정 ===== export async function updatePurchase(id: string, data: Partial): Promise> { return executeServerAction({ url: `${API_URL}/api/v1/purchases/${id}`, method: 'PUT', body: transformFrontendToApi(data), transform: (data: PurchaseApiData) => transformApiToFrontend(data), errorMessage: '매입 수정에 실패했습니다.', }); } // ===== 세금계산서 수취 상태 토글 ===== export async function togglePurchaseTaxInvoice( id: string, value: boolean ): Promise> { try { return await updatePurchase(id, { taxInvoiceReceived: value }); } catch (error) { if (isNextRedirectError(error)) { return { success: false, error: '세션이 만료되었습니다. 다시 로그인해주세요.' }; } throw error; } } // ===== 매입 삭제 ===== export async function deletePurchase(id: string): Promise { return executeServerAction({ url: `${API_URL}/api/v1/purchases/${id}`, method: 'DELETE', errorMessage: '매입 삭제에 실패했습니다.', }); } // ===== 매입 확정 ===== export async function confirmPurchase(id: string): Promise> { return executeServerAction({ url: `${API_URL}/api/v1/purchases/${id}/confirm`, method: 'PUT', transform: (data: PurchaseApiData) => transformApiToFrontend(data), errorMessage: '매입 확정에 실패했습니다.', }); } // ===== 은행 계좌 목록 조회 ===== 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 }; } // ===== 거래처 목록 조회 ===== export async function getVendors(): Promise<{ success: boolean; data: { id: string; name: string }[]; error?: string; }> { const result = await fetchVendorOptions(); return { success: result.success, data: result.data || [], error: result.error }; }