diff --git a/src/components/production/WorkerScreen/MaterialInputModal.tsx b/src/components/production/WorkerScreen/MaterialInputModal.tsx index 7da630c3..37a6c420 100644 --- a/src/components/production/WorkerScreen/MaterialInputModal.tsx +++ b/src/components/production/WorkerScreen/MaterialInputModal.tsx @@ -37,7 +37,8 @@ interface MaterialInputModalProps { open: boolean; onOpenChange: (open: boolean) => void; order: WorkOrder | null; - workOrderItemId?: number; // 개소(작업지시품목) ID + workOrderItemId?: number; // 개소(작업지시품목) ID (첫 번째 item, 호환용) + workOrderItemIds?: number[]; // 개소 내 모든 작업지시품목 IDs (절곡 등 복수 item) workOrderItemName?: string; // 개소명 (모달 헤더 표시용) onComplete?: () => void; isCompletionFlow?: boolean; @@ -69,6 +70,7 @@ export function MaterialInputModal({ onOpenChange, order, workOrderItemId, + workOrderItemIds, workOrderItemName, onComplete, isCompletionFlow = false, @@ -190,15 +192,44 @@ export function MaterialInputModal({ return; } - // 개소별 API vs 전체 API 분기 - const result = workOrderItemId - ? await getMaterialsForItem(order.id, workOrderItemId) - : await getMaterialsForWorkOrder(order.id); + // 복수 item IDs가 있으면 각각 병렬 조회 후 합치기 (절곡 공정 등) + const itemIds = workOrderItemIds && workOrderItemIds.length > 0 + ? workOrderItemIds + : workOrderItemId ? [workOrderItemId] : null; - if (result.success) { - setMaterials(result.data); + if (itemIds && itemIds.length > 0) { + // 개소 내 모든 items의 자재를 병렬 조회 + const results = await Promise.all( + itemIds.map((id) => getMaterialsForItem(order.id, id)) + ); + const allMaterials: MaterialForItemInput[] = []; + let hasError = false; + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result.success) { + // 각 자재에 소스 work_order_item_id 태깅 (submit 시 올바른 item에 등록하기 위함) + const tagged = result.data.map((m) => ({ + ...m, + workOrderItemId: m.workOrderItemId || itemIds[i], + })); + allMaterials.push(...tagged); + } else { + hasError = true; + } + } + if (allMaterials.length > 0) { + setMaterials(allMaterials); + } else if (hasError) { + toast.error('자재 목록 조회에 실패했습니다.'); + } } else { - toast.error(result.error || '자재 목록 조회에 실패했습니다.'); + // 전체 작업지시 기준 조회 + const result = await getMaterialsForWorkOrder(order.id); + if (result.success) { + setMaterials(result.data); + } else { + toast.error(result.error || '자재 목록 조회에 실패했습니다.'); + } } } catch (error) { if (isNextRedirectError(error)) throw error; @@ -207,7 +238,7 @@ export function MaterialInputModal({ } finally { setIsLoading(false); } - }, [order, workOrderItemId]); + }, [order, workOrderItemId, workOrderItemIds]); // 모달이 열릴 때 데이터 로드 + 선택 초기화 useEffect(() => { @@ -246,10 +277,34 @@ export function MaterialInputModal({ setIsSubmitting(true); try { - // 개소별 API vs 전체 API 분기 - const result = workOrderItemId - ? await registerMaterialInputForItem(order.id, workOrderItemId, inputs) - : await registerMaterialInput(order.id, inputs); + // 복수 item IDs가 있으면 item별로 그룹핑하여 각각 등록 + let result: { success: boolean; error?: string }; + + if (workOrderItemIds && workOrderItemIds.length > 1) { + // inputs를 work_order_item_id별로 그룹핑 + const grouped = new Map(); + for (const input of inputs) { + const woItemId = input.work_order_item_id; + if (woItemId) { + if (!grouped.has(woItemId)) grouped.set(woItemId, []); + grouped.get(woItemId)!.push({ stock_lot_id: input.stock_lot_id, qty: input.qty }); + } + } + // 각 item별로 병렬 등록 + const results = await Promise.all( + Array.from(grouped.entries()).map(([itemId, itemInputs]) => + registerMaterialInputForItem(order.id, itemId, itemInputs) + ) + ); + const hasFailure = results.some((r) => !r.success); + result = hasFailure + ? { success: false, error: results.find((r) => !r.success)?.error } + : { success: true }; + } else if (workOrderItemId) { + result = await registerMaterialInputForItem(order.id, workOrderItemId, inputs); + } else { + result = await registerMaterialInput(order.id, inputs); + } if (result.success) { toast.success('자재 투입이 등록되었습니다.'); diff --git a/src/components/production/WorkerScreen/actions.ts b/src/components/production/WorkerScreen/actions.ts index 6c229917..d63dcb2e 100644 --- a/src/components/production/WorkerScreen/actions.ts +++ b/src/components/production/WorkerScreen/actions.ts @@ -329,6 +329,8 @@ export async function getMaterialsForItem( 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; } const result = await executeServerAction({ url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/materials`, @@ -345,6 +347,8 @@ export async function getMaterialsForItem( fifoRank: item.fifo_rank, alreadyInputted: item.already_inputted, remainingRequiredQty: item.remaining_required_qty, + workOrderItemId: item.work_order_item_id, lotPrefix: item.lot_prefix, + partType: item.part_type, category: item.category, })), }; } diff --git a/src/components/production/WorkerScreen/index.tsx b/src/components/production/WorkerScreen/index.tsx index 1856d502..b453cf66 100644 --- a/src/components/production/WorkerScreen/index.tsx +++ b/src/components/production/WorkerScreen/index.tsx @@ -393,6 +393,7 @@ export default function WorkerScreen() { const [isCompletionDialogOpen, setIsCompletionDialogOpen] = useState(false); const [isMaterialModalOpen, setIsMaterialModalOpen] = useState(false); const [selectedWorkOrderItemId, setSelectedWorkOrderItemId] = useState(); + const [selectedWorkOrderItemIds, setSelectedWorkOrderItemIds] = useState(); const [selectedWorkOrderItemName, setSelectedWorkOrderItemName] = useState(); const [isWorkLogModalOpen, setIsWorkLogModalOpen] = useState(false); const [isInspectionModalOpen, setIsInspectionModalOpen] = useState(false); @@ -688,9 +689,13 @@ export default function WorkerScreen() { } } + // 개소 내 모든 item IDs 수집 (절곡 공정 등에서 복수 items) + const allItemIds = group.items.map((it) => it.id as number).filter(Boolean); + const workItem: WorkItemData = { id: `${selectedOrder.id}-node-${nodeKey}`, apiItemId: firstItem?.id as number | undefined, + apiItemIds: allItemIds.length > 0 ? allItemIds : undefined, workOrderId: selectedOrder.id, itemNo: index + 1, itemCode: selectedOrder.orderNo || '-', @@ -967,8 +972,9 @@ export default function WorkerScreen() { const workItem = workItems.find((item) => item.id === itemId); if (order) { setSelectedOrder(order); - // 개소별 API 호출을 위한 apiItemId 설정 + // 개소별 API 호출을 위한 apiItemId(s) 설정 setSelectedWorkOrderItemId(workItem?.apiItemId); + setSelectedWorkOrderItemIds(workItem?.apiItemIds); setSelectedWorkOrderItemName(workItem ? `${workItem.itemName} (${workItem.code})` : undefined); setIsMaterialModalOpen(true); } else { @@ -1658,11 +1664,13 @@ export default function WorkerScreen() { setIsMaterialModalOpen(open); if (!open) { setSelectedWorkOrderItemId(undefined); + setSelectedWorkOrderItemIds(undefined); setSelectedWorkOrderItemName(undefined); } }} order={selectedOrder} workOrderItemId={selectedWorkOrderItemId} + workOrderItemIds={selectedWorkOrderItemIds} workOrderItemName={selectedWorkOrderItemName} isCompletionFlow={isCompletionFlow} onComplete={handleWorkCompletion} diff --git a/src/components/production/WorkerScreen/types.ts b/src/components/production/WorkerScreen/types.ts index 235c6f33..3d72e050 100644 --- a/src/components/production/WorkerScreen/types.ts +++ b/src/components/production/WorkerScreen/types.ts @@ -31,7 +31,8 @@ export interface WorkInfo { // ===== 작업 아이템 (카드 1개 단위) ===== export interface WorkItemData { id: string; - apiItemId?: number; // 실제 work_order_items.id (API 호출용) + apiItemId?: number; // 실제 work_order_items.id (API 호출용, 첫 번째 item) + apiItemIds?: number[]; // 개소 내 모든 work_order_items.id 배열 (절곡 등 복수 item) workOrderId?: string; // 소속 작업지시 ID (API 호출용) itemNo: number; // 번호 (1, 2, 3...) itemCode: string; // 품목코드 (KWWS03)