/** * 작업자 화면 서버 액션 * API 연동 (2025-12-26 초기, 2026-02-06 options + step-progress 확장) * * WorkOrders API를 호출하고 WorkerScreen에 맞는 형식으로 변환 */ 'use server'; import { executeServerAction } from '@/lib/api/execute-server-action'; import { buildApiUrl } from '@/lib/api/query-params'; import type { WorkOrder, WorkOrderStatus } from '../ProductionDashboard/types'; import type { WorkItemData, WorkStepData, ProcessTab } from './types'; // ===== API 타입 ===== interface WorkOrderApiItem { id: number; work_order_no: string; project_name: string | null; process_id: number | null; process?: { id: number; process_name: string; process_code: string; department?: string | null; options?: { needs_inspection?: boolean; needs_work_log?: boolean; } | null; }; /** @deprecated process_id + process relation 사용 */ 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; item?: { id: number; code: string; name: string } | null; client?: { id: number; name: string }; client_contact?: string; options?: { manager_name?: string; [key: string]: unknown }; root_nodes_count?: number; }; team_id?: number | null; team?: { id: number; name: string } | null; assignee?: { id: number; name: string }; assignees?: { id: number; user_id: number; user?: { id: number; name: string } }[]; items?: { id: number; item_name: string; item_id?: number | null; item?: { id: number; code: string; name: string } | null; quantity: number; specification?: string | null; options?: Record | null; source_order_item?: { id: number; order_node_id: number | null; floor_code?: string | null; symbol_code?: string | null; node?: { id: number; name: string; code: string } | null; } | null; material_inputs?: { id: number; stock_lot_id: number; item_id: number; qty: number; input_by: number | null; input_at: string | null; stock_lot?: { id: number; lot_no: string } | null; item?: { id: number; code: string; name: string; unit: string } | null; }[]; }[]; } // ===== 상태 변환 ===== 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 + Number(item.quantity), 0); const productCode = (api.items?.[0]?.options?.product_code as string) || api.sales_order?.item?.code || '-'; const productName = (api.items?.[0]?.options?.product_name as string) || 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; // process relation → processCode/processName 매핑 // 신규: process relation 사용, 폴백: deprecated process_type 필드 const rawProcessName = api.process?.process_name || ''; const rawProcessCode = api.process?.process_code || ''; // 한글 공정명 → 탭 키 매핑 (필터링에 사용) const nameToTab: Record = { '스크린': { code: 'screen', name: '스크린' }, '슬랫': { code: 'slat', name: '슬랫' }, '절곡': { code: 'bending', name: '절곡' }, }; // 1차: process relation의 process_name으로 매핑 let processInfo = Object.entries(nameToTab).find( ([key]) => rawProcessName.includes(key) )?.[1]; // 2차: deprecated process_type 폴백 if (!processInfo && api.process_type) { const legacyMap: Record = { screen: { code: 'screen', name: '스크린' }, slat: { code: 'slat', name: '슬랫' }, bending: { code: 'bending', name: '절곡' }, }; processInfo = legacyMap[api.process_type]; } // 3차: 그래도 없으면 원시값 사용 if (!processInfo) { processInfo = { code: rawProcessCode || 'unknown', name: rawProcessName || '알수없음' }; } // 아이템을 개소(order_node_id)별로 그룹핑 (floor_code/symbol_code는 표시용) const nodeMap = new Map(); for (const item of (api.items || [])) { const nodeId = item.source_order_item?.order_node_id ?? null; const floorCode = item.source_order_item?.floor_code; const symbolCode = item.source_order_item?.symbol_code; const floorLabel = [floorCode, symbolCode].filter(v => v && v !== '-').join('/'); const nodeName = floorLabel || item.source_order_item?.node?.name || '미지정'; const key = nodeId != null ? String(nodeId) : (floorLabel || `unassigned-${item.id}`); if (!nodeMap.has(key)) { nodeMap.set(key, { nodeId, nodeName, items: [] }); } nodeMap.get(key)!.items!.push(item); } const nodeGroups = Array.from(nodeMap.values()).map((g) => ({ nodeId: g.nodeId, nodeName: g.nodeName, items: (g.items || []).map((it) => ({ id: it.id, itemCode: it.item?.code || null, itemName: it.item_name, quantity: Number(it.quantity), specification: it.specification, options: it.options, materialInputs: (it.material_inputs || []).map((mi) => ({ id: mi.id, stockLotId: mi.stock_lot_id, lotNo: mi.stock_lot?.lot_no || null, itemId: mi.item_id, materialName: mi.item?.name || null, qty: Number(mi.qty), unit: mi.item?.unit || 'EA', })), })), totalQuantity: (g.items || []).reduce((sum, it) => sum + Number(it.quantity), 0), })); return { id: String(api.id), orderNo: api.work_order_no, productCode, productName, processCode: processInfo.code, processName: processInfo.name, client: api.sales_order?.client?.name || '-', projectName: api.project_name || '-', assignees: api.assignees?.length ? api.assignees.map((a) => a.user?.name || '').filter(Boolean) : api.assignee ? [api.assignee.name] : [], quantity: totalQuantity, shutterCount: nodeGroups.length || api.sales_order?.root_nodes_count || 0, dueDate, priority: 5, // 기본 우선순위 status: mapApiStatus(api.status), isUrgent: false, // 긴급 여부는 별도 필드 필요 isDelayed, delayDays, instruction: api.memo || undefined, salesOrderNo: api.sales_order?.order_no || undefined, salesManager: api.sales_order?.options?.manager_name as string || undefined, managerPhone: api.sales_order?.client_contact || undefined, teamId: api.team_id ?? null, teamName: api.team?.name || undefined, processDepartment: api.process?.department || undefined, scheduledDate: api.scheduled_date || undefined, createdAt: api.created_at, processOptions: { needsInspection: api.process?.options?.needs_inspection ?? false, needsWorkLog: api.process?.options?.needs_work_log ?? false, }, nodeGroups, }; } const API_URL = process.env.NEXT_PUBLIC_API_URL; // ===== 내 작업 목록 조회 ===== export async function getMyWorkOrders(): Promise<{ success: boolean; data: WorkOrder[]; error?: string; }> { interface PaginatedWO { data: WorkOrderApiItem[] } const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders?per_page=100&worker_screen=1`, errorMessage: '작업 목록 조회에 실패했습니다.', }); if (!result.success || !result.data) return { success: false, data: [], error: result.error }; const workOrders = (result.data.data || []) .filter((item) => !['completed', 'shipped'].includes(item.status)) .map(transformToWorkerScreenFormat); return { success: true, data: workOrders }; } // ===== 작업 완료 처리 ===== export async function completeWorkOrder( id: string, materials?: { materialId: number; quantity: number; lotNo?: string }[] ): Promise<{ success: boolean; lotNo?: string; error?: string }> { const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${id}/status`, method: 'PATCH', body: { status: 'completed', materials }, errorMessage: '작업 완료 처리에 실패했습니다.', }); if (!result.success) return { success: false, error: result.error }; // 백엔드에서 생성한 실제 LOT 번호 사용 const lotNo = (result.data as { lot_no?: string })?.lot_no; return { success: true, lotNo }; } // ===== 자재 목록 조회 (로트 기준) ===== export interface MaterialForInput { stockLotId: number | null; // StockLot ID (null이면 재고 없음) itemId: number; lotNo: string | null; // 실제 입고 로트번호 materialCode: string; materialName: string; specification: string; unit: string; requiredQty: number; // 필요 수량 lotAvailableQty: number; // 로트별 가용 수량 fifoRank: number; // dynamic_bom 추가 필드 (절곡 세부품목용) workOrderItemId?: number; // 개소(작업지시품목) ID lotPrefix?: string; // LOT prefix (RS, RM 등) partType?: string; // 파트 타입 (finish, body 등) category?: string; // 카테고리 (guideRail, bottomBar 등) } export async function getMaterialsForWorkOrder( workOrderId: string ): Promise<{ success: boolean; data: MaterialForInput[]; error?: string; }> { interface MaterialApiItem { stock_lot_id: number | null; item_id: number; lot_no: string | null; material_code: string; material_name: string; specification: string; unit: string; required_qty: number; lot_available_qty: number; fifo_rank: number; // dynamic_bom 추가 필드 work_order_item_id?: number; lot_prefix?: string; part_type?: string; category?: string; } const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/materials`, errorMessage: '자재 목록 조회에 실패했습니다.', }); if (!result.success || !result.data) return { success: false, data: [], error: result.error }; return { success: true, data: result.data.map((item) => ({ stockLotId: item.stock_lot_id, itemId: item.item_id, lotNo: item.lot_no, materialCode: item.material_code, materialName: item.material_name, specification: item.specification ?? '', unit: item.unit, requiredQty: item.required_qty, lotAvailableQty: item.lot_available_qty, fifoRank: item.fifo_rank, workOrderItemId: item.work_order_item_id, lotPrefix: item.lot_prefix, partType: item.part_type, category: item.category, })), }; } // ===== 자재 투입 등록 (로트별 수량) ===== export async function registerMaterialInput( workOrderId: string, inputs: { stock_lot_id: number; qty: number; work_order_item_id?: number }[] ): Promise<{ success: boolean; error?: string }> { const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/material-inputs`, method: 'POST', body: { inputs }, errorMessage: '자재 투입 등록에 실패했습니다.', }); return { success: result.success, error: result.error }; } // ===== 개소별 자재 목록 조회 ===== export interface MaterialForItemInput extends MaterialForInput { alreadyInputted: number; // 이미 투입된 수량 remainingRequiredQty: number; // 남은 필요 수량 lotInputtedQty: number; // 해당 LOT의 기투입 수량 bomGroupKey?: string; // BOM 엔트리별 고유 그룹키 (category+partType 기반) } export async function getMaterialsForItem( workOrderId: string, itemId: number ): Promise<{ success: boolean; data: MaterialForItemInput[]; error?: string; }> { interface MaterialItemApiItem { stock_lot_id: number | null; item_id: number; lot_no: string | null; material_code: string; material_name: string; specification: string; unit: string; bom_qty: number; required_qty: number; already_inputted: number; remaining_required_qty: number; lot_inputted_qty: number; lot_available_qty: number; fifo_rank: number; lot_qty: number; lot_reserved_qty: number; receipt_date: string | null; supplier: string | null; // dynamic_bom 추가 필드 work_order_item_id?: number; lot_prefix?: string; part_type?: string; category?: string; bom_group_key?: string; } const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/materials`, errorMessage: '개소별 자재 목록 조회에 실패했습니다.', }); if (!result.success || !result.data) return { success: false, data: [], error: result.error }; return { success: true, data: result.data.map((item) => ({ stockLotId: item.stock_lot_id, itemId: item.item_id, lotNo: item.lot_no, materialCode: item.material_code, materialName: item.material_name, specification: item.specification ?? '', unit: item.unit, requiredQty: item.required_qty, lotAvailableQty: item.lot_available_qty, fifoRank: item.fifo_rank, alreadyInputted: item.already_inputted, remainingRequiredQty: item.remaining_required_qty, lotInputtedQty: item.lot_inputted_qty ?? 0, workOrderItemId: item.work_order_item_id, lotPrefix: item.lot_prefix, partType: item.part_type, category: item.category, bomGroupKey: item.bom_group_key, })), }; } // ===== 개소별 자재 투입 등록 ===== export async function registerMaterialInputForItem( workOrderId: string, itemId: number, inputs: { stock_lot_id: number; qty: number; bom_group_key?: string }[], replace = false ): Promise<{ success: boolean; error?: string }> { const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/material-inputs`, method: 'POST', body: { inputs, replace }, errorMessage: '개소별 자재 투입 등록에 실패했습니다.', }); return { success: result.success, error: result.error }; } // ===== 개소별 자재 투입 이력 조회 ===== export interface MaterialInputHistoryItem { id: number; stockLotId: number; lotNo: string | null; itemId: number; materialCode: string | null; materialName: string | null; qty: number; unit: string; inputBy: number | null; inputByName: string | null; inputAt: string | null; } export async function getMaterialInputsForItem( workOrderId: string, itemId: number ): Promise<{ success: boolean; data: MaterialInputHistoryItem[]; error?: string; }> { interface HistoryApiItem { id: number; stock_lot_id: number; lot_no: string | null; item_id: number; material_code: string | null; material_name: string | null; qty: number; unit: string; input_by: number | null; input_by_name: string | null; input_at: string | null; } const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/material-inputs`, errorMessage: '개소별 투입 이력 조회에 실패했습니다.', }); if (!result.success || !result.data) return { success: false, data: [], error: result.error }; return { success: true, data: result.data.map((item) => ({ id: item.id, stockLotId: item.stock_lot_id, lotNo: item.lot_no, itemId: item.item_id, materialCode: item.material_code, materialName: item.material_name, qty: item.qty, unit: item.unit, inputBy: item.input_by, inputByName: item.input_by_name, inputAt: item.input_at, })), }; } // ===== 자재 투입 삭제 (재고 복원) ===== export async function deleteMaterialInput( workOrderId: string, inputId: number ): Promise<{ success: boolean; error?: string }> { const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/material-inputs/${inputId}`, method: 'DELETE', errorMessage: '자재 투입 삭제에 실패했습니다.', }); return { success: result.success, error: result.error }; } // ===== 자재 투입 수량 수정 ===== export async function updateMaterialInput( workOrderId: string, inputId: number, qty: number ): Promise<{ success: boolean; data?: { id: number; qty: number; changed: boolean }; error?: string }> { const result = await executeServerAction<{ id: number; qty: number; changed: boolean }>({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/material-inputs/${inputId}`, method: 'PATCH', body: { qty }, errorMessage: '자재 투입 수정에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; } // ===== 이슈 보고 ===== export async function reportIssue( workOrderId: string, data: { title: string; description?: string; priority?: 'low' | 'medium' | 'high'; } ): Promise<{ success: boolean; error?: string }> { const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/issues`, method: 'POST', body: data, errorMessage: '이슈 보고에 실패했습니다.', }); return { success: result.success, error: result.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; }> { interface StepApiItem { 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 }[]; } const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/process-steps`, errorMessage: '공정 단계 조회에 실패했습니다.', }); if (!result.success || !result.data) return { success: false, data: [], error: result.error }; return { success: true, data: result.data.map((step) => ({ 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, })), })), }; } // ===== 검사 요청 ===== export async function requestInspection( workOrderId: string, stepId: string ): Promise<{ success: boolean; error?: string }> { const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/process-steps/${stepId}/inspection-request`, method: 'POST', body: {}, errorMessage: '검사 요청에 실패했습니다.', }); return { success: result.success, error: result.error }; } // ===== 공정 단계 진행 현황 조회 ===== export interface StepProgressItem { id: number; process_step_id: number; work_order_item_id: number | null; step_code: string; step_name: string; sort_order: number; needs_inspection: boolean; connection_type: string | null; completion_type: string | null; status: string; is_completed: boolean; completed_at: string | null; completed_by: number | null; } export async function getStepProgress( workOrderId: string ): Promise<{ success: boolean; data: StepProgressItem[]; error?: string; }> { const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/step-progress`, errorMessage: '단계 진행 조회에 실패했습니다.', }); if (!result.success || !result.data) return { success: false, data: [], error: result.error }; return { success: true, data: result.data }; } // ===== 공정 단계 완료 토글 ===== export async function toggleStepProgress( workOrderId: string, progressId: number ): Promise<{ success: boolean; data?: StepProgressItem; error?: string }> { const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/step-progress/${progressId}/toggle`, method: 'PATCH', errorMessage: '단계 토글에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; } // ===== 작업지시 상세 조회 (items + options 포함) ===== export async function getWorkOrderDetail( workOrderId: string ): Promise<{ success: boolean; data: WorkItemData[]; error?: string; }> { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}`, errorMessage: '작업지시 상세 조회에 실패했습니다.', }); if (!result.success || !result.data) return { success: false, data: [], error: result.error }; const wo = result.data; const processName = (wo.process?.process_name || '').toLowerCase(); const processType: ProcessTab = processName.includes('스크린') ? 'screen' : processName.includes('슬랫') ? 'slat' : processName.includes('절곡') ? 'bending' : 'screen'; const items: WorkItemData[] = (wo.items || []).map((item: { id: number; item_name: string; specification: string | null; quantity: string; unit: string | null; status: string; sort_order: number; options: Record | null; }, index: number) => { const opts = item.options || {}; const stepProgressList = wo.step_progress || []; const processSteps = wo.process?.steps || []; let steps: WorkStepData[]; if (stepProgressList.length > 0) { steps = stepProgressList .filter((sp: { work_order_item_id: number | null }) => !sp.work_order_item_id || sp.work_order_item_id === item.id) .map((sp: { id: number; process_step: { step_name: string; step_code: string; needs_inspection?: boolean; connection_type?: string; completion_type?: string; } | null; status: string; }) => ({ id: String(sp.id), name: sp.process_step?.step_name || '', isMaterialInput: (sp.process_step?.step_code || '').includes('MAT') || (sp.process_step?.step_name || '').includes('자재투입'), isInspection: sp.process_step?.needs_inspection || false, isCompleted: sp.status === 'completed', stepProgressId: sp.id, needsInspection: sp.process_step?.needs_inspection || false, connectionType: sp.process_step?.connection_type || undefined, connectionTarget: undefined, // step_progress API에 미포함, processListCache에서 보완 completionType: sp.process_step?.completion_type || undefined, })); } else { steps = processSteps.map((ps: { id: number; step_name: string; step_code: string; needs_inspection?: boolean; connection_type?: string; completion_type?: string; }, si: number) => ({ id: `${item.id}-step-${si}`, name: ps.step_name, isMaterialInput: ps.step_code.includes('MAT') || ps.step_name.includes('자재투입'), isInspection: ps.needs_inspection || false, isCompleted: false, needsInspection: ps.needs_inspection || false, connectionType: ps.connection_type || undefined, completionType: ps.completion_type || undefined, })); } const workItem: WorkItemData = { id: String(item.id), itemNo: index + 1, itemCode: wo.work_order_no || '-', itemName: item.item_name || '-', floor: (opts.floor as string) || '-', code: (opts.code as string) || '-', width: (opts.width as number) || 0, height: (opts.height as number) || 0, quantity: Number(item.quantity) || 0, processType, steps, materialInputs: [], }; if (opts.cutting_info) { const ci = opts.cutting_info as { width: number; sheets: number }; workItem.cuttingInfo = { width: ci.width, sheets: ci.sheets }; } if (opts.slat_info) { const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number; glass_qty: number }; workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar, glassQty: si.glass_qty || 0 }; } if (opts.bending_info) { const bi = opts.bending_info as { common: { kind: string; type: string; length_quantities: { length: number; quantity: number }[] }; detail_parts: { part_name: string; material: string; barcy_info: string }[]; }; workItem.bendingInfo = { common: { kind: bi.common.kind, type: bi.common.type, lengthQuantities: bi.common.length_quantities || [] }, detailParts: (bi.detail_parts || []).map(dp => ({ partName: dp.part_name, material: dp.material, barcyInfo: dp.barcy_info })), }; } if (opts.is_wip) { workItem.isWip = true; const wi = opts.wip_info as { specification: string; length_quantity: string } | undefined; if (wi) { workItem.wipInfo = { specification: wi.specification, lengthQuantity: wi.length_quantity }; } } if (opts.is_joint_bar) { workItem.isJointBar = true; const jb = opts.slat_joint_bar_info as { specification: string; length: number; quantity: number } | undefined; if (jb) workItem.slatJointBarInfo = jb; } return workItem; }); return { success: true, data: items }; } // ===== 개소별 중간검사 데이터 저장 ===== export async function saveItemInspection( workOrderId: string, itemId: number, processType: string, inspectionData: Record ): Promise<{ success: boolean; data?: Record; error?: string }> { const result = await executeServerAction>({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/inspection`, method: 'POST', body: { process_type: processType, inspection_data: inspectionData }, errorMessage: '검사 데이터 저장에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; } // ===== 작업지시 전체 검사 데이터 조회 ===== export interface InspectionDataItem { item_id: number; item_name: string; specification: string | null; quantity: number; sort_order: number; options: Record | null; inspection_data: Record; } export async function getWorkOrderInspectionData( workOrderId: string ): Promise<{ success: boolean; data?: { work_order_id: number; items: InspectionDataItem[]; total: number }; error?: string; }> { const result = await executeServerAction<{ work_order_id: number; items: InspectionDataItem[]; total: number }>({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/inspection-data`, errorMessage: '검사 데이터 조회에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; } // ===== 작업일지 저장 ===== export async function saveWorkLog( workOrderId: string, data: { basic_data?: Record; table_data?: Array>; remarks?: string; title?: string; rendered_html?: string; } ): Promise<{ success: boolean; data?: { document_id: number; document_no: string; status: string }; error?: string; }> { const result = await executeServerAction<{ document_id: number; document_no: string; status: string }>({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/work-log`, method: 'POST', body: data, errorMessage: '작업일지 저장에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; } // ===== 작업일지 조회 ===== export async function getWorkLog( workOrderId: string ): Promise<{ success: boolean; data?: { template: Record; document: Record | null; auto_values: Record; work_stats: Record; bending_images: Record; }; error?: string; }> { const result = await executeServerAction<{ template: Record; document: Record | null; auto_values: Record; work_stats: Record; bending_images: Record; }>({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/work-log`, errorMessage: '작업일지 조회에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; } // ===== 검사 문서 템플릿 타입 (types.ts에서 import) ===== import type { InspectionTemplateData } from './types'; export async function getInspectionTemplate( workOrderId: string ): Promise<{ success: boolean; data?: InspectionTemplateData; error?: string; }> { const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/inspection-template`, errorMessage: '검사 템플릿 조회에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; } // ===== 검사 문서 동기화 (원본: work_order_items → document_data) ===== export async function saveInspectionDocument( workOrderId: string, data: { title?: string; approvers?: { role_name: string; user_id?: number }[]; } = {} ): Promise<{ success: boolean; data?: { document_id: number; document_no: string; status: string }; error?: string; }> { const result = await executeServerAction<{ document_id: number; document_no: string; status: string }>({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/inspection-document`, method: 'POST', body: data, errorMessage: '검사 문서 동기화에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; } // ===== 부서 목록 조회 (작업 정보용) ===== export interface DepartmentOption { id: number; name: string; } export async function getDepartments(): Promise<{ success: boolean; data: DepartmentOption[]; error?: string; }> { interface DeptApiItem { id: number; name: string; parent_id?: number | null; [key: string]: unknown } const result = await executeServerAction<{ data: DeptApiItem[] } | DeptApiItem[]>({ url: buildApiUrl('/api/v1/departments', { per_page: 100 }), errorMessage: '부서 목록 조회에 실패했습니다.', }); if (!result.success || !result.data) return { success: false, data: [], error: result.error }; const list = Array.isArray(result.data) ? result.data : (result.data.data || []); return { success: true, data: list.map((d) => ({ id: d.id, name: d.name })), }; } // ===== 부서별 사용자 목록 조회 ===== export interface DepartmentUser { id: number; name: string; } export async function getDepartmentUsers(departmentId: number): Promise<{ success: boolean; data: DepartmentUser[]; error?: string; }> { interface UserApiItem { id: number; name: string; [key: string]: unknown } const result = await executeServerAction<{ data: UserApiItem[] } | UserApiItem[]>({ url: buildApiUrl(`/api/v1/departments/${departmentId}/users`), errorMessage: '부서 사용자 목록 조회에 실패했습니다.', }); if (!result.success || !result.data) return { success: false, data: [], error: result.error }; const list = Array.isArray(result.data) ? result.data : (result.data.data || []); return { success: true, data: list.map((u) => ({ id: u.id, name: u.name })), }; } // ===== 자재 투입용 재고 검색 ===== export interface StockSearchResult { itemId: number; itemCode: string; itemName: string; stockQty: number; availableQty: number; lotCount: number; lots: Array<{ lotNo: string; availableQty: number; fifoOrder: number }>; } export async function searchStockByCode( search: string ): Promise<{ success: boolean; data: StockSearchResult[]; error?: string }> { const result = await executeServerAction<{ data: Array<{ id: number; item_id: number; item_code: string; item_name: string; stock_qty: number; available_qty: number; lot_count: number; lots?: Array<{ lot_no: string; available_qty: number; fifo_order: number }>; }>; }>({ url: buildApiUrl('/api/v1/stocks', { search, per_page: 10, with_lots: 'true' }), errorMessage: '재고 검색에 실패했습니다.', }); if (!result.success || !result.data) return { success: false, data: [], error: result.error }; const items = Array.isArray(result.data) ? result.data : (result.data.data || []); return { success: true, data: items.map((s) => ({ itemId: s.item_id, itemCode: s.item_code, itemName: s.item_name, stockQty: s.stock_qty || 0, availableQty: s.available_qty || 0, lotCount: s.lot_count || 0, lots: (s.lots || []).map((l) => ({ lotNo: l.lot_no, availableQty: l.available_qty, fifoOrder: l.fifo_order, })), })), }; }