/** * 작업자 화면 서버 액션 * API 연동 완료 (2025-12-26) * * WorkOrders API를 호출하고 WorkerScreen에 맞는 형식으로 변환 */ 'use server'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { WorkOrder, WorkOrderStatus } from '../ProductionDashboard/types'; // ===== API 타입 ===== interface WorkOrderApiItem { id: number; work_order_no: string; project_name: string | null; process_type: 'screen' | 'slat' | 'bending'; status: 'unassigned' | 'pending' | 'waiting' | 'in_progress' | 'completed' | 'shipped'; scheduled_date: string | null; memo: string | null; created_at: string; sales_order?: { id: number; order_no: string; client?: { id: number; name: string }; }; assignee?: { id: number; name: string }; items?: { id: number; item_name: string; quantity: number }[]; } // ===== 상태 변환 ===== function mapApiStatus(status: WorkOrderApiItem['status']): WorkOrderStatus { switch (status) { case 'in_progress': return 'inProgress'; case 'completed': case 'shipped': return 'completed'; default: return 'waiting'; } } // ===== API → WorkOrder 변환 ===== function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder { const totalQuantity = (api.items || []).reduce((sum, item) => sum + item.quantity, 0); const productName = api.items?.[0]?.item_name || '-'; // 납기일 계산 (지연 여부) const dueDate = api.scheduled_date || ''; const today = new Date(); today.setHours(0, 0, 0, 0); const due = dueDate ? new Date(dueDate) : null; const isDelayed = due ? due < today : false; const delayDays = due && isDelayed ? Math.ceil((today.getTime() - due.getTime()) / (1000 * 60 * 60 * 24)) : undefined; return { id: String(api.id), orderNo: api.work_order_no, productName, process: api.process_type, client: api.sales_order?.client?.name || '-', projectName: api.project_name || '-', assignees: api.assignee ? [api.assignee.name] : [], quantity: totalQuantity, dueDate, priority: 5, // 기본 우선순위 status: mapApiStatus(api.status), isUrgent: false, // 긴급 여부는 별도 필드 필요 isDelayed, delayDays, instruction: api.memo || undefined, createdAt: api.created_at, }; } // ===== 내 작업 목록 조회 ===== export async function getMyWorkOrders(): Promise<{ success: boolean; data: WorkOrder[]; error?: string; }> { try { // 작업 대기 + 작업중 상태만 조회 (완료 제외) const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders?per_page=100&assigned_to_me=1`; console.log('[WorkerScreenActions] GET my work orders:', url); const { response, error } = await serverFetch(url, { method: 'GET' }); if (error || !response) { console.warn('[WorkerScreenActions] GET error:', error?.message); return { success: false, data: [], error: error?.message || '네트워크 오류가 발생했습니다.', }; } if (!response.ok) { console.warn('[WorkerScreenActions] GET 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 || '작업 목록 조회에 실패했습니다.', }; } const apiData = result.data?.data || []; // 완료/출하 상태 제외하고 변환 const workOrders = apiData .filter((item: WorkOrderApiItem) => !['completed', 'shipped'].includes(item.status)) .map(transformToWorkerScreenFormat); return { success: true, data: workOrders, }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[WorkerScreenActions] getMyWorkOrders error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.', }; } } // ===== 작업 완료 처리 ===== export async function completeWorkOrder( id: string, materials?: { materialId: number; quantity: number; lotNo?: string }[] ): Promise<{ success: boolean; lotNo?: string; error?: string }> { try { // 상태를 completed로 변경 const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/status`, { method: 'PATCH', body: JSON.stringify({ status: 'completed', materials, }), } ); if (error || !response) { return { success: false, error: error?.message || '네트워크 오류가 발생했습니다.', }; } const result = await response.json(); console.log('[WorkerScreenActions] Complete response:', result); if (!response.ok || !result.success) { return { success: false, error: result.message || '작업 완료 처리에 실패했습니다.', }; } // LOT 번호 생성 (임시) const lotNo = `KD-SA-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`; return { success: true, lotNo, }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[WorkerScreenActions] completeWorkOrder error:', error); return { success: false, error: '서버 오류가 발생했습니다.', }; } } // ===== 자재 목록 조회 (BOM 기준) ===== export interface MaterialForInput { id: number; materialCode: string; materialName: string; unit: string; currentStock: number; fifoRank: number; } export async function getMaterialsForWorkOrder( workOrderId: string ): Promise<{ success: boolean; data: MaterialForInput[]; error?: string; }> { try { // 작업지시 BOM 기준 자재 목록 조회 const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/materials`; console.log('[WorkerScreenActions] GET materials for work order:', url); const { response, error } = await serverFetch(url, { method: 'GET' }); if (error || !response) { console.warn('[WorkerScreenActions] GET materials error:', error?.message); return { success: false, data: [], error: error?.message || '네트워크 오류가 발생했습니다.', }; } if (!response.ok) { console.warn('[WorkerScreenActions] GET materials 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 || '자재 목록 조회에 실패했습니다.', }; } // API 응답을 MaterialForInput 형식으로 변환 const materials: MaterialForInput[] = (result.data || []).map((item: { id: number; material_code: string; material_name: string; unit: string; current_stock: number; fifo_rank: number; }) => ({ id: item.id, materialCode: item.material_code, materialName: item.material_name, unit: item.unit, currentStock: item.current_stock, fifoRank: item.fifo_rank, })); return { success: true, data: materials, }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[WorkerScreenActions] getMaterialsForWorkOrder error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.', }; } } // ===== 자재 투입 등록 ===== export async function registerMaterialInput( workOrderId: string, materialIds: number[] ): Promise<{ success: boolean; error?: string }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/material-inputs`, { method: 'POST', body: JSON.stringify({ material_ids: materialIds }), } ); if (error || !response) { return { success: false, error: error?.message || '네트워크 오류가 발생했습니다.', }; } const result = await response.json(); console.log('[WorkerScreenActions] Register material input response:', result); if (!response.ok || !result.success) { return { success: false, error: result.message || '자재 투입 등록에 실패했습니다.', }; } return { success: true }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[WorkerScreenActions] registerMaterialInput error:', error); return { success: false, error: '서버 오류가 발생했습니다.', }; } } // ===== 이슈 보고 ===== export async function reportIssue( workOrderId: string, data: { title: string; description?: string; priority?: 'low' | 'medium' | 'high'; } ): Promise<{ success: boolean; error?: string }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/issues`, { method: 'POST', body: JSON.stringify(data), } ); if (error || !response) { return { success: false, error: error?.message || '네트워크 오류가 발생했습니다.', }; } const result = await response.json(); console.log('[WorkerScreenActions] Report issue response:', result); if (!response.ok || !result.success) { return { success: false, error: result.message || '이슈 보고에 실패했습니다.', }; } return { success: true }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[WorkerScreenActions] reportIssue error:', error); return { success: false, error: '서버 오류가 발생했습니다.', }; } } // ===== 공정 단계 조회 ===== export interface ProcessStepItem { id: string; itemNo: string; location: string; isPriority: boolean; spec: string; material: string; lot: string; } export interface ProcessStep { id: string; stepNo: number; name: string; isInspection?: boolean; completed: number; total: number; items: ProcessStepItem[]; } export async function getProcessSteps( workOrderId: string ): Promise<{ success: boolean; data: ProcessStep[]; error?: string; }> { try { const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/process-steps`; console.log('[WorkerScreenActions] GET process steps:', url); const { response, error } = await serverFetch(url, { method: 'GET' }); if (error || !response) { console.warn('[WorkerScreenActions] GET process steps error:', error?.message); return { success: false, data: [], error: error?.message || '네트워크 오류가 발생했습니다.', }; } if (!response.ok) { console.warn('[WorkerScreenActions] GET process steps 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 || '공정 단계 조회에 실패했습니다.', }; } // API 응답을 ProcessStep 형식으로 변환 const steps: ProcessStep[] = (result.data || []).map((step: { id: number; step_no: number; name: string; is_inspection?: boolean; completed: number; total: number; items?: { id: number; item_no: string; location: string; is_priority: boolean; spec: string; material: string; lot: string; }[]; }) => ({ id: String(step.id), stepNo: step.step_no, name: step.name, isInspection: step.is_inspection, completed: step.completed, total: step.total, items: (step.items || []).map((item) => ({ id: String(item.id), itemNo: item.item_no, location: item.location, isPriority: item.is_priority, spec: item.spec, material: item.material, lot: item.lot, })), })); return { success: true, data: steps, }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[WorkerScreenActions] getProcessSteps error:', error); return { success: false, data: [], error: '서버 오류가 발생했습니다.', }; } } // ===== 검사 요청 ===== export async function requestInspection( workOrderId: string, stepId: string ): Promise<{ success: boolean; error?: string }> { try { const { response, error } = await serverFetch( `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/process-steps/${stepId}/inspection-request`, { method: 'POST', body: JSON.stringify({}), } ); if (error || !response) { return { success: false, error: error?.message || '네트워크 오류가 발생했습니다.', }; } const result = await response.json(); console.log('[WorkerScreenActions] Inspection request response:', result); if (!response.ok || !result.success) { return { success: false, error: result.message || '검사 요청에 실패했습니다.', }; } return { success: true }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[WorkerScreenActions] requestInspection error:', error); return { success: false, error: '서버 오류가 발생했습니다.', }; } }