/** * 출하 관리 서버 액션 * * API Endpoints: * - GET /api/v1/shipments - 목록 조회 * - GET /api/v1/shipments/stats - 통계 조회 * - GET /api/v1/shipments/stats-by-status - 상태별 통계 조회 * - GET /api/v1/shipments/{id} - 상세 조회 * - POST /api/v1/shipments - 등록 * - PUT /api/v1/shipments/{id} - 수정 * - PATCH /api/v1/shipments/{id}/status - 상태 변경 * - DELETE /api/v1/shipments/{id} - 삭제 * - GET /api/v1/shipments/options/lots - LOT 옵션 조회 * - GET /api/v1/shipments/options/logistics - 물류사 옵션 조회 * - GET /api/v1/shipments/options/vehicle-tonnage - 차량 톤수 옵션 조회 */ 'use server'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { ShipmentItem, ShipmentDetail, ShipmentProduct, ShipmentStats, ShipmentStatus, ShipmentPriority, DeliveryMethod, ShipmentCreateFormData, ShipmentEditFormData, LotOption, LogisticsOption, VehicleTonnageOption, } from './types'; // ===== API 데이터 타입 ===== interface ShipmentApiData { id: number; shipment_no: string; lot_no?: string; order_id?: number; scheduled_date: string; status: ShipmentStatus; priority: ShipmentPriority; delivery_method: DeliveryMethod; client_id?: number; customer_name?: string; site_name?: string; delivery_address?: string; receiver?: string; receiver_contact?: string; can_ship: boolean; deposit_confirmed: boolean; invoice_issued: boolean; customer_grade?: string; loading_manager?: string; loading_time?: string; loading_completed_at?: string; logistics_company?: string; vehicle_tonnage?: string; shipping_cost?: string | number; vehicle_no?: string; driver_name?: string; driver_contact?: string; expected_arrival?: string; confirmed_arrival?: string; remarks?: string; created_by?: number; updated_by?: number; creator?: { id: number; name: string }; created_at?: string; updated_at?: string; items?: ShipmentItemApiData[]; status_label?: string; priority_label?: string; delivery_method_label?: string; total_quantity?: number; item_count?: number; } interface ShipmentItemApiData { id: number; shipment_id: number; seq: number; item_code?: string; item_name: string; floor_unit?: string; specification?: string; quantity: string | number; unit?: string; lot_no?: string; stock_lot_id?: number; remarks?: string; } interface ShipmentApiPaginatedResponse { data: ShipmentApiData[]; current_page: number; last_page: number; per_page: number; total: number; } interface ShipmentApiStatsResponse { today_shipment_count: number; scheduled_count: number; shipping_count: number; urgent_count: number; } interface ShipmentApiStatsByStatusResponse { all: number; scheduled: number; ready: number; shipping: number; completed: number; } // ===== API → Frontend 변환 (목록용) ===== function transformApiToListItem(data: ShipmentApiData): ShipmentItem { return { id: String(data.id), shipmentNo: data.shipment_no, lotNo: data.lot_no || '', scheduledDate: data.scheduled_date, status: data.status, priority: data.priority, deliveryMethod: data.delivery_method, customerName: data.customer_name || '', siteName: data.site_name || '', manager: data.loading_manager, canShip: data.can_ship, depositConfirmed: data.deposit_confirmed, invoiceIssued: data.invoice_issued, deliveryTime: data.expected_arrival, }; } // ===== API → Frontend 변환 (품목용) ===== function transformApiToProduct(data: ShipmentItemApiData): ShipmentProduct { return { id: String(data.id), no: data.seq, itemCode: data.item_code || '', itemName: data.item_name, floorUnit: data.floor_unit || '', specification: data.specification || '', quantity: parseFloat(String(data.quantity)) || 0, lotNo: data.lot_no || '', }; } // ===== API → Frontend 변환 (상세용) ===== function transformApiToDetail(data: ShipmentApiData): ShipmentDetail { return { id: String(data.id), shipmentNo: data.shipment_no, lotNo: data.lot_no || '', scheduledDate: data.scheduled_date, status: data.status, priority: data.priority, deliveryMethod: data.delivery_method, depositConfirmed: data.deposit_confirmed, invoiceIssued: data.invoice_issued, customerGrade: data.customer_grade || '', canShip: data.can_ship, loadingManager: data.loading_manager, loadingCompleted: data.loading_completed_at, registrant: data.creator?.name, customerName: data.customer_name || '', siteName: data.site_name || '', deliveryAddress: data.delivery_address || '', receiver: data.receiver, receiverContact: data.receiver_contact, products: (data.items || []).map(transformApiToProduct), logisticsCompany: data.logistics_company, vehicleTonnage: data.vehicle_tonnage, shippingCost: data.shipping_cost ? parseFloat(String(data.shipping_cost)) : undefined, vehicleNo: data.vehicle_no, driverName: data.driver_name, driverContact: data.driver_contact, remarks: data.remarks, }; } // ===== API → Frontend 변환 (통계용) ===== function transformApiToStats(data: ShipmentApiStatsResponse): ShipmentStats { return { todayShipmentCount: data.today_shipment_count, scheduledCount: data.scheduled_count, shippingCount: data.shipping_count, urgentCount: data.urgent_count, }; } // ===== Frontend → API 변환 (등록용) ===== function transformCreateFormToApi( data: ShipmentCreateFormData ): Record { return { lot_no: data.lotNo, scheduled_date: data.scheduledDate, priority: data.priority, delivery_method: data.deliveryMethod, logistics_company: data.logisticsCompany, vehicle_tonnage: data.vehicleTonnage, loading_time: data.loadingTime, loading_manager: data.loadingManager, remarks: data.remarks, }; } // ===== Frontend → API 변환 (수정용) ===== function transformEditFormToApi( data: Partial ): Record { const result: Record = {}; if (data.scheduledDate !== undefined) result.scheduled_date = data.scheduledDate; if (data.priority !== undefined) result.priority = data.priority; if (data.deliveryMethod !== undefined) result.delivery_method = data.deliveryMethod; if (data.loadingManager !== undefined) result.loading_manager = data.loadingManager; if (data.logisticsCompany !== undefined) result.logistics_company = data.logisticsCompany; if (data.vehicleTonnage !== undefined) result.vehicle_tonnage = data.vehicleTonnage; if (data.vehicleNo !== undefined) result.vehicle_no = data.vehicleNo; if (data.shippingCost !== undefined) result.shipping_cost = data.shippingCost; if (data.driverName !== undefined) result.driver_name = data.driverName; if (data.driverContact !== undefined) result.driver_contact = data.driverContact; if (data.expectedArrival !== undefined) result.expected_arrival = data.expectedArrival; if (data.confirmedArrival !== undefined) result.confirmed_arrival = data.confirmedArrival; if (data.remarks !== undefined) result.remarks = data.remarks; return result; } // ===== 페이지네이션 타입 ===== interface PaginationMeta { currentPage: number; lastPage: number; perPage: number; total: number; } // ===== 출하 목록 조회 ===== export async function getShipments(params?: { page?: number; perPage?: number; search?: string; status?: string; priority?: string; deliveryMethod?: string; scheduledFrom?: string; scheduledTo?: string; canShip?: boolean; depositConfirmed?: boolean; sortBy?: string; sortDir?: string; }): Promise<{ success: boolean; data: ShipmentItem[]; pagination: PaginationMeta; error?: string; __authError?: boolean; }> { try { 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?.search) searchParams.set('search', params.search); if (params?.status && params.status !== 'all') { searchParams.set('status', params.status); } if (params?.priority && params.priority !== 'all') { searchParams.set('priority', params.priority); } if (params?.deliveryMethod && params.deliveryMethod !== 'all') { searchParams.set('delivery_method', params.deliveryMethod); } if (params?.scheduledFrom) searchParams.set('scheduled_from', params.scheduledFrom); if (params?.scheduledTo) searchParams.set('scheduled_to', params.scheduledTo); if (params?.canShip !== undefined) searchParams.set('can_ship', String(params.canShip)); if (params?.depositConfirmed !== undefined) { searchParams.set('deposit_confirmed', String(params.depositConfirmed)); } if (params?.sortBy) searchParams.set('sort_by', params.sortBy); if (params?.sortDir) searchParams.set('sort_dir', params.sortDir); const queryString = searchParams.toString(); const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments${queryString ? `?${queryString}` : ''}`; console.log('[ShipmentActions] GET shipments:', url); const { response, error } = await serverFetch(url, { method: 'GET', cache: 'no-store', }); if (error) { return { success: false, data: [], pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 }, error: error.message, __authError: error.code === 'UNAUTHORIZED', }; } if (!response || !response.ok) { console.warn('[ShipmentActions] GET shipments error:', response?.status); return { success: false, data: [], pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 }, error: `API 오류: ${response?.status}`, }; } const result = await response.json(); if (!result.success) { return { success: false, data: [], pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 }, error: result.message || '출하 목록 조회에 실패했습니다.', }; } const paginatedData: ShipmentApiPaginatedResponse = result.data || { data: [], current_page: 1, last_page: 1, per_page: 20, total: 0, }; const shipments = (paginatedData.data || []).map(transformApiToListItem); return { success: true, data: shipments, pagination: { currentPage: paginatedData.current_page, lastPage: paginatedData.last_page, perPage: paginatedData.per_page, total: paginatedData.total, }, }; } catch (error) { console.error('[ShipmentActions] getShipments error:', error); return { success: false, data: [], pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 }, error: '서버 오류가 발생했습니다.', }; } } // ===== 출하 통계 조회 ===== export async function getShipmentStats(): Promise<{ success: boolean; data?: ShipmentStats; error?: string; __authError?: boolean; }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/stats`, { method: 'GET', cache: 'no-store', } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response || !response.ok) { console.warn('[ShipmentActions] GET stats error:', response?.status); return { success: false, error: `API 오류: ${response?.status}` }; } const result = await response.json(); if (!result.success || !result.data) { return { success: false, error: result.message || '출하 통계 조회에 실패했습니다.' }; } return { success: true, data: transformApiToStats(result.data) }; } catch (error) { console.error('[ShipmentActions] getShipmentStats error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } // ===== 상태별 통계 조회 (탭용) ===== export async function getShipmentStatsByStatus(): Promise<{ success: boolean; data?: ShipmentApiStatsByStatusResponse; error?: string; __authError?: boolean; }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/stats-by-status`, { method: 'GET', cache: 'no-store', } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response || !response.ok) { console.warn('[ShipmentActions] GET stats-by-status error:', response?.status); return { success: false, error: `API 오류: ${response?.status}` }; } const result = await response.json(); if (!result.success || !result.data) { return { success: false, error: result.message || '상태별 통계 조회에 실패했습니다.' }; } return { success: true, data: result.data }; } catch (error) { console.error('[ShipmentActions] getShipmentStatsByStatus error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } // ===== 출하 상세 조회 ===== export async function getShipmentById(id: string): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean; }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/${id}`, { method: 'GET', cache: 'no-store', } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response || !response.ok) { console.error('[ShipmentActions] GET shipment error:', response?.status); return { success: false, error: `API 오류: ${response?.status}` }; } const result = await response.json(); if (!result.success || !result.data) { return { success: false, error: result.message || '출하 조회에 실패했습니다.' }; } return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { console.error('[ShipmentActions] getShipmentById error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } // ===== 출하 등록 ===== export async function createShipment( data: ShipmentCreateFormData ): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> { try { const apiData = transformCreateFormToApi(data); console.log('[ShipmentActions] POST shipment request:', apiData); const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments`, { 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 = await response.json(); console.log('[ShipmentActions] POST shipment response:', result); if (!response.ok || !result.success) { return { success: false, error: result.message || '출하 등록에 실패했습니다.' }; } return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { console.error('[ShipmentActions] createShipment error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } // ===== 출하 수정 ===== export async function updateShipment( id: string, data: Partial ): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> { try { const apiData = transformEditFormToApi(data); console.log('[ShipmentActions] PUT shipment request:', apiData); const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/${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 = await response.json(); console.log('[ShipmentActions] PUT shipment response:', result); if (!response.ok || !result.success) { return { success: false, error: result.message || '출하 수정에 실패했습니다.' }; } return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { console.error('[ShipmentActions] updateShipment error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } // ===== 출하 상태 변경 ===== export async function updateShipmentStatus( id: string, status: ShipmentStatus, additionalData?: { loadingTime?: string; loadingCompletedAt?: string; vehicleNo?: string; driverName?: string; driverContact?: string; confirmedArrival?: string; } ): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> { try { const apiData: Record = { status }; if (additionalData?.loadingTime) apiData.loading_time = additionalData.loadingTime; if (additionalData?.loadingCompletedAt) apiData.loading_completed_at = additionalData.loadingCompletedAt; if (additionalData?.vehicleNo) apiData.vehicle_no = additionalData.vehicleNo; if (additionalData?.driverName) apiData.driver_name = additionalData.driverName; if (additionalData?.driverContact) apiData.driver_contact = additionalData.driverContact; if (additionalData?.confirmedArrival) apiData.confirmed_arrival = additionalData.confirmedArrival; console.log('[ShipmentActions] PATCH status request:', apiData); const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/${id}/status`, { method: 'PATCH', body: JSON.stringify(apiData), } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response) { return { success: false, error: '상태 변경에 실패했습니다.' }; } const result = await response.json(); console.log('[ShipmentActions] PATCH status response:', result); if (!response.ok || !result.success) { return { success: false, error: result.message || '상태 변경에 실패했습니다.' }; } return { success: true, data: transformApiToDetail(result.data) }; } catch (error) { console.error('[ShipmentActions] updateShipmentStatus error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } // ===== 출하 삭제 ===== export async function deleteShipment( id: string ): Promise<{ success: boolean; error?: string; __authError?: boolean }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/${id}`, { method: 'DELETE', } ); if (error) { return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response) { return { success: false, error: '출하 삭제에 실패했습니다.' }; } const result = await response.json(); console.log('[ShipmentActions] DELETE shipment response:', result); if (!response.ok || !result.success) { return { success: false, error: result.message || '출하 삭제에 실패했습니다.' }; } return { success: true }; } catch (error) { console.error('[ShipmentActions] deleteShipment error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } // ===== LOT 옵션 조회 ===== export async function getLotOptions(): Promise<{ success: boolean; data: LotOption[]; error?: string; __authError?: boolean; }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/options/lots`, { method: 'GET', cache: 'no-store', } ); if (error) { return { success: false, data: [], error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response || !response.ok) { console.warn('[ShipmentActions] GET lot options error:', response?.status); return { success: false, data: [], error: `API 오류: ${response?.status}` }; } const result = await response.json(); if (!result.success) { return { success: false, data: [], error: result.message || 'LOT 옵션 조회에 실패했습니다.' }; } return { success: true, data: result.data || [] }; } catch (error) { console.error('[ShipmentActions] getLotOptions error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } } // ===== 물류사 옵션 조회 ===== export async function getLogisticsOptions(): Promise<{ success: boolean; data: LogisticsOption[]; error?: string; __authError?: boolean; }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/options/logistics`, { method: 'GET', cache: 'no-store', } ); if (error) { return { success: false, data: [], error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response || !response.ok) { console.warn('[ShipmentActions] GET logistics options error:', response?.status); return { success: false, data: [], error: `API 오류: ${response?.status}` }; } const result = await response.json(); if (!result.success) { return { success: false, data: [], error: result.message || '물류사 옵션 조회에 실패했습니다.' }; } return { success: true, data: result.data || [] }; } catch (error) { console.error('[ShipmentActions] getLogisticsOptions error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } } // ===== 차량 톤수 옵션 조회 ===== export async function getVehicleTonnageOptions(): Promise<{ success: boolean; data: VehicleTonnageOption[]; error?: string; __authError?: boolean; }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/options/vehicle-tonnage`, { method: 'GET', cache: 'no-store', } ); if (error) { return { success: false, data: [], error: error.message, __authError: error.code === 'UNAUTHORIZED' }; } if (!response || !response.ok) { console.warn('[ShipmentActions] GET vehicle tonnage options error:', response?.status); return { success: false, data: [], error: `API 오류: ${response?.status}` }; } const result = await response.json(); if (!result.success) { return { success: false, data: [], error: result.message || '차량 톤수 옵션 조회에 실패했습니다.' }; } return { success: true, data: result.data || [] }; } catch (error) { console.error('[ShipmentActions] getVehicleTonnageOptions error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } }