feat: 자재투입 모달 복수 작업지시품목 병렬 조회 지원
- workOrderItemIds prop 추가 (절곡 등 복수 item 공정 대응) - Promise.all로 복수 item 자재 병렬 조회 후 합치기 - 각 자재에 소스 workOrderItemId 태깅 (submit 시 올바른 item에 등록) - 기존 단일 workOrderItemId 호환 유지
This commit is contained in:
@@ -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<number, { stock_lot_id: number; qty: number }[]>();
|
||||
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('자재 투입이 등록되었습니다.');
|
||||
|
||||
@@ -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<MaterialItemApiItem[]>({
|
||||
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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@ export default function WorkerScreen() {
|
||||
const [isCompletionDialogOpen, setIsCompletionDialogOpen] = useState(false);
|
||||
const [isMaterialModalOpen, setIsMaterialModalOpen] = useState(false);
|
||||
const [selectedWorkOrderItemId, setSelectedWorkOrderItemId] = useState<number | undefined>();
|
||||
const [selectedWorkOrderItemIds, setSelectedWorkOrderItemIds] = useState<number[] | undefined>();
|
||||
const [selectedWorkOrderItemName, setSelectedWorkOrderItemName] = useState<string | undefined>();
|
||||
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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user