'use server'; import { serverFetch } from '@/lib/api/fetch-wrapper'; // ============================================================================ // API 타입 정의 // ============================================================================ interface ApiOrder { id: number; tenant_id: number; quote_id: number | null; order_no: string; order_type_code: string; status_code: string; category_code: string | null; client_id: number | null; client_name: string | null; client_contact: string | null; site_name: string | null; quantity: number; supply_amount: number; tax_amount: number; total_amount: number; discount_rate: number; discount_amount: number; delivery_date: string | null; delivery_method_code: string | null; received_at: string | null; memo: string | null; remarks: string | null; note: string | null; created_by: number | null; updated_by: number | null; created_at: string; updated_at: string; client?: ApiClient | null; items?: ApiOrderItem[]; quote?: ApiQuote | null; } interface ApiOrderItem { id: number; order_id: number; item_id: number | null; item_name: string; specification: string | null; quantity: number; unit: string | null; unit_price: number; supply_amount: number; tax_amount: number; total_amount: number; sort_order: number; } interface ApiClient { id: number; name: string; business_no?: string; representative?: string; phone?: string; email?: string; } interface ApiQuote { id: number; quote_no: string; quote_number?: string; site_name: string | null; } interface ApiWorkOrder { id: number; tenant_id: number; work_order_no: string; sales_order_id: number; project_name: string | null; process_type: string; status: string; assignee_id: number | null; team_id: number | null; scheduled_date: string | null; memo: string | null; is_active: boolean; created_at: string; updated_at: string; assignee?: { id: number; name: string } | null; team?: { id: number; name: string } | null; } interface ApiProductionOrderResponse { work_order: ApiWorkOrder; order: ApiOrder; } interface ApiOrderStats { total: number; draft: number; confirmed: number; in_progress: number; completed: number; cancelled: number; total_amount: number; confirmed_amount: number; } interface ApiResponse { success: boolean; message: string; data: T; } interface PaginatedResponse { current_page: number; data: T[]; last_page: number; per_page: number; total: number; } // ============================================================================ // Frontend 타입 정의 // ============================================================================ // 수주 상태 타입 (API와 매핑) export type OrderStatus = | 'order_registered' // DRAFT | 'order_confirmed' // CONFIRMED | 'production_ordered' // IN_PROGRESS | 'in_production' // IN_PROGRESS (세부) | 'rework' // IN_PROGRESS (세부) | 'work_completed' // IN_PROGRESS (세부) | 'shipped' // COMPLETED | 'cancelled'; // CANCELLED export interface Order { id: string; lotNumber: string; // order_no quoteNumber: string; // quote.quote_no quoteId?: number; orderDate: string; // received_at client: string; // client_name clientId?: number; siteName: string; // site_name status: OrderStatus; statusCode: string; // 원본 status_code expectedShipDate?: string; // delivery_date deliveryMethod?: string; // delivery_method_code amount: number; // total_amount supplyAmount: number; taxAmount: number; itemCount: number; // items.length hasReceivable?: boolean; // 미수 여부 (추후 구현) memo?: string; remarks?: string; note?: string; items?: OrderItem[]; } export interface OrderItem { id: string; itemId?: number; itemName: string; specification?: string; quantity: number; unit?: string; unitPrice: number; supplyAmount: number; taxAmount: number; totalAmount: number; sortOrder: number; } export interface OrderFormData { orderTypeCode?: string; categoryCode?: string; clientId?: number; clientName?: string; clientContact?: string; siteName?: string; supplyAmount?: number; taxAmount?: number; totalAmount?: number; discountRate?: number; discountAmount?: number; deliveryDate?: string; deliveryMethodCode?: string; receivedAt?: string; memo?: string; remarks?: string; note?: string; items?: OrderItemFormData[]; } export interface OrderItemFormData { itemId?: number; itemName: string; specification?: string; quantity: number; unit?: string; unitPrice: number; } export interface OrderStats { total: number; draft: number; confirmed: number; inProgress: number; completed: number; cancelled: number; totalAmount: number; confirmedAmount: number; } // 견적→수주 변환용 export interface CreateFromQuoteData { deliveryDate?: string; memo?: string; } // 생산지시 생성용 export interface CreateProductionOrderData { processType?: 'screen' | 'slat' | 'bending'; assigneeId?: number; teamId?: number; scheduledDate?: string; memo?: string; } // 생산지시(작업지시) 타입 export interface WorkOrder { id: string; workOrderNo: string; salesOrderId: number; projectName: string | null; processType: string; status: string; assigneeId?: number; assigneeName?: string; teamId?: number; teamName?: string; scheduledDate?: string; memo?: string; isActive: boolean; createdAt: string; updatedAt: string; } // 생산지시 생성 결과 export interface ProductionOrderResult { workOrder: WorkOrder; order: Order; } // ============================================================================ // 상태 매핑 // ============================================================================ const API_TO_FRONTEND_STATUS: Record = { 'DRAFT': 'order_registered', 'CONFIRMED': 'order_confirmed', 'IN_PROGRESS': 'production_ordered', 'COMPLETED': 'shipped', 'CANCELLED': 'cancelled', }; const FRONTEND_TO_API_STATUS: Record = { 'order_registered': 'DRAFT', 'order_confirmed': 'CONFIRMED', 'production_ordered': 'IN_PROGRESS', 'in_production': 'IN_PROGRESS', 'rework': 'IN_PROGRESS', 'work_completed': 'IN_PROGRESS', 'shipped': 'COMPLETED', 'cancelled': 'CANCELLED', }; // ============================================================================ // 데이터 변환 함수 // ============================================================================ function transformApiToFrontend(apiData: ApiOrder): Order { return { id: String(apiData.id), lotNumber: apiData.order_no, quoteNumber: apiData.quote?.quote_no || '', quoteId: apiData.quote_id ?? undefined, orderDate: apiData.received_at || apiData.created_at.split('T')[0], client: apiData.client_name || apiData.client?.name || '', clientId: apiData.client_id ?? undefined, siteName: apiData.site_name || '', status: API_TO_FRONTEND_STATUS[apiData.status_code] || 'order_registered', statusCode: apiData.status_code, expectedShipDate: apiData.delivery_date ?? undefined, deliveryMethod: apiData.delivery_method_code ?? undefined, amount: apiData.total_amount, supplyAmount: apiData.supply_amount, taxAmount: apiData.tax_amount, itemCount: apiData.items?.length || 0, hasReceivable: false, // 추후 구현 memo: apiData.memo ?? undefined, remarks: apiData.remarks ?? undefined, note: apiData.note ?? undefined, items: apiData.items?.map(transformItemApiToFrontend), }; } function transformItemApiToFrontend(apiItem: ApiOrderItem): OrderItem { return { id: String(apiItem.id), itemId: apiItem.item_id ?? undefined, itemName: apiItem.item_name, specification: apiItem.specification ?? undefined, quantity: apiItem.quantity, unit: apiItem.unit ?? undefined, unitPrice: apiItem.unit_price, supplyAmount: apiItem.supply_amount, taxAmount: apiItem.tax_amount, totalAmount: apiItem.total_amount, sortOrder: apiItem.sort_order, }; } function transformFrontendToApi(data: OrderFormData): Record { return { order_type_code: data.orderTypeCode || 'ORDER', category_code: data.categoryCode || null, client_id: data.clientId || null, client_name: data.clientName || null, client_contact: data.clientContact || null, site_name: data.siteName || null, supply_amount: data.supplyAmount || 0, tax_amount: data.taxAmount || 0, total_amount: data.totalAmount || 0, discount_rate: data.discountRate || 0, discount_amount: data.discountAmount || 0, delivery_date: data.deliveryDate || null, delivery_method_code: data.deliveryMethodCode || null, received_at: data.receivedAt || null, memo: data.memo || null, remarks: data.remarks || null, note: data.note || null, items: data.items?.map((item) => ({ item_id: item.itemId || null, item_name: item.itemName, specification: item.specification || null, quantity: item.quantity, unit: item.unit || null, unit_price: item.unitPrice, })) || [], }; } function transformWorkOrderApiToFrontend(apiData: ApiWorkOrder): WorkOrder { return { id: String(apiData.id), workOrderNo: apiData.work_order_no, salesOrderId: apiData.sales_order_id, projectName: apiData.project_name, processType: apiData.process_type, status: apiData.status, assigneeId: apiData.assignee_id ?? undefined, assigneeName: apiData.assignee?.name ?? undefined, teamId: apiData.team_id ?? undefined, teamName: apiData.team?.name ?? undefined, scheduledDate: apiData.scheduled_date ?? undefined, memo: apiData.memo ?? undefined, isActive: apiData.is_active, createdAt: apiData.created_at, updatedAt: apiData.updated_at, }; } // ============================================================================ // API 함수 // ============================================================================ /** * 수주 목록 조회 */ export async function getOrders(params?: { page?: number; size?: number; q?: string; status?: string; order_type?: string; client_id?: number; date_from?: string; date_to?: string; }): Promise<{ success: boolean; data?: { items: Order[]; total: number; page: number; totalPages: number }; error?: string; __authError?: boolean; }> { try { const searchParams = new URLSearchParams(); if (params?.page) searchParams.set('page', String(params.page)); if (params?.size) searchParams.set('size', String(params.size)); if (params?.q) searchParams.set('q', params.q); if (params?.status) { // Frontend status를 API status로 변환 const apiStatus = FRONTEND_TO_API_STATUS[params.status as OrderStatus]; if (apiStatus) searchParams.set('status', apiStatus); } if (params?.order_type) searchParams.set('order_type', params.order_type); if (params?.client_id) searchParams.set('client_id', String(params.client_id)); if (params?.date_from) searchParams.set('date_from', params.date_from); if (params?.date_to) searchParams.set('date_to', params.date_to); const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders?${searchParams.toString()}`, { method: 'GET', cache: 'no-store' } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response) { return { success: false, error: '목록 조회에 실패했습니다.' }; } const result: ApiResponse> = await response.json(); if (!response.ok || !result.success) { return { success: false, error: result.message || '목록 조회에 실패했습니다.' }; } return { success: true, data: { items: result.data.data.map(transformApiToFrontend), total: result.data.total, page: result.data.current_page, totalPages: result.data.last_page, }, }; } catch (error) { console.error('[getOrders] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } /** * 수주 상세 조회 */ export async function getOrderById(id: string): Promise<{ success: boolean; data?: Order; error?: string; __authError?: boolean; }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}`, { method: 'GET', cache: 'no-store' } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response) { return { success: false, error: '조회에 실패했습니다.' }; } const result: ApiResponse = await response.json(); if (!response.ok || !result.success) { return { success: false, error: result.message || '조회에 실패했습니다.' }; } return { success: true, data: transformApiToFrontend(result.data) }; } catch (error) { console.error('[getOrderById] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } /** * 수주 생성 */ export async function createOrder(data: OrderFormData): Promise<{ success: boolean; data?: Order; error?: string; __authError?: boolean; }> { try { const apiData = transformFrontendToApi(data); const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders`, { method: 'POST', body: JSON.stringify(apiData) } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response) { return { success: false, error: '등록에 실패했습니다.' }; } const result: ApiResponse = await response.json(); if (!response.ok || !result.success) { return { success: false, error: result.message || '등록에 실패했습니다.' }; } return { success: true, data: transformApiToFrontend(result.data) }; } catch (error) { console.error('[createOrder] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } /** * 수주 수정 */ export async function updateOrder(id: string, data: OrderFormData): Promise<{ success: boolean; data?: Order; error?: string; __authError?: boolean; }> { try { const apiData = transformFrontendToApi(data); const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}`, { method: 'PUT', body: JSON.stringify(apiData) } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response) { return { success: false, error: '수정에 실패했습니다.' }; } const result: ApiResponse = await response.json(); if (!response.ok || !result.success) { return { success: false, error: result.message || '수정에 실패했습니다.' }; } return { success: true, data: transformApiToFrontend(result.data) }; } catch (error) { console.error('[updateOrder] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } /** * 수주 삭제 */ export async function deleteOrder(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean; }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}`, { method: 'DELETE' } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response) { return { success: false, error: '삭제에 실패했습니다.' }; } const result: ApiResponse = await response.json(); if (!response.ok || !result.success) { return { success: false, error: result.message || '삭제에 실패했습니다.' }; } return { success: true }; } catch (error) { console.error('[deleteOrder] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } /** * 수주 상태 변경 */ export async function updateOrderStatus(id: string, status: OrderStatus): Promise<{ success: boolean; data?: Order; error?: string; __authError?: boolean; }> { try { const apiStatus = FRONTEND_TO_API_STATUS[status]; if (!apiStatus) { return { success: false, error: '유효하지 않은 상태입니다.' }; } const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}/status`, { method: 'PATCH', body: JSON.stringify({ status: apiStatus }) } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response) { return { success: false, error: '상태 변경에 실패했습니다.' }; } const result: ApiResponse = await response.json(); if (!response.ok || !result.success) { return { success: false, error: result.message || '상태 변경에 실패했습니다.' }; } return { success: true, data: transformApiToFrontend(result.data) }; } catch (error) { console.error('[updateOrderStatus] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } /** * 수주 통계 조회 */ export async function getOrderStats(): Promise<{ success: boolean; data?: OrderStats; error?: string; __authError?: boolean; }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/stats`, { method: 'GET', cache: 'no-store' } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response) { return { success: false, error: '통계 조회에 실패했습니다.' }; } const result: ApiResponse = await response.json(); if (!response.ok || !result.success) { return { success: false, error: result.message || '통계 조회에 실패했습니다.' }; } return { success: true, data: { total: result.data.total, draft: result.data.draft, confirmed: result.data.confirmed, inProgress: result.data.in_progress, completed: result.data.completed, cancelled: result.data.cancelled, totalAmount: result.data.total_amount, confirmedAmount: result.data.confirmed_amount, }, }; } catch (error) { console.error('[getOrderStats] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } /** * 수주 일괄 삭제 */ export async function deleteOrders(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string; __authError?: boolean; }> { try { // 순차적으로 삭제 (API에 bulk delete가 없으므로) let deletedCount = 0; const errors: string[] = []; for (const id of ids) { const result = await deleteOrder(id); if (result.success) { deletedCount++; } else { errors.push(result.error || `ID ${id} 삭제 실패`); } } if (deletedCount === 0 && errors.length > 0) { return { success: false, error: errors[0] }; } return { success: true, deletedCount }; } catch (error) { console.error('[deleteOrders] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } /** * 견적에서 수주 생성 */ export async function createOrderFromQuote( quoteId: number, data?: CreateFromQuoteData ): Promise<{ success: boolean; data?: Order; error?: string; __authError?: boolean; }> { try { const apiData: Record = {}; if (data?.deliveryDate) apiData.delivery_date = data.deliveryDate; if (data?.memo) apiData.memo = data.memo; const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/from-quote/${quoteId}`, { method: 'POST', body: JSON.stringify(apiData) } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response) { return { success: false, error: '수주 생성에 실패했습니다.' }; } const result: ApiResponse = await response.json(); if (!response.ok || !result.success) { return { success: false, error: result.message || '수주 생성에 실패했습니다.' }; } return { success: true, data: transformApiToFrontend(result.data) }; } catch (error) { console.error('[createOrderFromQuote] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } /** * 생산지시 생성 */ export async function createProductionOrder( orderId: string, data?: CreateProductionOrderData ): Promise<{ success: boolean; data?: ProductionOrderResult; error?: string; __authError?: boolean; }> { try { const apiData: Record = {}; if (data?.processType) apiData.process_type = data.processType; if (data?.assigneeId) apiData.assignee_id = data.assigneeId; if (data?.teamId) apiData.team_id = data.teamId; if (data?.scheduledDate) apiData.scheduled_date = data.scheduledDate; if (data?.memo) apiData.memo = data.memo; const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${orderId}/production-order`, { method: 'POST', body: JSON.stringify(apiData) } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response) { return { success: false, error: '생산지시 생성에 실패했습니다.' }; } const result: ApiResponse = await response.json(); if (!response.ok || !result.success) { return { success: false, error: result.message || '생산지시 생성에 실패했습니다.' }; } return { success: true, data: { workOrder: transformWorkOrderApiToFrontend(result.data.work_order), order: transformApiToFrontend(result.data.order), }, }; } catch (error) { console.error('[createProductionOrder] Error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } }