From e3657b099ddc2a0395e500a75c7b54a90c0ef851 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Fri, 10 Apr 2026 17:30:01 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A0=95=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC/=EB=8B=A4=EC=A4=91=ED=92=88=EB=AA=A9=20?= =?UTF-8?q?=EB=B1=83=EC=A7=80=20+=20=ED=92=88=EB=AA=A9=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단일품목: 회색 뱃지 [단일 · 제품] - 다중품목: 파랑 뱃지 [다중 1/2 · 반제품] - 리워크: 주황 뱃지 유지 (기존) - item_info.type으로 품목타입(제품/반제품/원재료/부재료) 표시 - workInstructionController: getList에 item_type 추가 - WorkOrderList: multiBatchInfo useMemo로 단일/다중 판단 - ProcessWork: batchBadge로 헤더에 뱃지 표시 --- .../controllers/workInstructionController.ts | 3 +- .../pop/hardcoded/production/ProcessWork.tsx | 72 ++++++++++++++-- .../hardcoded/production/WorkOrderList.tsx | 84 ++++++++++++++++++- 3 files changed, 150 insertions(+), 9 deletions(-) diff --git a/backend-node/src/controllers/workInstructionController.ts b/backend-node/src/controllers/workInstructionController.ts index a95b08f1..1de120a7 100644 --- a/backend-node/src/controllers/workInstructionController.ts +++ b/backend-node/src/controllers/workInstructionController.ts @@ -86,6 +86,7 @@ export async function getList(req: AuthenticatedRequest, res: Response) { d.source_id, d.routing_version_id AS detail_routing_version_id, COALESCE(itm.item_name, '') AS item_name, + COALESCE(itm.type, '') AS item_type, COALESCE(itm.size, '') AS item_spec, COALESCE(e.equipment_name, '') AS equipment_name, COALESCE(e.equipment_code, '') AS equipment_code, @@ -97,7 +98,7 @@ export async function getList(req: AuthenticatedRequest, res: Response) { INNER JOIN work_instruction_detail d ON d.work_instruction_no = wi.work_instruction_no AND d.company_code = wi.company_code LEFT JOIN LATERAL ( - SELECT item_name, size FROM item_info + SELECT item_name, size, type FROM item_info WHERE item_number = d.item_number AND company_code = wi.company_code LIMIT 1 ) itm ON true LEFT JOIN equipment_mng e ON wi.equipment_id = e.id AND wi.company_code = e.company_code diff --git a/frontend/components/pop/hardcoded/production/ProcessWork.tsx b/frontend/components/pop/hardcoded/production/ProcessWork.tsx index 811b9650..93e78193 100644 --- a/frontend/components/pop/hardcoded/production/ProcessWork.tsx +++ b/frontend/components/pop/hardcoded/production/ProcessWork.tsx @@ -404,6 +404,14 @@ export function ProcessWork({ processId }: ProcessWorkProps) { const [packageUnit, setPackageUnit] = useState(""); const [inboundDone, setInboundDone] = useState(false); + /* ---- Batch Badge (단일/다중품목) ---- */ + const [batchBadge, setBatchBadge] = useState<{ + isMulti: boolean; + index: number; + total: number; + itemType: string; + } | null>(null); + /* ---- Batch History ---- */ const [history, setHistory] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); @@ -453,6 +461,7 @@ export function ProcessWork({ processId }: ProcessWorkProps) { } // batch_id가 있으면 해당 품목의 이름을 조회 (다중 품목 지원) + let batchItemType = ""; if (procData.batch_id) { try { const batchItemRes = await dataApi.getTableData("item_info", { @@ -463,6 +472,7 @@ export function ProcessWork({ processId }: ProcessWorkProps) { if (batchItem) { itemName = String(batchItem.item_name || procData.batch_id); itemCode = String(batchItem.item_number || procData.batch_id); + batchItemType = String(batchItem.type || ""); } else { itemName = procData.batch_id; itemCode = procData.batch_id; @@ -472,6 +482,23 @@ export function ProcessWork({ processId }: ProcessWorkProps) { itemCode = procData.batch_id; } } + // item_type이 없으면 WI의 item_number로 조회 + if (!batchItemType && wi.item_number) { + try { + const wiItemRes = await dataApi.getTableData("item_info", { + size: 1, + filters: { item_number: String(wi.item_number) }, + }); + const wiItem = wiItemRes.data?.[0] as Record | undefined; + if (wiItem) { + batchItemType = String(wiItem.type || ""); + } + } catch { + /* non-critical */ + } + } + // batchItemType을 임시 저장 (step 6에서 사용) + (procData as unknown as Record)._itemType = batchItemType; setWiInfo({ work_instruction_no: String(wi.work_instruction_no || ""), @@ -524,22 +551,40 @@ export function ProcessWork({ processId }: ProcessWorkProps) { size: 100, filters: { wo_id: procData.wo_id }, }); - const masters = ((plRes.data ?? []) as ProcessData[]) + const allSiblings = (plRes.data ?? []) as ProcessData[]; + const masters = allSiblings .filter((p) => !p.parent_process_id) - .sort((a, b) => parseInt(a.seq_no, 10) - parseInt(b.seq_no, 10)) - .map((p) => ({ - process_code: p.process_code, - process_name: p.process_name, - })); + .sort((a, b) => parseInt(a.seq_no, 10) - parseInt(b.seq_no, 10)); // 중복 제거 const seen = new Set(); setProcessList( - masters.filter((m) => { + masters.map((p) => ({ + process_code: p.process_code, + process_name: p.process_name, + })).filter((m) => { if (seen.has(m.process_code)) return false; seen.add(m.process_code); return true; }), ); + // 다중품목 판단: 마스터 공정의 DISTINCT batch_id + const uniqueBatches: string[] = []; + for (const p of masters) { + const bid = p.batch_id || ""; + if (bid && !uniqueBatches.includes(bid)) { + uniqueBatches.push(bid); + } + } + const currentBid = procData?.batch_id || ""; + const isMultiBatch = uniqueBatches.length > 1; + const bIdx = currentBid ? uniqueBatches.indexOf(currentBid) + 1 : 1; + const fetchedItemType = String((procData as unknown as Record)?._itemType || ""); + setBatchBadge({ + isMulti: isMultiBatch, + index: Math.max(bIdx, 1), + total: Math.max(uniqueBatches.length, 1), + itemType: fetchedItemType, + }); } catch { setProcessList([]); } @@ -1113,6 +1158,19 @@ export function ProcessWork({ processId }: ProcessWorkProps) { )} + {batchBadge && ( +
+ {batchBadge.isMulti ? ( + + 다중 {batchBadge.index}/{batchBadge.total}{batchBadge.itemType ? ` · ${batchBadge.itemType}` : ""} + + ) : ( + + 단일{batchBadge.itemType ? ` · ${batchBadge.itemType}` : ""} + + )} +
+ )}
공정 diff --git a/frontend/components/pop/hardcoded/production/WorkOrderList.tsx b/frontend/components/pop/hardcoded/production/WorkOrderList.tsx index 258e9a2d..ec88679b 100644 --- a/frontend/components/pop/hardcoded/production/WorkOrderList.tsx +++ b/frontend/components/pop/hardcoded/production/WorkOrderList.tsx @@ -225,6 +225,7 @@ function FullscreenWorkModal({ myProcesses, instructionMap, itemNameMap, + multiBatchInfo, onSwitch, onClose, }: { @@ -232,6 +233,7 @@ function FullscreenWorkModal({ myProcesses: WorkOrderProcess[]; instructionMap: Record; itemNameMap: Record; + multiBatchInfo: Record; onSwitch: (id: string) => void; onClose: () => void; }) { @@ -305,6 +307,20 @@ function FullscreenWorkModal({
{wi?.work_instruction_no || "작업지시"}
+ {(() => { + const bInfo = multiBatchInfo[proc.id]; + if (!bInfo) return null; + const typeLabel = bInfo.itemType || ""; + return bInfo.isMulti ? ( + + 다중 {bInfo.index}/{bInfo.total}{typeLabel ? ` · ${typeLabel}` : ""} + + ) : ( + + 단일{typeLabel ? ` · ${typeLabel}` : ""} + + ); + })()} 📦 {proc.batch_id ? `${itemNameMap[proc.batch_id] || proc.batch_id}(${proc.batch_id})` @@ -971,6 +987,7 @@ export function WorkOrderList() { const [processList, setProcessList] = useState([]); const [equipmentList, setEquipmentList] = useState([]); const [itemNameMap, setItemNameMap] = useState>({}); + const [itemTypeMap, setItemTypeMap] = useState>({}); const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); const [activeTab, setActiveTab] = useState("acceptable"); @@ -1056,14 +1073,19 @@ export function WorkOrderList() { const seen = new Set(); const wiData: WorkInstruction[] = []; const newItemNameMap: Record = {}; + const newItemTypeMap: Record = {}; for (const raw of wiRaw) { const wiId = String(raw.wi_id || raw.id || ""); - // item_number → item_name 매핑 (모든 행에서 수집) + // item_number → item_name / item_type 매핑 (모든 행에서 수집) const rawItemNumber = String(raw.item_number || ""); const rawItemName = String(raw.item_name || ""); + const rawItemType = String(raw.item_type || ""); if (rawItemNumber && rawItemName) { newItemNameMap[rawItemNumber] = rawItemName; } + if (rawItemNumber && rawItemType) { + newItemTypeMap[rawItemNumber] = rawItemType; + } if (!wiId || seen.has(wiId)) continue; seen.add(wiId); wiData.push({ @@ -1077,6 +1099,7 @@ export function WorkOrderList() { } setInstructions(wiData); setItemNameMap(newItemNameMap); + setItemTypeMap(newItemTypeMap); const procRes = await dataApi.getTableData("work_order_process", { size: 1000, @@ -1141,6 +1164,44 @@ export function WorkOrderList() { return map; }, [allProcesses]); + /** 다중품목 판단: wo_id별 DISTINCT batch_id 집합 + 순번 매핑 */ + const multiBatchInfo = useMemo(() => { + // wo_id → 고유 batch_id 목록 (마스터 행 기준) + const woBatches: Record = {}; + for (const proc of allProcesses) { + if (proc.parent_process_id) continue; // 마스터만 + if (!proc.wo_id) continue; + if (!woBatches[proc.wo_id]) woBatches[proc.wo_id] = []; + const bid = proc.batch_id || ""; + if (bid && !woBatches[proc.wo_id].includes(bid)) { + woBatches[proc.wo_id].push(bid); + } + } + // proc.id → { isMulti, index, total, itemType } + const info: Record = {}; + for (const proc of allProcesses) { + if (!proc.wo_id) continue; + const batches = woBatches[proc.wo_id] || []; + const bid = proc.batch_id || ""; + const isMulti = batches.length > 1; + const index = bid ? batches.indexOf(bid) + 1 : 1; + const total = Math.max(batches.length, 1); + // item_type: batch_id가 있으면 itemTypeMap에서, 없으면 WI의 item_number로 + let itemType = ""; + if (bid) { + itemType = itemTypeMap[bid] || ""; + } + if (!itemType) { + const wi = instructionMap[proc.wo_id]; + if (wi?.item_number) { + itemType = itemTypeMap[wi.item_number] || ""; + } + } + info[proc.id] = { isMulti, index, total, itemType }; + } + return info; + }, [allProcesses, itemTypeMap, instructionMap]); + const masterProcesses = useMemo(() => { // 마스터 행 + 분할 행(진행중/완료/리워크) — 중복 제거 const seen = new Set(); @@ -1910,6 +1971,26 @@ export function WorkOrderList() {
+ {/* 단일/다중품목 뱃지 */} + {(() => { + const bInfo = multiBatchInfo[proc.id]; + if (!bInfo) return null; + const typeLabel = bInfo.itemType || ""; + return ( +
+ {bInfo.isMulti ? ( + + 다중 {bInfo.index}/{bInfo.total}{typeLabel ? ` · ${typeLabel}` : ""} + + ) : ( + + 단일{typeLabel ? ` · ${typeLabel}` : ""} + + )} +
+ ); + })()} + {/* Sub-info: item name + equipment */} 📦 {proc.batch_id @@ -2107,6 +2188,7 @@ export function WorkOrderList() { )} instructionMap={instructionMap} itemNameMap={itemNameMap} + multiBatchInfo={multiBatchInfo} onSwitch={(id) => setWorkModalProcessId(id)} onClose={() => { setWorkModalProcessId(null);