Files
sam-react-prod/src/components/accounting/PurchaseManagement/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

210 lines
8.5 KiB
TypeScript

/**
* 매입 관리 서버 액션
*
* 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<string, unknown>;
form?: { id: number; name: string; category: string };
};
tax_invoice_received: boolean;
created_at?: string;
updated_at?: string;
}
type PurchaseApiPaginatedResponse = PaginatedApiResponse<PurchaseApiData>;
// ===== 변환 함수 =====
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<PurchaseRecord>): Record<string, unknown> {
const result: Record<string, unknown> = {};
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<ActionResult<PurchaseRecord>> {
return executeServerAction({
url: `${API_URL}/api/v1/purchases/${id}`,
transform: (data: PurchaseApiData) => transformApiToFrontend(data),
errorMessage: '매입 조회에 실패했습니다.',
});
}
// ===== 매입 등록 =====
export async function createPurchase(data: Partial<PurchaseRecord>): Promise<ActionResult<PurchaseRecord>> {
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<PurchaseRecord>): Promise<ActionResult<PurchaseRecord>> {
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<ActionResult<PurchaseRecord>> {
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<ActionResult> {
return executeServerAction({
url: `${API_URL}/api/v1/purchases/${id}`,
method: 'DELETE',
errorMessage: '매입 삭제에 실패했습니다.',
});
}
// ===== 매입 확정 =====
export async function confirmPurchase(id: string): Promise<ActionResult<PurchaseRecord>> {
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 };
}