From a96d5ac2c1e95b62decc65af39cc35851e161f91 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 8 Apr 2026 11:06:50 +0900 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20=EA=B3=B5=EC=A0=95=EC=8B=A4=ED=96=89?= =?UTF-8?q?=20=EC=B9=B4=EB=93=9C=EC=97=90=20=EC=A0=9C=ED=92=88=EB=AA=85(?= =?UTF-8?q?=EC=A0=9C=ED=92=88=EC=BD=94=EB=93=9C)=20=ED=98=95=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/pop/hardcoded/production/WorkOrderList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/pop/hardcoded/production/WorkOrderList.tsx b/frontend/components/pop/hardcoded/production/WorkOrderList.tsx index 97d7dc27..a14d6a3a 100644 --- a/frontend/components/pop/hardcoded/production/WorkOrderList.tsx +++ b/frontend/components/pop/hardcoded/production/WorkOrderList.tsx @@ -1402,7 +1402,7 @@ export function WorkOrderList() { {/* Sub-info: item name + equipment */}
- ๐Ÿ“ฆ {wi?.item_name || wi?.item_code || wi?.item_number || "ํ’ˆ๋ชฉ"} + ๐Ÿ“ฆ {wi?.item_name || "ํ’ˆ๋ชฉ"}{(wi?.item_code || wi?.item_number) ? `(${wi?.item_code || wi?.item_number})` : ""} {!isRework && ( โš™๏ธ {eqName} )} From 659bd9caad4b4a39e8f33effaec25a6bd746a6c7 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 8 Apr 2026 11:16:24 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A0=95=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=EC=B9=B4=EB=93=9C=20=EA=B8=B4=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=90=EB=8F=99=20=EC=8A=AC=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AutoScrollText ์ปดํฌ๋„ŒํŠธ: ํ…์ŠคํŠธ๊ฐ€ ์˜์—ญ์„ ๋„˜์œผ๋ฉด ์ž๋™ ๋งˆํ‚ค ์• ๋‹ˆ๋ฉ”์ด์…˜ - ์ œํ’ˆ๋ช…(์ œํ’ˆ์ฝ”๋“œ) ๊ธด ๊ฒฝ์šฐ ์ž๋™ ์Šฌ๋ผ์ด๋“œ - ์งง์œผ๋ฉด ์ •์ง€ ์ƒํƒœ๋กœ ํ‘œ์‹œ - ์ ‘์ˆ˜๊ฐ€๋Šฅ ์นด๋“œ + ์ง„ํ–‰์ค‘ ์นด๋“œ ๋ชจ๋‘ ์ ์šฉ --- .../hardcoded/production/WorkOrderList.tsx | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/frontend/components/pop/hardcoded/production/WorkOrderList.tsx b/frontend/components/pop/hardcoded/production/WorkOrderList.tsx index a14d6a3a..fc45e0e7 100644 --- a/frontend/components/pop/hardcoded/production/WorkOrderList.tsx +++ b/frontend/components/pop/hardcoded/production/WorkOrderList.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useCallback, useMemo } from "react"; +import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/hooks/useAuth"; import { apiClient } from "@/lib/api/client"; @@ -9,6 +9,41 @@ import { AcceptProcessModal } from "./AcceptProcessModal"; import { ProcessDetailModal, ProcessStep } from "./ProcessDetailModal"; import { ProcessWork } from "./ProcessWork"; +/* ํ…์ŠคํŠธ๊ฐ€ ๋„˜์น  ๋•Œ ์ž๋™ ์Šฌ๋ผ์ด๋“œ (๋งˆํ‚ค) */ +function AutoScrollText({ children, className }: { children: React.ReactNode; className?: string }) { + const outerRef = useRef(null); + const innerRef = useRef(null); + const [needsScroll, setNeedsScroll] = useState(false); + + useEffect(() => { + const check = () => { + if (outerRef.current && innerRef.current) { + setNeedsScroll(innerRef.current.scrollWidth > outerRef.current.clientWidth + 2); + } + }; + check(); + window.addEventListener("resize", check); + return () => window.removeEventListener("resize", check); + }, [children]); + + return ( +
+ + {children} + {needsScroll && } + {needsScroll && children} + + {needsScroll && ( + + )} +
+ ); +} + /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ @@ -176,9 +211,9 @@ function FullscreenWorkModal({
{wi?.work_instruction_no || "์ž‘์—…์ง€์‹œ"}
-
- {wi?.item_name || ""}{(wi?.item_code || wi?.item_number) ? `(${wi?.item_code || wi?.item_number})` : ""} -
+ + ๐Ÿ“ฆ {wi?.item_name || ""}{(wi?.item_code || wi?.item_number) ? `(${wi?.item_code || wi?.item_number})` : ""} +
{proc.process_name} ยท {proc.equipment_code || "๋ฏธ๋ฐฐ์ •"}
@@ -1401,8 +1436,10 @@ export function WorkOrderList() {
{/* Sub-info: item name + equipment */} -
- ๐Ÿ“ฆ {wi?.item_name || "ํ’ˆ๋ชฉ"}{(wi?.item_code || wi?.item_number) ? `(${wi?.item_code || wi?.item_number})` : ""} +
+ + ๐Ÿ“ฆ {wi?.item_name || "ํ’ˆ๋ชฉ"}{(wi?.item_code || wi?.item_number) ? `(${wi?.item_code || wi?.item_number})` : ""} + {!isRework && ( โš™๏ธ {eqName} )} From cdea6297e7ee8f1b7faeda12eec88e3acfb8a5ea Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 8 Apr 2026 11:17:53 +0900 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20=ED=92=88=EB=AA=A9+=EC=84=A4?= =?UTF-8?q?=EB=B9=84=EB=AA=85=20=ED=95=A9=EC=B3=90=EC=84=9C=20=ED=95=9C=20?= =?UTF-8?q?=EC=A4=84=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=93=9C=20(=EB=91=98?= =?UTF-8?q?=20=EB=8B=A4=20=EA=B8=B8=20=EB=95=8C=20=EB=8C=80=EC=9D=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pop/hardcoded/production/WorkOrderList.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/frontend/components/pop/hardcoded/production/WorkOrderList.tsx b/frontend/components/pop/hardcoded/production/WorkOrderList.tsx index fc45e0e7..92d8968e 100644 --- a/frontend/components/pop/hardcoded/production/WorkOrderList.tsx +++ b/frontend/components/pop/hardcoded/production/WorkOrderList.tsx @@ -1436,17 +1436,11 @@ export function WorkOrderList() {
{/* Sub-info: item name + equipment */} -
- - ๐Ÿ“ฆ {wi?.item_name || "ํ’ˆ๋ชฉ"}{(wi?.item_code || wi?.item_number) ? `(${wi?.item_code || wi?.item_number})` : ""} - - {!isRework && ( - โš™๏ธ {eqName} - )} - {isRework && ( - โš™๏ธ {proc.process_name || proc.process_code} - )} -
+ + ๐Ÿ“ฆ {wi?.item_name || "ํ’ˆ๋ชฉ"}{(wi?.item_code || wi?.item_number) ? `(${wi?.item_code || wi?.item_number})` : ""} + {" ยท "} + {!isRework ? `โš™๏ธ ${eqName}` : `โš™๏ธ ${proc.process_name || proc.process_code}`} + {/* Process steps (compressed) โ€” both normal and rework cards */} {siblingProcesses.length > 1 && ( From 0d62af8c8b9521c7f4fe0361f01fb00431482634 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 8 Apr 2026 12:21:34 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EC=9E=91=EC=97=85=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=97=A4=EB=8D=94+=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EA=B8=80=EC=9E=90=20=EC=9C=84=EA=B3=84=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95=20(=EC=82=B0=EC=97=85=ED=98=84=EC=9E=A5=201m=20?= =?UTF-8?q?=EA=B1=B0=EB=A6=AC=20=EA=B8=B0=EC=A4=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ์˜ต์…˜ B ์ ์šฉ โ€” ๋ฉ”์ธ ๋ณธ๋ฌธ์€ ๊ทธ๋Œ€๋กœ, ํ—ค๋”์™€ ์‚ฌ์ด๋“œ๋ฐ”๋งŒ ํ•œ ๋‹จ๊ณ„ ์œ„๊ณ„ ์—…๊ทธ๋ ˆ์ด๋“œ ์ƒ๋‹จ ํ—ค๋”: - ๋ผ๋ฒจ (์ž‘์—…์ง€์‹œ/ํ’ˆ๋ชฉ/๊ณต์ •/์ง€์‹œ): 12px โ†’ 14px - ๊ฐ’ (CODE-00003 ๋“ฑ): 14px โ†’ 16px - ์ ‘์ˆ˜ ์ˆ˜๋Ÿ‰ (๊ฐ€์žฅ ์ค‘์š”): 14px โ†’ 18px - ์ƒํƒœ/์žฌ์ž‘์—… ๋ฐฐ์ง€: 12px โ†’ 13px ์‚ฌ์ด๋“œ๋ฐ”: - Phase ๋ผ๋ฒจ (์ž‘์—… ์ „/์ค‘/ํ›„, ์‹ค์ , ์ž…๊ณ ): 12px โ†’ 16px - Phase ์นด์šดํ„ฐ: 12px โ†’ 13px - ๊ทธ๋ฃน ํ•ญ๋ชฉ (๋ฒ ์…€ ์ƒํƒœ ํ™•์ธ ๋“ฑ): 12px โ†’ 14px - ๊ทธ๋ฃน ์นด์šดํ„ฐ: 12px โ†’ 13px - ์„น์…˜ (์ž์žฌ ํˆฌ์ž…/์‹ค์  ์ž…๋ ฅ/์žฌ๊ณ  ์ž…๊ณ ): 12px โ†’ 14px ๋ฉ”์ธ ์˜์—ญ(Timer/Quantity/Register)์€ ์œ„๊ณ„๊ฐ€ ์ด๋ฏธ ์ž˜ ์žกํ˜€์žˆ์–ด ๋ณ€๊ฒฝ ์—†์Œ --- .../pop/hardcoded/production/ProcessWork.tsx | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/frontend/components/pop/hardcoded/production/ProcessWork.tsx b/frontend/components/pop/hardcoded/production/ProcessWork.tsx index bba2caa8..7bfe1c10 100644 --- a/frontend/components/pop/hardcoded/production/ProcessWork.tsx +++ b/frontend/components/pop/hardcoded/production/ProcessWork.tsx @@ -114,7 +114,7 @@ const PHASE_LABELS: Record = { PRE: "์ž‘์—… ์ „", IN: "์ž‘์—… const DESIGN = { bg: { page: "#F5F5F5", card: "#FFFFFF", header: "#1a1a2e", infoBar: "#1a1a2e" }, - sidebar: { width: 240 }, + sidebar: { width: 280 }, timer: { fontSize: 48 }, button: { height: 60, touchMin: 48 }, input: { height: 52 }, @@ -933,34 +933,34 @@ export function ProcessWork({ processId }: ProcessWorkProps) {
{wiInfo && (
- ์ž‘์—…์ง€์‹œ - {wiInfo.work_instruction_no} + ์ž‘์—…์ง€์‹œ + {wiInfo.work_instruction_no}
)} {wiInfo && (
- ํ’ˆ๋ชฉ - {wiInfo.item_name} + ํ’ˆ๋ชฉ + {wiInfo.item_name}
)}
- ๊ณต์ • - + ๊ณต์ • + {process.seq_no ? `${process.seq_no}. ` : ""}{process.process_name || "๊ณต์ •"}
- ์ง€์‹œ - {parseInt(process.plan_qty || "0", 10).toLocaleString()} + ์ง€์‹œ + {parseInt(process.plan_qty || "0", 10).toLocaleString()}
- ์ ‘์ˆ˜ - {inputQty.toLocaleString()} + ์ ‘์ˆ˜ + {inputQty.toLocaleString()}
{/* Status badge */} {process.is_rework === "Y" && ( - + ์žฌ์ž‘์—… )} @@ -1010,10 +1010,10 @@ export function ProcessWork({ processId }: ProcessWorkProps) {
- + {PHASE_LABELS[phase] || phase} - + {phaseDone}/{phaseTotal} @@ -1040,13 +1040,13 @@ export function ProcessWork({ processId }: ProcessWorkProps) { }`} /> {g.title} - + {g.completed}/{g.total} @@ -1072,7 +1072,7 @@ export function ProcessWork({ processId }: ProcessWorkProps) { }`} > ๐Ÿ“ฆ - + ์ž์žฌ ํˆฌ์ž… } @@ -1083,7 +1083,7 @@ export function ProcessWork({ processId }: ProcessWorkProps) { - ์‹ค์  + ์‹ค์  @@ -1118,7 +1118,7 @@ export function ProcessWork({ processId }: ProcessWorkProps) { - ์ž…๊ณ  + ์ž…๊ณ  +
+ +
+ + + + ); +} diff --git a/frontend/components/pop/hardcoded/inbound/InboundCart.tsx b/frontend/components/pop/hardcoded/inbound/InboundCart.tsx index 36efb141..e9649cc9 100644 --- a/frontend/components/pop/hardcoded/inbound/InboundCart.tsx +++ b/frontend/components/pop/hardcoded/inbound/InboundCart.tsx @@ -197,6 +197,55 @@ export function InboundCart({ const res = await apiClient.post("/receiving", payload); if (res.data?.success) { + // 2-1. ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๊ฐ€ ์žˆ๋Š” ํ•ญ๋ชฉ โ†’ inspection_result์— ์ €์žฅ + const insertedDetails: any[] = res.data?.data?.details ?? res.data?.data?.items ?? []; + const inboundHeaderNo = res.data?.data?.header?.inbound_number || inboundNumber || ""; + const inspectionPromises = items + .map((item, idx) => { + if (!item.inspectionResult?.completed) return null; + const matchedDetail = insertedDetails[idx] ?? {}; + const referenceId = matchedDetail.id || matchedDetail.detail_id || `${inboundHeaderNo}-${idx + 1}`; + const goodQty = item.inspectionResult.goodQty || 0; + const badQty = item.inspectionResult.badQty || 0; + const totalQty = goodQty + badQty; + const overallJudgment = badQty === 0 ? "ํ•ฉ๊ฒฉ" : "๋ถˆํ•ฉ๊ฒฉ"; + return apiClient.post("/pop/inspection-result", { + inspectionNumber: item.inspectionResult.inspectionNumber, // ์นดํŠธ์—์„œ ๋ฐ›์€ ๊ฒ€์‚ฌ๋ฒˆํ˜ธ ์žฌ์‚ฌ์šฉ + referenceTable: "inbound_mng", + referenceId, + screenId: "pop_inbound_inspection", + itemId: item.item_id || null, + itemCode: item.item_code, + itemName: item.item_name, + inspectionType: "์ž…๊ณ ๊ฒ€์‚ฌ", + overallJudgment, + totalQty, + goodQty, + badQty, + defectDescription: badQty > 0 ? `๋ถˆ๋Ÿ‰ ${badQty}๊ฑด` : "", + memo: item.inspectionResult.remark || "", + supplierCode: item.supplier_code || null, + supplierName: item.supplier_name || null, + isCompleted: true, + items: item.inspectionResult.items.map((insp: any) => ({ + inspectionInfoId: insp.id || null, + inspectionItemName: insp.inspection_item_name, + inspectionStandard: insp.inspection_standard, + passCriteria: insp.pass_criteria, + isRequired: insp.is_required || "Y", + measuredValue: insp.measured_value || "", + judgment: insp.result || null, + })), + }).catch((err) => { + console.error("[inspection_result ์ €์žฅ ์‹คํŒจ]", item.item_code, err?.message); + }); + }) + .filter(Boolean); + + if (inspectionPromises.length > 0) { + await Promise.allSettled(inspectionPromises); + } + // 3. cart_items DB ์ •๋ฆฌ (๋ฐฑ๊ทธ๋ผ์šด๋“œ, ๋…ผ๋ธ”๋กœํ‚น) // cart_items.row_key ๋กœ ์‚ญ์ œ (row_key = source_id ๋กœ ์ €์žฅ๋จ) const rowKeys = items.map((item) => item.source_id || item.id).filter(Boolean); diff --git a/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx b/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx index d6a90ec3..c8b5ccfb 100644 --- a/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx +++ b/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx @@ -108,6 +108,37 @@ export function InboundCartPage() { } }, [items]); + /* Sync inspectionResults with cart.row.inspectionResult + * ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ/์žฌ์ง„์ž… ์‹œ cart_items์— ์ €์žฅ๋œ inspectionResult๋ฅผ Map์œผ๋กœ ๋ณต์›. + * ์ฃผ์˜: delete๋Š” ๋ช…์‹œ์  ๊ฒ€์‚ฌ ์ทจ์†Œ(handleCancel)์—์„œ๋งŒ ์ฒ˜๋ฆฌ. + * (cart.saveToDb ํ›„ row JSON์ด staleํ•  ์ˆ˜ ์žˆ์–ด delete ๋กœ์ง์€ race condition ์œ ๋ฐœ) */ + useEffect(() => { + setInspectionResults((prev) => { + const next = new Map(prev); + let changed = false; + cart.cartItems.forEach((c) => { + const stored = (c.row as Record)?.inspectionResult; + if (stored && typeof stored === "object") { + // ์œ ํšจํ•œ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ โ†’ Map์— ์ถ”๊ฐ€ (๋ฎ์–ด์“ฐ์ง€ ์•Š์Œ, ๋กœ์ปฌ ์šฐ์„ ) + if (!next.has(c.rowKey)) { + next.set(c.rowKey, stored as InspectionResult); + changed = true; + } + } + // null/undefined์—ฌ๋„ Map์—์„œ ์ž๋™ ์ œ๊ฑฐํ•˜์ง€ ์•Š์Œ โ€” ๋ช…์‹œ์  cancel๋งŒ ์ฒ˜๋ฆฌ + }); + // ์นดํŠธ์—์„œ ์‚ฌ๋ผ์ง„ rowKey๋งŒ ์ •๋ฆฌ (์‹ค์ œ ์นดํŠธ ์‚ญ์ œ ์‹œ) + const cartKeys = new Set(cart.cartItems.map((c) => c.rowKey)); + Array.from(next.keys()).forEach((k) => { + if (!cartKeys.has(k)) { + next.delete(k); + changed = true; + } + }); + return changed ? next : prev; + }); + }, [cart.cartItems]); + /* Warehouse */ const [warehouses, setWarehouses] = useState([]); const [selectedWarehouse, setSelectedWarehouse] = useState(""); @@ -248,29 +279,38 @@ export function InboundCartPage() { const handleInspectionComplete = (result: InspectionResult) => { if (!inspectionTarget) return; + const targetRowKey = inspectionTarget.rowKey; setInspectionResults((prev) => { const next = new Map(prev); - next.set(inspectionTarget.rowKey, result); + next.set(targetRowKey, result); return next; }); + // cart_items.row_data์— ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์ €์žฅ (ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์œ ์ง€) + cart.updateItemRow(targetRowKey, { inspectionResult: result }); setInspectionTarget(null); + // ์ฆ‰์‹œ DB ์ €์žฅ (์ž๋™์ €์žฅ ๋””๋ฐ”์šด์Šค๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์Œ) + setTimeout(() => { + cart.saveToDb().catch((err) => console.error("[๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์ €์žฅ ์‹คํŒจ]", err)); + }, 100); }; /* Pass inspection (non-required only) */ const handlePassInspection = (rowKey: string) => { const item = items.find((i) => i.rowKey === rowKey); if (!item) return; + const result: InspectionResult = { + items: [], + goodQty: item.inbound_qty, + badQty: 0, + remark: "pass", + completed: true, + }; setInspectionResults((prev) => { const next = new Map(prev); - next.set(rowKey, { - items: [], - goodQty: item.inbound_qty, - badQty: 0, - remark: "pass", - completed: true, - }); + next.set(rowKey, result); return next; }); + cart.updateItemRow(rowKey, { inspectionResult: result }); }; const getInspectionResult = (rowKey: string): InspectionResult | null => { @@ -282,6 +322,8 @@ export function InboundCartPage() { /* ------------------------------------------------------------------ */ const selectedItemsList = items.filter((i) => selectedItems.has(i.id)); + // CEO ์ •์ฑ… (2026-04-09 ์‹œ์—ฐ ๊ฒฐ์ •): ๊ฒ€์‚ฌ ํ•„์ˆ˜ ํ•ญ๋ชฉ ๋ฏธ์™„๋ฃŒ ์‹œ ํ™•์ • ์ฐจ๋‹จ + // ๊ฒ€์‚ฌ ๋น ์ง„ ์ž…๊ณ ๊ฐ€ ๊ฒ€์‚ฌ๊ด€๋ฆฌ์—์„œ ์ถ”์  ์•ˆ ๋˜๋ฏ€๋กœ, ์ž…๋ ฅ ์‹œ์ ์— ๋ง‰์Œ const hasUnfinishedRequiredInspection = selectedItemsList.some( (item) => item.inspection_required && @@ -300,10 +342,8 @@ export function InboundCartPage() { return; } - if (hasUnfinishedRequiredInspection) { - setResultMsg("์˜ค๋ฅ˜: ํ•„์ˆ˜ ๊ฒ€์‚ฌ๋ฅผ ์™„๋ฃŒํ•ด์ฃผ์„ธ์š”."); - return; - } + // ๊ฒ€์‚ฌ ๋ฏธ์™„๋ฃŒ์—ฌ๋„ ํ™•์ • ๊ฐ€๋Šฅ. ๋‹จ์ง€ inspection_result์— ์•ˆ ๋“ค์–ด๊ฐ€๊ฑฐ๋‚˜ "๋Œ€๊ธฐ" ์ƒํƒœ๋กœ ๊ธฐ๋ก. + // (CEO ์ •์ฑ…: ์ž…๊ณ  ์ž์ฒด๋Š” ์ง„ํ–‰, ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋งŒ ๋ˆ„๋ฝ/๋Œ€๊ธฐ ์ƒํƒœ๋กœ ํ‘œ์‹œ) setConfirming(true); setResultMsg(null); @@ -366,6 +406,66 @@ export function InboundCartPage() { const res = await apiClient.post("/receiving", payload); if (res.data?.success) { + // ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋ฅผ inspection_result_mng + inspection_result์— ์ €์žฅ + const insertedDetails: Array> = + (res.data?.data?.details as Array>) ?? + (res.data?.data?.items as Array>) ?? + []; + const inboundHeaderNo: string = + (res.data?.data?.header as { inbound_number?: string } | undefined)?.inbound_number || + finalNumber || ""; + const inspectionPromises = selectedItemsList + .map((item, idx) => { + const inspResult = getInspectionResult(item.rowKey); + if (!inspResult?.completed) return null; + const matchedDetail = insertedDetails[idx] ?? {}; + const referenceId = + (matchedDetail.id as string) || + (matchedDetail.detail_id as string) || + `${inboundHeaderNo}-${idx + 1}`; + const goodQty = inspResult.goodQty || 0; + const badQty = inspResult.badQty || 0; + const totalQty = goodQty + badQty; + const overallJudgment = badQty === 0 ? "ํ•ฉ๊ฒฉ" : "๋ถˆํ•ฉ๊ฒฉ"; + return apiClient + .post("/pop/inspection-result", { + inspectionNumber: inspResult.inspectionNumber, + referenceTable: "inbound_mng", + referenceId, + screenId: "pop_inbound_inspection", + itemId: item.item_id || null, + itemCode: item.item_code, + itemName: item.item_name, + inspectionType: "์ž…๊ณ ๊ฒ€์‚ฌ", + overallJudgment, + totalQty, + goodQty, + badQty, + defectDescription: badQty > 0 ? `๋ถˆ๋Ÿ‰ ${badQty}๊ฑด` : "", + memo: inspResult.remark || "", + supplierCode: item.supplier_code || null, + supplierName: item.supplier_name || null, + isCompleted: true, + items: inspResult.items.map((insp) => ({ + inspectionInfoId: insp.id || null, + inspectionItemName: insp.inspection_item_name, + inspectionStandard: insp.inspection_standard, + passCriteria: insp.pass_criteria, + isRequired: insp.is_required || "Y", + measuredValue: insp.measured_value || "", + judgment: insp.result || null, + })), + }) + .catch((err: unknown) => { + const e = err as { message?: string }; + console.error("[inspection_result ์ €์žฅ ์‹คํŒจ]", item.item_code, e?.message); + }); + }) + .filter(Boolean); + if (inspectionPromises.length > 0) { + await Promise.all(inspectionPromises); + } + // Remove confirmed items from cart - direct DB delete for reliability const confirmedItems = [...selectedItemsList]; const { dataApi } = await import("@/lib/api/data"); @@ -878,45 +978,17 @@ export function InboundCartPage() { )} - {/* ===== Footer summary (no confirm button -- header only) ===== */} - {items.length > 0 && ( -
- {/* Result message */} - {resultMsg && ( -
- {resultMsg} -
- )} - - {/* Required inspection warning */} - {hasUnfinishedRequiredInspection && ( -
- ํ•„์ˆ˜ ๊ฒ€์‚ฌ๋ฅผ ์™„๋ฃŒํ•ด์ฃผ์„ธ์š”. ๊ฒ€์‚ฌ ๋ฏธ์™„๋ฃŒ ํ’ˆ๋ชฉ์ด ์žˆ์–ด ํ™•์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. -
- )} - - {/* Summary only (no big confirm button) */} -
- - ์„ ํƒ{" "} - - {selectedItemsList.length} - - /{items.length}๊ฑด - - - ํ•ฉ๊ณ„ ์ˆ˜๋Ÿ‰:{" "} - - {totalQty.toLocaleString()} - {" "} - EA - + {/* ===== Result toast (only when message exists) ===== */} + {resultMsg && ( +
+
+ {resultMsg}
)} @@ -1036,6 +1108,17 @@ export function InboundCartPage() { setInspectionTarget(null); }} onComplete={handleInspectionComplete} + onCancel={() => { + // ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ๋ฌดํšจํ™” (์™„๋ฃŒ โ†’ ๋Œ€๊ธฐ ํ’€๋ฆผ) + const targetRowKey = inspectionTarget.rowKey; + setInspectionResults((prev) => { + const next = new Map(prev); + next.delete(targetRowKey); + return next; + }); + cart.updateItemRow(targetRowKey, { inspectionResult: null }); + setTimeout(() => cart.saveToDb().catch(() => {}), 100); + }} itemCode={inspectionTarget.item_code} itemName={inspectionTarget.item_name} totalQty={inspectionTarget.inbound_qty} diff --git a/frontend/components/pop/hardcoded/inbound/InspectionModal.tsx b/frontend/components/pop/hardcoded/inbound/InspectionModal.tsx index bd4ba346..b85254fa 100644 --- a/frontend/components/pop/hardcoded/inbound/InspectionModal.tsx +++ b/frontend/components/pop/hardcoded/inbound/InspectionModal.tsx @@ -26,12 +26,14 @@ export interface InspectionResult { badQty: number; remark: string; completed: boolean; + inspectionNumber?: string; // ๊ฒ€์‚ฌ ์™„๋ฃŒ ์‹œ ์ฑ„๋ฒˆ ๋ฐ›์Œ (์žฌ์‚ฌ์šฉ) } interface InspectionModalProps { open: boolean; onClose: () => void; onComplete: (result: InspectionResult) => void; + onCancel?: () => void; // ์ทจ์†Œ = ๊ฒ€์‚ฌ ๋ฌดํšจํ™” (์™„๋ฃŒ โ†’ ๋Œ€๊ธฐ) itemCode: string; itemName: string; totalQty: number; @@ -93,6 +95,7 @@ export function InspectionModal({ open, onClose, onComplete, + onCancel, itemCode, itemName, totalQty, @@ -102,19 +105,28 @@ export function InspectionModal({ const [loading, setLoading] = useState(false); const [goodQty, setGoodQty] = useState(0); const [badQty, setBadQty] = useState(0); + /* NumPad state */ + const [numpadOpen, setNumpadOpen] = useState(false); + const [numpadTitle, setNumpadTitle] = useState(""); + const [numpadValue, setNumpadValue] = useState(""); + const [numpadMax, setNumpadMax] = useState(undefined); + const numpadCallbackRef = React.useRef<((val: string) => void) | null>(null); + + const openNumpad = (title: string, currentValue: string | number, onConfirm: (v: string) => void, max?: number) => { + setNumpadTitle(title); + setNumpadValue(String(currentValue || "")); + setNumpadMax(max); + numpadCallbackRef.current = onConfirm; + setNumpadOpen(true); + }; const [remark, setRemark] = useState(""); /* Fetch inspection items from DB */ const fetchInspectionItems = useCallback(async () => { setLoading(true); try { - const res = await apiClient.get("/pop/execute-action", { - params: { - taskType: "data-list", - targetTable: "item_inspection_info", - filters: JSON.stringify({ item_code: itemCode }), - pageSize: "50", - }, + const res = await apiClient.get("/pop/inspection-result/info", { + params: { itemCode }, }); const data = res.data?.data; if (Array.isArray(data) && data.length > 0) { @@ -125,7 +137,7 @@ export function InspectionModal({ inspection_standard: String(r.inspection_standard ?? ""), inspection_method: String(r.inspection_method ?? ""), pass_criteria: String(r.pass_criteria ?? ""), - is_required: String(r.is_required ?? ""), + is_required: String(r.is_required ?? "Y"), measured_value: "", result: null, })) @@ -180,34 +192,66 @@ export function InspectionModal({ setGoodQty(totalQty - v); }; + /* ๊ฒ€์‚ฌ ์™„๋ฃŒ ๊ฐ€๋Šฅ ์—ฌ๋ถ€: ํ•„์ˆ˜ ํ•ญ๋ชฉ์ด ๋ชจ๋‘ pass */ + const canComplete = inspItems + .filter((it) => it.is_required === "Y") + .every((it) => it.result === "pass"); + /* Complete */ - const handleComplete = () => { - onComplete({ - items: inspItems, - goodQty, - badQty, - remark, - completed: true, - }); - onClose(); + const [allocating, setAllocating] = useState(false); + const handleComplete = async () => { + if (!canComplete) return; + setAllocating(true); + try { + // 1. ๊ธฐ์กด inspectionNumber ์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ฑ„๋ฒˆ ํ˜ธ์ถœ + let inspectionNumber = initialResult?.inspectionNumber; + if (!inspectionNumber) { + try { + const res = await apiClient.post("/pop/inspection-result/allocate-number"); + inspectionNumber = res.data?.data?.inspectionNumber; + } catch (err) { + console.error("[๊ฒ€์‚ฌ๋ฒˆํ˜ธ ์ฑ„๋ฒˆ ์‹คํŒจ]", err); + } + } + // 2. ๊ฒฐ๊ณผ ์ „๋‹ฌ (์ฑ„๋ฒˆ ํฌํ•จ) + onComplete({ + items: inspItems, + goodQty, + badQty, + remark, + completed: true, + inspectionNumber, + }); + onClose(); + } finally { + setAllocating(false); + } }; if (!open) return null; return ( -
- {/* Full-screen slide panel */} -
+
+ {/* Overlay */} +
+ + {/* Bottom sheet */}
e.stopPropagation()} > + {/* Handle bar */} +
+
+
+ {/* Header */} -
+

์ž์ฃผ๊ฒ€์‚ฌ

EA
- handleBadQtyChange(parseInt(e.target.value, 10) || 0)} - className="flex-1 min-w-0 h-10 px-2 text-center text-sm font-semibold border-2 border-red-400 rounded-lg bg-red-50 text-red-700 outline-none focus:ring-2 focus:ring-red-200" - /> + EA
@@ -401,10 +455,16 @@ export function InspectionModal({ {/* Footer */}
- {/* Global keyframe (only mounts once) */} - +
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Sub-components */ +/* ------------------------------------------------------------------ */ + +function DetailField({ label, value }: { label: string; value: string }) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +function KpiCell({ icon, value, label, color }: { icon: string; value: string; label: string; color: string }) { + return ( +
+ {icon} + + {value} + + {label} +
+ ); +} diff --git a/frontend/components/pop/hardcoded/inventory/InventoryHome.tsx b/frontend/components/pop/hardcoded/inventory/InventoryHome.tsx new file mode 100644 index 00000000..07907d7f --- /dev/null +++ b/frontend/components/pop/hardcoded/inventory/InventoryHome.tsx @@ -0,0 +1,285 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { apiClient } from "@/lib/api/client"; + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +interface RecentItem { + id: string; + time: string; + direction: "์ž…๊ณ " | "์ถœ๊ณ "; + type: string; + itemName: string; + qty: string; + partnerName: string; + statusColor: string; + statusLabel: string; +} + +interface KpiData { + todayInbound: number; + todayOutbound: number; + todayTotal: number; +} + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +function getStatusStyle(status: string | null): { color: string; label: string } { + switch (status) { + case "์™„๋ฃŒ": + case "์ž…๊ณ ์™„๋ฃŒ": + case "์ถœ๊ณ ์™„๋ฃŒ": + return { color: "text-green-600 bg-green-50", label: "์™„๋ฃŒ" }; + case "๋Œ€๊ธฐ": + return { color: "text-amber-600 bg-amber-50", label: "๋Œ€๊ธฐ" }; + case "์ง„ํ–‰์ค‘": + return { color: "text-blue-600 bg-blue-50", label: "์ง„ํ–‰์ค‘" }; + default: + return { color: "text-gray-600 bg-gray-50", label: status || "๋Œ€๊ธฐ" }; + } +} + +/* ------------------------------------------------------------------ */ +/* Menu Items */ +/* ------------------------------------------------------------------ */ + +const MENU_ITEMS = [ + { + id: "history", + title: "์ž…์ถœ๊ณ ๊ด€๋ฆฌ", + gradient: "linear-gradient(135deg,#3b82f6,#1d4ed8)", + shadowColor: "rgba(59,130,246,.3)", + icon: ( + + + + ), + href: "/pop/inventory/history", + }, + { + id: "adjust", + title: "์žฌ๊ณ ์กฐ์ •", + gradient: "linear-gradient(135deg,#f59e0b,#d97706)", + shadowColor: "rgba(245,158,11,.3)", + icon: ( + + + + ), + href: "#", + }, +]; + +/* ------------------------------------------------------------------ */ +/* Component */ +/* ------------------------------------------------------------------ */ + +export function InventoryHome() { + const router = useRouter(); + + const [kpi, setKpi] = useState({ todayInbound: 0, todayOutbound: 0, todayTotal: 0 }); + const [recentItems, setRecentItems] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + const today = new Date().toISOString().slice(0, 10); + + const [inRes, outRes] = await Promise.all([ + apiClient.get("/receiving/list", { params: { date_from: today, date_to: today } }), + apiClient.get("/outbound/list", { params: { date_from: today, date_to: today } }), + ]); + + const inRows: any[] = inRes.data?.data ?? []; + const outRows: any[] = outRes.data?.data ?? []; + + setKpi({ + todayInbound: inRows.length, + todayOutbound: outRows.length, + todayTotal: inRows.length + outRows.length, + }); + + const combined: RecentItem[] = [ + ...inRows.map((r: any, idx: number) => { + const st = getStatusStyle(r.inbound_status); + return { + id: `in-${r.detail_id || r.id}-${idx}`, + time: r.created_date ? new Date(r.created_date).toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" }) : "--:--", + direction: "์ž…๊ณ " as const, + type: r.inbound_type || "์ž…๊ณ ", + itemName: r.item_name || r.item_number || "-", + qty: `${Number(r.inbound_qty || 0).toLocaleString()} ${r.unit || "EA"}`, + partnerName: r.supplier_name || "-", + statusColor: st.color, + statusLabel: st.label, + }; + }), + ...outRows.map((r: any, idx: number) => { + const st = getStatusStyle(r.outbound_status); + return { + id: `out-${r.id}-${idx}`, + time: r.created_date ? new Date(r.created_date).toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" }) : "--:--", + direction: "์ถœ๊ณ " as const, + type: r.outbound_type || "์ถœ๊ณ ", + itemName: r.item_name || r.item_code || "-", + qty: `${Number(r.outbound_qty || 0).toLocaleString()} ${r.unit || "EA"}`, + partnerName: r.customer_name || "-", + statusColor: st.color, + statusLabel: st.label, + }; + }), + ] + .sort((a, b) => b.time.localeCompare(a.time)) + .slice(0, 5); + + setRecentItems(combined); + } catch { + // keep empty + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + const handleMenuClick = (item: (typeof MENU_ITEMS)[number]) => { + if (item.href === "#") { + alert(`${item.title} ํ™”๋ฉด์€ ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค.`); + } else { + router.push(item.href); + } + }; + + return ( +
+ {/* Back + Title */} +
+ +
+

์žฌ๊ณ 

+

์ž…์ถœ๊ณ  ํ˜„ํ™ฉ ๋ฐ ์žฌ๊ณ  ๊ด€๋ฆฌ

+
+
+ + {/* KPI */} +
+
+ + + +
+
+ + {/* Menu Icons */} +
+
+
+

์žฌ๊ณ  ๊ด€๋ฆฌ

+
+
+ {MENU_ITEMS.map((item) => ( +
handleMenuClick(item)} + > +
+ {item.icon} +
+ + {item.title} + +
+ ))} +
+
+ + {/* Recent Activity */} +
+
+
+

์ตœ๊ทผ ์ž…์ถœ๊ณ 

+ ์ตœ๊ทผ 5๊ฑด +
+
+ {loading ? ( +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+ ))} +
+ ) : recentItems.length === 0 ? ( +
๊ธˆ์ผ ์ž…์ถœ๊ณ  ๋‚ด์—ญ์ด ์—†์Šต๋‹ˆ๋‹ค
+ ) : ( + recentItems.map((item) => ( +
+ + {item.time} + +
+
+ + {item.direction} + + {item.itemName} + + {item.statusLabel} + +
+
+ {item.type} | {item.partnerName} | {item.qty} +
+
+
+ )) + )} +
+
+
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Sub-components */ +/* ------------------------------------------------------------------ */ + +function KpiCell({ value, label, color }: { value: string; label: string; color: string }) { + return ( +
+ + {value} + + {label} +
+ ); +} diff --git a/frontend/components/pop/hardcoded/inventory/index.ts b/frontend/components/pop/hardcoded/inventory/index.ts new file mode 100644 index 00000000..6ebfce58 --- /dev/null +++ b/frontend/components/pop/hardcoded/inventory/index.ts @@ -0,0 +1,2 @@ +export { InventoryHome } from "./InventoryHome"; +export { InOutHistory } from "./InOutHistory"; diff --git a/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx b/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx index 32bcc462..ee38c6f9 100644 --- a/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx +++ b/frontend/components/pop/hardcoded/outbound/OutboundCartPage.tsx @@ -381,8 +381,10 @@ export function OutboundCartPage() { ); } } catch (err: unknown) { - const msg = - err instanceof Error ? err.message : "์ถœ๊ณ  ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."; + // axios ์—๋Ÿฌ ์šฐ์„ : ๋ฐฑ์—”๋“œ message๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ์„ ํ‘œ์‹œ (์žฌ๊ณ  ๋ถ€์กฑ ๋“ฑ) + const e = err as { response?: { data?: { message?: string } }; message?: string }; + const backendMsg = e?.response?.data?.message; + const msg = backendMsg || e?.message || "์ถœ๊ณ  ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."; setResultMsg(`์˜ค๋ฅ˜: ${msg}`); } finally { setConfirming(false); diff --git a/frontend/components/pop/hardcoded/production/ProcessWork.tsx b/frontend/components/pop/hardcoded/production/ProcessWork.tsx index 7bfe1c10..237ccd85 100644 --- a/frontend/components/pop/hardcoded/production/ProcessWork.tsx +++ b/frontend/components/pop/hardcoded/production/ProcessWork.tsx @@ -7,6 +7,7 @@ import { usePopSettings } from "@/hooks/pop/usePopSettings"; import { dataApi } from "@/lib/api/data"; import { ProcessTimer, type TimerStatus } from "./ProcessTimer"; import { DefectTypeModal, type DefectEntry, type DefectType } from "./DefectTypeModal"; +import { ConfirmModal } from "../common/ConfirmModal"; /* ================================================================== */ /* Types */ @@ -337,6 +338,31 @@ export function ProcessWork({ processId }: ProcessWorkProps) { /* ---- Modals ---- */ const [prodQtyModal, setProdQtyModal] = useState(false); const [defectModal, setDefectModal] = useState(false); + const [confirmModalState, setConfirmModalState] = useState<{ + open: boolean; + message: string; + title?: string; + confirmText?: string; + variant?: "primary" | "danger" | "success"; + onConfirm: () => void; + }>({ open: false, message: "", onConfirm: () => {} }); + const askConfirm = ( + message: string, + onConfirm: () => void, + opts?: { title?: string; confirmText?: string; variant?: "primary" | "danger" | "success" } + ) => { + setConfirmModalState({ + open: true, + message, + title: opts?.title, + confirmText: opts?.confirmText, + variant: opts?.variant, + onConfirm: () => { + setConfirmModalState((s) => ({ ...s, open: false })); + onConfirm(); + }, + }); + }; /* ---- Last Process / Warehouse ---- */ const [isLastProcess, setIsLastProcess] = useState(false); @@ -799,25 +825,30 @@ export function ProcessWork({ processId }: ProcessWorkProps) { }; /* ---- Confirm Result ---- */ - const handleConfirmResult = async () => { - if (!confirm("์‹ค์ ์„ ํ™•์ •ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?\nํ™•์ • ํ›„์—๋Š” ์ถ”๊ฐ€ ๋“ฑ๋ก์ด ๋ถˆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.")) return; - setSaving(true); - try { - const res = await apiClient.post("/pop/production/confirm-result", { - work_order_process_id: processId, - }); - if (res.data?.success) { - await fetchProcess(); - alert("์‹ค์ ์ด ํ™•์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); - } else { - alert(res.data?.message || "ํ™•์ • ์‹คํŒจ"); - } - } catch (error: unknown) { - const err = error as { response?: { data?: { message?: string } } }; - alert(err.response?.data?.message || "ํ™•์ • ์ค‘ ์˜ค๋ฅ˜"); - } finally { - setSaving(false); - } + const handleConfirmResult = () => { + askConfirm( + "์‹ค์ ์„ ํ™•์ •ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?\nํ™•์ • ํ›„์—๋Š” ์ถ”๊ฐ€ ๋“ฑ๋ก์ด ๋ถˆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.", + async () => { + setSaving(true); + try { + const res = await apiClient.post("/pop/production/confirm-result", { + work_order_process_id: processId, + }); + if (res.data?.success) { + await fetchProcess(); + alert("์‹ค์ ์ด ํ™•์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } else { + alert(res.data?.message || "ํ™•์ • ์‹คํŒจ"); + } + } catch (error: unknown) { + const err = error as { response?: { data?: { message?: string } } }; + alert(err.response?.data?.message || "ํ™•์ • ์ค‘ ์˜ค๋ฅ˜"); + } finally { + setSaving(false); + } + }, + { title: "์‹ค์  ํ™•์ •", confirmText: "ํ™•์ •", variant: "success" } + ); }; /* ================================================================ */ @@ -837,39 +868,44 @@ export function ProcessWork({ processId }: ProcessWorkProps) { } }, []); - const handleInbound = async () => { + const handleInbound = () => { if (!selectedWarehouse) { alert("์ฐฝ๊ณ ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return; } - if (!confirm("์ƒ์‚ฐ์ž…๊ณ ๋ฅผ ์ง„ํ–‰ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?")) return; - setSaving(true); - try { - const wh = warehouses.find((w) => w.id === selectedWarehouse); - const warehouseCode = wh?.warehouse_code || selectedWarehouse; - const res = await apiClient.post("/pop/production/inventory-inbound", { - work_order_process_id: processId, - warehouse_code: warehouseCode, - location_code: selectedLocation || undefined, - }); - if (res.data?.success) { - setInboundDone(true); - alert(`์žฌ๊ณ  ์ž…๊ณ  ์™„๋ฃŒ: ${res.data.data?.qty || 0}๊ฐœ`); - } else { - alert(res.data?.message || "์ž…๊ณ  ์‹คํŒจ"); - } - } catch (error: unknown) { - const err = error as { response?: { data?: { message?: string }; status?: number } }; - const msg = err.response?.data?.message; - if (err.response?.status === 409) { - setInboundDone(true); - alert(msg || "์ด๋ฏธ ์ž…๊ณ  ์™„๋ฃŒ"); - } else { - alert(msg || "์ž…๊ณ  ์ค‘ ์˜ค๋ฅ˜"); - } - } finally { - setSaving(false); - } + askConfirm( + "์ƒ์‚ฐ์ž…๊ณ ๋ฅผ ์ง„ํ–‰ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", + async () => { + setSaving(true); + try { + const wh = warehouses.find((w) => w.id === selectedWarehouse); + const warehouseCode = wh?.warehouse_code || selectedWarehouse; + const res = await apiClient.post("/pop/production/inventory-inbound", { + work_order_process_id: processId, + warehouse_code: warehouseCode, + location_code: selectedLocation || undefined, + }); + if (res.data?.success) { + setInboundDone(true); + alert(`์žฌ๊ณ  ์ž…๊ณ  ์™„๋ฃŒ: ${res.data.data?.qty || 0}๊ฐœ`); + } else { + alert(res.data?.message || "์ž…๊ณ  ์‹คํŒจ"); + } + } catch (error: unknown) { + const err = error as { response?: { data?: { message?: string }; status?: number } }; + const msg = err.response?.data?.message; + if (err.response?.status === 409) { + setInboundDone(true); + alert(msg || "์ด๋ฏธ ์ž…๊ณ  ์™„๋ฃŒ"); + } else { + alert(msg || "์ž…๊ณ  ์ค‘ ์˜ค๋ฅ˜"); + } + } finally { + setSaving(false); + } + }, + { title: "์ƒ์‚ฐ ์ž…๊ณ ", confirmText: "์ž…๊ณ ", variant: "primary" } + ); }; /* ================================================================ */ @@ -1460,20 +1496,7 @@ export function ProcessWork({ processId }: ProcessWorkProps) { {saving ? "์ €์žฅ์ค‘..." : (totalProduced + productionQty >= inputQty && inputQty > 0) ? "์ž‘์—… ์™„๋ฃŒ" : "๋ถ„ํ•  ์™„๋ฃŒ"} - {totalProduced > 0 && ( - - )} + {/* ๊ณต์ • ํ™•์ • ๋ฒ„ํŠผ ์ œ๊ฑฐ (CEO ๊ฒฐ์ • 2026-04-09): ์ž‘์—… ์™„๋ฃŒ/๋ถ„ํ•  ์™„๋ฃŒ๋กœ ์ถฉ๋ถ„ */}
{/* Batch History */} @@ -1722,35 +1745,7 @@ export function ProcessWork({ processId }: ProcessWorkProps) { {isConfirmed ? "์‹ค์  ํ™•์ • ์™„๋ฃŒ" : "๊ณต์ • ์™„๋ฃŒ"}
- ) : ( - <> - - {totalProduced > 0 && ( - - )} - - )} + ) : null}
{/* ============================================================ */} @@ -1774,6 +1769,16 @@ export function ProcessWork({ processId }: ProcessWorkProps) { initialEntries={defectEntries} processList={processList} /> + + setConfirmModalState((s) => ({ ...s, open: false }))} + />
); } @@ -2165,17 +2170,22 @@ function MaterialInputSection({ processId }: { processId: string }) { }>>([]); const [loading, setLoading] = React.useState(true); const [saving, setSaving] = React.useState(false); + const [defaultWarehouseCode, setDefaultWarehouseCode] = React.useState(""); React.useEffect(() => { const fetchData = async () => { setLoading(true); try { - const [bomRes, inputRes] = await Promise.all([ + const [bomRes, inputRes, whRes] = await Promise.all([ apiClient.get(`/pop/production/bom-materials/${processId}`), apiClient.get(`/pop/production/material-inputs/${processId}`), + apiClient.get("/pop/production/warehouses"), ]); setBomMaterials(bomRes.data?.data?.materials || []); setInputted(inputRes.data?.data || []); + // ์ฒซ ๋ฒˆ์งธ ์ฐฝ๊ณ ๋ฅผ ๊ธฐ๋ณธ ์ž์žฌ ์ถœ๊ณ  ์ฐฝ๊ณ ๋กœ ์‚ฌ์šฉ (์žฌ๊ณ  ์ฐจ๊ฐ์šฉ) + const wh = whRes.data?.data?.[0]; + if (wh?.warehouse_code) setDefaultWarehouseCode(wh.warehouse_code); } catch { /* non-critical */ } setLoading(false); }; @@ -2196,6 +2206,9 @@ function MaterialInputSection({ processId }: { processId: string }) { unit: m.unit, bom_detail_id: m.id, required_qty: m.required_qty, + // ์žฌ๊ณ  ์ฐจ๊ฐ์„ ์œ„ํ•œ ์ฐฝ๊ณ  ์ฝ”๋“œ (๊ธฐ๋ณธ ์ฐฝ๊ณ ) + warehouse_code: defaultWarehouseCode || undefined, + location_code: defaultWarehouseCode || undefined, })); if (inputs.length === 0) { diff --git a/frontend/components/pop/hardcoded/production/WorkOrderList.tsx b/frontend/components/pop/hardcoded/production/WorkOrderList.tsx index 92d8968e..550eef16 100644 --- a/frontend/components/pop/hardcoded/production/WorkOrderList.tsx +++ b/frontend/components/pop/hardcoded/production/WorkOrderList.tsx @@ -8,6 +8,7 @@ import { dataApi } from "@/lib/api/data"; import { AcceptProcessModal } from "./AcceptProcessModal"; import { ProcessDetailModal, ProcessStep } from "./ProcessDetailModal"; import { ProcessWork } from "./ProcessWork"; +import { ConfirmModal } from "../common/ConfirmModal"; /* ํ…์ŠคํŠธ๊ฐ€ ๋„˜์น  ๋•Œ ์ž๋™ ์Šฌ๋ผ์ด๋“œ (๋งˆํ‚ค) */ function AutoScrollText({ children, className }: { children: React.ReactNode; className?: string }) { @@ -774,6 +775,11 @@ export function WorkOrderList() { }>({ open: false, processId: "", processName: "", seqNo: "", maxQty: 0 }); const [acceptLoading, setAcceptLoading] = useState(false); + const [cancelConfirm, setCancelConfirm] = useState<{ + open: boolean; + processId: string; + }>({ open: false, processId: "" }); + /* Process Detail Modal */ const [detailModal, setDetailModal] = useState<{ open: boolean; @@ -1526,22 +1532,9 @@ export function WorkOrderList() { )} {proc.status === "in_progress" && parseInt(proc.total_production_qty || "0", 10) === 0 && proc.parent_process_id && (
); } diff --git a/frontend/components/pop/hardcoded/quality/InspectionList.tsx b/frontend/components/pop/hardcoded/quality/InspectionList.tsx new file mode 100644 index 00000000..63efa1c2 --- /dev/null +++ b/frontend/components/pop/hardcoded/quality/InspectionList.tsx @@ -0,0 +1,542 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { apiClient } from "@/lib/api/client"; +import { DateRangePicker } from "../inventory/DateRangePicker"; + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +interface InspectionRow { + id: string; + inspectionNumber: string; + itemCode: string; + itemName: string; + inspectionType: string; + totalQty: number; + goodQty: number; + badQty: number; + passRate: number; + overallJudgment: string; + defectDescription: string; + referenceTable: string; + referenceId: string; + memo: string; + inspector: string; + supplierCode: string; + supplierName: string; + isCompleted: string; + completedDate: string; + createdDate: string; + time: string; + date: string; + fullDate: string; +} + +interface DetailRow { + inspectionItemName: string; + inspectionStandard: string; + passCriteria: string; + measuredValue: string; + judgment: string; +} + +interface KpiData { + total: number; + pass: number; + fail: number; + waiting: number; + passRate: number; +} + +type TabKey = "all" | "incoming" | "process" | "outgoing"; + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +function getJudgmentStyle(judgment: string): { color: string; label: string } { + if (judgment === "ํ•ฉ๊ฒฉ" || judgment === "pass") return { color: "text-green-600 bg-green-50", label: "ํ•ฉ๊ฒฉ" }; + if (judgment === "๋ถˆํ•ฉ๊ฒฉ" || judgment === "fail") return { color: "text-red-600 bg-red-50", label: "๋ถˆํ•ฉ๊ฒฉ" }; + return { color: "text-amber-600 bg-amber-50", label: "๋Œ€๊ธฐ" }; +} + +function classifyTab(inspectionType: string): TabKey { + if (inspectionType?.includes("์ž…๊ณ ")) return "incoming"; + if (inspectionType?.includes("๊ณต์ •") || inspectionType?.includes("์ƒ์‚ฐ")) return "process"; + if (inspectionType?.includes("์ถœํ•˜") || inspectionType?.includes("์ถœ๊ณ ")) return "outgoing"; + return "all"; +} + +/* ------------------------------------------------------------------ */ +/* Component */ +/* ------------------------------------------------------------------ */ + +export function InspectionList() { + const router = useRouter(); + + const [dateFrom, setDateFrom] = useState(() => new Date().toISOString().slice(0, 10)); + const [dateTo, setDateTo] = useState(() => new Date().toISOString().slice(0, 10)); + const [keyword, setKeyword] = useState(""); + const [judgmentFilter, setJudgmentFilter] = useState("์ „์ฒด"); + + const [items, setItems] = useState([]); + const [kpi, setKpi] = useState({ total: 0, pass: 0, fail: 0, waiting: 0, passRate: 0 }); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState("all"); + const [selectedItem, setSelectedItem] = useState(null); + const [selectedDetails, setSelectedDetails] = useState([]); + + /* Fetch data โ€” ๋งˆ์Šคํ„ฐ (inspection_result_mng) */ + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await apiClient.post("/table-management/tables/inspection_result_mng/data", { + page: 1, + pageSize: 500, + }); + + const rows: any[] = res.data?.data?.data ?? res.data?.data ?? res.data?.rows ?? []; + + const filtered = rows.filter((r: any) => { + const d = (r.inspection_date || r.created_date || "").slice(0, 10); + if (!d) return true; + if (dateFrom && d < dateFrom) return false; + if (dateTo && d > dateTo) return false; + return true; + }); + + const mapped: InspectionRow[] = filtered.map((r: any, idx: number) => { + const overall = r.overall_judgment || ""; + const totalQ = Number(r.total_qty || 0); + const goodQ = Number(r.good_qty || 0); + const passRate = totalQ > 0 ? Math.round((goodQ / totalQ) * 100) : 0; + return { + id: `${r.id || idx}`, + inspectionNumber: r.inspection_number || "", + itemCode: r.item_code || "", + itemName: r.item_name || "-", + inspectionType: r.inspection_type || "", + totalQty: totalQ, + goodQty: goodQ, + badQty: Number(r.bad_qty || 0), + passRate, + overallJudgment: overall, + defectDescription: r.defect_description || "", + referenceTable: r.reference_table || "", + referenceId: r.reference_id || "", + memo: r.memo || "", + inspector: r.inspector || r.writer || "", + supplierCode: r.supplier_code || "", + supplierName: r.supplier_name || "", + isCompleted: r.is_completed || "N", + completedDate: r.completed_date || "", + createdDate: r.created_date || "", + time: r.inspection_date ? new Date(r.inspection_date).toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" }) : "--:--", + date: (r.inspection_date || r.created_date || "").slice(0, 10), + fullDate: r.inspection_date ? new Date(r.inspection_date).toLocaleString("ko-KR") : "-", + }; + }); + + setItems(mapped); + + const total = mapped.length; + const pass = mapped.filter((m) => m.overallJudgment === "ํ•ฉ๊ฒฉ").length; + const fail = mapped.filter((m) => m.overallJudgment === "๋ถˆํ•ฉ๊ฒฉ").length; + const waiting = total - pass - fail; + const passRate = total > 0 ? Math.round((pass / total) * 100) : 0; + + setKpi({ total, pass, fail, waiting, passRate }); + } catch { + setItems([]); + setKpi({ total: 0, pass: 0, fail: 0, waiting: 0, passRate: 0 }); + } finally { + setLoading(false); + } + }, [dateFrom, dateTo]); + + /* Fetch detail when selected */ + useEffect(() => { + if (!selectedItem) { + setSelectedDetails([]); + return; + } + apiClient.post("/table-management/tables/inspection_result/data", { + page: 1, + pageSize: 100, + filters: { master_id: selectedItem.id }, + }).then((res) => { + const rows: any[] = res.data?.data?.data ?? res.data?.data ?? []; + const details: DetailRow[] = rows + .filter((r: any) => r.master_id === selectedItem.id) + .map((r: any) => ({ + inspectionItemName: r.inspection_item_name || "-", + inspectionStandard: r.inspection_standard || r.pass_criteria || "-", + passCriteria: r.pass_criteria || "-", + measuredValue: r.measured_value || "-", + judgment: r.judgment || "", + })); + setSelectedDetails(details); + }).catch(() => setSelectedDetails([])); + }, [selectedItem]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + /* Filter */ + const filtered = items.filter((item) => { + if (activeTab !== "all") { + const tab = classifyTab(item.inspectionType); + if (tab !== activeTab) return false; + } + if (keyword) { + const kw = keyword.toLowerCase(); + if (!item.itemName.toLowerCase().includes(kw) && !item.itemCode.toLowerCase().includes(kw)) return false; + } + if (judgmentFilter !== "์ „์ฒด") { + const j = item.overallJudgment; + if (judgmentFilter === "ํ•ฉ๊ฒฉ" && !(j === "ํ•ฉ๊ฒฉ" || j === "pass")) return false; + if (judgmentFilter === "๋ถˆํ•ฉ๊ฒฉ" && !(j === "๋ถˆํ•ฉ๊ฒฉ" || j === "fail")) return false; + if (judgmentFilter === "๋Œ€๊ธฐ" && (j === "ํ•ฉ๊ฒฉ" || j === "pass" || j === "๋ถˆํ•ฉ๊ฒฉ" || j === "fail")) return false; + } + return true; + }); + + // ํƒญ๋ณ„ ์นด์šดํŠธ + const counts = { + all: items.length, + incoming: items.filter((i) => classifyTab(i.inspectionType) === "incoming").length, + process: items.filter((i) => classifyTab(i.inspectionType) === "process").length, + outgoing: items.filter((i) => classifyTab(i.inspectionType) === "outgoing").length, + }; + + const TABS: { key: TabKey; label: string; count: number }[] = [ + { key: "all", label: "์ „์ฒด", count: counts.all }, + { key: "incoming", label: "์ž…๊ณ ๊ฒ€์‚ฌ", count: counts.incoming }, + { key: "process", label: "๊ณต์ •๊ฒ€์‚ฌ", count: counts.process }, + { key: "outgoing", label: "์ถœํ•˜๊ฒ€์‚ฌ", count: counts.outgoing }, + ]; + + return ( +
+ {/* Back + Title */} +
+ +
+

๊ฒ€์‚ฌ๊ด€๋ฆฌ

+

๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ๋‚ด์—ญ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค

+
+
+ + {/* Filters */} +
+
+
+ { setDateFrom(f); setDateTo(t); }} /> +
+ + setKeyword(e.target.value)} + placeholder="ํ’ˆ๋ชฉ๋ช… ๋˜๋Š” ๊ฒ€์‚ฌ๋ฒˆํ˜ธ" + className="w-full px-2 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-violet-400" + /> +
+
+ + +
+
+
+ + +
+
+
+ + {/* KPI */} +
+
+ + + + + +
+
+ + {/* Tabs */} +
+ {TABS.map((tab) => ( + + ))} +
+ + {/* List */} +
+
+ ๊ฒ€์‚ฌ ๋‚ด์—ญ + ์ด {filtered.length}๊ฑด +
+ + {loading ? ( +
+ {[1, 2, 3, 4].map((i) => ( +
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) : filtered.length === 0 ? ( +
+ + + +

๊ฒ€์‚ฌ ๋‚ด์—ญ์ด ์—†์Šต๋‹ˆ๋‹ค

+

๊ธฐ๊ฐ„์„ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ์ž…๊ณ /์ƒ์‚ฐ ์‹œ ๊ฒ€์‚ฌ๋ฅผ ์ง„ํ–‰ํ•ด์ฃผ์„ธ์š”

+
+ ) : ( + filtered.map((item) => { + const js = getJudgmentStyle(item.overallJudgment); + return ( +
setSelectedItem(item)} + className="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 hover:shadow-md transition-shadow cursor-pointer active:scale-[0.98]" + > +
+
+ ๐Ÿ” +
+
+
+ {item.inspectionNumber} + + {js.label} + +
+
+ {item.itemName} + {item.itemCode ? ` (${item.itemCode})` : ""} +
+
+ {item.inspectionType} + {item.supplierName ? ` ยท ${item.supplierName}` : ""} +
+
+
+

+ {item.goodQty} + / + {item.badQty} +

+

{item.passRate}%

+

{item.time}

+
+
+
+ ); + }) + )} +
+ + {/* Detail Bottom Sheet */} + {selectedItem && ( +
setSelectedItem(null)}> +
+
e.stopPropagation()} + > +
+
+
+
+

{selectedItem.inspectionType} ์ƒ์„ธ โ€” {selectedItem.inspectionNumber}

+ +
+
+
+ +
+

๊ฒ€์‚ฌ์œ ํ˜•

+ + {selectedItem.inspectionType} + +
+
+
+ +
+

ํŒ์ •

+ + {getJudgmentStyle(selectedItem.overallJudgment).label} + +
+
+
+
+

ํ’ˆ๋ชฉ

+

+ {selectedItem.itemName} + {selectedItem.itemCode ? ` (${selectedItem.itemCode})` : ""} +

+
+
+ +
+

ํ•ฉ๊ฒฉ๋ฅ 

+

{selectedItem.passRate}%

+
+
+
+
+

๊ฒ€์‚ฌ์ˆ˜๋Ÿ‰

+

{selectedItem.totalQty.toLocaleString()}

+
+
+

ํ•ฉ๊ฒฉ

+

{selectedItem.goodQty.toLocaleString()}

+
+
+

๋ถˆํ•ฉ๊ฒฉ

+

{selectedItem.badQty.toLocaleString()}

+
+
+ {selectedItem.defectDescription && ( + + )} + + {selectedItem.memo && ( + + )} + + {/* ๊ฒ€์‚ฌ ํ•ญ๋ชฉ๋ณ„ ๊ฒฐ๊ณผ (๋””ํ…Œ์ผ) */} + {selectedDetails.length > 0 && ( +
+

๊ฒ€์‚ฌ ํ•ญ๋ชฉ๋ณ„ ๊ฒฐ๊ณผ

+
+ {selectedDetails.map((d, idx) => { + const dj = getJudgmentStyle(d.judgment); + return ( +
+
+ {d.inspectionItemName} + + {dj.label} + +
+
+
+ ๊ธฐ์ค€ +

{d.inspectionStandard}

+
+
+ ์ธก์ •๊ฐ’ +

{d.measuredValue}

+
+
+
+ ); + })} +
+
+ )} +
+
+ +
+
+
+ )} + +
+ ); +} + +function DetailField({ label, value }: { label: string; value: string }) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +function KpiCell({ icon, value, label, color }: { icon: string; value: string; label: string; color: string }) { + return ( +
+ {icon} + + {value} + + {label} +
+ ); +} diff --git a/frontend/components/pop/hardcoded/quality/QualityHome.tsx b/frontend/components/pop/hardcoded/quality/QualityHome.tsx new file mode 100644 index 00000000..54f94a23 --- /dev/null +++ b/frontend/components/pop/hardcoded/quality/QualityHome.tsx @@ -0,0 +1,219 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { apiClient } from "@/lib/api/client"; + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +interface RecentItem { + id: string; + itemName: string; + itemCode: string; + inspectionType: string; + judgment: string; + judgmentColor: string; + judgmentLabel: string; + time: string; +} + +interface KpiData { + todayTotal: number; + todayPass: number; + todayFail: number; + passRate: number; +} + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +function getJudgmentStyle(j: string): { color: string; label: string } { + if (j === "ํ•ฉ๊ฒฉ" || j === "pass") return { color: "text-green-600 bg-green-50", label: "ํ•ฉ๊ฒฉ" }; + if (j === "๋ถˆํ•ฉ๊ฒฉ" || j === "fail") return { color: "text-red-600 bg-red-50", label: "๋ถˆํ•ฉ๊ฒฉ" }; + return { color: "text-amber-600 bg-amber-50", label: "๋Œ€๊ธฐ" }; +} + +const MENU_ITEMS = [ + { + id: "inspection", + title: "๊ฒ€์‚ฌ๊ด€๋ฆฌ", + gradient: "linear-gradient(135deg,#8b5cf6,#6d28d9)", + shadowColor: "rgba(139,92,246,.3)", + icon: ( + + + + ), + href: "/pop/quality/inspection", + }, +]; + +/* ------------------------------------------------------------------ */ +/* Component */ +/* ------------------------------------------------------------------ */ + +export function QualityHome() { + const router = useRouter(); + + const [kpi, setKpi] = useState({ todayTotal: 0, todayPass: 0, todayFail: 0, passRate: 0 }); + const [recentItems, setRecentItems] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + const today = new Date().toISOString().slice(0, 10); + + const res = await apiClient.post("/table-management/tables/inspection_result_mng/data", { + page: 1, + pageSize: 500, + }); + + const rows: any[] = res.data?.data?.data ?? res.data?.data ?? res.data?.rows ?? []; + const todayRows = rows.filter((r: any) => (r.created_date || "").slice(0, 10) === today); + + const total = todayRows.length; + const pass = todayRows.filter((r: any) => r.overall_judgment === "ํ•ฉ๊ฒฉ" || r.overall_judgment === "pass").length; + const fail = todayRows.filter((r: any) => r.overall_judgment === "๋ถˆํ•ฉ๊ฒฉ" || r.overall_judgment === "fail").length; + const passRate = total > 0 ? Math.round((pass / total) * 100) : 0; + + setKpi({ todayTotal: total, todayPass: pass, todayFail: fail, passRate }); + + // ์ตœ๊ทผ 5๊ฑด + const sorted = [...rows].sort((a: any, b: any) => (b.created_date || "").localeCompare(a.created_date || "")); + const top5 = sorted.slice(0, 5).map((r: any, idx: number) => { + const js = getJudgmentStyle(r.overall_judgment || r.judgment || ""); + return { + id: `${r.id || idx}`, + itemName: r.item_name || "-", + itemCode: r.item_code || "", + inspectionType: r.inspection_type || "", + judgment: r.overall_judgment || "", + judgmentColor: js.color, + judgmentLabel: js.label, + time: r.created_date ? new Date(r.created_date).toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit" }) : "--:--", + }; + }); + setRecentItems(top5); + } catch { + // empty + } finally { + setLoading(false); + } + }; + fetchData(); + }, []); + + return ( +
+ {/* Back + Title */} +
+ +
+

ํ’ˆ์งˆ

+

๊ฒ€์‚ฌ ํ˜„ํ™ฉ ๋ฐ ํ’ˆ์งˆ ๊ด€๋ฆฌ

+
+
+ + {/* KPI */} +
+
+ + + + +
+
+ + {/* Menu Icons */} +
+
+
+

ํ’ˆ์งˆ ๊ด€๋ฆฌ

+
+
+ {MENU_ITEMS.map((item) => ( +
router.push(item.href)} + > +
+ {item.icon} +
+ + {item.title} + +
+ ))} +
+
+ + {/* Recent Activity */} +
+
+
+

์ตœ๊ทผ ๊ฒ€์‚ฌ

+ ์ตœ๊ทผ 5๊ฑด +
+
+ {loading ? ( +
๋กœ๋”ฉ ์ค‘...
+ ) : recentItems.length === 0 ? ( +
์ตœ๊ทผ ๊ฒ€์‚ฌ ๋‚ด์—ญ์ด ์—†์Šต๋‹ˆ๋‹ค
+ ) : ( + recentItems.map((item) => ( +
+ + {item.time} + +
+
+ + {item.itemName} + {item.itemCode ? ` (${item.itemCode})` : ""} + + + {item.judgmentLabel} + +
+
{item.inspectionType}
+
+
+ )) + )} +
+
+
+
+ ); +} + +function KpiCell({ value, label, color }: { value: string; label: string; color: string }) { + return ( +
+ + {value} + + {label} +
+ ); +} diff --git a/frontend/components/pop/hardcoded/quality/index.ts b/frontend/components/pop/hardcoded/quality/index.ts new file mode 100644 index 00000000..eb98534c --- /dev/null +++ b/frontend/components/pop/hardcoded/quality/index.ts @@ -0,0 +1,2 @@ +export { QualityHome } from "./QualityHome"; +export { InspectionList } from "./InspectionList"; diff --git a/frontend/hooks/pop/useCartSync.ts b/frontend/hooks/pop/useCartSync.ts index 759ed18c..651e42f0 100644 --- a/frontend/hooks/pop/useCartSync.ts +++ b/frontend/hooks/pop/useCartSync.ts @@ -51,6 +51,7 @@ export interface UseCartSyncReturn { addItem: (item: CartItem, rowKey: string) => void; removeItem: (rowKey: string) => void; updateItemQuantity: (rowKey: string, quantity: number, packageUnit?: string, packageEntries?: CartItem["packageEntries"]) => void; + updateItemRow: (rowKey: string, partialRow: Record) => void; isItemInCart: (rowKey: string) => boolean; getCartItem: (rowKey: string) => CartItemWithId | undefined; @@ -137,7 +138,7 @@ function areItemsEqual(a: CartItemWithId[], b: CartItemWithId[]): boolean { const serialize = (items: CartItemWithId[]) => items - .map((item) => `${item.rowKey}:${item.quantity}:${item.packageUnit || ""}:${item.status}`) + .map((item) => `${item.rowKey}:${item.quantity}:${item.packageUnit || ""}:${item.status}:${JSON.stringify(item.row)}`) .sort() .join("|"); @@ -249,6 +250,20 @@ export function useCartSync( [], ); + // row ๊ฐ์ฒด์— ์ž„์˜ ํ•„๋“œ๋ฅผ ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ (์˜ˆ: inspectionResult) + const updateItemRow = useCallback( + (rowKey: string, partialRow: Record) => { + setCartItems((prev) => + prev.map((i) => + i.rowKey === rowKey + ? { ...i, row: { ...i.row, ...partialRow } } + : i, + ), + ); + }, + [], + ); + const isItemInCart = useCallback( (rowKey: string) => cartItems.some((i) => i.rowKey === rowKey), [cartItems], @@ -272,7 +287,9 @@ export function useCartSync( if (!c.cartId) return false; const saved = savedMap.get(c.rowKey); if (!saved) return false; - return c.quantity !== saved.quantity || c.packageUnit !== saved.packageUnit || c.status !== saved.status; + // row JSON ๋น„๊ต (๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ๋“ฑ ํฌํ•จ) + const rowChanged = JSON.stringify(c.row) !== JSON.stringify(saved.row); + return c.quantity !== saved.quantity || c.packageUnit !== saved.packageUnit || c.status !== saved.status || rowChanged; }); return { @@ -301,10 +318,12 @@ export function useCartSync( if (!c.cartId) return false; const saved = savedMap.get(c.rowKey); if (!saved) return false; + const rowChanged = JSON.stringify(c.row) !== JSON.stringify(saved.row); return ( c.quantity !== saved.quantity || c.packageUnit !== saved.packageUnit || - c.status !== saved.status + c.status !== saved.status || + rowChanged ); }); @@ -354,6 +373,7 @@ export function useCartSync( addItem, removeItem, updateItemQuantity, + updateItemRow, isItemInCart, getCartItem, getChanges, From 327b4d01c23e0a203e27bd73526fb23d11e7615b Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 9 Apr 2026 14:38:28 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20POP=20=EC=8B=9C=EC=97=B0=20?= =?UTF-8?q?=EC=A4=80=EB=B9=84=20=E2=80=94=205=EA=B0=9C=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20+=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20+=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=B0=BD=EA=B3=A0=20=EB=A7=A4=EC=B9=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ๊ตฌ๋งค์ž…๊ณ : ๊ฒ€์‚ฌ๊ธฐ์ค€ API ์ˆ˜์ •, ๊ฒ€์‚ฌ๊ฒฐ๊ณผ DB ์ €์žฅ, ๊ฒ€์‚ฌ ๋ฏธ์™„๋ฃŒ ํ™•์ • ์ฐจ๋‹จ - ํŒ๋งค์ถœ๊ณ : ์žฌ๊ณ  ๋ถ€์กฑ ์‚ฌ์ „ ๊ฒ€์ฆ, ์ˆ˜์ฃผ์ƒ์„ธ ship_qty ๋ฐ˜์˜, ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๊ฐœ์„  - ๊ณต์ •์‹คํ–‰: seq_no ๋น„์ˆœ์ฐจ ๋Œ€์‘(3๊ณณ), ์ž์žฌํˆฌ์ž… ์ž๋™ ์ฐฝ๊ณ  ๋งค์นญ ์žฌ๊ณ ์ฐจ๊ฐ, ๋ถˆํ•„์š” ๋ฒ„ํŠผ ์ œ๊ฑฐ - ๊ฒ€์‚ฌ๊ด€๋ฆฌ+์ž…์ถœ๊ณ ๊ด€๋ฆฌ: ์‹ ๊ทœ ํ™”๋ฉด (quality, inventory) - ๊ณตํ†ต: ConfirmModal ์ปค์Šคํ…€ ๋ชจ๋‹ฌ (native confirm ๋Œ€์ฒด) --- .../src/controllers/outboundController.ts | 802 +-- .../controllers/popProductionController.ts | 4451 ++++++++------- .../src/controllers/receivingController.ts | 1171 ++-- .../src/routes/inspectionResultRoutes.ts | 439 +- .../app/(pop)/pop/inventory/history/page.tsx | 10 +- frontend/app/(pop)/pop/inventory/page.tsx | 10 +- .../app/(pop)/pop/quality/inspection/page.tsx | 10 +- frontend/app/(pop)/pop/quality/page.tsx | 10 +- .../components/pop/hardcoded/MenuIcons.tsx | 334 +- .../pop/hardcoded/common/ConfirmModal.tsx | 128 +- .../pop/hardcoded/inbound/InboundCart.tsx | 1186 ++-- .../pop/hardcoded/inbound/InboundCartPage.tsx | 2452 ++++---- .../pop/hardcoded/inbound/InspectionModal.tsx | 1211 ++-- frontend/components/pop/hardcoded/index.ts | 28 +- .../hardcoded/inventory/DateRangePicker.tsx | 480 +- .../pop/hardcoded/inventory/InOutHistory.tsx | 1140 ++-- .../pop/hardcoded/inventory/InventoryHome.tsx | 562 +- .../pop/hardcoded/inventory/index.ts | 2 +- .../hardcoded/outbound/OutboundCartPage.tsx | 2249 ++++---- .../pop/hardcoded/production/ProcessWork.tsx | 4987 ++++++++++------- .../hardcoded/production/WorkOrderList.tsx | 3463 +++++++----- .../pop/hardcoded/quality/InspectionList.tsx | 1159 ++-- .../pop/hardcoded/quality/QualityHome.tsx | 450 +- .../components/pop/hardcoded/quality/index.ts | 2 +- frontend/hooks/pop/useCartSync.ts | 631 ++- 25 files changed, 15182 insertions(+), 12185 deletions(-) diff --git a/backend-node/src/controllers/outboundController.ts b/backend-node/src/controllers/outboundController.ts index 9c9d2eca..896b25b1 100644 --- a/backend-node/src/controllers/outboundController.ts +++ b/backend-node/src/controllers/outboundController.ts @@ -7,70 +7,70 @@ * - ๊ธฐํƒ€์ถœ๊ณ  โ†’ item_info (ํ’ˆ๋ชฉ) */ -import { Response } from "express"; -import { AuthenticatedRequest } from "../types/auth"; +import type { Response } from "express"; import { getPool } from "../database/db"; +import type { AuthenticatedRequest } from "../types/auth"; import { logger } from "../utils/logger"; // ์ถœ๊ณ  ๋ชฉ๋ก ์กฐํšŒ export async function getList(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const { - outbound_type, - outbound_status, - search_keyword, - date_from, - date_to, - } = req.query; + try { + const companyCode = req.user!.companyCode; + const { + outbound_type, + outbound_status, + search_keyword, + date_from, + date_to, + } = req.query; - const conditions: string[] = []; - const params: any[] = []; - let paramIdx = 1; + const conditions: string[] = []; + const params: any[] = []; + let paramIdx = 1; - if (companyCode === "*") { - // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ์ „์ฒด ์กฐํšŒ - } else { - conditions.push(`om.company_code = $${paramIdx}`); - params.push(companyCode); - paramIdx++; - } + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ์ „์ฒด ์กฐํšŒ + } else { + conditions.push(`om.company_code = $${paramIdx}`); + params.push(companyCode); + paramIdx++; + } - if (outbound_type && outbound_type !== "all") { - conditions.push(`om.outbound_type = $${paramIdx}`); - params.push(outbound_type); - paramIdx++; - } + if (outbound_type && outbound_type !== "all") { + conditions.push(`om.outbound_type = $${paramIdx}`); + params.push(outbound_type); + paramIdx++; + } - if (outbound_status && outbound_status !== "all") { - conditions.push(`om.outbound_status = $${paramIdx}`); - params.push(outbound_status); - paramIdx++; - } + if (outbound_status && outbound_status !== "all") { + conditions.push(`om.outbound_status = $${paramIdx}`); + params.push(outbound_status); + paramIdx++; + } - if (search_keyword) { - conditions.push( - `(om.outbound_number ILIKE $${paramIdx} OR om.item_name ILIKE $${paramIdx} OR om.item_code ILIKE $${paramIdx} OR om.customer_name ILIKE $${paramIdx} OR om.reference_number ILIKE $${paramIdx})` - ); - params.push(`%${search_keyword}%`); - paramIdx++; - } + if (search_keyword) { + conditions.push( + `(om.outbound_number ILIKE $${paramIdx} OR om.item_name ILIKE $${paramIdx} OR om.item_code ILIKE $${paramIdx} OR om.customer_name ILIKE $${paramIdx} OR om.reference_number ILIKE $${paramIdx})`, + ); + params.push(`%${search_keyword}%`); + paramIdx++; + } - if (date_from) { - conditions.push(`om.outbound_date >= $${paramIdx}`); - params.push(date_from); - paramIdx++; - } - if (date_to) { - conditions.push(`om.outbound_date <= $${paramIdx}`); - params.push(date_to); - paramIdx++; - } + if (date_from) { + conditions.push(`om.outbound_date >= $${paramIdx}`); + params.push(date_from); + paramIdx++; + } + if (date_to) { + conditions.push(`om.outbound_date <= $${paramIdx}`); + params.push(date_to); + paramIdx++; + } - const whereClause = - conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + const whereClause = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; - const query = ` + const query = ` SELECT om.*, wh.warehouse_name @@ -82,42 +82,52 @@ export async function getList(req: AuthenticatedRequest, res: Response) { ORDER BY om.created_date DESC `; - const pool = getPool(); - const result = await pool.query(query, params); + const pool = getPool(); + const result = await pool.query(query, params); - logger.info("์ถœ๊ณ  ๋ชฉ๋ก ์กฐํšŒ", { - companyCode, - rowCount: result.rowCount, - }); + logger.info("์ถœ๊ณ  ๋ชฉ๋ก ์กฐํšŒ", { + companyCode, + rowCount: result.rowCount, + }); - return res.json({ success: true, data: result.rows }); - } catch (error: any) { - logger.error("์ถœ๊ณ  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("์ถœ๊ณ  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ์ถœ๊ณ  ๋“ฑ๋ก (๋‹ค๊ฑด) export async function create(req: AuthenticatedRequest, res: Response) { - const pool = getPool(); - const client = await pool.connect(); + const pool = getPool(); + const client = await pool.connect(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const { items, outbound_number, outbound_date, warehouse_code, location_code, manager_id, memo } = req.body; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { + items, + outbound_number, + outbound_date, + warehouse_code, + location_code, + manager_id, + memo, + } = req.body; - if (!items || !Array.isArray(items) || items.length === 0) { - return res.status(400).json({ success: false, message: "์ถœ๊ณ  ํ’ˆ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค." }); - } + if (!items || !Array.isArray(items) || items.length === 0) { + return res + .status(400) + .json({ success: false, message: "์ถœ๊ณ  ํ’ˆ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค." }); + } - await client.query("BEGIN"); + await client.query("BEGIN"); - const insertedRows: any[] = []; + const insertedRows: any[] = []; - for (const item of items) { - const result = await client.query( - `INSERT INTO outbound_mng ( + for (const item of items) { + const result = await client.query( + `INSERT INTO outbound_mng ( id, company_code, outbound_number, outbound_type, outbound_date, reference_number, customer_code, customer_name, item_code, item_name, specification, material, unit, @@ -138,182 +148,202 @@ export async function create(req: AuthenticatedRequest, res: Response) { $26, $27, $28, NOW(), $29, $29, '์ถœ๊ณ ' ) RETURNING *`, - [ - companyCode, - outbound_number || item.outbound_number, - item.outbound_type, - outbound_date || item.outbound_date, - item.reference_number || null, - item.customer_code || null, - item.customer_name || null, - item.item_code || item.item_number || null, - item.item_name || null, - item.spec || item.specification || null, - item.material || null, - item.unit || "EA", - item.outbound_qty || 0, - item.unit_price || 0, - item.total_amount || 0, - item.lot_number || null, - warehouse_code || item.warehouse_code || null, - location_code || item.location_code || null, - item.outbound_status || "๋Œ€๊ธฐ", - manager_id || item.manager_id || null, - memo || item.memo || null, - item.source_type || null, - item.sales_order_id || null, - item.shipment_plan_id || null, - item.item_info_id || null, - item.destination_code || null, - item.delivery_destination || null, - item.delivery_address || null, - userId, - ] - ); + [ + companyCode, + outbound_number || item.outbound_number, + item.outbound_type, + outbound_date || item.outbound_date, + item.reference_number || null, + item.customer_code || null, + item.customer_name || null, + item.item_code || item.item_number || null, + item.item_name || null, + item.spec || item.specification || null, + item.material || null, + item.unit || "EA", + item.outbound_qty || 0, + item.unit_price || 0, + item.total_amount || 0, + item.lot_number || null, + warehouse_code || item.warehouse_code || null, + location_code || item.location_code || null, + item.outbound_status || "๋Œ€๊ธฐ", + manager_id || item.manager_id || null, + memo || item.memo || null, + item.source_type || null, + item.sales_order_id || null, + item.shipment_plan_id || null, + item.item_info_id || null, + item.destination_code || null, + item.delivery_destination || null, + item.delivery_address || null, + userId, + ], + ); - insertedRows.push(result.rows[0]); + insertedRows.push(result.rows[0]); - // ์žฌ๊ณ  ์—…๋ฐ์ดํŠธ (inventory_stock): ์ถœ๊ณ  ์ˆ˜๋Ÿ‰ ์ฐจ๊ฐ - const itemCode = item.item_code || item.item_number || null; - const whCode = warehouse_code || item.warehouse_code || null; - const locCode = location_code || item.location_code || null; - const outQty = Number(item.outbound_qty) || 0; - if (itemCode && outQty > 0) { - // ์žฌ๊ณ  ์‚ฌ์ „ ๊ฒ€์ฆ: ๋ถ€์กฑ ์‹œ ์ฆ‰์‹œ ์—๋Ÿฌ (ํŠธ๋žœ์žญ์…˜ ROLLBACK) - const stockCheck = await client.query( - `SELECT COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) as cur + // ์žฌ๊ณ  ์—…๋ฐ์ดํŠธ (inventory_stock): ์ถœ๊ณ  ์ˆ˜๋Ÿ‰ ์ฐจ๊ฐ + const itemCode = item.item_code || item.item_number || null; + const whCode = warehouse_code || item.warehouse_code || null; + const locCode = location_code || item.location_code || null; + const outQty = Number(item.outbound_qty) || 0; + if (itemCode && outQty > 0) { + // ์žฌ๊ณ  ์‚ฌ์ „ ๊ฒ€์ฆ: ๋ถ€์กฑ ์‹œ ์ฆ‰์‹œ ์—๋Ÿฌ (ํŠธ๋žœ์žญ์…˜ ROLLBACK) + const stockCheck = await client.query( + `SELECT COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) as cur FROM inventory_stock WHERE company_code = $1 AND item_code = $2 AND COALESCE(warehouse_code, '') = COALESCE($3, '') AND COALESCE(location_code, '') = COALESCE($4, '') LIMIT 1`, - [companyCode, itemCode, whCode || '', locCode || ''] - ); - const currentStock = parseFloat(stockCheck.rows[0]?.cur || '0'); - if (currentStock < outQty) { - throw new Error( - `์žฌ๊ณ  ๋ถ€์กฑ: ${item.item_name || itemCode} (์ฐฝ๊ณ  ${whCode || '๋ฏธ์ง€์ •'}) โ€” ํ˜„์žฌ ์žฌ๊ณ  ${currentStock}, ์š”์ฒญ ์ถœ๊ณ  ${outQty}` - ); - } + [companyCode, itemCode, whCode || "", locCode || ""], + ); + const currentStock = parseFloat(stockCheck.rows[0]?.cur || "0"); + if (currentStock < outQty) { + throw new Error( + `์žฌ๊ณ  ๋ถ€์กฑ: ${item.item_name || itemCode} (์ฐฝ๊ณ  ${whCode || "๋ฏธ์ง€์ •"}) โ€” ํ˜„์žฌ ์žฌ๊ณ  ${currentStock}, ์š”์ฒญ ์ถœ๊ณ  ${outQty}`, + ); + } - const existingStock = await client.query( - `SELECT id FROM inventory_stock + const existingStock = await client.query( + `SELECT id FROM inventory_stock WHERE company_code = $1 AND item_code = $2 AND COALESCE(warehouse_code, '') = COALESCE($3, '') AND COALESCE(location_code, '') = COALESCE($4, '') LIMIT 1`, - [companyCode, itemCode, whCode || '', locCode || ''] - ); + [companyCode, itemCode, whCode || "", locCode || ""], + ); - if (existingStock.rows.length > 0) { - await client.query( - `UPDATE inventory_stock + if (existingStock.rows.length > 0) { + await client.query( + `UPDATE inventory_stock SET current_qty = CAST(GREATEST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) - $1, 0) AS text), last_out_date = NOW(), updated_date = NOW() WHERE id = $2`, - [outQty, existingStock.rows[0].id] - ); - } else { - // ์žฌ๊ณ  ๋ ˆ์ฝ”๋“œ๊ฐ€ ์—†์œผ๋ฉด 0์œผ๋กœ ์ƒ์„ฑ (๋งˆ์ด๋„ˆ์Šค ๋ฐฉ์ง€) - await client.query( - `INSERT INTO inventory_stock ( + [outQty, existingStock.rows[0].id], + ); + } else { + // ์žฌ๊ณ  ๋ ˆ์ฝ”๋“œ๊ฐ€ ์—†์œผ๋ฉด 0์œผ๋กœ ์ƒ์„ฑ (๋งˆ์ด๋„ˆ์Šค ๋ฐฉ์ง€) + await client.query( + `INSERT INTO inventory_stock ( id, company_code, item_code, warehouse_code, location_code, current_qty, safety_qty, last_out_date, created_date, updated_date, writer ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '0', '0', NOW(), NOW(), NOW(), $5)`, - [companyCode, itemCode, whCode, locCode, userId] - ); - } + [companyCode, itemCode, whCode, locCode, userId], + ); + } - // ์žฌ๊ณ  ์ด๋ ฅ ๊ธฐ๋ก (inventory_history) - const afterStockRes = await client.query( - `SELECT current_qty FROM inventory_stock + // ์žฌ๊ณ  ์ด๋ ฅ ๊ธฐ๋ก (inventory_history) + const afterStockRes = await client.query( + `SELECT current_qty FROM inventory_stock WHERE company_code = $1 AND item_code = $2 AND COALESCE(warehouse_code, '') = COALESCE($3, '') AND COALESCE(location_code, '') = COALESCE($4, '') LIMIT 1`, - [companyCode, itemCode, whCode || '', locCode || ''] - ); - const afterQty = afterStockRes.rows[0]?.current_qty || '0'; - await client.query( - `INSERT INTO inventory_history ( + [companyCode, itemCode, whCode || "", locCode || ""], + ); + const afterQty = afterStockRes.rows[0]?.current_qty || "0"; + await client.query( + `INSERT INTO inventory_history ( id, company_code, item_code, warehouse_code, location_code, transaction_type, transaction_date, quantity, balance_qty, remark, writer, created_date ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '์ถœ๊ณ ', NOW(), $5, $6, $7, $8, NOW())`, - [companyCode, itemCode, whCode, locCode, String(-outQty), afterQty, item.outbound_type || '์ถœ๊ณ ', userId] - ); - } + [ + companyCode, + itemCode, + whCode, + locCode, + String(-outQty), + afterQty, + item.outbound_type || "์ถœ๊ณ ", + userId, + ], + ); + } - // ํŒ๋งค์ถœ๊ณ ์ธ ๊ฒฝ์šฐ ์ถœํ•˜์ง€์‹œ์˜ ship_qty ์—…๋ฐ์ดํŠธ + ์ˆ˜์ฃผ์ƒ์„ธ ship_qty ๋ฐ˜์˜ - if (item.outbound_type === "ํŒ๋งค์ถœ๊ณ " && item.source_id && item.source_type === "shipment_instruction_detail") { - const outQtyNum = Number(item.outbound_qty) || 0; - await client.query( - `UPDATE shipment_instruction_detail + // ํŒ๋งค์ถœ๊ณ ์ธ ๊ฒฝ์šฐ ์ถœํ•˜์ง€์‹œ์˜ ship_qty ์—…๋ฐ์ดํŠธ + ์ˆ˜์ฃผ์ƒ์„ธ ship_qty ๋ฐ˜์˜ + if ( + item.outbound_type === "ํŒ๋งค์ถœ๊ณ " && + item.source_id && + item.source_type === "shipment_instruction_detail" + ) { + const outQtyNum = Number(item.outbound_qty) || 0; + await client.query( + `UPDATE shipment_instruction_detail SET ship_qty = COALESCE(ship_qty, 0) + $1, updated_date = NOW() WHERE id = $2 AND company_code = $3`, - [outQtyNum, item.source_id, companyCode] - ); + [outQtyNum, item.source_id, companyCode], + ); - // ์ถœํ•˜์ง€์‹œ ์ƒ์„ธ์˜ detail_id๋กœ ์ˆ˜์ฃผ์ƒ์„ธ(sales_order_detail) ship_qty๋„ ์—…๋ฐ์ดํŠธ - const sidRes = await client.query( - `SELECT detail_id FROM shipment_instruction_detail WHERE id = $1 AND company_code = $2`, - [item.source_id, companyCode] - ); - const detailId = sidRes.rows[0]?.detail_id; - if (detailId) { - await client.query( - `UPDATE sales_order_detail + // ์ถœํ•˜์ง€์‹œ ์ƒ์„ธ์˜ detail_id๋กœ ์ˆ˜์ฃผ์ƒ์„ธ(sales_order_detail) ship_qty๋„ ์—…๋ฐ์ดํŠธ + const sidRes = await client.query( + `SELECT detail_id FROM shipment_instruction_detail WHERE id = $1 AND company_code = $2`, + [item.source_id, companyCode], + ); + const detailId = sidRes.rows[0]?.detail_id; + if (detailId) { + await client.query( + `UPDATE sales_order_detail SET ship_qty = (COALESCE(NULLIF(ship_qty,'')::numeric, 0) + $1)::text, balance_qty = (COALESCE(NULLIF(qty,'')::numeric, 0) - COALESCE(NULLIF(ship_qty,'')::numeric, 0) - $1)::text, updated_date = NOW() WHERE id = $2 AND company_code = $3`, - [outQtyNum, detailId, companyCode] - ); - } - } - } + [outQtyNum, detailId, companyCode], + ); + } + } + } - await client.query("COMMIT"); + await client.query("COMMIT"); - logger.info("์ถœ๊ณ  ๋“ฑ๋ก ์™„๋ฃŒ", { - companyCode, - userId, - count: insertedRows.length, - outbound_number, - }); + logger.info("์ถœ๊ณ  ๋“ฑ๋ก ์™„๋ฃŒ", { + companyCode, + userId, + count: insertedRows.length, + outbound_number, + }); - return res.json({ - success: true, - data: insertedRows, - message: `${insertedRows.length}๊ฑด ์ถœ๊ณ  ๋“ฑ๋ก ์™„๋ฃŒ`, - }); - } catch (error: any) { - await client.query("ROLLBACK"); - logger.error("์ถœ๊ณ  ๋“ฑ๋ก ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } finally { - client.release(); - } + return res.json({ + success: true, + data: insertedRows, + message: `${insertedRows.length}๊ฑด ์ถœ๊ณ  ๋“ฑ๋ก ์™„๋ฃŒ`, + }); + } catch (error: any) { + await client.query("ROLLBACK"); + logger.error("์ถœ๊ณ  ๋“ฑ๋ก ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } finally { + client.release(); + } } // ์ถœ๊ณ  ์ˆ˜์ • export async function update(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const { id } = req.params; - const { - outbound_date, outbound_qty, unit_price, total_amount, - lot_number, warehouse_code, location_code, - outbound_status, manager_id: mgr, memo, - } = req.body; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { id } = req.params; + const { + outbound_date, + outbound_qty, + unit_price, + total_amount, + lot_number, + warehouse_code, + location_code, + outbound_status, + manager_id: mgr, + memo, + } = req.body; - const pool = getPool(); - const result = await pool.query( - `UPDATE outbound_mng SET + const pool = getPool(); + const result = await pool.query( + `UPDATE outbound_mng SET outbound_date = COALESCE($1, outbound_date), outbound_qty = COALESCE($2, outbound_qty), unit_price = COALESCE($3, unit_price), @@ -328,73 +358,89 @@ export async function update(req: AuthenticatedRequest, res: Response) { updated_by = $11 WHERE id = $12 AND company_code = $13 RETURNING *`, - [ - outbound_date, outbound_qty, unit_price, total_amount, - lot_number, warehouse_code, location_code, - outbound_status, mgr, memo, - userId, id, companyCode, - ] - ); + [ + outbound_date, + outbound_qty, + unit_price, + total_amount, + lot_number, + warehouse_code, + location_code, + outbound_status, + mgr, + memo, + userId, + id, + companyCode, + ], + ); - if (result.rowCount === 0) { - return res.status(404).json({ success: false, message: "์ถœ๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); - } + if (result.rowCount === 0) { + return res + .status(404) + .json({ success: false, message: "์ถœ๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); + } - logger.info("์ถœ๊ณ  ์ˆ˜์ •", { companyCode, userId, id }); + logger.info("์ถœ๊ณ  ์ˆ˜์ •", { companyCode, userId, id }); - return res.json({ success: true, data: result.rows[0] }); - } catch (error: any) { - logger.error("์ถœ๊ณ  ์ˆ˜์ • ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: result.rows[0] }); + } catch (error: any) { + logger.error("์ถœ๊ณ  ์ˆ˜์ • ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ์ถœ๊ณ  ์‚ญ์ œ export async function deleteOutbound(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const { id } = req.params; - const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const { id } = req.params; + const pool = getPool(); - const result = await pool.query( - `DELETE FROM outbound_mng WHERE id = $1 AND company_code = $2 RETURNING id`, - [id, companyCode] - ); + const result = await pool.query( + `DELETE FROM outbound_mng WHERE id = $1 AND company_code = $2 RETURNING id`, + [id, companyCode], + ); - if (result.rowCount === 0) { - return res.status(404).json({ success: false, message: "๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); - } + if (result.rowCount === 0) { + return res + .status(404) + .json({ success: false, message: "๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); + } - logger.info("์ถœ๊ณ  ์‚ญ์ œ", { companyCode, id }); + logger.info("์ถœ๊ณ  ์‚ญ์ œ", { companyCode, id }); - return res.json({ success: true, message: "์‚ญ์ œ ์™„๋ฃŒ" }); - } catch (error: any) { - logger.error("์ถœ๊ณ  ์‚ญ์ œ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, message: "์‚ญ์ œ ์™„๋ฃŒ" }); + } catch (error: any) { + logger.error("์ถœ๊ณ  ์‚ญ์ œ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ํŒ๋งค์ถœ๊ณ ์šฉ: ์ถœํ•˜์ง€์‹œ ๋ฐ์ดํ„ฐ ์กฐํšŒ -export async function getShipmentInstructions(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const { keyword } = req.query; +export async function getShipmentInstructions( + req: AuthenticatedRequest, + res: Response, +) { + try { + const companyCode = req.user!.companyCode; + const { keyword } = req.query; - const conditions: string[] = ["si.company_code = $1"]; - const params: any[] = [companyCode]; - let paramIdx = 2; + const conditions: string[] = ["si.company_code = $1"]; + const params: any[] = [companyCode]; + let paramIdx = 2; - if (keyword) { - conditions.push( - `(si.instruction_no ILIKE $${paramIdx} OR sid.item_name ILIKE $${paramIdx} OR sid.item_code ILIKE $${paramIdx})` - ); - params.push(`%${keyword}%`); - paramIdx++; - } + if (keyword) { + conditions.push( + `(si.instruction_no ILIKE $${paramIdx} OR sid.item_name ILIKE $${paramIdx} OR sid.item_code ILIKE $${paramIdx})`, + ); + params.push(`%${keyword}%`); + paramIdx++; + } - const pool = getPool(); - const result = await pool.query( - `SELECT + const pool = getPool(); + const result = await pool.query( + `SELECT sid.id AS detail_id, si.id AS instruction_id, si.instruction_no, @@ -417,42 +463,45 @@ export async function getShipmentInstructions(req: AuthenticatedRequest, res: Re WHERE ${conditions.join(" AND ")} AND COALESCE(sid.plan_qty, 0) > COALESCE(sid.ship_qty, 0) ORDER BY si.instruction_date DESC, si.instruction_no`, - params - ); + params, + ); - return res.json({ success: true, data: result.rows }); - } catch (error: any) { - logger.error("์ถœํ•˜์ง€์‹œ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("์ถœํ•˜์ง€์‹œ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ๋ฐ˜ํ’ˆ์ถœ๊ณ ์šฉ: ๋ฐœ์ฃผ(์ž…๊ณ ) ๋ฐ์ดํ„ฐ ์กฐํšŒ -export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const { keyword } = req.query; +export async function getPurchaseOrders( + req: AuthenticatedRequest, + res: Response, +) { + try { + const companyCode = req.user!.companyCode; + const { keyword } = req.query; - const conditions: string[] = ["company_code = $1"]; - const params: any[] = [companyCode]; - let paramIdx = 2; + const conditions: string[] = ["company_code = $1"]; + const params: any[] = [companyCode]; + let paramIdx = 2; - // ์ž…๊ณ ๋œ ๊ฒƒ๋งŒ (๋ฐ˜ํ’ˆ ๋Œ€์ƒ) - conditions.push( - `COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) > 0` - ); + // ์ž…๊ณ ๋œ ๊ฒƒ๋งŒ (๋ฐ˜ํ’ˆ ๋Œ€์ƒ) + conditions.push( + `COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) > 0`, + ); - if (keyword) { - conditions.push( - `(purchase_no ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx} OR item_code ILIKE $${paramIdx} OR supplier_name ILIKE $${paramIdx})` - ); - params.push(`%${keyword}%`); - paramIdx++; - } + if (keyword) { + conditions.push( + `(purchase_no ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx} OR item_code ILIKE $${paramIdx} OR supplier_name ILIKE $${paramIdx})`, + ); + params.push(`%${keyword}%`); + paramIdx++; + } - const pool = getPool(); - const result = await pool.query( - `SELECT + const pool = getPool(); + const result = await pool.query( + `SELECT id, purchase_no, order_date, supplier_code, supplier_name, item_code, item_name, spec, material, COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) AS order_qty, @@ -462,137 +511,146 @@ export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response FROM purchase_order_mng WHERE ${conditions.join(" AND ")} ORDER BY order_date DESC, purchase_no`, - params - ); + params, + ); - return res.json({ success: true, data: result.rows }); - } catch (error: any) { - logger.error("๋ฐœ์ฃผ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("๋ฐœ์ฃผ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ๊ธฐํƒ€์ถœ๊ณ ์šฉ: ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ ์กฐํšŒ export async function getItems(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const { keyword } = req.query; + try { + const companyCode = req.user!.companyCode; + const { keyword } = req.query; - const conditions: string[] = ["company_code = $1"]; - const params: any[] = [companyCode]; - let paramIdx = 2; + const conditions: string[] = ["company_code = $1"]; + const params: any[] = [companyCode]; + let paramIdx = 2; - if (keyword) { - conditions.push( - `(item_number ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx})` - ); - params.push(`%${keyword}%`); - paramIdx++; - } + if (keyword) { + conditions.push( + `(item_number ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx})`, + ); + params.push(`%${keyword}%`); + paramIdx++; + } - const pool = getPool(); - const result = await pool.query( - `SELECT + const pool = getPool(); + const result = await pool.query( + `SELECT id, item_number, item_name, size AS spec, material, unit, COALESCE(CAST(NULLIF(standard_price, '') AS numeric), 0) AS standard_price FROM item_info WHERE ${conditions.join(" AND ")} ORDER BY item_name`, - params - ); + params, + ); - return res.json({ success: true, data: result.rows }); - } catch (error: any) { - logger.error("ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ์ถœ๊ณ ๋ฒˆํ˜ธ ์ž๋™์ƒ์„ฑ export async function generateNumber(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const ruleId = (req.query.ruleId as string) || (req.query.rule_id as string); + try { + const companyCode = req.user!.companyCode; + const ruleId = + (req.query.ruleId as string) || (req.query.rule_id as string); - // 1์ˆœ์œ„: POP ํ™”๋ฉด์„ค์ •์—์„œ ์„ ํƒํ•œ ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ - if (ruleId && ruleId !== "__none__") { - try { - const { numberingRuleService } = await import("../services/numberingRuleService"); - const newNumber = await numberingRuleService.allocateCode(ruleId, companyCode); - return res.json({ success: true, data: newNumber }); - } catch (e: any) { - logger.warn("์„ ํƒํ•œ ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ ์‹คํŒจ, ๊ธฐ๋ณธ ์ฑ„๋ฒˆ์œผ๋กœ ํด๋ฐฑ", { ruleId, error: e.message }); - } - } + // 1์ˆœ์œ„: POP ํ™”๋ฉด์„ค์ •์—์„œ ์„ ํƒํ•œ ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ + if (ruleId && ruleId !== "__none__") { + try { + const { numberingRuleService } = await import( + "../services/numberingRuleService" + ); + const newNumber = await numberingRuleService.allocateCode( + ruleId, + companyCode, + ); + return res.json({ success: true, data: newNumber }); + } catch (e: any) { + logger.warn("์„ ํƒํ•œ ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ ์‹คํŒจ, ๊ธฐ๋ณธ ์ฑ„๋ฒˆ์œผ๋กœ ํด๋ฐฑ", { + ruleId, + error: e.message, + }); + } + } - // 2์ˆœ์œ„: ๊ธฐ๋ณธ ํ•˜๋“œ์ฝ”๋”ฉ ์ฑ„๋ฒˆ (OUT-YYYY-XXXX) - const pool = getPool(); - const today = new Date(); - const yyyy = today.getFullYear(); - const prefix = `OUT-${yyyy}-`; + // 2์ˆœ์œ„: ๊ธฐ๋ณธ ํ•˜๋“œ์ฝ”๋”ฉ ์ฑ„๋ฒˆ (OUT-YYYY-XXXX) + const pool = getPool(); + const today = new Date(); + const yyyy = today.getFullYear(); + const prefix = `OUT-${yyyy}-`; - const result = await pool.query( - `SELECT outbound_number FROM outbound_mng + const result = await pool.query( + `SELECT outbound_number FROM outbound_mng WHERE company_code = $1 AND outbound_number LIKE $2 ORDER BY outbound_number DESC LIMIT 1`, - [companyCode, `${prefix}%`] - ); + [companyCode, `${prefix}%`], + ); - let seq = 1; - if (result.rows.length > 0) { - const lastNo = result.rows[0].outbound_number; - const lastSeq = parseInt(lastNo.replace(prefix, ""), 10); - if (!isNaN(lastSeq)) seq = lastSeq + 1; - } + let seq = 1; + if (result.rows.length > 0) { + const lastNo = result.rows[0].outbound_number; + const lastSeq = parseInt(lastNo.replace(prefix, ""), 10); + if (!isNaN(lastSeq)) seq = lastSeq + 1; + } - const newNumber = `${prefix}${String(seq).padStart(4, "0")}`; + const newNumber = `${prefix}${String(seq).padStart(4, "0")}`; - return res.json({ success: true, data: newNumber }); - } catch (error: any) { - logger.error("์ถœ๊ณ ๋ฒˆํ˜ธ ์ƒ์„ฑ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: newNumber }); + } catch (error: any) { + logger.error("์ถœ๊ณ ๋ฒˆํ˜ธ ์ƒ์„ฑ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ์ฐฝ๊ณ  ๋ชฉ๋ก ์กฐํšŒ export async function getWarehouses(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const pool = getPool(); - const result = await pool.query( - `SELECT warehouse_code, warehouse_name, warehouse_type + const result = await pool.query( + `SELECT warehouse_code, warehouse_name, warehouse_type FROM warehouse_info WHERE company_code = $1 AND COALESCE(status, '') != '์‚ญ์ œ' ORDER BY warehouse_name`, - [companyCode] - ); + [companyCode], + ); - return res.json({ success: true, data: result.rows }); - } catch (error: any) { - logger.error("์ฐฝ๊ณ  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("์ฐฝ๊ณ  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ์ฐฝ๊ณ ๋ณ„ ์œ„์น˜ ๋ชฉ๋ก ์กฐํšŒ export async function getLocations(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const warehouseCode = req.query.warehouse_code as string; - const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const warehouseCode = req.query.warehouse_code as string; + const pool = getPool(); - const result = await pool.query( - `SELECT location_code, location_name, warehouse_code + const result = await pool.query( + `SELECT location_code, location_name, warehouse_code FROM warehouse_location WHERE company_code = $1 ${warehouseCode ? "AND warehouse_code = $2" : ""} ORDER BY location_code`, - warehouseCode ? [companyCode, warehouseCode] : [companyCode] - ); + warehouseCode ? [companyCode, warehouseCode] : [companyCode], + ); - return res.json({ success: true, data: result.rows }); - } catch (error: any) { - logger.error("์œ„์น˜ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("์œ„์น˜ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } diff --git a/backend-node/src/controllers/popProductionController.ts b/backend-node/src/controllers/popProductionController.ts index 14334b75..92cba89d 100644 --- a/backend-node/src/controllers/popProductionController.ts +++ b/backend-node/src/controllers/popProductionController.ts @@ -1,25 +1,29 @@ -import { Response } from "express"; +import type { Response } from "express"; import { getPool } from "../database/db"; +import type { AuthenticatedRequest } from "../middleware/authMiddleware"; import logger from "../utils/logger"; -import { AuthenticatedRequest } from "../middleware/authMiddleware"; // ๋ถˆ๋Ÿ‰ ์ƒ์„ธ ํ•ญ๋ชฉ ํƒ€์ž… interface DefectDetailItem { - defect_code: string; - defect_name: string; - qty: string; - disposition: string; + defect_code: string; + defect_name: string; + qty: string; + disposition: string; } // ์ž๋™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜: batch_id ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (๋ฐฐ์น˜/๋กœํŠธ ์ถ”์ ์šฉ) let _batchMigrationDone = false; async function ensureBatchIdColumn() { - if (_batchMigrationDone) return; - try { - const pool = getPool(); - await pool.query("ALTER TABLE work_order_process ADD COLUMN IF NOT EXISTS batch_id VARCHAR(100)"); - _batchMigrationDone = true; - } catch { /* ์ด๋ฏธ ์กด์žฌํ•˜๊ฑฐ๋‚˜ ๊ถŒํ•œ ๋ฌธ์ œ ์‹œ ๋ฌด์‹œ */ } + if (_batchMigrationDone) return; + try { + const pool = getPool(); + await pool.query( + "ALTER TABLE work_order_process ADD COLUMN IF NOT EXISTS batch_id VARCHAR(100)", + ); + _batchMigrationDone = true; + } catch { + /* ์ด๋ฏธ ์กด์žฌํ•˜๊ฑฐ๋‚˜ ๊ถŒํ•œ ๋ฌธ์ œ ์‹œ ๋ฌด์‹œ */ + } } /** @@ -28,46 +32,46 @@ async function ensureBatchIdColumn() { * (inventory_stock์— UNIQUE ์ œ์•ฝ์กฐ๊ฑด์ด ์—†์œผ๋ฏ€๋กœ ON CONFLICT ์‚ฌ์šฉ ๋ถˆ๊ฐ€) */ async function upsertInventoryStock( - client: { query: (text: string, values?: any[]) => Promise }, - companyCode: string, - itemCode: string, - warehouseCode: string, - locationCode: string | null, - qty: number, - userId: string + client: { query: (text: string, values?: any[]) => Promise }, + companyCode: string, + itemCode: string, + warehouseCode: string, + locationCode: string | null, + qty: number, + userId: string, ): Promise { - const whCode = warehouseCode || null; - const locCode = locationCode || null; + const whCode = warehouseCode || null; + const locCode = locationCode || null; - const existing = await client.query( - `SELECT id FROM inventory_stock + const existing = await client.query( + `SELECT id FROM inventory_stock WHERE company_code = $1 AND item_code = $2 AND COALESCE(warehouse_code, '') = COALESCE($3, '') AND COALESCE(location_code, '') = COALESCE($4, '') LIMIT 1`, - [companyCode, itemCode, whCode || '', locCode || ''] - ); + [companyCode, itemCode, whCode || "", locCode || ""], + ); - if (existing.rows.length > 0) { - await client.query( - `UPDATE inventory_stock + if (existing.rows.length > 0) { + await client.query( + `UPDATE inventory_stock SET current_qty = CAST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) + $1 AS text), last_in_date = NOW(), updated_date = NOW(), writer = $2 WHERE id = $3`, - [qty, userId, existing.rows[0].id] - ); - } else { - await client.query( - `INSERT INTO inventory_stock ( + [qty, userId, existing.rows[0].id], + ); + } else { + await client.query( + `INSERT INTO inventory_stock ( id, company_code, item_code, warehouse_code, location_code, current_qty, safety_qty, last_in_date, created_date, updated_date, writer ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, '0', NOW(), NOW(), NOW(), $6)`, - [companyCode, itemCode, whCode, locCode, String(qty), userId] - ); - } + [companyCode, itemCode, whCode, locCode, String(qty), userId], + ); + } } /** @@ -77,17 +81,17 @@ async function upsertInventoryStock( * ์ „๋žต: routingDetailId๊ฐ€ ์žˆ์œผ๋ฉด ์›๋ณธ ํ…œํ”Œ๋ฆฟ์—์„œ, ์—†์œผ๋ฉด ๋งˆ์Šคํ„ฐ์˜ ๊ธฐ์กด ๊ฒฐ๊ณผ์—์„œ ๋ณต์‚ฌ */ async function copyChecklistToSplit( - client: { query: (text: string, values?: any[]) => Promise }, - masterProcessId: string, - newProcessId: string, - routingDetailId: string | null, - companyCode: string, - userId: string + client: { query: (text: string, values?: any[]) => Promise }, + masterProcessId: string, + newProcessId: string, + routingDetailId: string | null, + companyCode: string, + userId: string, ): Promise { - // A. routing_detail_id๊ฐ€ ์žˆ์œผ๋ฉด ์›๋ณธ ํ…œํ”Œ๋ฆฟ(process_work_item + detail)์—์„œ ๋ณต์‚ฌ - if (routingDetailId) { - const result = await client.query( - `INSERT INTO process_work_result ( + // A. routing_detail_id๊ฐ€ ์žˆ์œผ๋ฉด ์›๋ณธ ํ…œํ”Œ๋ฆฟ(process_work_item + detail)์—์„œ ๋ณต์‚ฌ + if (routingDetailId) { + const result = await client.query( + `INSERT INTO process_work_result ( id, company_code, work_order_process_id, source_work_item_id, source_detail_id, work_phase, item_title, item_sort_order, @@ -110,16 +114,16 @@ async function copyChecklistToSplit( WHERE pwi.routing_detail_id = $3 AND pwi.company_code = $4 ORDER BY pwi.sort_order, pwd.sort_order`, - [newProcessId, userId, routingDetailId, companyCode] - ); - const countA = result.rowCount ?? 0; - if (countA > 0) return countA; - // A ์ „๋žต์—์„œ 0๊ฑด์ด๋ฉด B ์ „๋žต(๋งˆ์Šคํ„ฐ ํ–‰์˜ ๊ธฐ์กด ๊ฒฐ๊ณผ ๋ณต์‚ฌ)์œผ๋กœ fallthrough - } + [newProcessId, userId, routingDetailId, companyCode], + ); + const countA = result.rowCount ?? 0; + if (countA > 0) return countA; + // A ์ „๋žต์—์„œ 0๊ฑด์ด๋ฉด B ์ „๋žต(๋งˆ์Šคํ„ฐ ํ–‰์˜ ๊ธฐ์กด ๊ฒฐ๊ณผ ๋ณต์‚ฌ)์œผ๋กœ fallthrough + } - // B. routing_detail_id๊ฐ€ ์—†๊ฑฐ๋‚˜ A ์ „๋žต์—์„œ 0๊ฑด์ด๋ฉด ๋งˆ์Šคํ„ฐ ํ–‰์˜ process_work_result์—์„œ ๊ตฌ์กฐ๋งŒ ๋ณต์‚ฌ (ํƒ€์ด๋จธ/๊ฒฐ๊ณผ๊ฐ’ ์ดˆ๊ธฐํ™”) - const result = await client.query( - `INSERT INTO process_work_result ( + // B. routing_detail_id๊ฐ€ ์—†๊ฑฐ๋‚˜ A ์ „๋žต์—์„œ 0๊ฑด์ด๋ฉด ๋งˆ์Šคํ„ฐ ํ–‰์˜ process_work_result์—์„œ ๊ตฌ์กฐ๋งŒ ๋ณต์‚ฌ (ํƒ€์ด๋จธ/๊ฒฐ๊ณผ๊ฐ’ ์ดˆ๊ธฐํ™”) + const result = await client.query( + `INSERT INTO process_work_result ( company_code, work_order_process_id, source_work_item_id, source_detail_id, work_phase, item_title, item_sort_order, @@ -140,9 +144,9 @@ async function copyChecklistToSplit( WHERE work_order_process_id = $3 AND company_code = $4 ORDER BY item_sort_order, detail_sort_order`, - [newProcessId, userId, masterProcessId, companyCode] - ); - return result.rowCount ?? 0; + [newProcessId, userId, masterProcessId, companyCode], + ); + return result.rowCount ?? 0; } /** @@ -152,29 +156,34 @@ async function copyChecklistToSplit( * @returns ์ƒ์„ฑ๋œ ๊ณต์ • ๋ชฉ๋ก + ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์ด ์ˆ˜. ์ด๋ฏธ ์กด์žฌํ•˜๋ฉด null ๋ฐ˜ํ™˜. */ async function generateWorkProcessesForInstruction( - client: { query: (text: string, values?: any[]) => Promise }, - workInstructionId: string, - routingVersionId: string, - planQty: string | null, - companyCode: string, - userId: string + client: { query: (text: string, values?: any[]) => Promise }, + workInstructionId: string, + routingVersionId: string, + planQty: string | null, + companyCode: string, + userId: string, ): Promise<{ - processes: Array<{ id: string; seq_no: string; process_name: string; checklist_count: number }>; - total_checklists: number; + processes: Array<{ + id: string; + seq_no: string; + process_name: string; + checklist_count: number; + }>; + total_checklists: number; } | null> { - // ์ค‘๋ณต ํ˜ธ์ถœ ๋ฐฉ์ง€: ์ด๋ฏธ ์ƒ์„ฑ๋œ ๊ณต์ •์ด ์žˆ๋Š”์ง€ ํ™•์ธ - const existCheck = await client.query( - `SELECT COUNT(*) as cnt FROM work_order_process + // ์ค‘๋ณต ํ˜ธ์ถœ ๋ฐฉ์ง€: ์ด๋ฏธ ์ƒ์„ฑ๋œ ๊ณต์ •์ด ์žˆ๋Š”์ง€ ํ™•์ธ + const existCheck = await client.query( + `SELECT COUNT(*) as cnt FROM work_order_process WHERE wo_id = $1 AND company_code = $2`, - [workInstructionId, companyCode] - ); - if (parseInt(existCheck.rows[0].cnt, 10) > 0) { - return null; // ์ด๋ฏธ ์กด์žฌ - } + [workInstructionId, companyCode], + ); + if (parseInt(existCheck.rows[0].cnt, 10) > 0) { + return null; // ์ด๋ฏธ ์กด์žฌ + } - // 1. item_routing_detail + process_mng JOIN (๊ณต์ • ๋ชฉ๋ก + ๊ณต์ •๋ช…) - const routingDetails = await client.query( - `SELECT rd.id, rd.seq_no, rd.process_code, + // 1. item_routing_detail + process_mng JOIN (๊ณต์ • ๋ชฉ๋ก + ๊ณต์ •๋ช…) + const routingDetails = await client.query( + `SELECT rd.id, rd.seq_no, rd.process_code, COALESCE(pm.process_name, rd.process_code) as process_name, rd.is_required, rd.is_fixed_order, rd.standard_time FROM item_routing_detail rd @@ -182,62 +191,69 @@ async function generateWorkProcessesForInstruction( AND pm.company_code = rd.company_code WHERE rd.routing_version_id = $1 AND rd.company_code = $2 ORDER BY CAST(rd.seq_no AS int) NULLS LAST`, - [routingVersionId, companyCode] - ); + [routingVersionId, companyCode], + ); - if (routingDetails.rows.length === 0) { - return null; // ๊ณต์ • ์—†์Œ - } + if (routingDetails.rows.length === 0) { + return null; // ๊ณต์ • ์—†์Œ + } - const processes: Array<{ - id: string; - seq_no: string; - process_name: string; - checklist_count: number; - }> = []; - let totalChecklists = 0; + const processes: Array<{ + id: string; + seq_no: string; + process_name: string; + checklist_count: number; + }> = []; + let totalChecklists = 0; - for (const rd of routingDetails.rows) { - // 2. work_order_process INSERT - const wopResult = await client.query( - `INSERT INTO work_order_process ( + for (const rd of routingDetails.rows) { + // 2. work_order_process INSERT + const wopResult = await client.query( + `INSERT INTO work_order_process ( id, company_code, wo_id, seq_no, process_code, process_name, is_required, is_fixed_order, standard_time, plan_qty, status, routing_detail_id, writer ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id`, - [ - companyCode, - workInstructionId, - rd.seq_no, - rd.process_code, - rd.process_name, - rd.is_required, - rd.is_fixed_order, - rd.standard_time, - planQty || null, - parseInt(rd.seq_no, 10) === 1 || rd.is_fixed_order === "Y" ? "acceptable" : "waiting", - rd.id, - userId, - ] - ); - const wopId = wopResult.rows[0].id; + [ + companyCode, + workInstructionId, + rd.seq_no, + rd.process_code, + rd.process_name, + rd.is_required, + rd.is_fixed_order, + rd.standard_time, + planQty || null, + parseInt(rd.seq_no, 10) === 1 || rd.is_fixed_order === "Y" + ? "acceptable" + : "waiting", + rd.id, + userId, + ], + ); + const wopId = wopResult.rows[0].id; - // 3. process_work_result INSERT (๊ณตํ†ต ํ•จ์ˆ˜๋กœ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋ณต์‚ฌ) - const checklistCount = await copyChecklistToSplit( - client, wopId, wopId, rd.id, companyCode, userId - ); - totalChecklists += checklistCount; + // 3. process_work_result INSERT (๊ณตํ†ต ํ•จ์ˆ˜๋กœ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋ณต์‚ฌ) + const checklistCount = await copyChecklistToSplit( + client, + wopId, + wopId, + rd.id, + companyCode, + userId, + ); + totalChecklists += checklistCount; - processes.push({ - id: wopId, - seq_no: rd.seq_no, - process_name: rd.process_name, - checklist_count: checklistCount, - }); - } + processes.push({ + id: wopId, + seq_no: rd.seq_no, + process_name: rd.process_name, + checklist_count: checklistCount, + }); + } - return { processes, total_checklists: totalChecklists }; + return { processes, total_checklists: totalChecklists }; } /** @@ -245,77 +261,81 @@ async function generateWorkProcessesForInstruction( * PC์—์„œ ์ž‘์—…์ง€์‹œ ์ƒ์„ฑ ํ›„ ํ˜ธ์ถœ. 1 ํŠธ๋žœ์žญ์…˜์œผ๋กœ work_order_process + process_work_result ์ผ๊ด„ ์ƒ์„ฑ. */ export const createWorkProcesses = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); - const client = await pool.connect(); + const pool = getPool(); + const client = await pool.connect(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; - const { work_instruction_id, item_code, routing_version_id, plan_qty } = - req.body; + const { work_instruction_id, item_code, routing_version_id, plan_qty } = + req.body; - if (!work_instruction_id || !routing_version_id) { - return res.status(400).json({ - success: false, - message: - "work_instruction_id์™€ routing_version_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", - }); - } + if (!work_instruction_id || !routing_version_id) { + return res.status(400).json({ + success: false, + message: "work_instruction_id์™€ routing_version_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", + }); + } - logger.info("[pop/production] create-work-processes ์š”์ฒญ", { - companyCode, - userId, - work_instruction_id, - item_code, - routing_version_id, - plan_qty, - }); + logger.info("[pop/production] create-work-processes ์š”์ฒญ", { + companyCode, + userId, + work_instruction_id, + item_code, + routing_version_id, + plan_qty, + }); - await client.query("BEGIN"); + await client.query("BEGIN"); - const result = await generateWorkProcessesForInstruction( - client, work_instruction_id, routing_version_id, plan_qty, companyCode, userId - ); + const result = await generateWorkProcessesForInstruction( + client, + work_instruction_id, + routing_version_id, + plan_qty, + companyCode, + userId, + ); - if (!result) { - await client.query("ROLLBACK"); - return res.status(409).json({ - success: false, - message: "์ด๋ฏธ ๊ณต์ •์ด ์ƒ์„ฑ๋œ ์ž‘์—…์ง€์‹œ์ด๊ฑฐ๋‚˜ ๋ผ์šฐํŒ…์— ๊ณต์ •์ด ์—†์Šต๋‹ˆ๋‹ค.", - }); - } + if (!result) { + await client.query("ROLLBACK"); + return res.status(409).json({ + success: false, + message: "์ด๋ฏธ ๊ณต์ •์ด ์ƒ์„ฑ๋œ ์ž‘์—…์ง€์‹œ์ด๊ฑฐ๋‚˜ ๋ผ์šฐํŒ…์— ๊ณต์ •์ด ์—†์Šต๋‹ˆ๋‹ค.", + }); + } - await client.query("COMMIT"); + await client.query("COMMIT"); - logger.info("[pop/production] create-work-processes ์™„๋ฃŒ", { - companyCode, - work_instruction_id, - total_processes: result.processes.length, - total_checklists: result.total_checklists, - }); + logger.info("[pop/production] create-work-processes ์™„๋ฃŒ", { + companyCode, + work_instruction_id, + total_processes: result.processes.length, + total_checklists: result.total_checklists, + }); - return res.json({ - success: true, - data: { - processes: result.processes, - total_processes: result.processes.length, - total_checklists: result.total_checklists, - }, - }); - } catch (error: any) { - await client.query("ROLLBACK"); - logger.error("[pop/production] create-work-processes ์˜ค๋ฅ˜:", error); - return res.status(500).json({ - success: false, - message: error.message || "๊ณต์ • ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - }); - } finally { - client.release(); - } + return res.json({ + success: true, + data: { + processes: result.processes, + total_processes: result.processes.length, + total_checklists: result.total_checklists, + }, + }); + } catch (error: any) { + await client.query("ROLLBACK"); + logger.error("[pop/production] create-work-processes ์˜ค๋ฅ˜:", error); + return res.status(500).json({ + success: false, + message: error.message || "๊ณต์ • ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } finally { + client.release(); + } }; /** @@ -324,23 +344,23 @@ export const createWorkProcesses = async ( * ๊ฐ ๊ฑด๋ณ„ ๊ฐœ๋ณ„ try-catch๋กœ ํ•˜๋‚˜ ์‹คํŒจํ•ด๋„ ๋‚˜๋จธ์ง€ ์ง„ํ–‰. */ export const syncWorkInstructions = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); + const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; - logger.info("[pop/production] sync-work-instructions ์š”์ฒญ", { - companyCode, - userId, - }); + logger.info("[pop/production] sync-work-instructions ์š”์ฒญ", { + companyCode, + userId, + }); - // ๋ฏธ๋™๊ธฐํ™” ์ž‘์—…์ง€์‹œ ์กฐํšŒ: routing์ด ์žˆ์ง€๋งŒ work_order_process๊ฐ€ ์—†๋Š” ํ•ญ๋ชฉ - const unsyncedResult = await pool.query( - `SELECT wi.id, wi.work_instruction_no, wi.routing, wi.qty + // ๋ฏธ๋™๊ธฐํ™” ์ž‘์—…์ง€์‹œ ์กฐํšŒ: routing์ด ์žˆ์ง€๋งŒ work_order_process๊ฐ€ ์—†๋Š” ํ•ญ๋ชฉ + const unsyncedResult = await pool.query( + `SELECT wi.id, wi.work_instruction_no, wi.routing, wi.qty FROM work_instruction wi WHERE wi.company_code = $1 AND wi.routing IS NOT NULL @@ -348,168 +368,172 @@ export const syncWorkInstructions = async ( SELECT 1 FROM work_order_process wop WHERE wop.wo_id = wi.id AND wop.company_code = $1 )`, - [companyCode] - ); + [companyCode], + ); - const unsynced = unsyncedResult.rows; + const unsynced = unsyncedResult.rows; - if (unsynced.length === 0) { - return res.json({ - success: true, - data: { synced: 0, skipped: 0, errors: 0, details: [] }, - }); - } + if (unsynced.length === 0) { + return res.json({ + success: true, + data: { synced: 0, skipped: 0, errors: 0, details: [] }, + }); + } - let synced = 0; - let skipped = 0; - let errors = 0; - const details: Array<{ - work_instruction_id: string; - work_instruction_no: string; - status: "synced" | "skipped" | "error"; - process_count?: number; - error?: string; - }> = []; + let synced = 0; + let skipped = 0; + let errors = 0; + const details: Array<{ + work_instruction_id: string; + work_instruction_no: string; + status: "synced" | "skipped" | "error"; + process_count?: number; + error?: string; + }> = []; - for (const wi of unsynced) { - const client = await pool.connect(); - try { - await client.query("BEGIN"); + for (const wi of unsynced) { + const client = await pool.connect(); + try { + await client.query("BEGIN"); - const result = await generateWorkProcessesForInstruction( - client, wi.id, wi.routing, wi.qty || null, companyCode, userId - ); + const result = await generateWorkProcessesForInstruction( + client, + wi.id, + wi.routing, + wi.qty || null, + companyCode, + userId, + ); - if (!result) { - await client.query("ROLLBACK"); - skipped++; - details.push({ - work_instruction_id: wi.id, - work_instruction_no: wi.work_instruction_no, - status: "skipped", - }); - continue; - } + if (!result) { + await client.query("ROLLBACK"); + skipped++; + details.push({ + work_instruction_id: wi.id, + work_instruction_no: wi.work_instruction_no, + status: "skipped", + }); + continue; + } - await client.query("COMMIT"); - synced++; - details.push({ - work_instruction_id: wi.id, - work_instruction_no: wi.work_instruction_no, - status: "synced", - process_count: result.processes.length, - }); + await client.query("COMMIT"); + synced++; + details.push({ + work_instruction_id: wi.id, + work_instruction_no: wi.work_instruction_no, + status: "synced", + process_count: result.processes.length, + }); - logger.info("[pop/production] sync: ๊ณต์ • ์ƒ์„ฑ ์™„๋ฃŒ", { - work_instruction_no: wi.work_instruction_no, - process_count: result.processes.length, - }); - } catch (err: any) { - await client.query("ROLLBACK"); - errors++; - details.push({ - work_instruction_id: wi.id, - work_instruction_no: wi.work_instruction_no, - status: "error", - error: err.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜", - }); - logger.error("[pop/production] sync: ๊ฐœ๋ณ„ ์˜ค๋ฅ˜", { - work_instruction_no: wi.work_instruction_no, - error: err.message, - }); - } finally { - client.release(); - } - } + logger.info("[pop/production] sync: ๊ณต์ • ์ƒ์„ฑ ์™„๋ฃŒ", { + work_instruction_no: wi.work_instruction_no, + process_count: result.processes.length, + }); + } catch (err: any) { + await client.query("ROLLBACK"); + errors++; + details.push({ + work_instruction_id: wi.id, + work_instruction_no: wi.work_instruction_no, + status: "error", + error: err.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜", + }); + logger.error("[pop/production] sync: ๊ฐœ๋ณ„ ์˜ค๋ฅ˜", { + work_instruction_no: wi.work_instruction_no, + error: err.message, + }); + } finally { + client.release(); + } + } - logger.info("[pop/production] sync-work-instructions ์™„๋ฃŒ", { - companyCode, - synced, - skipped, - errors, - }); + logger.info("[pop/production] sync-work-instructions ์™„๋ฃŒ", { + companyCode, + synced, + skipped, + errors, + }); - return res.json({ - success: true, - data: { synced, skipped, errors, details }, - }); - } catch (error: any) { - logger.error("[pop/production] sync-work-instructions ์˜ค๋ฅ˜:", error); - return res.status(500).json({ - success: false, - message: error.message || "์ž‘์—…์ง€์‹œ ๋™๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - }); - } + return res.json({ + success: true, + data: { synced, skipped, errors, details }, + }); + } catch (error: any) { + logger.error("[pop/production] sync-work-instructions ์˜ค๋ฅ˜:", error); + return res.status(500).json({ + success: false, + message: error.message || "์ž‘์—…์ง€์‹œ ๋™๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } }; /** * D-BE2: ํƒ€์ด๋จธ API (์‹œ์ž‘/์ผ์‹œ์ •์ง€/์žฌ์‹œ์ž‘) */ export const controlTimer = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); + const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; - const { work_order_process_id, action } = req.body; + const { work_order_process_id, action } = req.body; - if (!work_order_process_id || !action) { - return res.status(400).json({ - success: false, - message: "work_order_process_id์™€ action์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", - }); - } + if (!work_order_process_id || !action) { + return res.status(400).json({ + success: false, + message: "work_order_process_id์™€ action์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", + }); + } - if (!["start", "pause", "resume", "complete"].includes(action)) { - return res.status(400).json({ - success: false, - message: - "action์€ start, pause, resume, complete ์ค‘ ํ•˜๋‚˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", - }); - } + if (!["start", "pause", "resume", "complete"].includes(action)) { + return res.status(400).json({ + success: false, + message: "action์€ start, pause, resume, complete ์ค‘ ํ•˜๋‚˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + }); + } - logger.info("[pop/production] timer ์š”์ฒญ", { - companyCode, - userId, - work_order_process_id, - action, - }); + logger.info("[pop/production] timer ์š”์ฒญ", { + companyCode, + userId, + work_order_process_id, + action, + }); - let result; + let result; - switch (action) { - case "start": - // ์ตœ์ดˆ 1ํšŒ๋งŒ ์„ค์ •, ์ด๋ฏธ ์žˆ์œผ๋ฉด ๋ฌด์‹œ - result = await pool.query( - `UPDATE work_order_process + switch (action) { + case "start": + // ์ตœ์ดˆ 1ํšŒ๋งŒ ์„ค์ •, ์ด๋ฏธ ์žˆ์œผ๋ฉด ๋ฌด์‹œ + result = await pool.query( + `UPDATE work_order_process SET started_at = CASE WHEN started_at IS NULL THEN NOW()::text ELSE started_at END, status = CASE WHEN status = 'waiting' THEN 'in_progress' ELSE status END, updated_date = NOW() WHERE id = $1 AND company_code = $2 RETURNING id, started_at, status`, - [work_order_process_id, companyCode] - ); - break; + [work_order_process_id, companyCode], + ); + break; - case "pause": - result = await pool.query( - `UPDATE work_order_process + case "pause": + result = await pool.query( + `UPDATE work_order_process SET paused_at = NOW()::text, updated_date = NOW() WHERE id = $1 AND company_code = $2 AND paused_at IS NULL RETURNING id, paused_at`, - [work_order_process_id, companyCode] - ); - break; + [work_order_process_id, companyCode], + ); + break; - case "resume": - // ์ผ์‹œ์ •์ง€ ์‹œ๊ฐ„ ๋ˆ„์  ํ›„ paused_at ์ดˆ๊ธฐํ™” - result = await pool.query( - `UPDATE work_order_process + case "resume": + // ์ผ์‹œ์ •์ง€ ์‹œ๊ฐ„ ๋ˆ„์  ํ›„ paused_at ์ดˆ๊ธฐํ™” + result = await pool.query( + `UPDATE work_order_process SET total_paused_time = ( COALESCE(total_paused_time::int, 0) + EXTRACT(EPOCH FROM NOW() - paused_at::timestamp)::int @@ -518,15 +542,15 @@ export const controlTimer = async ( updated_date = NOW() WHERE id = $1 AND company_code = $2 AND paused_at IS NOT NULL RETURNING id, total_paused_time`, - [work_order_process_id, companyCode] - ); - break; + [work_order_process_id, companyCode], + ); + break; - case "complete": { - const { good_qty, defect_qty } = req.body; + case "complete": { + const { good_qty, defect_qty } = req.body; - const groupSumResult = await pool.query( - `SELECT COALESCE(SUM( + const groupSumResult = await pool.query( + `SELECT COALESCE(SUM( CASE WHEN group_started_at IS NOT NULL AND group_completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM group_completed_at::timestamp - group_started_at::timestamp)::int - COALESCE(group_total_paused_time::int, 0) @@ -534,12 +558,13 @@ export const controlTimer = async ( ), 0)::text AS total_work_seconds FROM process_work_result WHERE work_order_process_id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); - const calculatedWorkTime = groupSumResult.rows[0]?.total_work_seconds || "0"; + [work_order_process_id, companyCode], + ); + const calculatedWorkTime = + groupSumResult.rows[0]?.total_work_seconds || "0"; - result = await pool.query( - `UPDATE work_order_process + result = await pool.query( + `UPDATE work_order_process SET status = 'completed', completed_at = NOW()::text, completed_by = $3, @@ -551,43 +576,43 @@ export const controlTimer = async ( WHERE id = $1 AND company_code = $2 AND status != 'completed' RETURNING id, status, completed_at, completed_by, actual_work_time, good_qty, defect_qty`, - [ - work_order_process_id, - companyCode, - userId, - calculatedWorkTime, - good_qty || null, - defect_qty || null, - ] - ); - break; - } - } + [ + work_order_process_id, + companyCode, + userId, + calculatedWorkTime, + good_qty || null, + defect_qty || null, + ], + ); + break; + } + } - if (!result || result.rowCount === 0) { - return res.status(404).json({ - success: false, - message: "๋Œ€์ƒ ๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ํ˜„์žฌ ์ƒํƒœ์—์„œ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - }); - } + if (!result || result.rowCount === 0) { + return res.status(404).json({ + success: false, + message: "๋Œ€์ƒ ๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ํ˜„์žฌ ์ƒํƒœ์—์„œ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }); + } - logger.info("[pop/production] timer ์™„๋ฃŒ", { - action, - work_order_process_id, - result: result.rows[0], - }); + logger.info("[pop/production] timer ์™„๋ฃŒ", { + action, + work_order_process_id, + result: result.rows[0], + }); - return res.json({ - success: true, - data: result.rows[0], - }); - } catch (error: any) { - logger.error("[pop/production] timer ์˜ค๋ฅ˜:", error); - return res.status(500).json({ - success: false, - message: error.message || "ํƒ€์ด๋จธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - }); - } + return res.json({ + success: true, + data: result.rows[0], + }); + } catch (error: any) { + logger.error("[pop/production] timer ์˜ค๋ฅ˜:", error); + return res.status(500).json({ + success: false, + message: error.message || "ํƒ€์ด๋จธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } }; /** @@ -595,75 +620,78 @@ export const controlTimer = async ( * ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ”์˜ ๊ฐ ์ž‘์—… ๊ทธ๋ฃน๋งˆ๋‹ค ๋…๋ฆฝ์ ์ธ ์‹œ์ž‘/์ •์ง€/์žฌ๊ฐœ/์™„๋ฃŒ ํƒ€์ด๋จธ */ export const controlGroupTimer = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); + const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const { work_order_process_id, source_work_item_id, action } = req.body; + try { + const companyCode = req.user!.companyCode; + const { work_order_process_id, source_work_item_id, action } = req.body; - if (!work_order_process_id || !source_work_item_id || !action) { - return res.status(400).json({ - success: false, - message: - "work_order_process_id, source_work_item_id, action์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", - }); - } + if (!work_order_process_id || !source_work_item_id || !action) { + return res.status(400).json({ + success: false, + message: + "work_order_process_id, source_work_item_id, action์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", + }); + } - if (!["start", "pause", "resume", "complete"].includes(action)) { - return res.status(400).json({ - success: false, - message: - "action์€ start, pause, resume, complete ์ค‘ ํ•˜๋‚˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", - }); - } + if (!["start", "pause", "resume", "complete"].includes(action)) { + return res.status(400).json({ + success: false, + message: "action์€ start, pause, resume, complete ์ค‘ ํ•˜๋‚˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + }); + } - logger.info("[pop/production] group-timer ์š”์ฒญ", { - companyCode, - work_order_process_id, - source_work_item_id, - action, - }); + logger.info("[pop/production] group-timer ์š”์ฒญ", { + companyCode, + work_order_process_id, + source_work_item_id, + action, + }); - const whereClause = `work_order_process_id = $1 AND source_work_item_id = $2 AND company_code = $3`; - const baseParams = [work_order_process_id, source_work_item_id, companyCode]; + const whereClause = `work_order_process_id = $1 AND source_work_item_id = $2 AND company_code = $3`; + const baseParams = [ + work_order_process_id, + source_work_item_id, + companyCode, + ]; - let result; + let result; - switch (action) { - case "start": - result = await pool.query( - `UPDATE process_work_result + switch (action) { + case "start": + result = await pool.query( + `UPDATE process_work_result SET group_started_at = CASE WHEN group_started_at IS NULL THEN NOW()::text ELSE group_started_at END, updated_date = NOW() WHERE ${whereClause} RETURNING id, group_started_at`, - baseParams - ); - await pool.query( - `UPDATE work_order_process + baseParams, + ); + await pool.query( + `UPDATE work_order_process SET started_at = NOW()::text, updated_date = NOW() WHERE id = $1 AND company_code = $2 AND started_at IS NULL`, - [work_order_process_id, companyCode] - ); - break; + [work_order_process_id, companyCode], + ); + break; - case "pause": - result = await pool.query( - `UPDATE process_work_result + case "pause": + result = await pool.query( + `UPDATE process_work_result SET group_paused_at = NOW()::text, updated_date = NOW() WHERE ${whereClause} AND group_paused_at IS NULL RETURNING id, group_paused_at`, - baseParams - ); - break; + baseParams, + ); + break; - case "resume": - result = await pool.query( - `UPDATE process_work_result + case "resume": + result = await pool.query( + `UPDATE process_work_result SET group_total_paused_time = ( COALESCE(group_total_paused_time::int, 0) + EXTRACT(EPOCH FROM NOW() - group_paused_at::timestamp)::int @@ -672,13 +700,13 @@ export const controlGroupTimer = async ( updated_date = NOW() WHERE ${whereClause} AND group_paused_at IS NOT NULL RETURNING id, group_total_paused_time`, - baseParams - ); - break; + baseParams, + ); + break; - case "complete": { - result = await pool.query( - `UPDATE process_work_result + case "complete": { + result = await pool.query( + `UPDATE process_work_result SET group_completed_at = NOW()::text, group_total_paused_time = CASE WHEN group_paused_at IS NOT NULL THEN ( @@ -691,88 +719,88 @@ export const controlGroupTimer = async ( updated_date = NOW() WHERE ${whereClause} RETURNING id, group_started_at, group_completed_at, group_total_paused_time`, - baseParams - ); - break; - } - } + baseParams, + ); + break; + } + } - if (!result || result.rowCount === 0) { - return res.status(404).json({ - success: false, - message: "๋Œ€์ƒ ๊ทธ๋ฃน์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ํ˜„์žฌ ์ƒํƒœ์—์„œ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - }); - } + if (!result || result.rowCount === 0) { + return res.status(404).json({ + success: false, + message: "๋Œ€์ƒ ๊ทธ๋ฃน์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ํ˜„์žฌ ์ƒํƒœ์—์„œ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }); + } - logger.info("[pop/production] group-timer ์™„๋ฃŒ", { - action, - source_work_item_id, - affectedRows: result.rowCount, - }); + logger.info("[pop/production] group-timer ์™„๋ฃŒ", { + action, + source_work_item_id, + affectedRows: result.rowCount, + }); - return res.json({ - success: true, - data: result.rows[0], - affectedRows: result.rowCount, - }); - } catch (error: any) { - logger.error("[pop/production] group-timer ์˜ค๋ฅ˜:", error); - return res.status(500).json({ - success: false, - message: error.message || "๊ทธ๋ฃน ํƒ€์ด๋จธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - }); - } + return res.json({ + success: true, + data: result.rows[0], + affectedRows: result.rowCount, + }); + } catch (error: any) { + logger.error("[pop/production] group-timer ์˜ค๋ฅ˜:", error); + return res.status(500).json({ + success: false, + message: error.message || "๊ทธ๋ฃน ํƒ€์ด๋จธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } }; /** * ๋ถˆ๋Ÿ‰ ์œ ํ˜• ๋ชฉ๋ก ์กฐํšŒ (defect_standard_mng) */ export const getDefectTypes = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); + const pool = getPool(); - try { - const companyCode = req.user!.companyCode; + try { + const companyCode = req.user!.companyCode; - let query: string; - let params: unknown[]; + let query: string; + let params: unknown[]; - if (companyCode === "*") { - query = ` + if (companyCode === "*") { + query = ` SELECT id, defect_code, defect_name, defect_type, severity, company_code FROM defect_standard_mng WHERE is_active = 'Y' ORDER BY defect_code`; - params = []; - } else { - query = ` + params = []; + } else { + query = ` SELECT id, defect_code, defect_name, defect_type, severity, company_code FROM defect_standard_mng WHERE is_active = 'Y' AND company_code = $1 ORDER BY defect_code`; - params = [companyCode]; - } + params = [companyCode]; + } - const result = await pool.query(query, params); + const result = await pool.query(query, params); - logger.info("[pop/production] defect-types ์กฐํšŒ", { - companyCode, - count: result.rowCount, - }); + logger.info("[pop/production] defect-types ์กฐํšŒ", { + companyCode, + count: result.rowCount, + }); - return res.json({ - success: true, - data: result.rows, - }); - } catch (error: any) { - logger.error("[pop/production] defect-types ์˜ค๋ฅ˜:", error); - return res.status(500).json({ - success: false, - message: error.message || "๋ถˆ๋Ÿ‰ ์œ ํ˜• ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - }); - } + return res.json({ + success: true, + data: result.rows, + }); + } catch (error: any) { + logger.error("[pop/production] defect-types ์˜ค๋ฅ˜:", error); + return res.status(500).json({ + success: false, + message: error.message || "๋ถˆ๋Ÿ‰ ์œ ํ˜• ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } }; /** @@ -780,149 +808,157 @@ export const getDefectTypes = async ( * ์ด๋ฒˆ ์ฐจ์ˆ˜ ์ƒ์‚ฐ์ˆ˜๋Ÿ‰์„ ๊ธฐ์กด ๋ˆ„์ ์น˜์— ๋”ํ•œ๋‹ค. * result_status๋Š” 'draft' ์œ ์ง€ (ํ™•์ • ์ „๊นŒ์ง€ ๊ณ„์† ์ถ”๊ฐ€ ๋“ฑ๋ก ๊ฐ€๋Šฅ) */ -export const saveResult = async ( - req: AuthenticatedRequest, - res: Response -) => { - const pool = getPool(); +export const saveResult = async (req: AuthenticatedRequest, res: Response) => { + const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; - const { - work_order_process_id, - production_qty, - good_qty, - defect_qty, - defect_detail, - result_note, - } = req.body; + const { + work_order_process_id, + production_qty, + good_qty, + defect_qty, + defect_detail, + result_note, + } = req.body; - if (!work_order_process_id) { - return res.status(400).json({ - success: false, - message: "work_order_process_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", - }); - } + if (!work_order_process_id) { + return res.status(400).json({ + success: false, + message: "work_order_process_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", + }); + } - if (!production_qty || parseInt(production_qty, 10) <= 0) { - return res.status(400).json({ - success: false, - message: "์ƒ์‚ฐ์ˆ˜๋Ÿ‰์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", - }); - } + if (!production_qty || parseInt(production_qty, 10) <= 0) { + return res.status(400).json({ + success: false, + message: "์ƒ์‚ฐ์ˆ˜๋Ÿ‰์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", + }); + } - const statusCheck = await pool.query( - `SELECT wop.status, wop.result_status, wop.total_production_qty, wop.good_qty, + const statusCheck = await pool.query( + `SELECT wop.status, wop.result_status, wop.total_production_qty, wop.good_qty, wop.defect_qty, wop.concession_qty, wop.defect_detail, wop.input_qty, wop.parent_process_id, wop.wo_id, wop.seq_no FROM work_order_process wop WHERE wop.id = $1 AND wop.company_code = $2`, - [work_order_process_id, companyCode] - ); + [work_order_process_id, companyCode], + ); - if (statusCheck.rowCount === 0) { - return res.status(404).json({ - success: false, - message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - }); - } + if (statusCheck.rowCount === 0) { + return res.status(404).json({ + success: false, + message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }); + } - const prev = statusCheck.rows[0]; + const prev = statusCheck.rows[0]; - // ๋งˆ์Šคํ„ฐ ํ–‰์— ์ง์ ‘ ์‹ค์  ๋“ฑ๋ก ๋ฐฉ์ง€ (๋ถ„ํ•  ํ–‰์ด ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ) - if (!prev.parent_process_id) { - const splitCheck = await pool.query( - `SELECT COUNT(*) as cnt FROM work_order_process + // ๋งˆ์Šคํ„ฐ ํ–‰์— ์ง์ ‘ ์‹ค์  ๋“ฑ๋ก ๋ฐฉ์ง€ (๋ถ„ํ•  ํ–‰์ด ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ) + if (!prev.parent_process_id) { + const splitCheck = await pool.query( + `SELECT COUNT(*) as cnt FROM work_order_process WHERE parent_process_id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); - if (parseInt(splitCheck.rows[0].cnt, 10) > 0) { - return res.status(400).json({ - success: false, - message: "์›๋ณธ ๊ณต์ •์—๋Š” ์ง์ ‘ ์‹ค์ ์„ ๋“ฑ๋กํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ถ„ํ• ๋œ ์ ‘์ˆ˜ ์นด๋“œ์—์„œ ๋“ฑ๋กํ•ด์ฃผ์„ธ์š”.", - }); - } - } + [work_order_process_id, companyCode], + ); + if (parseInt(splitCheck.rows[0].cnt, 10) > 0) { + return res.status(400).json({ + success: false, + message: + "์›๋ณธ ๊ณต์ •์—๋Š” ์ง์ ‘ ์‹ค์ ์„ ๋“ฑ๋กํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ถ„ํ• ๋œ ์ ‘์ˆ˜ ์นด๋“œ์—์„œ ๋“ฑ๋กํ•ด์ฃผ์„ธ์š”.", + }); + } + } - if (prev.result_status === "confirmed") { - return res.status(403).json({ - success: false, - message: "์ด๋ฏธ ํ™•์ •๋œ ์‹ค์ ์ž…๋‹ˆ๋‹ค. ์ถ”๊ฐ€ ๋“ฑ๋ก์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.", - }); - } + if (prev.result_status === "confirmed") { + return res.status(403).json({ + success: false, + message: "์ด๋ฏธ ํ™•์ •๋œ ์‹ค์ ์ž…๋‹ˆ๋‹ค. ์ถ”๊ฐ€ ๋“ฑ๋ก์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.", + }); + } - // ์ดˆ๊ณผ ์ƒ์‚ฐ ๊ฒฝ๊ณ  (์ฐจ๋‹จํ•˜์ง€ ์•Š์Œ - ํ˜„์žฅ ์œ ์—ฐ์„ฑ) - const prevTotal = parseInt(prev.total_production_qty, 10) || 0; - const acceptedQty = parseInt(prev.input_qty, 10) || 0; - const requestedQty = parseInt(production_qty, 10) || 0; - if (acceptedQty > 0 && (prevTotal + requestedQty) > acceptedQty) { - logger.warn("[pop/production] ์ดˆ๊ณผ ์ƒ์‚ฐ ๊ฐ์ง€", { - work_order_process_id, - prevTotal, requestedQty, acceptedQty, - overAmount: (prevTotal + requestedQty) - acceptedQty, - }); - } + // ์ดˆ๊ณผ ์ƒ์‚ฐ ๊ฒฝ๊ณ  (์ฐจ๋‹จํ•˜์ง€ ์•Š์Œ - ํ˜„์žฅ ์œ ์—ฐ์„ฑ) + const prevTotal = parseInt(prev.total_production_qty, 10) || 0; + const acceptedQty = parseInt(prev.input_qty, 10) || 0; + const requestedQty = parseInt(production_qty, 10) || 0; + if (acceptedQty > 0 && prevTotal + requestedQty > acceptedQty) { + logger.warn("[pop/production] ์ดˆ๊ณผ ์ƒ์‚ฐ ๊ฐ์ง€", { + work_order_process_id, + prevTotal, + requestedQty, + acceptedQty, + overAmount: prevTotal + requestedQty - acceptedQty, + }); + } - // ์„œ๋ฒ„ ์ธก ์–‘ํ’ˆ/๋ถˆ๋Ÿ‰/ํŠน์ฑ„ ๊ณ„์‚ฐ (ํด๋ผ์ด์–ธํŠธ good_qty๋Š” ์ฐธ๊ณ ๋งŒ) - const addProduction = parseInt(production_qty, 10) || 0; - let addDefect = 0; - let addConcession = 0; + // ์„œ๋ฒ„ ์ธก ์–‘ํ’ˆ/๋ถˆ๋Ÿ‰/ํŠน์ฑ„ ๊ณ„์‚ฐ (ํด๋ผ์ด์–ธํŠธ good_qty๋Š” ์ฐธ๊ณ ๋งŒ) + const addProduction = parseInt(production_qty, 10) || 0; + let addDefect = 0; + let addConcession = 0; - let defectDetailStr: string | null = null; - if (defect_detail && Array.isArray(defect_detail)) { - const validated = defect_detail.map((item: DefectDetailItem) => ({ - defect_code: item.defect_code || "", - defect_name: item.defect_name || "", - qty: item.qty || "0", - disposition: item.disposition || "scrap", - })); - defectDetailStr = JSON.stringify(validated); + let defectDetailStr: string | null = null; + if (defect_detail && Array.isArray(defect_detail)) { + const validated = defect_detail.map((item: DefectDetailItem) => ({ + defect_code: item.defect_code || "", + defect_name: item.defect_name || "", + qty: item.qty || "0", + disposition: item.disposition || "scrap", + })); + defectDetailStr = JSON.stringify(validated); - for (const item of validated) { - const itemQty = parseInt(item.qty, 10) || 0; - addDefect += itemQty; - if (item.disposition === "accept") { - addConcession += itemQty; - } - } - } else { - addDefect = parseInt(defect_qty, 10) || 0; - } - const addGood = addProduction - addDefect; + for (const item of validated) { + const itemQty = parseInt(item.qty, 10) || 0; + addDefect += itemQty; + if (item.disposition === "accept") { + addConcession += itemQty; + } + } + } else { + addDefect = parseInt(defect_qty, 10) || 0; + } + const addGood = addProduction - addDefect; - const newTotal = (parseInt(prev.total_production_qty, 10) || 0) + addProduction; - const newGood = (parseInt(prev.good_qty, 10) || 0) + addGood; - const newDefect = (parseInt(prev.defect_qty, 10) || 0) + addDefect; - const newConcession = (parseInt(prev.concession_qty, 10) || 0) + addConcession; + const newTotal = + (parseInt(prev.total_production_qty, 10) || 0) + addProduction; + const newGood = (parseInt(prev.good_qty, 10) || 0) + addGood; + const newDefect = (parseInt(prev.defect_qty, 10) || 0) + addDefect; + const newConcession = + (parseInt(prev.concession_qty, 10) || 0) + addConcession; - // ๊ธฐ์กด defect_detail์— ์ด๋ฒˆ ์ฐจ์ˆ˜ ์ƒ์„ธ๋ฅผ ๋ณ‘ํ•ฉ - let mergedDefectDetail: string | null = null; - if (defectDetailStr) { - let existingEntries: DefectDetailItem[] = []; - try { - existingEntries = prev.defect_detail ? JSON.parse(prev.defect_detail) : []; - } catch { /* ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ๋นˆ ๋ฐฐ์—ด */ } - const newEntries: DefectDetailItem[] = JSON.parse(defectDetailStr); - const merged = [...existingEntries]; - for (const ne of newEntries) { - const existing = merged.find( - (e) => e.defect_code === ne.defect_code && e.disposition === ne.disposition - ); - if (existing) { - existing.qty = String( - (parseInt(existing.qty, 10) || 0) + (parseInt(ne.qty, 10) || 0) - ); - } else { - merged.push(ne); - } - } - mergedDefectDetail = JSON.stringify(merged); - } + // ๊ธฐ์กด defect_detail์— ์ด๋ฒˆ ์ฐจ์ˆ˜ ์ƒ์„ธ๋ฅผ ๋ณ‘ํ•ฉ + let mergedDefectDetail: string | null = null; + if (defectDetailStr) { + let existingEntries: DefectDetailItem[] = []; + try { + existingEntries = prev.defect_detail + ? JSON.parse(prev.defect_detail) + : []; + } catch { + /* ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ๋นˆ ๋ฐฐ์—ด */ + } + const newEntries: DefectDetailItem[] = JSON.parse(defectDetailStr); + const merged = [...existingEntries]; + for (const ne of newEntries) { + const existing = merged.find( + (e) => + e.defect_code === ne.defect_code && + e.disposition === ne.disposition, + ); + if (existing) { + existing.qty = String( + (parseInt(existing.qty, 10) || 0) + (parseInt(ne.qty, 10) || 0), + ); + } else { + merged.push(ne); + } + } + mergedDefectDetail = JSON.stringify(merged); + } - const result = await pool.query( - `UPDATE work_order_process + const result = await pool.query( + `UPDATE work_order_process SET total_production_qty = $3, good_qty = $4, defect_qty = $5, @@ -935,30 +971,30 @@ export const saveResult = async ( updated_date = NOW() WHERE id = $1 AND company_code = $2 RETURNING id, total_production_qty, good_qty, defect_qty, concession_qty, defect_detail, result_note, result_status, status`, - [ - work_order_process_id, - companyCode, - String(newTotal), - String(newGood), - String(newDefect), - mergedDefectDetail, - result_note || null, - userId, - String(newConcession), - ] - ); + [ + work_order_process_id, + companyCode, + String(newTotal), + String(newGood), + String(newDefect), + mergedDefectDetail, + result_note || null, + userId, + String(newConcession), + ], + ); - if (result.rowCount === 0) { - return res.status(404).json({ - success: false, - message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.", - }); - } + if (result.rowCount === 0) { + return res.status(404).json({ + success: false, + message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.", + }); + } - // === BUG-2 FIX: SPLIT ์‹ค์  ์ €์žฅ ํ›„ master ํ–‰์— ํ•ฉ์‚ฐ === - if (prev.parent_process_id) { - await pool.query( - `UPDATE work_order_process + // === BUG-2 FIX: SPLIT ์‹ค์  ์ €์žฅ ํ›„ master ํ–‰์— ํ•ฉ์‚ฐ === + if (prev.parent_process_id) { + await pool.query( + `UPDATE work_order_process SET good_qty = sub.sum_good, defect_qty = sub.sum_defect, total_production_qty = sub.sum_total, @@ -974,17 +1010,17 @@ export const saveResult = async ( WHERE parent_process_id = $1 AND company_code = $2 ) sub WHERE id = $1 AND company_code = $2`, - [prev.parent_process_id, companyCode] - ); - logger.info("[pop/production] master ํ•ฉ์‚ฐ ์—…๋ฐ์ดํŠธ", { - masterId: prev.parent_process_id, - splitId: work_order_process_id, - }); - } + [prev.parent_process_id, companyCode], + ); + logger.info("[pop/production] master ํ•ฉ์‚ฐ ์—…๋ฐ์ดํŠธ", { + masterId: prev.parent_process_id, + splitId: work_order_process_id, + }); + } - // ํ˜„์žฌ ๋ถ„ํ•  ํ–‰์˜ ๊ณต์ • ์ •๋ณด ์กฐํšŒ - const currentSeq = await pool.query( - `SELECT wop.seq_no, wop.wo_id, wop.input_qty as current_input_qty, + // ํ˜„์žฌ ๋ถ„ํ•  ํ–‰์˜ ๊ณต์ • ์ •๋ณด ์กฐํšŒ + const currentSeq = await pool.query( + `SELECT wop.seq_no, wop.wo_id, wop.input_qty as current_input_qty, wop.parent_process_id, wop.process_code, wop.process_name, wop.is_required, wop.is_fixed_order, wop.standard_time, wop.equipment_code, wop.routing_detail_id, @@ -992,52 +1028,57 @@ export const saveResult = async ( FROM work_order_process wop JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code WHERE wop.id = $1 AND wop.company_code = $2`, - [work_order_process_id, companyCode] - ); + [work_order_process_id, companyCode], + ); - // ์žฌ์ž‘์—… ์นด๋“œ ์ž๋™ ์ƒ์„ฑ (disposition = 'rework' ํ•ญ๋ชฉ์ด ์žˆ์„ ๋•Œ) - if (currentSeq.rowCount > 0 && defect_detail && Array.isArray(defect_detail)) { - let totalReworkQty = 0; - let targetProcessCode: string | null = null; - for (const item of defect_detail) { - if (item.disposition === "rework") { - totalReworkQty += parseInt(item.qty, 10) || 0; - if (item.target_process_code) targetProcessCode = item.target_process_code; - } - } - if (totalReworkQty > 0) { - const proc = currentSeq.rows[0]; - const masterId = proc.parent_process_id || work_order_process_id; + // ์žฌ์ž‘์—… ์นด๋“œ ์ž๋™ ์ƒ์„ฑ (disposition = 'rework' ํ•ญ๋ชฉ์ด ์žˆ์„ ๋•Œ) + if ( + currentSeq.rowCount > 0 && + defect_detail && + Array.isArray(defect_detail) + ) { + let totalReworkQty = 0; + let targetProcessCode: string | null = null; + for (const item of defect_detail) { + if (item.disposition === "rework") { + totalReworkQty += parseInt(item.qty, 10) || 0; + if (item.target_process_code) + targetProcessCode = item.target_process_code; + } + } + if (totalReworkQty > 0) { + const proc = currentSeq.rows[0]; + const masterId = proc.parent_process_id || work_order_process_id; - // ์žฌ์ž‘์—… ๋Œ€์ƒ ๊ณต์ • ๊ฒฐ์ • - let reworkSeqNo = proc.seq_no; - let reworkProcessCode = proc.process_code; - let reworkProcessName = proc.process_name; - let reworkRoutingDetailId = proc.routing_detail_id; - let reworkMasterId = masterId; + // ์žฌ์ž‘์—… ๋Œ€์ƒ ๊ณต์ • ๊ฒฐ์ • + let reworkSeqNo = proc.seq_no; + let reworkProcessCode = proc.process_code; + let reworkProcessName = proc.process_name; + let reworkRoutingDetailId = proc.routing_detail_id; + let reworkMasterId = masterId; - // target_process_code๊ฐ€ ์ง€์ •๋˜๋ฉด ํ•ด๋‹น ๊ณต์ • ์ •๋ณด๋ฅผ ์กฐํšŒ - if (targetProcessCode) { - const targetProc = await pool.query( - `SELECT id, seq_no, process_code, process_name, routing_detail_id + // target_process_code๊ฐ€ ์ง€์ •๋˜๋ฉด ํ•ด๋‹น ๊ณต์ • ์ •๋ณด๋ฅผ ์กฐํšŒ + if (targetProcessCode) { + const targetProc = await pool.query( + `SELECT id, seq_no, process_code, process_name, routing_detail_id FROM work_order_process WHERE wo_id = $1 AND process_code = $2 AND company_code = $3 AND parent_process_id IS NULL LIMIT 1`, - [proc.wo_id, targetProcessCode, companyCode] - ); - if (targetProc.rowCount > 0) { - const tp = targetProc.rows[0]; - reworkSeqNo = tp.seq_no; - reworkProcessCode = tp.process_code; - reworkProcessName = tp.process_name; - reworkRoutingDetailId = tp.routing_detail_id; - reworkMasterId = tp.id; // ์ง€์ • ๊ณต์ •์˜ ๋งˆ์Šคํ„ฐ ID - } - } + [proc.wo_id, targetProcessCode, companyCode], + ); + if (targetProc.rowCount > 0) { + const tp = targetProc.rows[0]; + reworkSeqNo = tp.seq_no; + reworkProcessCode = tp.process_code; + reworkProcessName = tp.process_name; + reworkRoutingDetailId = tp.routing_detail_id; + reworkMasterId = tp.id; // ์ง€์ • ๊ณต์ •์˜ ๋งˆ์Šคํ„ฐ ID + } + } - const reworkInsert = await pool.query( - `INSERT INTO work_order_process ( + const reworkInsert = await pool.query( + `INSERT INTO work_order_process ( id, wo_id, seq_no, process_code, process_name, is_required, is_fixed_order, standard_time, equipment_code, routing_detail_id, status, input_qty, good_qty, defect_qty, concession_qty, total_production_qty, @@ -1049,182 +1090,213 @@ export const saveResult = async ( 'draft', 'Y', $11, $12, $13, $14 ) RETURNING id`, - [ - proc.wo_id, reworkSeqNo, reworkProcessCode, reworkProcessName, - proc.is_required, proc.is_fixed_order, proc.standard_time, - proc.equipment_code, reworkRoutingDetailId, - String(totalReworkQty), work_order_process_id, - reworkMasterId, companyCode, userId, - ] - ); - // ์žฌ์ž‘์—… ์นด๋“œ์— ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋ณต์‚ฌ - const reworkId = reworkInsert.rows[0]?.id; - if (reworkId) { - const reworkChecklistCount = await copyChecklistToSplit( - pool, reworkMasterId, reworkId, reworkRoutingDetailId, companyCode, userId - ); - logger.info("[pop/production] ์žฌ์ž‘์—… ์นด๋“œ ์ž๋™ ์ƒ์„ฑ", { - reworkId, - sourceId: work_order_process_id, - reworkQty: totalReworkQty, - targetProcess: targetProcessCode || "(๊ฐ™์€ ๊ณต์ •)", - reworkSeqNo, - checklistCount: reworkChecklistCount, - }); - } - } - } + [ + proc.wo_id, + reworkSeqNo, + reworkProcessCode, + reworkProcessName, + proc.is_required, + proc.is_fixed_order, + proc.standard_time, + proc.equipment_code, + reworkRoutingDetailId, + String(totalReworkQty), + work_order_process_id, + reworkMasterId, + companyCode, + userId, + ], + ); + // ์žฌ์ž‘์—… ์นด๋“œ์— ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋ณต์‚ฌ + const reworkId = reworkInsert.rows[0]?.id; + if (reworkId) { + const reworkChecklistCount = await copyChecklistToSplit( + pool, + reworkMasterId, + reworkId, + reworkRoutingDetailId, + companyCode, + userId, + ); + logger.info("[pop/production] ์žฌ์ž‘์—… ์นด๋“œ ์ž๋™ ์ƒ์„ฑ", { + reworkId, + sourceId: work_order_process_id, + reworkQty: totalReworkQty, + targetProcess: targetProcessCode || "(๊ฐ™์€ ๊ณต์ •)", + reworkSeqNo, + checklistCount: reworkChecklistCount, + }); + } + } + } - // ๊ฐœ๋ณ„ ๋ถ„ํ•  ํ–‰ ์ž๋™์™„๋ฃŒ (๋‹ค์Œ ๊ณต์ • ํ™œ์„ฑํ™”๋ณด๋‹ค ๋จผ์ € ์‹คํ–‰) - if (currentSeq.rowCount > 0) { - const { seq_no: csSeq, wo_id: csWoId, current_input_qty: csInputQty, instruction_qty: csInstrQty, parent_process_id: csParentId } = currentSeq.rows[0]; - const csMyInput = parseInt(csInputQty, 10) || 0; + // ๊ฐœ๋ณ„ ๋ถ„ํ•  ํ–‰ ์ž๋™์™„๋ฃŒ (๋‹ค์Œ ๊ณต์ • ํ™œ์„ฑํ™”๋ณด๋‹ค ๋จผ์ € ์‹คํ–‰) + if (currentSeq.rowCount > 0) { + const { + seq_no: csSeq, + wo_id: csWoId, + current_input_qty: csInputQty, + instruction_qty: csInstrQty, + parent_process_id: csParentId, + } = currentSeq.rows[0]; + const csMyInput = parseInt(csInputQty, 10) || 0; - if (newTotal >= csMyInput && csMyInput > 0) { - await pool.query( - `UPDATE work_order_process SET status = 'completed', result_status = 'confirmed', + if (newTotal >= csMyInput && csMyInput > 0) { + await pool.query( + `UPDATE work_order_process SET status = 'completed', result_status = 'confirmed', completed_at = NOW()::text, completed_by = $3, updated_date = NOW() WHERE id = $1 AND company_code = $2 AND status != 'completed'`, - [work_order_process_id, companyCode, userId] - ); + [work_order_process_id, companyCode, userId], + ); - // ๊ฐ™์€ seq์˜ ๋ชจ๋“  ๋ถ„ํ•  ํ–‰ ์™„๋ฃŒ ์ฒดํฌ โ†’ ๋งˆ์Šคํ„ฐ๋„ completed - const csSeqNum = parseInt(csSeq, 10); - let csPrevGood = parseInt(csInstrQty, 10) || 0; - if (csSeqNum > 1) { - const prev = await pool.query( - `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as tg + // ๊ฐ™์€ seq์˜ ๋ชจ๋“  ๋ถ„ํ•  ํ–‰ ์™„๋ฃŒ ์ฒดํฌ โ†’ ๋งˆ์Šคํ„ฐ๋„ completed + const csSeqNum = parseInt(csSeq, 10); + let csPrevGood = parseInt(csInstrQty, 10) || 0; + if (csSeqNum > 1) { + const prev = await pool.query( + `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as tg FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL`, - [csWoId, String(csSeqNum - 1), companyCode] - ); - if (prev.rowCount > 0) csPrevGood = parseInt(prev.rows[0].tg, 10) || 0; - } - const sibCheck = await pool.query( - `SELECT COALESCE(SUM(input_qty::int), 0) as ti, COUNT(*) FILTER (WHERE status != 'completed') as ic + [csWoId, String(csSeqNum - 1), companyCode], + ); + if (prev.rowCount > 0) + csPrevGood = parseInt(prev.rows[0].tg, 10) || 0; + } + const sibCheck = await pool.query( + `SELECT COALESCE(SUM(input_qty::int), 0) as ti, COUNT(*) FILTER (WHERE status != 'completed') as ic FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL`, - [csWoId, csSeq, companyCode] - ); - const csTotalInput = parseInt(sibCheck.rows[0].ti, 10) || 0; - const csIncomplete = parseInt(sibCheck.rows[0].ic, 10) || 0; - if (csIncomplete === 0 && csPrevGood - csTotalInput <= 0 && csParentId) { - await pool.query( - `UPDATE work_order_process SET status = 'completed', result_status = 'confirmed', + [csWoId, csSeq, companyCode], + ); + const csTotalInput = parseInt(sibCheck.rows[0].ti, 10) || 0; + const csIncomplete = parseInt(sibCheck.rows[0].ic, 10) || 0; + if ( + csIncomplete === 0 && + csPrevGood - csTotalInput <= 0 && + csParentId + ) { + await pool.query( + `UPDATE work_order_process SET status = 'completed', result_status = 'confirmed', completed_at = NOW()::text, completed_by = $3, updated_date = NOW() WHERE id = $1 AND company_code = $2 AND status != 'completed'`, - [csParentId, companyCode, userId] - ); - } - } + [csParentId, companyCode, userId], + ); + } + } - await checkAndCompleteWorkInstruction(pool, csWoId, companyCode, userId); - } + await checkAndCompleteWorkInstruction(pool, csWoId, companyCode, userId); + } - // ๋‹ค์Œ ๊ณต์ • ํ™œ์„ฑํ™” (๋‹ค์ค‘๊ณต์ • ๋Œ€์‘) - // is_fixed_order='Y' ๊ทธ๋ฃน์ด๋ฉด ๊ทธ๋ฃน ์ „์ฒด ์™„๋ฃŒ ํ›„ ๋‹ค์Œ ํ™œ์„ฑํ™” - if (addGood > 0 && currentSeq.rowCount > 0) { - const { seq_no, wo_id, is_fixed_order } = currentSeq.rows[0]; - const seqNum = parseInt(seq_no, 10); + // ๋‹ค์Œ ๊ณต์ • ํ™œ์„ฑํ™” (๋‹ค์ค‘๊ณต์ • ๋Œ€์‘) + // is_fixed_order='Y' ๊ทธ๋ฃน์ด๋ฉด ๊ทธ๋ฃน ์ „์ฒด ์™„๋ฃŒ ํ›„ ๋‹ค์Œ ํ™œ์„ฑํ™” + if (addGood > 0 && currentSeq.rowCount > 0) { + const { seq_no, wo_id, is_fixed_order } = currentSeq.rows[0]; + const seqNum = parseInt(seq_no, 10); - let shouldActivateNext = true; + let shouldActivateNext = true; - if (is_fixed_order === "Y") { - // ๊ฐ™์€ seq_no์—์„œ is_fixed_order='Y'์ธ ๋ณ‘๋ ฌ ๊ณต์ •์ด ๋ชจ๋‘ ์™„๋ฃŒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ - // (๋ณ‘๋ ฌ ๊ทธ๋ฃน = ๊ฐ™์€ seq_no๋ฅผ ๊ณต์œ ํ•˜๋Š” ๊ณต์ •๋“ค) - const groupCheck = await pool.query( - `SELECT id, seq_no, status, + if (is_fixed_order === "Y") { + // ๊ฐ™์€ seq_no์—์„œ is_fixed_order='Y'์ธ ๋ณ‘๋ ฌ ๊ณต์ •์ด ๋ชจ๋‘ ์™„๋ฃŒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + // (๋ณ‘๋ ฌ ๊ทธ๋ฃน = ๊ฐ™์€ seq_no๋ฅผ ๊ณต์œ ํ•˜๋Š” ๊ณต์ •๋“ค) + const groupCheck = await pool.query( + `SELECT id, seq_no, status, COALESCE(good_qty::int, 0) + COALESCE(concession_qty::int, 0) as total_good FROM work_order_process WHERE wo_id = $1 AND company_code = $2 AND parent_process_id IS NULL AND seq_no = $3 ORDER BY CAST(seq_no AS int)`, - [wo_id, companyCode, seq_no] - ); + [wo_id, companyCode, seq_no], + ); - // ๊ฐ™์€ seq์˜ ๋ฏธ์™„๋ฃŒ ๊ณต์ • ํ™•์ธ (๋ณ‘๋ ฌ ๊ทธ๋ฃน ๋‚ด) - const incomplete = groupCheck.rows.filter((r: Record) => - String(r.status) !== "completed" && parseInt(String(r.total_good), 10) <= 0 - ); - shouldActivateNext = incomplete.length === 0; + // ๊ฐ™์€ seq์˜ ๋ฏธ์™„๋ฃŒ ๊ณต์ • ํ™•์ธ (๋ณ‘๋ ฌ ๊ทธ๋ฃน ๋‚ด) + const incomplete = groupCheck.rows.filter( + (r: Record) => + String(r.status) !== "completed" && + parseInt(String(r.total_good), 10) <= 0, + ); + shouldActivateNext = incomplete.length === 0; - if (!shouldActivateNext) { - logger.info("[pop/production] ๋ณ‘๋ ฌ ๊ทธ๋ฃน ๋ฏธ์™„๋ฃŒ โ€” ๋‹ค์Œ ๊ณต์ • ๋Œ€๊ธฐ", { - groupSize: groupCheck.rows.length, - incomplete: incomplete.length, - }); - } - } + if (!shouldActivateNext) { + logger.info("[pop/production] ๋ณ‘๋ ฌ ๊ทธ๋ฃน ๋ฏธ์™„๋ฃŒ โ€” ๋‹ค์Œ ๊ณต์ • ๋Œ€๊ธฐ", { + groupSize: groupCheck.rows.length, + incomplete: incomplete.length, + }); + } + } - if (shouldActivateNext) { - // ๋‹ค์Œ seq ํ™œ์„ฑํ™” (seq_no ๋น„์ˆœ์ฐจ ๋Œ€์‘: seqNum+1์ด ์•„๋‹ˆ๋ผ "ํ˜„์žฌ๋ณด๋‹ค ํฐ ๊ฐ€์žฅ ์ž‘์€ seq_no") - const nextSeqQuery = await pool.query( - `SELECT MIN(CAST(seq_no AS int)) as next_seq + if (shouldActivateNext) { + // ๋‹ค์Œ seq ํ™œ์„ฑํ™” (seq_no ๋น„์ˆœ์ฐจ ๋Œ€์‘: seqNum+1์ด ์•„๋‹ˆ๋ผ "ํ˜„์žฌ๋ณด๋‹ค ํฐ ๊ฐ€์žฅ ์ž‘์€ seq_no") + const nextSeqQuery = await pool.query( + `SELECT MIN(CAST(seq_no AS int)) as next_seq FROM work_order_process WHERE wo_id = $1 AND company_code = $2 AND parent_process_id IS NULL AND CAST(seq_no AS int) > $3`, - [wo_id, companyCode, seqNum] - ); - const actualNextSeq = nextSeqQuery.rows[0]?.next_seq; - if (actualNextSeq != null) { - const nextUpdate = await pool.query( - `UPDATE work_order_process + [wo_id, companyCode, seqNum], + ); + const actualNextSeq = nextSeqQuery.rows[0]?.next_seq; + if (actualNextSeq != null) { + const nextUpdate = await pool.query( + `UPDATE work_order_process SET status = 'acceptable', updated_date = NOW() WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NULL RETURNING id, process_name, status`, - [wo_id, String(actualNextSeq), companyCode] - ); - if (nextUpdate.rowCount > 0) { - logger.info("[pop/production] ๋‹ค์Œ ๊ณต์ • ์ƒํƒœ ์ „ํ™˜", { - nextProcess: nextUpdate.rows[0], - }); - } - } - } - } + [wo_id, String(actualNextSeq), companyCode], + ); + if (nextUpdate.rowCount > 0) { + logger.info("[pop/production] ๋‹ค์Œ ๊ณต์ • ์ƒํƒœ ์ „ํ™˜", { + nextProcess: nextUpdate.rows[0], + }); + } + } + } + } - // (๋ถ„ํ• ํ–‰ ์™„๋ฃŒ + ๋งˆ์Šคํ„ฐ ์บ์Šค์ผ€์ด๋“œ๋Š” ์œ„์—์„œ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ) + // (๋ถ„ํ• ํ–‰ ์™„๋ฃŒ + ๋งˆ์Šคํ„ฐ ์บ์Šค์ผ€์ด๋“œ๋Š” ์œ„์—์„œ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ) - logger.info("[pop/production] save-result ์™„๋ฃŒ (๋ˆ„์ )", { - companyCode, - work_order_process_id, - added: { production_qty: addProduction, good_qty: addGood, defect_qty: addDefect }, - accumulated: { total: newTotal, good: newGood, defect: newDefect }, - }); + logger.info("[pop/production] save-result ์™„๋ฃŒ (๋ˆ„์ )", { + companyCode, + work_order_process_id, + added: { + production_qty: addProduction, + good_qty: addGood, + defect_qty: addDefect, + }, + accumulated: { total: newTotal, good: newGood, defect: newDefect }, + }); - // ์ž๋™ ์™„๋ฃŒ ํ›„ ์ตœ์‹  ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ (status๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ์ˆ˜ ์žˆ์Œ) - const latestData = await pool.query( - `SELECT id, total_production_qty, good_qty, defect_qty, concession_qty, defect_detail, result_note, result_status, status, input_qty + // ์ž๋™ ์™„๋ฃŒ ํ›„ ์ตœ์‹  ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ (status๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ์ˆ˜ ์žˆ์Œ) + const latestData = await pool.query( + `SELECT id, total_production_qty, good_qty, defect_qty, concession_qty, defect_detail, result_note, result_status, status, input_qty FROM work_order_process WHERE id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); + [work_order_process_id, companyCode], + ); - // ๋ฆฌ์›Œํฌ ์ •๋ณด๋„ ์‘๋‹ต์— ํฌํ•จ (ํ”„๋ก ํŠธ์—์„œ ๋‹ค์Œ ๊ณต์ • ์ ‘์ˆ˜ ์‹œ ์ „๋‹ฌ ๊ฐ€๋Šฅ) - const responseData = latestData.rows[0] || result.rows[0]; - if (responseData) { - const reworkInfo = await pool.query( - `SELECT is_rework, rework_source_id FROM work_order_process WHERE id = $1`, - [work_order_process_id] - ); - if (reworkInfo.rows[0]?.rework_source_id) { - responseData.rework_source_id = reworkInfo.rows[0].rework_source_id; - responseData.is_rework = reworkInfo.rows[0].is_rework; - } - } + // ๋ฆฌ์›Œํฌ ์ •๋ณด๋„ ์‘๋‹ต์— ํฌํ•จ (ํ”„๋ก ํŠธ์—์„œ ๋‹ค์Œ ๊ณต์ • ์ ‘์ˆ˜ ์‹œ ์ „๋‹ฌ ๊ฐ€๋Šฅ) + const responseData = latestData.rows[0] || result.rows[0]; + if (responseData) { + const reworkInfo = await pool.query( + `SELECT is_rework, rework_source_id FROM work_order_process WHERE id = $1`, + [work_order_process_id], + ); + if (reworkInfo.rows[0]?.rework_source_id) { + responseData.rework_source_id = reworkInfo.rows[0].rework_source_id; + responseData.is_rework = reworkInfo.rows[0].is_rework; + } + } - return res.json({ - success: true, - data: responseData, - }); - } catch (error: any) { - logger.error("[pop/production] save-result ์˜ค๋ฅ˜:", error); - return res.status(500).json({ - success: false, - message: error.message || "์‹ค์  ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - }); - } + return res.json({ + success: true, + data: responseData, + }); + } catch (error: any) { + logger.error("[pop/production] save-result ์˜ค๋ฅ˜:", error); + return res.status(500).json({ + success: false, + message: error.message || "์‹ค์  ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } }; /** @@ -1232,44 +1304,44 @@ export const saveResult = async ( * ๋งˆ์ง€๋ง‰ ๊ณต์ •์˜ ๋ชจ๋“  ํ–‰์ด completed์ด๋ฉด ์ž‘์—…์ง€์‹œ๋„ ์™„๋ฃŒ ์ฒ˜๋ฆฌ */ const checkAndCompleteWorkInstruction = async ( - pool: any, - woId: string, - companyCode: string, - userId: string + pool: any, + woId: string, + companyCode: string, + userId: string, ) => { - const maxSeqResult = await pool.query( - `SELECT MAX(seq_no::int) as max_seq + const maxSeqResult = await pool.query( + `SELECT MAX(seq_no::int) as max_seq FROM work_order_process WHERE wo_id = $1 AND company_code = $2`, - [woId, companyCode] - ); + [woId, companyCode], + ); - if (maxSeqResult.rowCount === 0 || !maxSeqResult.rows[0].max_seq) return; + if (maxSeqResult.rowCount === 0 || !maxSeqResult.rows[0].max_seq) return; - const maxSeq = String(maxSeqResult.rows[0].max_seq); + const maxSeq = String(maxSeqResult.rows[0].max_seq); - const incompleteCheck = await pool.query( - `SELECT COUNT(*) as cnt + const incompleteCheck = await pool.query( + `SELECT COUNT(*) as cnt FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND status != 'completed'`, - [woId, maxSeq, companyCode] - ); + [woId, maxSeq, companyCode], + ); - if (parseInt(incompleteCheck.rows[0].cnt, 10) > 0) return; + if (parseInt(incompleteCheck.rows[0].cnt, 10) > 0) return; - const totalGoodResult = await pool.query( - `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good + const totalGoodResult = await pool.query( + `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL`, - [woId, maxSeq, companyCode] - ); + [woId, maxSeq, companyCode], + ); - const completedQty = totalGoodResult.rows[0].total_good; + const completedQty = totalGoodResult.rows[0].total_good; - const updateResult = await pool.query( - `UPDATE work_instruction + const updateResult = await pool.query( + `UPDATE work_instruction SET status = 'completed', progress_status = 'completed', completed_qty = $3, @@ -1278,58 +1350,85 @@ const checkAndCompleteWorkInstruction = async ( WHERE id = $1 AND company_code = $2 AND status != 'completed' RETURNING id, item_id`, - [woId, companyCode, String(completedQty), userId] - ); + [woId, companyCode, String(completedQty), userId], + ); - logger.info("[pop/production] ์ž‘์—…์ง€์‹œ ์ „์ฒด ์™„๋ฃŒ", { - woId, completedQty, companyCode, - }); + logger.info("[pop/production] ์ž‘์—…์ง€์‹œ ์ „์ฒด ์™„๋ฃŒ", { + woId, + completedQty, + companyCode, + }); - // ์ƒ์‚ฐ์™„๋ฃŒโ†’์žฌ๊ณ  ์ž…๊ณ : ๋งˆ์ง€๋ง‰ ๊ณต์ •์˜ target_warehouse_id๊ฐ€ ์„ค์ •๋œ ๊ฒฝ์šฐ inventory_stock UPSERT - if (updateResult.rowCount > 0 && completedQty > 0) { - try { - const itemId = updateResult.rows[0].item_id; + // ์ƒ์‚ฐ์™„๋ฃŒโ†’์žฌ๊ณ  ์ž…๊ณ : ๋งˆ์ง€๋ง‰ ๊ณต์ •์˜ target_warehouse_id๊ฐ€ ์„ค์ •๋œ ๊ฒฝ์šฐ inventory_stock UPSERT + if (updateResult.rowCount > 0 && completedQty > 0) { + try { + const itemId = updateResult.rows[0].item_id; - // item_info์—์„œ item_number ์กฐํšŒ - const itemResult = await pool.query( - `SELECT item_number FROM item_info WHERE id = $1 AND company_code = $2`, - [itemId, companyCode] - ); - if (itemResult.rowCount === 0) { - logger.warn("[pop/production] ์žฌ๊ณ ์ž…๊ณ  ๊ฑด๋„ˆ๋œ€: item_info ์—†์Œ", { itemId, companyCode }); - return; - } - const itemCode = itemResult.rows[0].item_number; + // item_info์—์„œ item_number ์กฐํšŒ + const itemResult = await pool.query( + `SELECT item_number FROM item_info WHERE id = $1 AND company_code = $2`, + [itemId, companyCode], + ); + if (itemResult.rowCount === 0) { + logger.warn("[pop/production] ์žฌ๊ณ ์ž…๊ณ  ๊ฑด๋„ˆ๋œ€: item_info ์—†์Œ", { + itemId, + companyCode, + }); + return; + } + const itemCode = itemResult.rows[0].item_number; - // ๋งˆ์ง€๋ง‰ ๊ณต์ •์˜ ์ฐฝ๊ณ  ์„ค์ • ์กฐํšŒ (๋งˆ์Šคํ„ฐ ํ–‰์—์„œ) - const warehouseResult = await pool.query( - `SELECT target_warehouse_id, target_location_code + // ๋งˆ์ง€๋ง‰ ๊ณต์ •์˜ ์ฐฝ๊ณ  ์„ค์ • ์กฐํšŒ (๋งˆ์Šคํ„ฐ ํ–‰์—์„œ) + const warehouseResult = await pool.query( + `SELECT target_warehouse_id, target_location_code FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NULL LIMIT 1`, - [woId, maxSeq, companyCode] - ); + [woId, maxSeq, companyCode], + ); - if (warehouseResult.rowCount === 0 || !warehouseResult.rows[0].target_warehouse_id) { - logger.info("[pop/production] ์žฌ๊ณ ์ž…๊ณ  ๊ฑด๋„ˆ๋œ€: ๋ชฉํ‘œ์ฐฝ๊ณ  ๋ฏธ์„ค์ •", { woId }); - return; - } + if ( + warehouseResult.rowCount === 0 || + !warehouseResult.rows[0].target_warehouse_id + ) { + logger.info("[pop/production] ์žฌ๊ณ ์ž…๊ณ  ๊ฑด๋„ˆ๋œ€: ๋ชฉํ‘œ์ฐฝ๊ณ  ๋ฏธ์„ค์ •", { + woId, + }); + return; + } - const warehouseCode = warehouseResult.rows[0].target_warehouse_id; - const locationCode = warehouseResult.rows[0].target_location_code || warehouseCode; + const warehouseCode = warehouseResult.rows[0].target_warehouse_id; + const locationCode = + warehouseResult.rows[0].target_location_code || warehouseCode; - // inventory_stock UPSERT (PC receivingController์™€ ๋™์ผํ•œ SELECTโ†’INSERT/UPDATE ํŒจํ„ด) - await upsertInventoryStock(pool, companyCode, itemCode, warehouseCode, locationCode, completedQty, userId); + // inventory_stock UPSERT (PC receivingController์™€ ๋™์ผํ•œ SELECTโ†’INSERT/UPDATE ํŒจํ„ด) + await upsertInventoryStock( + pool, + companyCode, + itemCode, + warehouseCode, + locationCode, + completedQty, + userId, + ); - logger.info("[pop/production] ์ƒ์‚ฐ์™„๋ฃŒโ†’์žฌ๊ณ  ์ž…๊ณ  ์™„๋ฃŒ", { - woId, itemCode, warehouseCode, locationCode, qty: completedQty, companyCode, - }); - } catch (inventoryError: any) { - // ์žฌ๊ณ  ์ž…๊ณ  ์‹คํŒจํ•ด๋„ ๊ณต์ • ์™„๋ฃŒ๋Š” ์œ ์ง€ (์žฌ๊ณ ๋Š” ๋ณด์กฐ ๊ธฐ๋Šฅ) - logger.error("[pop/production] ์žฌ๊ณ ์ž…๊ณ  ์˜ค๋ฅ˜ (๊ณต์ • ์™„๋ฃŒ๋Š” ์œ ์ง€):", inventoryError); - } - } + logger.info("[pop/production] ์ƒ์‚ฐ์™„๋ฃŒโ†’์žฌ๊ณ  ์ž…๊ณ  ์™„๋ฃŒ", { + woId, + itemCode, + warehouseCode, + locationCode, + qty: completedQty, + companyCode, + }); + } catch (inventoryError: any) { + // ์žฌ๊ณ  ์ž…๊ณ  ์‹คํŒจํ•ด๋„ ๊ณต์ • ์™„๋ฃŒ๋Š” ์œ ์ง€ (์žฌ๊ณ ๋Š” ๋ณด์กฐ ๊ธฐ๋Šฅ) + logger.error( + "[pop/production] ์žฌ๊ณ ์ž…๊ณ  ์˜ค๋ฅ˜ (๊ณต์ • ์™„๋ฃŒ๋Š” ์œ ์ง€):", + inventoryError, + ); + } + } }; /** @@ -1337,50 +1436,52 @@ const checkAndCompleteWorkInstruction = async ( * ๋งˆ์ง€๋ง‰ ๋“ฑ๋ก ํ™•์ธ ์šฉ๋„๋กœ ์œ ์ง€. ์‹ค์ ์€ save-result์—์„œ ์ฐจ์ˆ˜๋ณ„๋กœ ์Œ“์ž„. */ export const confirmResult = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); + const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; - const { work_order_process_id } = req.body; + const { work_order_process_id } = req.body; - if (!work_order_process_id) { - return res.status(400).json({ - success: false, - message: "work_order_process_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", - }); - } + if (!work_order_process_id) { + return res.status(400).json({ + success: false, + message: "work_order_process_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", + }); + } - const statusCheck = await pool.query( - `SELECT status, result_status, total_production_qty FROM work_order_process + const statusCheck = await pool.query( + `SELECT status, result_status, total_production_qty FROM work_order_process WHERE id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); + [work_order_process_id, companyCode], + ); - if (statusCheck.rowCount === 0) { - return res.status(404).json({ - success: false, - message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - }); - } + if (statusCheck.rowCount === 0) { + return res.status(404).json({ + success: false, + message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }); + } - const currentProcess = statusCheck.rows[0]; + const currentProcess = statusCheck.rows[0]; - if (!currentProcess.total_production_qty || - parseInt(currentProcess.total_production_qty, 10) <= 0) { - return res.status(400).json({ - success: false, - message: "๋“ฑ๋ก๋œ ์‹ค์ ์ด ์—†์Šต๋‹ˆ๋‹ค. ์‹ค์ ์„ ๋จผ์ € ๋“ฑ๋กํ•ด์ฃผ์„ธ์š”.", - }); - } + if ( + !currentProcess.total_production_qty || + parseInt(currentProcess.total_production_qty, 10) <= 0 + ) { + return res.status(400).json({ + success: false, + message: "๋“ฑ๋ก๋œ ์‹ค์ ์ด ์—†์Šต๋‹ˆ๋‹ค. ์‹ค์ ์„ ๋จผ์ € ๋“ฑ๋กํ•ด์ฃผ์„ธ์š”.", + }); + } - // ์ˆ˜๋™ ํ™•์ •: ๋ฌด์กฐ๊ฑด completed ์ฒ˜๋ฆฌ (์ˆ˜๋™ ์™„๋ฃŒ ์šฉ๋„) - const result = await pool.query( - `UPDATE work_order_process + // ์ˆ˜๋™ ํ™•์ •: ๋ฌด์กฐ๊ฑด completed ์ฒ˜๋ฆฌ (์ˆ˜๋™ ์™„๋ฃŒ ์šฉ๋„) + const result = await pool.query( + `UPDATE work_order_process SET result_status = 'confirmed', status = 'completed', completed_at = NOW()::text, @@ -1389,49 +1490,50 @@ export const confirmResult = async ( updated_date = NOW() WHERE id = $1 AND company_code = $2 RETURNING id, status, result_status, total_production_qty, good_qty, defect_qty`, - [work_order_process_id, companyCode, userId] - ); + [work_order_process_id, companyCode, userId], + ); - if (result.rowCount === 0) { - return res.status(404).json({ - success: false, - message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - }); - } + if (result.rowCount === 0) { + return res.status(404).json({ + success: false, + message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }); + } - // ๊ณต์ • ์ •๋ณด ์กฐํšŒ (๋‹ค์Œ ๊ณต์ • ํ™œ์„ฑํ™” + ๋งˆ์Šคํ„ฐ ์บ์Šค์ผ€์ด๋“œ์šฉ) - const seqCheck = await pool.query( - `SELECT wop.seq_no, wop.wo_id, wop.parent_process_id, + // ๊ณต์ • ์ •๋ณด ์กฐํšŒ (๋‹ค์Œ ๊ณต์ • ํ™œ์„ฑํ™” + ๋งˆ์Šคํ„ฐ ์บ์Šค์ผ€์ด๋“œ์šฉ) + const seqCheck = await pool.query( + `SELECT wop.seq_no, wop.wo_id, wop.parent_process_id, wi.qty as instruction_qty FROM work_order_process wop JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code WHERE wop.id = $1 AND wop.company_code = $2`, - [work_order_process_id, companyCode] - ); + [work_order_process_id, companyCode], + ); - if (seqCheck.rowCount > 0) { - const { seq_no, wo_id, parent_process_id, instruction_qty } = seqCheck.rows[0]; - const seqNum = parseInt(seq_no, 10); - const instrQty = parseInt(instruction_qty, 10) || 0; + if (seqCheck.rowCount > 0) { + const { seq_no, wo_id, parent_process_id, instruction_qty } = + seqCheck.rows[0]; + const seqNum = parseInt(seq_no, 10); + const instrQty = parseInt(instruction_qty, 10) || 0; - // ๋‹ค์Œ ๊ณต์ • ํ™œ์„ฑํ™” (์–‘ํ’ˆ์ด ์žˆ์œผ๋ฉด) - const goodQty = parseInt(result.rows[0].good_qty, 10) || 0; - if (goodQty > 0) { - const nextSeq = String(seqNum + 1); - await pool.query( - `UPDATE work_order_process + // ๋‹ค์Œ ๊ณต์ • ํ™œ์„ฑํ™” (์–‘ํ’ˆ์ด ์žˆ์œผ๋ฉด) + const goodQty = parseInt(result.rows[0].good_qty, 10) || 0; + if (goodQty > 0) { + const nextSeq = String(seqNum + 1); + await pool.query( + `UPDATE work_order_process SET status = 'acceptable', updated_date = NOW() WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NULL`, - [wo_id, nextSeq, companyCode] - ); - } + [wo_id, nextSeq, companyCode], + ); + } - // === BUG-2 FIX: confirmResult์—์„œ๋„ master ํ•ฉ์‚ฐ === - if (parent_process_id) { - await pool.query( - `UPDATE work_order_process + // === BUG-2 FIX: confirmResult์—์„œ๋„ master ํ•ฉ์‚ฐ === + if (parent_process_id) { + await pool.query( + `UPDATE work_order_process SET good_qty = sub.sum_good, defect_qty = sub.sum_defect, total_production_qty = sub.sum_total, @@ -1447,44 +1549,45 @@ export const confirmResult = async ( WHERE parent_process_id = $1 AND company_code = $2 ) sub WHERE id = $1 AND company_code = $2`, - [parent_process_id, companyCode] - ); - } + [parent_process_id, companyCode], + ); + } - // ๋งˆ์Šคํ„ฐ ์ž๋™์™„๋ฃŒ ์บ์Šค์ผ€์ด๋“œ (๋ถ„ํ•  ํ–‰์ธ ๊ฒฝ์šฐ) - if (parent_process_id) { - let prevGoodQty = instrQty; - if (seqNum > 1) { - const prevSeq = String(seqNum - 1); - const prevProcess = await pool.query( - `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good + // ๋งˆ์Šคํ„ฐ ์ž๋™์™„๋ฃŒ ์บ์Šค์ผ€์ด๋“œ (๋ถ„ํ•  ํ–‰์ธ ๊ฒฝ์šฐ) + if (parent_process_id) { + let prevGoodQty = instrQty; + if (seqNum > 1) { + const prevSeq = String(seqNum - 1); + const prevProcess = await pool.query( + `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL`, - [wo_id, prevSeq, companyCode] - ); - if (prevProcess.rowCount > 0) { - prevGoodQty = parseInt(prevProcess.rows[0].total_good, 10) || 0; - } - } + [wo_id, prevSeq, companyCode], + ); + if (prevProcess.rowCount > 0) { + prevGoodQty = parseInt(prevProcess.rows[0].total_good, 10) || 0; + } + } - const siblingCheck = await pool.query( - `SELECT + const siblingCheck = await pool.query( + `SELECT COALESCE(SUM(input_qty::int), 0) as total_input, COUNT(*) FILTER (WHERE status != 'completed') as incomplete_count FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL`, - [wo_id, seq_no, companyCode] - ); + [wo_id, seq_no, companyCode], + ); - const totalInput = parseInt(siblingCheck.rows[0].total_input, 10) || 0; - const incompleteCount = parseInt(siblingCheck.rows[0].incomplete_count, 10) || 0; - const remainingAcceptable = prevGoodQty - totalInput; + const totalInput = parseInt(siblingCheck.rows[0].total_input, 10) || 0; + const incompleteCount = + parseInt(siblingCheck.rows[0].incomplete_count, 10) || 0; + const remainingAcceptable = prevGoodQty - totalInput; - if (incompleteCount === 0 && remainingAcceptable <= 0) { - await pool.query( - `UPDATE work_order_process + if (incompleteCount === 0 && remainingAcceptable <= 0) { + await pool.query( + `UPDATE work_order_process SET status = 'completed', result_status = 'confirmed', completed_at = NOW()::text, @@ -1492,36 +1595,38 @@ export const confirmResult = async ( updated_date = NOW() WHERE id = $1 AND company_code = $2 AND status != 'completed'`, - [parent_process_id, companyCode, userId] - ); - logger.info("[pop/production] confirmResult: ๋งˆ์Šคํ„ฐ ์ž๋™ ์™„๋ฃŒ", { - masterId: parent_process_id, totalInput, prevGoodQty, - }); - } - } + [parent_process_id, companyCode, userId], + ); + logger.info("[pop/production] confirmResult: ๋งˆ์Šคํ„ฐ ์ž๋™ ์™„๋ฃŒ", { + masterId: parent_process_id, + totalInput, + prevGoodQty, + }); + } + } - // ์ž‘์—…์ง€์‹œ ์ „์ฒด ์™„๋ฃŒ ํŒ์ • - await checkAndCompleteWorkInstruction(pool, wo_id, companyCode, userId); - } + // ์ž‘์—…์ง€์‹œ ์ „์ฒด ์™„๋ฃŒ ํŒ์ • + await checkAndCompleteWorkInstruction(pool, wo_id, companyCode, userId); + } - logger.info("[pop/production] confirm-result ์™„๋ฃŒ", { - companyCode, - work_order_process_id, - userId, - finalStatus: result.rows[0].status, - }); + logger.info("[pop/production] confirm-result ์™„๋ฃŒ", { + companyCode, + work_order_process_id, + userId, + finalStatus: result.rows[0].status, + }); - return res.json({ - success: true, - data: result.rows[0], - }); - } catch (error: any) { - logger.error("[pop/production] confirm-result ์˜ค๋ฅ˜:", error); - return res.status(500).json({ - success: false, - message: error.message || "์‹ค์  ํ™•์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - }); - } + return res.json({ + success: true, + data: result.rows[0], + }); + } catch (error: any) { + logger.error("[pop/production] confirm-result ์˜ค๋ฅ˜:", error); + return res.status(500).json({ + success: false, + message: error.message || "์‹ค์  ํ™•์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } }; /** @@ -1529,36 +1634,40 @@ export const confirmResult = async ( * total_production_qty ๋ณ€๊ฒฝ ์ด๋ ฅ = ๊ฐ ์ฐจ์ˆ˜์˜ ๋“ฑ๋ก ๊ธฐ๋ก */ export const getResultHistory = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); + const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const rawWopId = req.query.work_order_process_id; - const work_order_process_id = Array.isArray(rawWopId) ? rawWopId[0] : rawWopId; + try { + const companyCode = req.user!.companyCode; + const rawWopId = req.query.work_order_process_id; + const work_order_process_id = Array.isArray(rawWopId) + ? rawWopId[0] + : rawWopId; - if (!work_order_process_id) { - return res.status(400).json({ - success: false, - message: "work_order_process_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", - }); - } + if (!work_order_process_id) { + return res.status(400).json({ + success: false, + message: "work_order_process_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", + }); + } - // ์†Œ์œ ๊ถŒ ํ™•์ธ - const ownerCheck = await pool.query( - `SELECT id FROM work_order_process WHERE id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); - if (ownerCheck.rowCount === 0) { - return res.status(404).json({ success: false, message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); - } + // ์†Œ์œ ๊ถŒ ํ™•์ธ + const ownerCheck = await pool.query( + `SELECT id FROM work_order_process WHERE id = $1 AND company_code = $2`, + [work_order_process_id, companyCode], + ); + if (ownerCheck.rowCount === 0) { + return res + .status(404) + .json({ success: false, message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); + } - // ๊ฐ™์€ changed_at ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ•‘ํ•˜์—ฌ ์ฐจ์ˆ˜๋ณ„ ์ด๋ ฅ ์ถ”์ถœ - // total_production_qty๊ฐ€ ์ฆ๊ฐ€ํ•œ(new_value > old_value) ๋กœ๊ทธ๋งŒ = ์‹ค์  ๋“ฑ๋ก ์‹œ์  - const historyResult = await pool.query( - `WITH grouped AS ( + // ๊ฐ™์€ changed_at ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ•‘ํ•˜์—ฌ ์ฐจ์ˆ˜๋ณ„ ์ด๋ ฅ ์ถ”์ถœ + // total_production_qty๊ฐ€ ์ฆ๊ฐ€ํ•œ(new_value > old_value) ๋กœ๊ทธ๋งŒ = ์‹ค์  ๋“ฑ๋ก ์‹œ์  + const historyResult = await pool.query( + `WITH grouped AS ( SELECT changed_at, MAX(changed_by) as changed_by, @@ -1578,41 +1687,45 @@ export const getResultHistory = async ( WHERE total_new IS NOT NULL AND (COALESCE(total_new::int, 0) - COALESCE(total_old::int, 0)) > 0 ORDER BY changed_at ASC`, - [work_order_process_id] - ); + [work_order_process_id], + ); - const batches = historyResult.rows.map((row: any, idx: number) => { - const batchQty = (parseInt(row.total_new, 10) || 0) - (parseInt(row.total_old, 10) || 0); - const batchGood = (parseInt(row.good_new, 10) || 0) - (parseInt(row.good_old, 10) || 0); - const batchDefect = (parseInt(row.defect_new, 10) || 0) - (parseInt(row.defect_old, 10) || 0); + const batches = historyResult.rows.map((row: any, idx: number) => { + const batchQty = + (parseInt(row.total_new, 10) || 0) - (parseInt(row.total_old, 10) || 0); + const batchGood = + (parseInt(row.good_new, 10) || 0) - (parseInt(row.good_old, 10) || 0); + const batchDefect = + (parseInt(row.defect_new, 10) || 0) - + (parseInt(row.defect_old, 10) || 0); - return { - seq: idx + 1, - batch_qty: batchQty, - batch_good: batchGood, - batch_defect: batchDefect, - accumulated_total: parseInt(row.total_new, 10) || 0, - changed_at: row.changed_at, - changed_by: row.changed_by, - }; - }); + return { + seq: idx + 1, + batch_qty: batchQty, + batch_good: batchGood, + batch_defect: batchDefect, + accumulated_total: parseInt(row.total_new, 10) || 0, + changed_at: row.changed_at, + changed_by: row.changed_by, + }; + }); - logger.info("[pop/production] result-history ์กฐํšŒ", { - work_order_process_id, - batchCount: batches.length, - }); + logger.info("[pop/production] result-history ์กฐํšŒ", { + work_order_process_id, + batchCount: batches.length, + }); - return res.json({ - success: true, - data: batches, - }); - } catch (error: any) { - logger.error("[pop/production] result-history ์˜ค๋ฅ˜:", error); - return res.status(500).json({ - success: false, - message: error.message || "์ด๋ ฅ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - }); - } + return res.json({ + success: true, + data: batches, + }); + } catch (error: any) { + logger.error("[pop/production] result-history ์˜ค๋ฅ˜:", error); + return res.status(500).json({ + success: false, + message: error.message || "์ด๋ ฅ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } }; /** @@ -1620,119 +1733,126 @@ export const getResultHistory = async ( * GET /api/pop/production/available-qty?work_order_process_id=xxx * ๋ฐ˜ํ™˜: { prevGoodQty, myInputQty, availableQty, instructionQty } */ -export const getAvailableQty = async (req: AuthenticatedRequest, res: Response) => { - const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const rawWopId = req.query.work_order_process_id; - const work_order_process_id = Array.isArray(rawWopId) ? rawWopId[0] : rawWopId; +export const getAvailableQty = async ( + req: AuthenticatedRequest, + res: Response, +) => { + const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const rawWopId = req.query.work_order_process_id; + const work_order_process_id = Array.isArray(rawWopId) + ? rawWopId[0] + : rawWopId; - if (!work_order_process_id) { - return res.status(400).json({ - success: false, - message: "work_order_process_id๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", - }); - } + if (!work_order_process_id) { + return res.status(400).json({ + success: false, + message: "work_order_process_id๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + } - const current = await pool.query( - `SELECT wop.seq_no, wop.wo_id, wop.parent_process_id, + const current = await pool.query( + `SELECT wop.seq_no, wop.wo_id, wop.parent_process_id, wi.qty as instruction_qty FROM work_order_process wop JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code WHERE wop.id = $1 AND wop.company_code = $2`, - [work_order_process_id, companyCode] - ); + [work_order_process_id, companyCode], + ); - if (current.rowCount === 0) { - return res.status(404).json({ success: false, message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); - } + if (current.rowCount === 0) { + return res + .status(404) + .json({ success: false, message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); + } - const { seq_no, wo_id, instruction_qty } = current.rows[0]; - const instrQty = parseInt(instruction_qty, 10) || 0; - const seqNum = parseInt(seq_no, 10); + const { seq_no, wo_id, instruction_qty } = current.rows[0]; + const instrQty = parseInt(instruction_qty, 10) || 0; + const seqNum = parseInt(seq_no, 10); - // ์žฌ์ž‘์—… ์นด๋“œ ์—ฌ๋ถ€ ํ™•์ธ - const reworkCheck = await pool.query( - `SELECT is_rework, input_qty FROM work_order_process WHERE id = $1`, - [work_order_process_id] - ); - const isRework = reworkCheck.rows[0]?.is_rework === "Y"; + // ์žฌ์ž‘์—… ์นด๋“œ ์—ฌ๋ถ€ ํ™•์ธ + const reworkCheck = await pool.query( + `SELECT is_rework, input_qty FROM work_order_process WHERE id = $1`, + [work_order_process_id], + ); + const isRework = reworkCheck.rows[0]?.is_rework === "Y"; - let myInputQty: number; - let prevGoodQty: number; - let availableQty: number; + let myInputQty: number; + let prevGoodQty: number; + let availableQty: number; - if (isRework) { - // ์žฌ์ž‘์—… ์นด๋“œ: ์ž์ฒด input_qty๊ฐ€ ์ ‘์ˆ˜ ๊ฐ€๋Šฅ ์ˆ˜๋Ÿ‰ - const reworkInput = parseInt(reworkCheck.rows[0]?.input_qty, 10) || 0; - myInputQty = 0; - prevGoodQty = reworkInput; - availableQty = reworkInput; - } else { - // ์ผ๋ฐ˜ ์นด๋“œ: ์•ž๊ณต์ • ์–‘ํ’ˆ - ๊ธฐ์ ‘์ˆ˜ํ•ฉ๊ณ„ (์žฌ์ž‘์—… ์นด๋“œ ์ œ์™ธ) - const totalAccepted = await pool.query( - `SELECT COALESCE(SUM(input_qty::int), 0) as total_input + if (isRework) { + // ์žฌ์ž‘์—… ์นด๋“œ: ์ž์ฒด input_qty๊ฐ€ ์ ‘์ˆ˜ ๊ฐ€๋Šฅ ์ˆ˜๋Ÿ‰ + const reworkInput = parseInt(reworkCheck.rows[0]?.input_qty, 10) || 0; + myInputQty = 0; + prevGoodQty = reworkInput; + availableQty = reworkInput; + } else { + // ์ผ๋ฐ˜ ์นด๋“œ: ์•ž๊ณต์ • ์–‘ํ’ˆ - ๊ธฐ์ ‘์ˆ˜ํ•ฉ๊ณ„ (์žฌ์ž‘์—… ์นด๋“œ ์ œ์™ธ) + const totalAccepted = await pool.query( + `SELECT COALESCE(SUM(input_qty::int), 0) as total_input FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL AND (is_rework IS NULL OR is_rework != 'Y')`, - [wo_id, seq_no, companyCode] - ); - myInputQty = parseInt(totalAccepted.rows[0].total_input, 10) || 0; + [wo_id, seq_no, companyCode], + ); + myInputQty = parseInt(totalAccepted.rows[0].total_input, 10) || 0; - prevGoodQty = instrQty; - // ์ฒซ ๊ณต์ • ์—ฌ๋ถ€๋ฅผ seq_no==1์ด ์•„๋‹ˆ๋ผ "์ด ๊ณต์ •๋ณด๋‹ค ์ž‘์€ seq_no๊ฐ€ ์žˆ๋Š”์ง€"๋กœ ํŒ๋‹จ - // (๋ผ์šฐํŒ… seq_no๊ฐ€ 1, 2, 3์ด ์•„๋‹ˆ๋ผ 10, 20, 30 ๊ฐ™์€ ๋น„์ˆœ์ฐจ์—ฌ๋„ ์ •์ƒ ๋™์ž‘) - const minSeqCheck = await pool.query( - `SELECT MIN(CAST(seq_no AS int)) as min_seq + prevGoodQty = instrQty; + // ์ฒซ ๊ณต์ • ์—ฌ๋ถ€๋ฅผ seq_no==1์ด ์•„๋‹ˆ๋ผ "์ด ๊ณต์ •๋ณด๋‹ค ์ž‘์€ seq_no๊ฐ€ ์žˆ๋Š”์ง€"๋กœ ํŒ๋‹จ + // (๋ผ์šฐํŒ… seq_no๊ฐ€ 1, 2, 3์ด ์•„๋‹ˆ๋ผ 10, 20, 30 ๊ฐ™์€ ๋น„์ˆœ์ฐจ์—ฌ๋„ ์ •์ƒ ๋™์ž‘) + const minSeqCheck = await pool.query( + `SELECT MIN(CAST(seq_no AS int)) as min_seq FROM work_order_process WHERE wo_id = $1 AND company_code = $2 AND parent_process_id IS NULL`, - [wo_id, companyCode] - ); - const minSeq = parseInt(minSeqCheck.rows[0]?.min_seq, 10) || seqNum; - const isFirstProcess = seqNum <= minSeq; - if (!isFirstProcess) { - // ์ด์ „ ๊ณต์ • ์ฐพ๊ธฐ (seq_no๊ฐ€ ๋” ์ž‘์€ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๊ณต์ •) - const prevProcessSeq = await pool.query( - `SELECT MAX(CAST(seq_no AS int)) as prev_seq + [wo_id, companyCode], + ); + const minSeq = parseInt(minSeqCheck.rows[0]?.min_seq, 10) || seqNum; + const isFirstProcess = seqNum <= minSeq; + if (!isFirstProcess) { + // ์ด์ „ ๊ณต์ • ์ฐพ๊ธฐ (seq_no๊ฐ€ ๋” ์ž‘์€ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๊ณต์ •) + const prevProcessSeq = await pool.query( + `SELECT MAX(CAST(seq_no AS int)) as prev_seq FROM work_order_process WHERE wo_id = $1 AND company_code = $2 AND parent_process_id IS NULL AND CAST(seq_no AS int) < $3`, - [wo_id, companyCode, seqNum] - ); - const actualPrevSeq = prevProcessSeq.rows[0]?.prev_seq; - if (actualPrevSeq != null) { - const prevProcess = await pool.query( - `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good + [wo_id, companyCode, seqNum], + ); + const actualPrevSeq = prevProcessSeq.rows[0]?.prev_seq; + if (actualPrevSeq != null) { + const prevProcess = await pool.query( + `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL`, - [wo_id, String(actualPrevSeq), companyCode] - ); - if (prevProcess.rowCount > 0) { - prevGoodQty = parseInt(prevProcess.rows[0].total_good, 10) || 0; - } - } - } - availableQty = Math.max(0, prevGoodQty - myInputQty); - } + [wo_id, String(actualPrevSeq), companyCode], + ); + if (prevProcess.rowCount > 0) { + prevGoodQty = parseInt(prevProcess.rows[0].total_good, 10) || 0; + } + } + } + availableQty = Math.max(0, prevGoodQty - myInputQty); + } - logger.info("[pop/production] available-qty ์กฐํšŒ", { - work_order_process_id, - prevGoodQty, - myInputQty, - availableQty, - instructionQty: instrQty, - }); + logger.info("[pop/production] available-qty ์กฐํšŒ", { + work_order_process_id, + prevGoodQty, + myInputQty, + availableQty, + instructionQty: instrQty, + }); - // ์•ž๊ณต์ •์—์„œ ๋ฆฌ์›Œํฌ๋กœ ์™„๋ฃŒ๋œ ์–‘ํ’ˆ ์ˆ˜๋Ÿ‰ (๋งˆํฌ ํ‘œ์‹œ์šฉ) - // rework_source_id๋ณ„๋กœ ๊ฐœ๋ณ„ ์ถ”์ ํ•˜์—ฌ ์ •ํ™•ํ•œ ๋ฏธ์†Œ์ง„ ๋ฆฌ์›Œํฌ ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ - let reworkAvailableQty = 0; - if (!isRework && seqNum > 1) { - const prevSeq = String(seqNum - 1); - // ์•ž๊ณต์ •์˜ ๋ฆฌ์›Œํฌ ์™„๋ฃŒ SPLIT๋“ค (rework_source_id๋ณ„) - const reworkSplits = await pool.query( - `SELECT rework_source_id, COALESCE(SUM(good_qty::int), 0) as rg + // ์•ž๊ณต์ •์—์„œ ๋ฆฌ์›Œํฌ๋กœ ์™„๋ฃŒ๋œ ์–‘ํ’ˆ ์ˆ˜๋Ÿ‰ (๋งˆํฌ ํ‘œ์‹œ์šฉ) + // rework_source_id๋ณ„๋กœ ๊ฐœ๋ณ„ ์ถ”์ ํ•˜์—ฌ ์ •ํ™•ํ•œ ๋ฏธ์†Œ์ง„ ๋ฆฌ์›Œํฌ ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ + let reworkAvailableQty = 0; + if (!isRework && seqNum > 1) { + const prevSeq = String(seqNum - 1); + // ์•ž๊ณต์ •์˜ ๋ฆฌ์›Œํฌ ์™„๋ฃŒ SPLIT๋“ค (rework_source_id๋ณ„) + const reworkSplits = await pool.query( + `SELECT rework_source_id, COALESCE(SUM(good_qty::int), 0) as rg FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL @@ -1740,43 +1860,43 @@ export const getAvailableQty = async (req: AuthenticatedRequest, res: Response) AND status = 'completed' AND good_qty::int > 0 GROUP BY rework_source_id`, - [wo_id, prevSeq, companyCode] - ); - // ํ˜„์žฌ ๊ณต์ •์—์„œ ๊ฐ rework_source_id๋ณ„๋กœ ์†Œ๋น„๋œ ์ˆ˜๋Ÿ‰ - for (const rs of reworkSplits.rows) { - const srcId = rs.rework_source_id; - const srcGood = parseInt(rs.rg, 10) || 0; - const consumedResult = await pool.query( - `SELECT COALESCE(SUM(input_qty::int), 0) as consumed + [wo_id, prevSeq, companyCode], + ); + // ํ˜„์žฌ ๊ณต์ •์—์„œ ๊ฐ rework_source_id๋ณ„๋กœ ์†Œ๋น„๋œ ์ˆ˜๋Ÿ‰ + for (const rs of reworkSplits.rows) { + const srcId = rs.rework_source_id; + const srcGood = parseInt(rs.rg, 10) || 0; + const consumedResult = await pool.query( + `SELECT COALESCE(SUM(input_qty::int), 0) as consumed FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL AND is_rework = 'Y' AND rework_source_id = $4`, - [wo_id, seq_no, companyCode, srcId] - ); - const consumed = parseInt(consumedResult.rows[0]?.consumed, 10) || 0; - reworkAvailableQty += Math.max(0, srcGood - consumed); - } - } + [wo_id, seq_no, companyCode, srcId], + ); + const consumed = parseInt(consumedResult.rows[0]?.consumed, 10) || 0; + reworkAvailableQty += Math.max(0, srcGood - consumed); + } + } - return res.json({ - success: true, - data: { - prevGoodQty, - myInputQty, - availableQty, - instructionQty: instrQty, - reworkAvailableQty, // ๋ฆฌ์›Œํฌ ๋ฌผ๋Ÿ‰ ํฌํ•จ ์ˆ˜๋Ÿ‰ - }, - }); - } catch (error: any) { - logger.error("[pop/production] available-qty ์˜ค๋ฅ˜:", error); - return res.status(500).json({ - success: false, - message: error.message || "์ ‘์ˆ˜๊ฐ€๋Šฅ๋Ÿ‰ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - }); - } + return res.json({ + success: true, + data: { + prevGoodQty, + myInputQty, + availableQty, + instructionQty: instrQty, + reworkAvailableQty, // ๋ฆฌ์›Œํฌ ๋ฌผ๋Ÿ‰ ํฌํ•จ ์ˆ˜๋Ÿ‰ + }, + }); + } catch (error: any) { + logger.error("[pop/production] available-qty ์˜ค๋ฅ˜:", error); + return res.status(500).json({ + success: false, + message: error.message || "์ ‘์ˆ˜๊ฐ€๋Šฅ๋Ÿ‰ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } }; /** @@ -1787,33 +1907,38 @@ export const getAvailableQty = async (req: AuthenticatedRequest, res: Response) * - ์ถ”๊ฐ€ ์ ‘์ˆ˜ ๊ฐ€๋Šฅ (in_progress ์ƒํƒœ์—์„œ๋„) * - status: acceptable/waiting -> in_progress (๋˜๋Š” ์ด๋ฏธ in_progress๋ฉด ์œ ์ง€) */ -export const acceptProcess = async (req: AuthenticatedRequest, res: Response) => { - const pool = getPool(); - const client = await pool.connect(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const { work_order_process_id, accept_qty } = req.body; +export const acceptProcess = async ( + req: AuthenticatedRequest, + res: Response, +) => { + const pool = getPool(); + const client = await pool.connect(); + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { work_order_process_id, accept_qty } = req.body; - if (!work_order_process_id || !accept_qty) { - client.release(); - return res.status(400).json({ - success: false, - message: "work_order_process_id์™€ accept_qty๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", - }); - } + if (!work_order_process_id || !accept_qty) { + client.release(); + return res.status(400).json({ + success: false, + message: "work_order_process_id์™€ accept_qty๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + } - const qty = parseInt(accept_qty, 10); - if (qty <= 0) { - client.release(); - return res.status(400).json({ success: false, message: "์ ‘์ˆ˜ ์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." }); - } + const qty = parseInt(accept_qty, 10); + if (qty <= 0) { + client.release(); + return res + .status(400) + .json({ success: false, message: "์ ‘์ˆ˜ ์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." }); + } - await client.query("BEGIN"); + await client.query("BEGIN"); - // ์›๋ณธ(๋งˆ์Šคํ„ฐ) ํ–‰ ์กฐํšŒ + FOR UPDATE (๋™์‹œ ์ ‘์ˆ˜ ๋ฐฉ์ง€) - const current = await client.query( - `SELECT wop.id, wop.seq_no, wop.wo_id, wop.status, wop.parent_process_id, + // ์›๋ณธ(๋งˆ์Šคํ„ฐ) ํ–‰ ์กฐํšŒ + FOR UPDATE (๋™์‹œ ์ ‘์ˆ˜ ๋ฐฉ์ง€) + const current = await client.query( + `SELECT wop.id, wop.seq_no, wop.wo_id, wop.status, wop.parent_process_id, wop.process_code, wop.process_name, wop.is_required, wop.is_fixed_order, wop.standard_time, wop.equipment_code, wop.routing_detail_id, wi.qty as instruction_qty @@ -1821,130 +1946,141 @@ export const acceptProcess = async (req: AuthenticatedRequest, res: Response) => JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code WHERE wop.id = $1 AND wop.company_code = $2 FOR UPDATE OF wop`, - [work_order_process_id, companyCode] - ); + [work_order_process_id, companyCode], + ); - if (current.rowCount === 0) { - await client.query("ROLLBACK"); - client.release(); - return res.status(404).json({ success: false, message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); - } + if (current.rowCount === 0) { + await client.query("ROLLBACK"); + client.release(); + return res + .status(404) + .json({ success: false, message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); + } - const row = current.rows[0]; - const masterId = row.parent_process_id || row.id; + const row = current.rows[0]; + const masterId = row.parent_process_id || row.id; - if (row.status === "completed") { - await client.query("ROLLBACK"); - client.release(); - return res.status(400).json({ success: false, message: "์ด๋ฏธ ์™„๋ฃŒ๋œ ๊ณต์ •์ž…๋‹ˆ๋‹ค." }); - } - if (row.status !== "acceptable") { - await client.query("ROLLBACK"); - client.release(); - return res.status(400).json({ success: false, message: `์›๋ณธ ๊ณต์ • ์ƒํƒœ(${row.status})์—์„œ๋Š” ์ ‘์ˆ˜ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.` }); - } + if (row.status === "completed") { + await client.query("ROLLBACK"); + client.release(); + return res + .status(400) + .json({ success: false, message: "์ด๋ฏธ ์™„๋ฃŒ๋œ ๊ณต์ •์ž…๋‹ˆ๋‹ค." }); + } + if (row.status !== "acceptable") { + await client.query("ROLLBACK"); + client.release(); + return res.status(400).json({ + success: false, + message: `์›๋ณธ ๊ณต์ • ์ƒํƒœ(${row.status})์—์„œ๋Š” ์ ‘์ˆ˜ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`, + }); + } - const instrQty = parseInt(row.instruction_qty, 10) || 0; - const seqNum = parseInt(row.seq_no, 10); + const instrQty = parseInt(row.instruction_qty, 10) || 0; + const seqNum = parseInt(row.seq_no, 10); - // ์žฌ์ž‘์—… ์นด๋“œ ์—ฌ๋ถ€ ํ™•์ธ - const isReworkCard = await client.query( - `SELECT is_rework, input_qty FROM work_order_process WHERE id = $1`, - [work_order_process_id] - ); - const isRework = isReworkCard.rows[0]?.is_rework === "Y"; - const reworkInputQty = parseInt(isReworkCard.rows[0]?.input_qty, 10) || 0; + // ์žฌ์ž‘์—… ์นด๋“œ ์—ฌ๋ถ€ ํ™•์ธ + const isReworkCard = await client.query( + `SELECT is_rework, input_qty FROM work_order_process WHERE id = $1`, + [work_order_process_id], + ); + const isRework = isReworkCard.rows[0]?.is_rework === "Y"; + const reworkInputQty = parseInt(isReworkCard.rows[0]?.input_qty, 10) || 0; - let prevGoodQty: number; - let currentTotalInput: number; - let availableQty: number; + let prevGoodQty: number; + let currentTotalInput: number; + let availableQty: number; - if (isRework) { - // ์žฌ์ž‘์—… ์นด๋“œ: ์ž์ฒด input_qty๊ฐ€ ์ ‘์ˆ˜ ๊ฐ€๋Šฅ ์ˆ˜๋Ÿ‰ (์•ž๊ณต์ •๊ณผ ๋ฌด๊ด€) - prevGoodQty = reworkInputQty; - currentTotalInput = 0; // ์žฌ์ž‘์—… ์นด๋“œ๋Š” ์ž์ฒด๊ฐ€ ๋งˆ์Šคํ„ฐ, ๋ถ„ํ•  ํ–‰ ์—†์Œ - availableQty = reworkInputQty; - } else { - // ์ผ๋ฐ˜ ์นด๋“œ: ์•ž๊ณต์ • ์–‘ํ’ˆ - ๊ธฐ์ ‘์ˆ˜ํ•ฉ๊ณ„ - const totalAccepted = await client.query( - `SELECT COALESCE(SUM(input_qty::int), 0) as total_input + if (isRework) { + // ์žฌ์ž‘์—… ์นด๋“œ: ์ž์ฒด input_qty๊ฐ€ ์ ‘์ˆ˜ ๊ฐ€๋Šฅ ์ˆ˜๋Ÿ‰ (์•ž๊ณต์ •๊ณผ ๋ฌด๊ด€) + prevGoodQty = reworkInputQty; + currentTotalInput = 0; // ์žฌ์ž‘์—… ์นด๋“œ๋Š” ์ž์ฒด๊ฐ€ ๋งˆ์Šคํ„ฐ, ๋ถ„ํ•  ํ–‰ ์—†์Œ + availableQty = reworkInputQty; + } else { + // ์ผ๋ฐ˜ ์นด๋“œ: ์•ž๊ณต์ • ์–‘ํ’ˆ - ๊ธฐ์ ‘์ˆ˜ํ•ฉ๊ณ„ + const totalAccepted = await client.query( + `SELECT COALESCE(SUM(input_qty::int), 0) as total_input FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL AND (is_rework IS NULL OR is_rework != 'Y')`, - [row.wo_id, row.seq_no, companyCode] - ); - currentTotalInput = parseInt(totalAccepted.rows[0].total_input, 10) || 0; + [row.wo_id, row.seq_no, companyCode], + ); + currentTotalInput = parseInt(totalAccepted.rows[0].total_input, 10) || 0; - prevGoodQty = instrQty; - // ์ฒซ ๊ณต์ • ์—ฌ๋ถ€๋ฅผ seq_no==1์ด ์•„๋‹ˆ๋ผ "์ด ๊ณต์ •๋ณด๋‹ค ์ž‘์€ seq_no๊ฐ€ ์žˆ๋Š”์ง€"๋กœ ํŒ๋‹จ - const minSeqCheck = await client.query( - `SELECT MIN(CAST(seq_no AS int)) as min_seq + prevGoodQty = instrQty; + // ์ฒซ ๊ณต์ • ์—ฌ๋ถ€๋ฅผ seq_no==1์ด ์•„๋‹ˆ๋ผ "์ด ๊ณต์ •๋ณด๋‹ค ์ž‘์€ seq_no๊ฐ€ ์žˆ๋Š”์ง€"๋กœ ํŒ๋‹จ + const minSeqCheck = await client.query( + `SELECT MIN(CAST(seq_no AS int)) as min_seq FROM work_order_process WHERE wo_id = $1 AND company_code = $2 AND parent_process_id IS NULL`, - [row.wo_id, companyCode] - ); - const minSeq = parseInt(minSeqCheck.rows[0]?.min_seq, 10) || seqNum; - const isFirstProcess = seqNum <= minSeq; - if (!isFirstProcess) { - // ์ด์ „ ๊ณต์ • = ์ด ๊ณต์ •๋ณด๋‹ค ์ž‘์€ seq_no ์ค‘ ๊ฐ€์žฅ ํฐ ๊ฐ’ - const prevProcessSeq = await client.query( - `SELECT MAX(CAST(seq_no AS int)) as prev_seq + [row.wo_id, companyCode], + ); + const minSeq = parseInt(minSeqCheck.rows[0]?.min_seq, 10) || seqNum; + const isFirstProcess = seqNum <= minSeq; + if (!isFirstProcess) { + // ์ด์ „ ๊ณต์ • = ์ด ๊ณต์ •๋ณด๋‹ค ์ž‘์€ seq_no ์ค‘ ๊ฐ€์žฅ ํฐ ๊ฐ’ + const prevProcessSeq = await client.query( + `SELECT MAX(CAST(seq_no AS int)) as prev_seq FROM work_order_process WHERE wo_id = $1 AND company_code = $2 AND parent_process_id IS NULL AND CAST(seq_no AS int) < $3`, - [row.wo_id, companyCode, seqNum] - ); - const actualPrevSeq = prevProcessSeq.rows[0]?.prev_seq; - const prevSeq = actualPrevSeq != null ? String(actualPrevSeq) : String(seqNum - 1); - const prevProcess = await client.query( - `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good + [row.wo_id, companyCode, seqNum], + ); + const actualPrevSeq = prevProcessSeq.rows[0]?.prev_seq; + const prevSeq = + actualPrevSeq != null ? String(actualPrevSeq) : String(seqNum - 1); + const prevProcess = await client.query( + `SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL`, - [row.wo_id, prevSeq, companyCode] - ); - if (prevProcess.rowCount > 0) { - prevGoodQty = parseInt(prevProcess.rows[0].total_good, 10) || 0; - } - } - availableQty = prevGoodQty - currentTotalInput; - } + [row.wo_id, prevSeq, companyCode], + ); + if (prevProcess.rowCount > 0) { + prevGoodQty = parseInt(prevProcess.rows[0].total_good, 10) || 0; + } + } + availableQty = prevGoodQty - currentTotalInput; + } - if (qty > availableQty) { - await client.query("ROLLBACK"); - client.release(); - return res.status(400).json({ - success: false, - message: `์ ‘์ˆ˜๊ฐ€๋Šฅ๋Ÿ‰(${availableQty})์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค. (์•ž๊ณต์ • ์™„๋ฃŒ: ${prevGoodQty}, ๊ธฐ์ ‘์ˆ˜ํ•ฉ๊ณ„: ${currentTotalInput})`, - }); - } + if (qty > availableQty) { + await client.query("ROLLBACK"); + client.release(); + return res.status(400).json({ + success: false, + message: `์ ‘์ˆ˜๊ฐ€๋Šฅ๋Ÿ‰(${availableQty})์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค. (์•ž๊ณต์ • ์™„๋ฃŒ: ${prevGoodQty}, ๊ธฐ์ ‘์ˆ˜ํ•ฉ๊ณ„: ${currentTotalInput})`, + }); + } - // batch_id: ์ปฌ๋Ÿผ์ด ์žˆ์œผ๋ฉด ํฌํ•จ, ์—†์œผ๋ฉด ์ œ์™ธ - const batchId = req.body.batch_id || `BATCH-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; - const hasBatchCol = _batchMigrationDone; + // batch_id: ์ปฌ๋Ÿผ์ด ์žˆ์œผ๋ฉด ํฌํ•จ, ์—†์œผ๋ฉด ์ œ์™ธ + const batchId = + req.body.batch_id || + `BATCH-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const hasBatchCol = _batchMigrationDone; - // ๋ฆฌ์›Œํฌ ์ •๋ณด ์ „๋‹ฌ: ๋ฆฌ์›Œํฌ ์นด๋“œ ์ ‘์ˆ˜ / ํ”„๋ก ํŠธ ์ „๋‹ฌ / ์ž๋™ ๊ฐ์ง€ - let splitIsRework: string | null = null; - let splitReworkSourceId: string | null = null; + // ๋ฆฌ์›Œํฌ ์ •๋ณด ์ „๋‹ฌ: ๋ฆฌ์›Œํฌ ์นด๋“œ ์ ‘์ˆ˜ / ํ”„๋ก ํŠธ ์ „๋‹ฌ / ์ž๋™ ๊ฐ์ง€ + let splitIsRework: string | null = null; + let splitReworkSourceId: string | null = null; - if (isRework) { - // ์ผ€์ด์Šค 1: ๋ฆฌ์›Œํฌ ์นด๋“œ์—์„œ ์ง์ ‘ ์ ‘์ˆ˜ - const parentReworkInfo = await client.query( - `SELECT is_rework, rework_source_id FROM work_order_process WHERE id = $1`, [work_order_process_id] - ); - splitIsRework = parentReworkInfo?.rows[0]?.is_rework || null; - splitReworkSourceId = parentReworkInfo?.rows[0]?.rework_source_id || null; - } else if (req.body.rework_source_id) { - // ์ผ€์ด์Šค 2: ํ”„๋ก ํŠธ์—์„œ ๋ฆฌ์›Œํฌ ์ถ”์  ์ •๋ณด ์ „๋‹ฌ - splitIsRework = "Y"; - splitReworkSourceId = req.body.rework_source_id; - } else if (seqNum > 1) { - // ์ผ€์ด์Šค 3: ์ž๋™ ๊ฐ์ง€ โ€” ์•ž๊ณต์ •์—์„œ ๋ฆฌ์›Œํฌ๋กœ ์™„๋ฃŒ๋œ ์–‘ํ’ˆ์ด ์žˆ๋Š”์ง€ ํ™•์ธ - const prevSeq = String(seqNum - 1); - // rework_source_id๋ณ„๋กœ ๊ฐœ๋ณ„ ์ถ”์  - const prevReworkSplits = await client.query( - `SELECT rework_source_id, COALESCE(SUM(good_qty::int), 0) as rework_good + if (isRework) { + // ์ผ€์ด์Šค 1: ๋ฆฌ์›Œํฌ ์นด๋“œ์—์„œ ์ง์ ‘ ์ ‘์ˆ˜ + const parentReworkInfo = await client.query( + `SELECT is_rework, rework_source_id FROM work_order_process WHERE id = $1`, + [work_order_process_id], + ); + splitIsRework = parentReworkInfo?.rows[0]?.is_rework || null; + splitReworkSourceId = parentReworkInfo?.rows[0]?.rework_source_id || null; + } else if (req.body.rework_source_id) { + // ์ผ€์ด์Šค 2: ํ”„๋ก ํŠธ์—์„œ ๋ฆฌ์›Œํฌ ์ถ”์  ์ •๋ณด ์ „๋‹ฌ + splitIsRework = "Y"; + splitReworkSourceId = req.body.rework_source_id; + } else if (seqNum > 1) { + // ์ผ€์ด์Šค 3: ์ž๋™ ๊ฐ์ง€ โ€” ์•ž๊ณต์ •์—์„œ ๋ฆฌ์›Œํฌ๋กœ ์™„๋ฃŒ๋œ ์–‘ํ’ˆ์ด ์žˆ๋Š”์ง€ ํ™•์ธ + const prevSeq = String(seqNum - 1); + // rework_source_id๋ณ„๋กœ ๊ฐœ๋ณ„ ์ถ”์  + const prevReworkSplits = await client.query( + `SELECT rework_source_id, COALESCE(SUM(good_qty::int), 0) as rework_good FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL @@ -1952,134 +2088,155 @@ export const acceptProcess = async (req: AuthenticatedRequest, res: Response) => AND status = 'completed' AND good_qty::int > 0 GROUP BY rework_source_id`, - [row.wo_id, prevSeq, companyCode] - ); + [row.wo_id, prevSeq, companyCode], + ); - // ๊ฐ rework_source๋ณ„๋กœ ๋ฏธ์†Œ์ง„ ์ˆ˜๋Ÿ‰ ํ™•์ธ - for (const rs of prevReworkSplits.rows) { - const srcId = rs.rework_source_id; - const srcGood = parseInt(rs.rework_good, 10) || 0; - const consumedResult = await client.query( - `SELECT COALESCE(SUM(input_qty::int), 0) as consumed + // ๊ฐ rework_source๋ณ„๋กœ ๋ฏธ์†Œ์ง„ ์ˆ˜๋Ÿ‰ ํ™•์ธ + for (const rs of prevReworkSplits.rows) { + const srcId = rs.rework_source_id; + const srcGood = parseInt(rs.rework_good, 10) || 0; + const consumedResult = await client.query( + `SELECT COALESCE(SUM(input_qty::int), 0) as consumed FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL AND is_rework = 'Y' AND rework_source_id = $4`, - [row.wo_id, row.seq_no, companyCode, srcId] - ); - const consumed = parseInt(consumedResult.rows[0]?.consumed, 10) || 0; - const remaining = srcGood - consumed; + [row.wo_id, row.seq_no, companyCode, srcId], + ); + const consumed = parseInt(consumedResult.rows[0]?.consumed, 10) || 0; + const remaining = srcGood - consumed; - if (remaining > 0 && qty <= remaining) { - // ํ•ฉ๋ฅ˜ ํŒ์ •: ์ผ๋ฐ˜ ๋ฌผ๋Ÿ‰์ด ์žˆ์œผ๋ฉด ํ•ฉ๋ฅ˜(๋งˆํฌ ์—†์Œ), ์—†์œผ๋ฉด ๋งˆํฌ ๋ถ€์ฐฉ - const normalAvailable = availableQty - remaining; - if (normalAvailable <= 0) { - // ์ผ๋ฐ˜ ๋ฌผ๋Ÿ‰ ์—†์Œ โ†’ ํ•ฉ๋ฅ˜ ๋ถˆ๊ฐ€ โ†’ ๋ฆฌ์›Œํฌ ๋งˆํฌ - splitIsRework = "Y"; - splitReworkSourceId = srcId; - } - // normalAvailable > 0 โ†’ ํ•ฉ๋ฅ˜ ๊ฐ€๋Šฅ โ†’ ๋งˆํฌ ์—†์Œ (splitIsRework = null) - break; - } - } - } + if (remaining > 0 && qty <= remaining) { + // ํ•ฉ๋ฅ˜ ํŒ์ •: ์ผ๋ฐ˜ ๋ฌผ๋Ÿ‰์ด ์žˆ์œผ๋ฉด ํ•ฉ๋ฅ˜(๋งˆํฌ ์—†์Œ), ์—†์œผ๋ฉด ๋งˆํฌ ๋ถ€์ฐฉ + const normalAvailable = availableQty - remaining; + if (normalAvailable <= 0) { + // ์ผ๋ฐ˜ ๋ฌผ๋Ÿ‰ ์—†์Œ โ†’ ํ•ฉ๋ฅ˜ ๋ถˆ๊ฐ€ โ†’ ๋ฆฌ์›Œํฌ ๋งˆํฌ + splitIsRework = "Y"; + splitReworkSourceId = srcId; + } + // normalAvailable > 0 โ†’ ํ•ฉ๋ฅ˜ ๊ฐ€๋Šฅ โ†’ ๋งˆํฌ ์—†์Œ (splitIsRework = null) + break; + } + } + } - // ๋ถ„ํ•  ํ–‰ INSERT (batch_id๋Š” ์ปฌ๋Ÿผ ์กด์žฌ ์‹œ์—๋งŒ, ๋ฆฌ์›Œํฌ ์ •๋ณด ํฌํ•จ) - const reworkCols = splitIsRework ? ", is_rework, rework_source_id" : ""; - const reworkVals = splitIsRework ? `, $${hasBatchCol ? 15 : 14}, $${hasBatchCol ? 16 : 15}` : ""; - const reworkParams = splitIsRework ? [splitIsRework, splitReworkSourceId] : []; + // ๋ถ„ํ•  ํ–‰ INSERT (batch_id๋Š” ์ปฌ๋Ÿผ ์กด์žฌ ์‹œ์—๋งŒ, ๋ฆฌ์›Œํฌ ์ •๋ณด ํฌํ•จ) + const reworkCols = splitIsRework ? ", is_rework, rework_source_id" : ""; + const reworkVals = splitIsRework + ? `, $${hasBatchCol ? 15 : 14}, $${hasBatchCol ? 16 : 15}` + : ""; + const reworkParams = splitIsRework + ? [splitIsRework, splitReworkSourceId] + : []; - const insertCols = `id, wo_id, seq_no, process_code, process_name, is_required, is_fixed_order, + const insertCols = `id, wo_id, seq_no, process_code, process_name, is_required, is_fixed_order, standard_time, equipment_code, routing_detail_id, status, input_qty, good_qty, defect_qty, total_production_qty, result_status, accepted_by, accepted_at, started_at, parent_process_id, company_code, writer${hasBatchCol ? ", batch_id" : ""}${reworkCols}`; - const insertVals = `gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, + const insertVals = `gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, 'in_progress', $10, '0', '0', '0', 'draft', $11, NOW()::text, NOW()::text, $12, $13, $11${hasBatchCol ? ", $14" : ""}${reworkVals}`; - const insertParams = [ - row.wo_id, row.seq_no, row.process_code, row.process_name, - row.is_required, row.is_fixed_order, row.standard_time, - row.equipment_code, row.routing_detail_id, - String(qty), userId, masterId, companyCode, - ...(hasBatchCol ? [batchId] : []), - ...reworkParams, - ]; + const insertParams = [ + row.wo_id, + row.seq_no, + row.process_code, + row.process_name, + row.is_required, + row.is_fixed_order, + row.standard_time, + row.equipment_code, + row.routing_detail_id, + String(qty), + userId, + masterId, + companyCode, + ...(hasBatchCol ? [batchId] : []), + ...reworkParams, + ]; - const result = await client.query( - `INSERT INTO work_order_process (${insertCols}) VALUES (${insertVals}) + const result = await client.query( + `INSERT INTO work_order_process (${insertCols}) VALUES (${insertVals}) RETURNING id, input_qty, status, process_name, result_status, accepted_by`, - insertParams - ); + insertParams, + ); - // ๋ถ„ํ•  ํ–‰์— ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋ณต์‚ฌ - const splitId = result.rows[0].id; - const checklistCount = await copyChecklistToSplit( - client, masterId, splitId, row.routing_detail_id, companyCode, userId - ); + // ๋ถ„ํ•  ํ–‰์— ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋ณต์‚ฌ + const splitId = result.rows[0].id; + const checklistCount = await copyChecklistToSplit( + client, + masterId, + splitId, + row.routing_detail_id, + companyCode, + userId, + ); - // ๋งˆ์Šคํ„ฐ ํ–‰์˜ input_qty๋ฅผ ๋ถ„ํ•  ํ•ฉ๊ณ„๋กœ ๊ฐฑ์‹  (๋ฆฌ์›Œํฌ ์ ‘์ˆ˜ ์‹œ์—๋Š” ๋งˆ์Šคํ„ฐ input_qty ๋ณ€๊ฒฝ ์•ˆ ํ•จ) - let newTotalInput = currentTotalInput + qty; - if (!isRework) { - await client.query( - `UPDATE work_order_process SET input_qty = $3, updated_date = NOW() + // ๋งˆ์Šคํ„ฐ ํ–‰์˜ input_qty๋ฅผ ๋ถ„ํ•  ํ•ฉ๊ณ„๋กœ ๊ฐฑ์‹  (๋ฆฌ์›Œํฌ ์ ‘์ˆ˜ ์‹œ์—๋Š” ๋งˆ์Šคํ„ฐ input_qty ๋ณ€๊ฒฝ ์•ˆ ํ•จ) + let newTotalInput = currentTotalInput + qty; + if (!isRework) { + await client.query( + `UPDATE work_order_process SET input_qty = $3, updated_date = NOW() WHERE id = $1 AND company_code = $2`, - [masterId, companyCode, String(newTotalInput)] - ); - } else { - newTotalInput = currentTotalInput; // ๋ฆฌ์›Œํฌ๋Š” ๊ธฐ์กด ํ•ฉ๊ณ„ ์œ ์ง€ - // ๋ฆฌ์›Œํฌ ์นด๋“œ: ์ „๋Ÿ‰ ์ ‘์ˆ˜ ์‹œ์—๋งŒ ์ด ์นด๋“œ๋งŒ completed๋กœ ๋ณ€๊ฒฝ - // (๋‹ค๋ฅธ ๋ฆฌ์›Œํฌ ์นด๋“œ์— ์˜ํ–ฅ ์—†๋„๋ก id ์ •ํ™•ํžˆ ์ง€์ •) - const reworkAlreadyAccepted = await client.query( - `SELECT COALESCE(SUM(input_qty::int), 0) as total + [masterId, companyCode, String(newTotalInput)], + ); + } else { + newTotalInput = currentTotalInput; // ๋ฆฌ์›Œํฌ๋Š” ๊ธฐ์กด ํ•ฉ๊ณ„ ์œ ์ง€ + // ๋ฆฌ์›Œํฌ ์นด๋“œ: ์ „๋Ÿ‰ ์ ‘์ˆ˜ ์‹œ์—๋งŒ ์ด ์นด๋“œ๋งŒ completed๋กœ ๋ณ€๊ฒฝ + // (๋‹ค๋ฅธ ๋ฆฌ์›Œํฌ ์นด๋“œ์— ์˜ํ–ฅ ์—†๋„๋ก id ์ •ํ™•ํžˆ ์ง€์ •) + const reworkAlreadyAccepted = await client.query( + `SELECT COALESCE(SUM(input_qty::int), 0) as total FROM work_order_process WHERE parent_process_id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); - const totalReworkAccepted = (parseInt(reworkAlreadyAccepted.rows[0]?.total, 10) || 0) + qty; - if (totalReworkAccepted >= reworkInputQty) { - await client.query( - `UPDATE work_order_process SET status = 'completed', updated_date = NOW() + [work_order_process_id, companyCode], + ); + const totalReworkAccepted = + (parseInt(reworkAlreadyAccepted.rows[0]?.total, 10) || 0) + qty; + if (totalReworkAccepted >= reworkInputQty) { + await client.query( + `UPDATE work_order_process SET status = 'completed', updated_date = NOW() WHERE id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); - } - } + [work_order_process_id, companyCode], + ); + } + } - await client.query("COMMIT"); + await client.query("COMMIT"); - logger.info("[pop/production] accept-process ๋ถ„ํ•  ์ ‘์ˆ˜ ์™„๋ฃŒ", { - companyCode, userId, masterId, - splitId, - acceptedQty: qty, - totalAccepted: newTotalInput, - prevGoodQty, - checklistCount, - }); + logger.info("[pop/production] accept-process ๋ถ„ํ•  ์ ‘์ˆ˜ ์™„๋ฃŒ", { + companyCode, + userId, + masterId, + splitId, + acceptedQty: qty, + totalAccepted: newTotalInput, + prevGoodQty, + checklistCount, + }); - const acceptData = result.rows[0] || {}; - if (splitReworkSourceId) { - acceptData.rework_source_id = splitReworkSourceId; - acceptData.is_rework = splitIsRework; - } + const acceptData = result.rows[0] || {}; + if (splitReworkSourceId) { + acceptData.rework_source_id = splitReworkSourceId; + acceptData.is_rework = splitIsRework; + } - return res.json({ - success: true, - data: acceptData, - message: `${qty}๊ฐœ ์ ‘์ˆ˜ ์™„๋ฃŒ (์ด ์ ‘์ˆ˜ํ•ฉ๊ณ„: ${newTotalInput})`, - }); - } catch (error: any) { - await client.query("ROLLBACK").catch(() => {}); - logger.error("[pop/production] accept-process ์˜ค๋ฅ˜:", error); - return res.status(500).json({ - success: false, - message: error.message || "์ ‘์ˆ˜ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - }); - } finally { - client.release(); - } + return res.json({ + success: true, + data: acceptData, + message: `${qty}๊ฐœ ์ ‘์ˆ˜ ์™„๋ฃŒ (์ด ์ ‘์ˆ˜ํ•ฉ๊ณ„: ${newTotalInput})`, + }); + } catch (error: any) { + await client.query("ROLLBACK").catch(() => {}); + logger.error("[pop/production] accept-process ์˜ค๋ฅ˜:", error); + return res.status(500).json({ + success: false, + message: error.message || "์ ‘์ˆ˜ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } finally { + client.release(); + } }; /** @@ -2087,192 +2244,209 @@ export const acceptProcess = async (req: AuthenticatedRequest, res: Response) => * ์กฐ๊ฑด: ์•„์ง ์‹ค์ (total_production_qty)์ด ์—†์–ด์•ผ ํ•จ */ export const cancelAccept = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); + const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const { work_order_process_id } = req.body; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { work_order_process_id } = req.body; - if (!work_order_process_id) { - return res.status(400).json({ - success: false, - message: "work_order_process_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", - }); - } + if (!work_order_process_id) { + return res.status(400).json({ + success: false, + message: "work_order_process_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", + }); + } - const current = await pool.query( - `SELECT id, status, input_qty, total_production_qty, result_status, + const current = await pool.query( + `SELECT id, status, input_qty, total_production_qty, result_status, parent_process_id, wo_id, seq_no, process_name, target_warehouse_id, target_location_code, good_qty, concession_qty FROM work_order_process WHERE id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); + [work_order_process_id, companyCode], + ); - if (current.rowCount === 0) { - return res.status(404).json({ success: false, message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); - } + if (current.rowCount === 0) { + return res + .status(404) + .json({ success: false, message: "๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); + } - const proc = current.rows[0]; + const proc = current.rows[0]; - // ๋ถ„ํ•  ํ–‰๋งŒ ์ทจ์†Œ ๊ฐ€๋Šฅ (์›๋ณธ ํ–‰์€ ์ทจ์†Œ ๋Œ€์ƒ์ด ์•„๋‹˜) - if (!proc.parent_process_id) { - return res.status(400).json({ - success: false, - message: "์›๋ณธ ๊ณต์ •์€ ์ ‘์ˆ˜ ์ทจ์†Œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ถ„ํ• ๋œ ์ ‘์ˆ˜ ์นด๋“œ์—์„œ ์ทจ์†Œํ•ด์ฃผ์„ธ์š”.", - }); - } + // ๋ถ„ํ•  ํ–‰๋งŒ ์ทจ์†Œ ๊ฐ€๋Šฅ (์›๋ณธ ํ–‰์€ ์ทจ์†Œ ๋Œ€์ƒ์ด ์•„๋‹˜) + if (!proc.parent_process_id) { + return res.status(400).json({ + success: false, + message: + "์›๋ณธ ๊ณต์ •์€ ์ ‘์ˆ˜ ์ทจ์†Œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ถ„ํ• ๋œ ์ ‘์ˆ˜ ์นด๋“œ์—์„œ ์ทจ์†Œํ•ด์ฃผ์„ธ์š”.", + }); + } - if (proc.status !== "in_progress") { - return res.status(400).json({ - success: false, - message: `ํ˜„์žฌ ์ƒํƒœ(${proc.status})์—์„œ๋Š” ์ ‘์ˆ˜ ์ทจ์†Œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ง„ํ–‰์ค‘ ์ƒํƒœ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.`, - }); - } + if (proc.status !== "in_progress") { + return res.status(400).json({ + success: false, + message: `ํ˜„์žฌ ์ƒํƒœ(${proc.status})์—์„œ๋Š” ์ ‘์ˆ˜ ์ทจ์†Œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ง„ํ–‰์ค‘ ์ƒํƒœ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.`, + }); + } - const totalProduced = parseInt(proc.total_production_qty ?? "0", 10) || 0; - const currentInputQty = parseInt(proc.input_qty ?? "0", 10) || 0; - const unproducedQty = currentInputQty - totalProduced; + const totalProduced = parseInt(proc.total_production_qty ?? "0", 10) || 0; + const currentInputQty = parseInt(proc.input_qty ?? "0", 10) || 0; + const unproducedQty = currentInputQty - totalProduced; - if (unproducedQty <= 0) { - return res.status(400).json({ - success: false, - message: "์ทจ์†Œํ•  ๋ฏธ์†Œ์ง„ ์ ‘์ˆ˜๋ถ„์ด ์—†์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์ ‘์ˆ˜๋Ÿ‰์— ๋Œ€ํ•ด ์‹ค์ ์ด ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", - }); - } + if (unproducedQty <= 0) { + return res.status(400).json({ + success: false, + message: + "์ทจ์†Œํ•  ๋ฏธ์†Œ์ง„ ์ ‘์ˆ˜๋ถ„์ด ์—†์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์ ‘์ˆ˜๋Ÿ‰์— ๋Œ€ํ•ด ์‹ค์ ์ด ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + }); + } - let cancelledQty = unproducedQty; - const client = await pool.connect(); + const cancelledQty = unproducedQty; + const client = await pool.connect(); - try { - await client.query("BEGIN"); + try { + await client.query("BEGIN"); - if (totalProduced === 0) { - // ์‹ค์ ์ด ์—†์œผ๋ฉด ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋จผ์ € ์‚ญ์ œ โ†’ ๋ถ„ํ•  ํ–‰ ์‚ญ์ œ - await client.query( - `DELETE FROM process_work_result WHERE work_order_process_id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); - await client.query( - `DELETE FROM work_order_process WHERE id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); - } else { - // ์‹ค์ ์ด ์žˆ์œผ๋ฉด input_qty๋ฅผ ์‹ค์  ์ˆ˜๋Ÿ‰์œผ๋กœ ์ถ•์†Œ + completed - await client.query( - `UPDATE work_order_process + if (totalProduced === 0) { + // ์‹ค์ ์ด ์—†์œผ๋ฉด ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋จผ์ € ์‚ญ์ œ โ†’ ๋ถ„ํ•  ํ–‰ ์‚ญ์ œ + await client.query( + `DELETE FROM process_work_result WHERE work_order_process_id = $1 AND company_code = $2`, + [work_order_process_id, companyCode], + ); + await client.query( + `DELETE FROM work_order_process WHERE id = $1 AND company_code = $2`, + [work_order_process_id, companyCode], + ); + } else { + // ์‹ค์ ์ด ์žˆ์œผ๋ฉด input_qty๋ฅผ ์‹ค์  ์ˆ˜๋Ÿ‰์œผ๋กœ ์ถ•์†Œ + completed + await client.query( + `UPDATE work_order_process SET input_qty = $3, status = 'completed', result_status = 'confirmed', completed_at = NOW()::text, completed_by = $4, updated_date = NOW(), writer = $4 WHERE id = $1 AND company_code = $2`, - [work_order_process_id, companyCode, String(totalProduced), userId] - ); - } + [work_order_process_id, companyCode, String(totalProduced), userId], + ); + } - // ์žฌ๊ณ  ์›๋ณต: ๋ถ„ํ•  ํ–‰์— target_warehouse_id๊ฐ€ ์žˆ์œผ๋ฉด ์ž…๊ณ ๋œ ์ˆ˜๋Ÿ‰์„ ์ฐจ๊ฐ - if (proc.target_warehouse_id) { - const inboundQty = parseInt(proc.good_qty || "0", 10) + parseInt(proc.concession_qty || "0", 10); - if (inboundQty > 0) { - // work_instruction์—์„œ item_id ์กฐํšŒ - const wiResult = await client.query( - `SELECT item_id FROM work_instruction WHERE id = $1 AND company_code = $2`, - [proc.wo_id, companyCode] - ); - if (wiResult.rowCount > 0) { - const itemResult = await client.query( - `SELECT item_number FROM item_info WHERE id = $1 AND company_code = $2`, - [wiResult.rows[0].item_id, companyCode] - ); - if (itemResult.rowCount > 0) { - const itemCode = itemResult.rows[0].item_number; - const locCode = proc.target_location_code || proc.target_warehouse_id; - await client.query( - `UPDATE inventory_stock + // ์žฌ๊ณ  ์›๋ณต: ๋ถ„ํ•  ํ–‰์— target_warehouse_id๊ฐ€ ์žˆ์œผ๋ฉด ์ž…๊ณ ๋œ ์ˆ˜๋Ÿ‰์„ ์ฐจ๊ฐ + if (proc.target_warehouse_id) { + const inboundQty = + parseInt(proc.good_qty || "0", 10) + + parseInt(proc.concession_qty || "0", 10); + if (inboundQty > 0) { + // work_instruction์—์„œ item_id ์กฐํšŒ + const wiResult = await client.query( + `SELECT item_id FROM work_instruction WHERE id = $1 AND company_code = $2`, + [proc.wo_id, companyCode], + ); + if (wiResult.rowCount > 0) { + const itemResult = await client.query( + `SELECT item_number FROM item_info WHERE id = $1 AND company_code = $2`, + [wiResult.rows[0].item_id, companyCode], + ); + if (itemResult.rowCount > 0) { + const itemCode = itemResult.rows[0].item_number; + const locCode = + proc.target_location_code || proc.target_warehouse_id; + await client.query( + `UPDATE inventory_stock SET current_qty = GREATEST((COALESCE(current_qty::numeric, 0) - $4::numeric), 0)::text, updated_date = NOW(), writer = $5 WHERE company_code = $1 AND item_code = $2 AND warehouse_code = $3 AND location_code = $6`, - [companyCode, itemCode, proc.target_warehouse_id, String(inboundQty), userId, locCode] - ); - } - } - } - } + [ + companyCode, + itemCode, + proc.target_warehouse_id, + String(inboundQty), + userId, + locCode, + ], + ); + } + } + } + } - // ๋งˆ์Šคํ„ฐ ํ–‰์˜ input_qty๋ฅผ ๋ถ„ํ•  ํ•ฉ๊ณ„๋กœ ์žฌ๊ณ„์‚ฐ - const remainingSplits = await client.query( - `SELECT COALESCE(SUM(input_qty::int), 0) as total_input + // ๋งˆ์Šคํ„ฐ ํ–‰์˜ input_qty๋ฅผ ๋ถ„ํ•  ํ•ฉ๊ณ„๋กœ ์žฌ๊ณ„์‚ฐ + const remainingSplits = await client.query( + `SELECT COALESCE(SUM(input_qty::int), 0) as total_input FROM work_order_process WHERE parent_process_id = $1 AND company_code = $2`, - [proc.parent_process_id, companyCode] - ); - const newMasterInput = parseInt(remainingSplits.rows[0].total_input, 10) || 0; + [proc.parent_process_id, companyCode], + ); + const newMasterInput = + parseInt(remainingSplits.rows[0].total_input, 10) || 0; - // ์›๋ณธ(๋งˆ์Šคํ„ฐ) ํ–‰: input_qty ๋ณต์› + acceptable ์ƒํƒœ ์œ ์ง€ - await client.query( - `UPDATE work_order_process + // ์›๋ณธ(๋งˆ์Šคํ„ฐ) ํ–‰: input_qty ๋ณต์› + acceptable ์ƒํƒœ ์œ ์ง€ + await client.query( + `UPDATE work_order_process SET status = 'acceptable', input_qty = $3, updated_date = NOW() WHERE id = $1 AND company_code = $2 AND parent_process_id IS NULL`, - [proc.parent_process_id, companyCode, String(newMasterInput)] - ); + [proc.parent_process_id, companyCode, String(newMasterInput)], + ); - await client.query("COMMIT"); - } catch (txErr) { - await client.query("ROLLBACK"); - throw txErr; - } finally { - client.release(); - } + await client.query("COMMIT"); + } catch (txErr) { + await client.query("ROLLBACK"); + throw txErr; + } finally { + client.release(); + } - logger.info("[pop/production] cancel-accept ์™„๋ฃŒ (๋ถ„ํ•  ํ–‰)", { - companyCode, userId, work_order_process_id, - masterId: proc.parent_process_id, - previousInputQty: currentInputQty, - totalProduced, - cancelledQty, - action: totalProduced === 0 ? "DELETE" : "SHRINK", - }); + logger.info("[pop/production] cancel-accept ์™„๋ฃŒ (๋ถ„ํ•  ํ–‰)", { + companyCode, + userId, + work_order_process_id, + masterId: proc.parent_process_id, + previousInputQty: currentInputQty, + totalProduced, + cancelledQty, + action: totalProduced === 0 ? "DELETE" : "SHRINK", + }); - return res.json({ - success: true, - data: { id: work_order_process_id, process_name: proc.process_name }, - message: `๋ฏธ์†Œ์ง„ ${cancelledQty}๊ฐœ ์ ‘์ˆ˜๊ฐ€ ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, - }); - } catch (error: any) { - logger.error("[pop/production] cancel-accept ์˜ค๋ฅ˜:", error); - return res.status(500).json({ - success: false, - message: error.message || "์ ‘์ˆ˜ ์ทจ์†Œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", - }); - } + return res.json({ + success: true, + data: { id: work_order_process_id, process_name: proc.process_name }, + message: `๋ฏธ์†Œ์ง„ ${cancelledQty}๊ฐœ ์ ‘์ˆ˜๊ฐ€ ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, + }); + } catch (error: any) { + logger.error("[pop/production] cancel-accept ์˜ค๋ฅ˜:", error); + return res.status(500).json({ + success: false, + message: error.message || "์ ‘์ˆ˜ ์ทจ์†Œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } }; /** * ์ฐฝ๊ณ  ๋ชฉ๋ก ์กฐํšŒ (POP ์ƒ์‚ฐ์šฉ) */ export const getWarehouses = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const result = await pool.query( - `SELECT id, warehouse_code, warehouse_name, warehouse_type + const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const result = await pool.query( + `SELECT id, warehouse_code, warehouse_name, warehouse_type FROM warehouse_info WHERE company_code = $1 AND COALESCE(status, '') != '์‚ญ์ œ' ORDER BY warehouse_name`, - [companyCode] - ); - return res.json({ success: true, data: result.rows }); - } catch (error: any) { - logger.error("[pop/production] ์ฐฝ๊ณ  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:", error); - return res.status(500).json({ success: false, message: error.message }); - } + [companyCode], + ); + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("[pop/production] ์ฐฝ๊ณ  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:", error); + return res.status(500).json({ success: false, message: error.message }); + } }; /** @@ -2280,39 +2454,41 @@ export const getWarehouses = async ( * warehouseId๋Š” warehouse_info.id โ†’ warehouse_code๋ฅผ ์กฐํšŒํ•ด์„œ warehouse_location๊ณผ ๋งค์นญ */ export const getWarehouseLocations = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const { warehouseId } = req.params; - if (!warehouseId) { - return res.status(400).json({ success: false, message: "warehouseId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค." }); - } + const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const { warehouseId } = req.params; + if (!warehouseId) { + return res + .status(400) + .json({ success: false, message: "warehouseId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค." }); + } - // warehouse_info.id โ†’ warehouse_code ๋ณ€ํ™˜ - const whInfo = await pool.query( - `SELECT warehouse_code FROM warehouse_info WHERE id = $1 AND company_code = $2`, - [warehouseId, companyCode] - ); - if (whInfo.rowCount === 0) { - return res.json({ success: true, data: [] }); - } - const warehouseCode = whInfo.rows[0].warehouse_code; + // warehouse_info.id โ†’ warehouse_code ๋ณ€ํ™˜ + const whInfo = await pool.query( + `SELECT warehouse_code FROM warehouse_info WHERE id = $1 AND company_code = $2`, + [warehouseId, companyCode], + ); + if (whInfo.rowCount === 0) { + return res.json({ success: true, data: [] }); + } + const warehouseCode = whInfo.rows[0].warehouse_code; - const result = await pool.query( - `SELECT id, location_code, location_name + const result = await pool.query( + `SELECT id, location_code, location_name FROM warehouse_location WHERE warehouse_code = $1 AND company_code = $2 ORDER BY location_name`, - [warehouseCode, companyCode] - ); - return res.json({ success: true, data: result.rows }); - } catch (error: any) { - logger.error("[pop/production] ์ฐฝ๊ณ  ์œ„์น˜ ์กฐํšŒ ์‹คํŒจ:", error); - return res.status(500).json({ success: false, message: error.message }); - } + [warehouseCode, companyCode], + ); + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("[pop/production] ์ฐฝ๊ณ  ์œ„์น˜ ์กฐํšŒ ์‹คํŒจ:", error); + return res.status(500).json({ success: false, message: error.message }); + } }; /** @@ -2320,73 +2496,73 @@ export const getWarehouseLocations = async ( * ๊ฐ™์€ wo_id์—์„œ ํ˜„์žฌ seq_no๋ณด๋‹ค ํฐ ๊ณต์ •(๋งˆ์Šคํ„ฐ ํ–‰)์ด ์—†์œผ๋ฉด ๋งˆ์ง€๋ง‰ */ export const isLastProcess = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const { processId } = req.params; - if (!processId) { - return res.json({ success: true, data: { isLast: false } }); - } + const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const { processId } = req.params; + if (!processId) { + return res.json({ success: true, data: { isLast: false } }); + } - // ํ˜„์žฌ ๊ณต์ •์˜ wo_id์™€ seq_no ์กฐํšŒ (๋ถ„ํ•  ํ–‰์ด๋ฉด parent์˜ seq_no ๊ธฐ์ค€) - const process = await pool.query( - `SELECT wo_id, seq_no, parent_process_id + // ํ˜„์žฌ ๊ณต์ •์˜ wo_id์™€ seq_no ์กฐํšŒ (๋ถ„ํ•  ํ–‰์ด๋ฉด parent์˜ seq_no ๊ธฐ์ค€) + const process = await pool.query( + `SELECT wo_id, seq_no, parent_process_id FROM work_order_process WHERE id = $1 AND company_code = $2`, - [processId, companyCode] - ); - if (process.rowCount === 0) { - return res.json({ success: true, data: { isLast: false } }); - } + [processId, companyCode], + ); + if (process.rowCount === 0) { + return res.json({ success: true, data: { isLast: false } }); + } - const { wo_id, seq_no, parent_process_id } = process.rows[0]; + const { wo_id, seq_no, parent_process_id } = process.rows[0]; - // ๋ถ„ํ•  ํ–‰์ด๋ฉด ๋งˆ์Šคํ„ฐ์˜ seq_no ๊ธฐ์ค€์œผ๋กœ ํŒ๋‹จ - let effectiveSeqNo = seq_no; - if (parent_process_id) { - const master = await pool.query( - `SELECT seq_no FROM work_order_process WHERE id = $1 AND company_code = $2`, - [parent_process_id, companyCode] - ); - if (master.rowCount > 0) { - effectiveSeqNo = master.rows[0].seq_no; - } - } + // ๋ถ„ํ•  ํ–‰์ด๋ฉด ๋งˆ์Šคํ„ฐ์˜ seq_no ๊ธฐ์ค€์œผ๋กœ ํŒ๋‹จ + let effectiveSeqNo = seq_no; + if (parent_process_id) { + const master = await pool.query( + `SELECT seq_no FROM work_order_process WHERE id = $1 AND company_code = $2`, + [parent_process_id, companyCode], + ); + if (master.rowCount > 0) { + effectiveSeqNo = master.rows[0].seq_no; + } + } - const next = await pool.query( - `SELECT id FROM work_order_process + const next = await pool.query( + `SELECT id FROM work_order_process WHERE wo_id = $1 AND company_code = $2 AND CAST(seq_no AS int) > CAST($3 AS int) AND parent_process_id IS NULL LIMIT 1`, - [wo_id, companyCode, effectiveSeqNo] - ); + [wo_id, companyCode, effectiveSeqNo], + ); - // ํ˜„์žฌ ๊ณต์ •์˜ ๊ธฐ์กด ์ฐฝ๊ณ  ์„ค์ •๋„ ๋ฐ˜ํ™˜ (๊ธฐ๋ณธ๊ฐ’ ์„ธํŒ…์šฉ) - const warehouseInfo = await pool.query( - `SELECT target_warehouse_id, target_location_code + // ํ˜„์žฌ ๊ณต์ •์˜ ๊ธฐ์กด ์ฐฝ๊ณ  ์„ค์ •๋„ ๋ฐ˜ํ™˜ (๊ธฐ๋ณธ๊ฐ’ ์„ธํŒ…์šฉ) + const warehouseInfo = await pool.query( + `SELECT target_warehouse_id, target_location_code FROM work_order_process WHERE id = $1 AND company_code = $2`, - [processId, companyCode] - ); + [processId, companyCode], + ); - return res.json({ - success: true, - data: { - isLast: next.rowCount === 0, - woId: wo_id, - seqNo: effectiveSeqNo, - targetWarehouseId: warehouseInfo.rows[0]?.target_warehouse_id || null, - targetLocationCode: warehouseInfo.rows[0]?.target_location_code || null, - }, - }); - } catch (error: any) { - logger.error("[pop/production] ๋งˆ์ง€๋ง‰ ๊ณต์ • ํ™•์ธ ์˜ค๋ฅ˜:", error); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ + success: true, + data: { + isLast: next.rowCount === 0, + woId: wo_id, + seqNo: effectiveSeqNo, + targetWarehouseId: warehouseInfo.rows[0]?.target_warehouse_id || null, + targetLocationCode: warehouseInfo.rows[0]?.target_location_code || null, + }, + }); + } catch (error: any) { + logger.error("[pop/production] ๋งˆ์ง€๋ง‰ ๊ณต์ • ํ™•์ธ ์˜ค๋ฅ˜:", error); + return res.status(500).json({ success: false, message: error.message }); + } }; /** @@ -2395,56 +2571,69 @@ export const isLastProcess = async ( * ๋งˆ์Šคํ„ฐ ํ–‰์— ์ €์žฅํ•˜์—ฌ checkAndCompleteWorkInstruction์ด ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค. */ export const updateTargetWarehouse = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const { work_order_process_id, target_warehouse_id, target_location_code } = req.body; + const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { work_order_process_id, target_warehouse_id, target_location_code } = + req.body; - if (!work_order_process_id || !target_warehouse_id) { - return res.status(400).json({ - success: false, - message: "work_order_process_id์™€ target_warehouse_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", - }); - } + if (!work_order_process_id || !target_warehouse_id) { + return res.status(400).json({ + success: false, + message: "work_order_process_id์™€ target_warehouse_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", + }); + } - // ๋ถ„ํ•  ํ–‰์ด๋ฉด ๋งˆ์Šคํ„ฐ ํ–‰๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ - const procInfo = await pool.query( - `SELECT parent_process_id FROM work_order_process WHERE id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); + // ๋ถ„ํ•  ํ–‰์ด๋ฉด ๋งˆ์Šคํ„ฐ ํ–‰๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ + const procInfo = await pool.query( + `SELECT parent_process_id FROM work_order_process WHERE id = $1 AND company_code = $2`, + [work_order_process_id, companyCode], + ); - const idsToUpdate = [work_order_process_id]; - if (procInfo.rowCount > 0 && procInfo.rows[0].parent_process_id) { - idsToUpdate.push(procInfo.rows[0].parent_process_id); - } + const idsToUpdate = [work_order_process_id]; + if (procInfo.rowCount > 0 && procInfo.rows[0].parent_process_id) { + idsToUpdate.push(procInfo.rows[0].parent_process_id); + } - for (const id of idsToUpdate) { - await pool.query( - `UPDATE work_order_process + for (const id of idsToUpdate) { + await pool.query( + `UPDATE work_order_process SET target_warehouse_id = $3, target_location_code = $4, writer = $5, updated_date = NOW() WHERE id = $1 AND company_code = $2`, - [id, companyCode, target_warehouse_id, target_location_code || null, userId] - ); - } + [ + id, + companyCode, + target_warehouse_id, + target_location_code || null, + userId, + ], + ); + } - logger.info("[pop/production] ๋ชฉํ‘œ ์ฐฝ๊ณ  ์—…๋ฐ์ดํŠธ", { - companyCode, userId, work_order_process_id, - target_warehouse_id, target_location_code, - updatedIds: idsToUpdate, - }); + logger.info("[pop/production] ๋ชฉํ‘œ ์ฐฝ๊ณ  ์—…๋ฐ์ดํŠธ", { + companyCode, + userId, + work_order_process_id, + target_warehouse_id, + target_location_code, + updatedIds: idsToUpdate, + }); - return res.json({ success: true, data: { target_warehouse_id, target_location_code } }); - } catch (error: any) { - logger.error("[pop/production] ๋ชฉํ‘œ ์ฐฝ๊ณ  ์—…๋ฐ์ดํŠธ ์˜ค๋ฅ˜:", error); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ + success: true, + data: { target_warehouse_id, target_location_code }, + }); + } catch (error: any) { + logger.error("[pop/production] ๋ชฉํ‘œ ์ฐฝ๊ณ  ์—…๋ฐ์ดํŠธ ์˜ค๋ฅ˜:", error); + return res.status(500).json({ success: false, message: error.message }); + } }; /** @@ -2454,153 +2643,167 @@ export const updateTargetWarehouse = async ( * ์ด์ค‘ ์ž…๊ณ  ๋ฐฉ์ง€: target_warehouse_id๊ฐ€ ์ด๋ฏธ ์„ค์ •๋œ ๊ฒฝ์šฐ "์ด๋ฏธ ์ž…๊ณ ๋จ" ๋ฐ˜ํ™˜. */ export const inventoryInbound = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); - const client = await pool.connect(); + const pool = getPool(); + const client = await pool.connect(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const { work_order_process_id, warehouse_code, location_code } = req.body; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { work_order_process_id, warehouse_code, location_code } = req.body; - if (!work_order_process_id || !warehouse_code) { - return res.status(400).json({ - success: false, - message: "work_order_process_id์™€ warehouse_code๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", - }); - } + if (!work_order_process_id || !warehouse_code) { + return res.status(400).json({ + success: false, + message: "work_order_process_id์™€ warehouse_code๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", + }); + } - await client.query("BEGIN"); + await client.query("BEGIN"); - // 1. work_order_process์—์„œ wo_id, good_qty, parent_process_id, ๊ธฐ์กด target_warehouse_id ์กฐํšŒ - const procResult = await client.query( - `SELECT wo_id, good_qty, concession_qty, parent_process_id, target_warehouse_id, seq_no, is_rework + // 1. work_order_process์—์„œ wo_id, good_qty, parent_process_id, ๊ธฐ์กด target_warehouse_id ์กฐํšŒ + const procResult = await client.query( + `SELECT wo_id, good_qty, concession_qty, parent_process_id, target_warehouse_id, seq_no, is_rework FROM work_order_process WHERE id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); + [work_order_process_id, companyCode], + ); - if (procResult.rowCount === 0) { - await client.query("ROLLBACK"); - return res.status(404).json({ - success: false, - message: "ํ•ด๋‹น ๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - }); - } + if (procResult.rowCount === 0) { + await client.query("ROLLBACK"); + return res.status(404).json({ + success: false, + message: "ํ•ด๋‹น ๊ณต์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }); + } - const proc = procResult.rows[0]; + const proc = procResult.rows[0]; - // ์ด์ค‘ ์ž…๊ณ  ๋ฐฉ์ง€: ์ด๋ฏธ target_warehouse_id๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฉด ๊ฑฐ๋ถ€ - if (proc.target_warehouse_id) { - await client.query("ROLLBACK"); - return res.status(409).json({ - success: false, - message: "์ด๋ฏธ ์žฌ๊ณ  ์ž…๊ณ ๊ฐ€ ์™„๋ฃŒ๋œ ๊ณต์ •์ž…๋‹ˆ๋‹ค.", - data: { existing_warehouse: proc.target_warehouse_id }, - }); - } + // ์ด์ค‘ ์ž…๊ณ  ๋ฐฉ์ง€: ์ด๋ฏธ target_warehouse_id๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฉด ๊ฑฐ๋ถ€ + if (proc.target_warehouse_id) { + await client.query("ROLLBACK"); + return res.status(409).json({ + success: false, + message: "์ด๋ฏธ ์žฌ๊ณ  ์ž…๊ณ ๊ฐ€ ์™„๋ฃŒ๋œ ๊ณต์ •์ž…๋‹ˆ๋‹ค.", + data: { existing_warehouse: proc.target_warehouse_id }, + }); + } - const goodQty = parseInt(proc.good_qty || "0", 10) + parseInt(proc.concession_qty || "0", 10); + const goodQty = + parseInt(proc.good_qty || "0", 10) + + parseInt(proc.concession_qty || "0", 10); - if (goodQty <= 0) { - await client.query("ROLLBACK"); - return res.status(400).json({ - success: false, - message: "์–‘ํ’ˆ ์ˆ˜๋Ÿ‰์ด 0์ด๋ฏ€๋กœ ์žฌ๊ณ  ์ž…๊ณ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - }); - } + if (goodQty <= 0) { + await client.query("ROLLBACK"); + return res.status(400).json({ + success: false, + message: "์–‘ํ’ˆ ์ˆ˜๋Ÿ‰์ด 0์ด๋ฏ€๋กœ ์žฌ๊ณ  ์ž…๊ณ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }); + } - // 2. work_instruction์—์„œ item_id ์กฐํšŒ - const wiResult = await client.query( - `SELECT item_id FROM work_instruction WHERE id = $1 AND company_code = $2`, - [proc.wo_id, companyCode] - ); + // 2. work_instruction์—์„œ item_id ์กฐํšŒ + const wiResult = await client.query( + `SELECT item_id FROM work_instruction WHERE id = $1 AND company_code = $2`, + [proc.wo_id, companyCode], + ); - if (wiResult.rowCount === 0) { - await client.query("ROLLBACK"); - return res.status(404).json({ - success: false, - message: "์ž‘์—…์ง€์‹œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - }); - } + if (wiResult.rowCount === 0) { + await client.query("ROLLBACK"); + return res.status(404).json({ + success: false, + message: "์ž‘์—…์ง€์‹œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }); + } - const itemId = wiResult.rows[0].item_id; + const itemId = wiResult.rows[0].item_id; - // 3. item_info์—์„œ item_number ์กฐํšŒ - const itemResult = await client.query( - `SELECT item_number FROM item_info WHERE id = $1 AND company_code = $2`, - [itemId, companyCode] - ); + // 3. item_info์—์„œ item_number ์กฐํšŒ + const itemResult = await client.query( + `SELECT item_number FROM item_info WHERE id = $1 AND company_code = $2`, + [itemId, companyCode], + ); - if (itemResult.rowCount === 0) { - await client.query("ROLLBACK"); - return res.status(404).json({ - success: false, - message: "ํ’ˆ๋ชฉ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - }); - } + if (itemResult.rowCount === 0) { + await client.query("ROLLBACK"); + return res.status(404).json({ + success: false, + message: "ํ’ˆ๋ชฉ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }); + } - const itemCode = itemResult.rows[0].item_number; - const effectiveLocationCode = location_code || null; + const itemCode = itemResult.rows[0].item_number; + const effectiveLocationCode = location_code || null; - // 4. inventory_stock UPSERT (PC receivingController์™€ ๋™์ผํ•œ SELECTโ†’INSERT/UPDATE ํŒจํ„ด) - await upsertInventoryStock(client, companyCode, itemCode, warehouse_code, effectiveLocationCode, goodQty, userId); + // 4. inventory_stock UPSERT (PC receivingController์™€ ๋™์ผํ•œ SELECTโ†’INSERT/UPDATE ํŒจํ„ด) + await upsertInventoryStock( + client, + companyCode, + itemCode, + warehouse_code, + effectiveLocationCode, + goodQty, + userId, + ); - // 5. work_order_process์— target_warehouse_id ์ €์žฅ (ํ˜„์žฌ ํ–‰ + ๋งˆ์Šคํ„ฐ ํ–‰) - const idsToUpdate = [work_order_process_id]; - if (proc.parent_process_id) { - idsToUpdate.push(proc.parent_process_id); - } + // 5. work_order_process์— target_warehouse_id ์ €์žฅ (ํ˜„์žฌ ํ–‰ + ๋งˆ์Šคํ„ฐ ํ–‰) + const idsToUpdate = [work_order_process_id]; + if (proc.parent_process_id) { + idsToUpdate.push(proc.parent_process_id); + } - for (const id of idsToUpdate) { - await client.query( - `UPDATE work_order_process + for (const id of idsToUpdate) { + await client.query( + `UPDATE work_order_process SET target_warehouse_id = $3, target_location_code = $4, writer = $5, updated_date = NOW() WHERE id = $1 AND company_code = $2`, - [id, companyCode, warehouse_code, location_code || null, userId] - ); - } + [id, companyCode, warehouse_code, location_code || null, userId], + ); + } - // 6. ๋ฆฌ์›Œํฌ ๋งˆํฌ ํ•ด์ œ (์ฐฝ๊ณ  ์ž…๊ณ  = ์ •์ƒ ์ œํ’ˆ ์ธ์ •, ์ด๋ ฅ์€ rework_source_id์— ์˜๊ตฌ ๋ณด์กด) - if (proc.is_rework === "Y") { - await client.query( - `UPDATE work_order_process SET is_rework = NULL, updated_date = NOW() + // 6. ๋ฆฌ์›Œํฌ ๋งˆํฌ ํ•ด์ œ (์ฐฝ๊ณ  ์ž…๊ณ  = ์ •์ƒ ์ œํ’ˆ ์ธ์ •, ์ด๋ ฅ์€ rework_source_id์— ์˜๊ตฌ ๋ณด์กด) + if (proc.is_rework === "Y") { + await client.query( + `UPDATE work_order_process SET is_rework = NULL, updated_date = NOW() WHERE id = $1 AND company_code = $2`, - [work_order_process_id, companyCode] - ); - } + [work_order_process_id, companyCode], + ); + } - await client.query("COMMIT"); + await client.query("COMMIT"); - logger.info("[pop/production] ๋…๋ฆฝ ์žฌ๊ณ  ์ž…๊ณ  ์™„๋ฃŒ", { - companyCode, userId, work_order_process_id, - itemCode, warehouse_code, location_code: effectiveLocationCode, - qty: goodQty, - reworkCleared: proc.is_rework === "Y", - }); + logger.info("[pop/production] ๋…๋ฆฝ ์žฌ๊ณ  ์ž…๊ณ  ์™„๋ฃŒ", { + companyCode, + userId, + work_order_process_id, + itemCode, + warehouse_code, + location_code: effectiveLocationCode, + qty: goodQty, + reworkCleared: proc.is_rework === "Y", + }); - return res.json({ - success: true, - message: "์žฌ๊ณ  ์ž…๊ณ ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", - data: { - item_code: itemCode, - warehouse_code, - location_code: effectiveLocationCode, - qty: goodQty, - }, - }); - } catch (error: any) { - await client.query("ROLLBACK").catch(() => {}); - logger.error("[pop/production] ๋…๋ฆฝ ์žฌ๊ณ  ์ž…๊ณ  ์˜ค๋ฅ˜:", error); - return res.status(500).json({ success: false, message: error.message }); - } finally { - client.release(); - } + return res.json({ + success: true, + message: "์žฌ๊ณ  ์ž…๊ณ ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + data: { + item_code: itemCode, + warehouse_code, + location_code: effectiveLocationCode, + qty: goodQty, + }, + }); + } catch (error: any) { + await client.query("ROLLBACK").catch(() => {}); + logger.error("[pop/production] ๋…๋ฆฝ ์žฌ๊ณ  ์ž…๊ณ  ์˜ค๋ฅ˜:", error); + return res.status(500).json({ success: false, message: error.message }); + } finally { + client.release(); + } }; /** @@ -2608,73 +2811,81 @@ export const inventoryInbound = async ( * ํ’ˆ๋ชฉ + ์ˆ˜๋Ÿ‰ + ์ฐฝ๊ณ ๋งŒ์œผ๋กœ inventory_stock UPSERT + inbound_mng ์ด๋ ฅ ๊ธฐ๋ก */ export const quickInventoryInbound = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); - const client = await pool.connect(); + const pool = getPool(); + const client = await pool.connect(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const { item_id, qty, warehouse_code, location_code, remark } = req.body; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { item_id, qty, warehouse_code, location_code, remark } = req.body; - // ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ - if (!item_id || !qty || !warehouse_code) { - return res.status(400).json({ - success: false, - message: "item_id, qty, warehouse_code๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", - }); - } + // ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ + if (!item_id || !qty || !warehouse_code) { + return res.status(400).json({ + success: false, + message: "item_id, qty, warehouse_code๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", + }); + } - const parsedQty = parseInt(String(qty), 10); - if (isNaN(parsedQty) || parsedQty <= 0) { - return res.status(400).json({ - success: false, - message: "์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์˜ ์ •์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", - }); - } + const parsedQty = parseInt(String(qty), 10); + if (isNaN(parsedQty) || parsedQty <= 0) { + return res.status(400).json({ + success: false, + message: "์ˆ˜๋Ÿ‰์€ 1 ์ด์ƒ์˜ ์ •์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + }); + } - await client.query("BEGIN"); + await client.query("BEGIN"); - // 1. item_info์—์„œ item_number, item_name ์กฐํšŒ - const itemResult = await client.query( - `SELECT item_number, item_name, size, material, unit + // 1. item_info์—์„œ item_number, item_name ์กฐํšŒ + const itemResult = await client.query( + `SELECT item_number, item_name, size, material, unit FROM item_info WHERE id = $1 AND company_code = $2`, - [item_id, companyCode] - ); + [item_id, companyCode], + ); - if (itemResult.rowCount === 0) { - await client.query("ROLLBACK"); - return res.status(404).json({ - success: false, - message: "ํ’ˆ๋ชฉ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", - }); - } + if (itemResult.rowCount === 0) { + await client.query("ROLLBACK"); + return res.status(404).json({ + success: false, + message: "ํ’ˆ๋ชฉ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + }); + } - const item = itemResult.rows[0]; - const itemCode = item.item_number; - const effectiveLocationCode = location_code || null; + const item = itemResult.rows[0]; + const itemCode = item.item_number; + const effectiveLocationCode = location_code || null; - // 2. inventory_stock UPSERT (PC receivingController์™€ ๋™์ผํ•œ SELECTโ†’INSERT/UPDATE ํŒจํ„ด) - await upsertInventoryStock(client, companyCode, itemCode, warehouse_code, effectiveLocationCode, parsedQty, userId); + // 2. inventory_stock UPSERT (PC receivingController์™€ ๋™์ผํ•œ SELECTโ†’INSERT/UPDATE ํŒจํ„ด) + await upsertInventoryStock( + client, + companyCode, + itemCode, + warehouse_code, + effectiveLocationCode, + parsedQty, + userId, + ); - // 3. inbound_mng์— ๊ฐ„์ด์ž…๊ณ  ์ด๋ ฅ ๊ธฐ๋ก - const seqResult = await client.query( - `SELECT COALESCE(MAX( + // 3. inbound_mng์— ๊ฐ„์ด์ž…๊ณ  ์ด๋ ฅ ๊ธฐ๋ก + const seqResult = await client.query( + `SELECT COALESCE(MAX( CASE WHEN inbound_number ~ '^QIB-[0-9]{4}-[0-9]+$' THEN CAST(SUBSTRING(inbound_number FROM '[0-9]+$') AS INTEGER) ELSE 0 END ), 0) + 1 AS next_seq FROM inbound_mng WHERE company_code = $1`, - [companyCode] - ); - const nextSeq = seqResult.rows[0].next_seq; - const year = new Date().getFullYear(); - const inboundNumber = `QIB-${year}-${String(nextSeq).padStart(4, "0")}`; + [companyCode], + ); + const nextSeq = seqResult.rows[0].next_seq; + const year = new Date().getFullYear(); + const inboundNumber = `QIB-${year}-${String(nextSeq).padStart(4, "0")}`; - await client.query( - `INSERT INTO inbound_mng ( + await client.query( + `INSERT INTO inbound_mng ( id, company_code, inbound_number, inbound_type, inbound_date, item_number, item_name, spec, material, unit, inbound_qty, warehouse_code, location_code, @@ -2687,42 +2898,55 @@ export const quickInventoryInbound = async ( '์™„๋ฃŒ', $11, $12, NOW(), NOW(), $13, $13, $13 )`, - [ - companyCode, inboundNumber, - item.item_number, item.item_name, item.size, item.material, item.unit, - parsedQty, warehouse_code, effectiveLocationCode, - remark || "POP ๊ฐ„์ด์ž…๊ณ ", remark || null, - userId, - ] - ); + [ + companyCode, + inboundNumber, + item.item_number, + item.item_name, + item.size, + item.material, + item.unit, + parsedQty, + warehouse_code, + effectiveLocationCode, + remark || "POP ๊ฐ„์ด์ž…๊ณ ", + remark || null, + userId, + ], + ); - await client.query("COMMIT"); + await client.query("COMMIT"); - logger.info("[pop/production] ๊ฐ„์ด ์žฌ๊ณ  ์ž…๊ณ  ์™„๋ฃŒ", { - companyCode, userId, item_id, - itemCode, warehouse_code, location_code: effectiveLocationCode, - qty: parsedQty, inboundNumber, - }); + logger.info("[pop/production] ๊ฐ„์ด ์žฌ๊ณ  ์ž…๊ณ  ์™„๋ฃŒ", { + companyCode, + userId, + item_id, + itemCode, + warehouse_code, + location_code: effectiveLocationCode, + qty: parsedQty, + inboundNumber, + }); - return res.json({ - success: true, - message: "๊ฐ„์ด ์žฌ๊ณ  ์ž…๊ณ ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", - data: { - inbound_number: inboundNumber, - item_code: itemCode, - item_name: item.item_name, - warehouse_code, - location_code: effectiveLocationCode, - qty: parsedQty, - }, - }); - } catch (error: any) { - await client.query("ROLLBACK").catch(() => {}); - logger.error("[pop/production] ๊ฐ„์ด ์žฌ๊ณ  ์ž…๊ณ  ์˜ค๋ฅ˜:", error); - return res.status(500).json({ success: false, message: error.message }); - } finally { - client.release(); - } + return res.json({ + success: true, + message: "๊ฐ„์ด ์žฌ๊ณ  ์ž…๊ณ ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + data: { + inbound_number: inboundNumber, + item_code: itemCode, + item_name: item.item_name, + warehouse_code, + location_code: effectiveLocationCode, + qty: parsedQty, + }, + }); + } catch (error: any) { + await client.query("ROLLBACK").catch(() => {}); + logger.error("[pop/production] ๊ฐ„์ด ์žฌ๊ณ  ์ž…๊ณ  ์˜ค๋ฅ˜:", error); + return res.status(500).json({ success: false, message: error.message }); + } finally { + client.release(); + } }; /** @@ -2730,17 +2954,22 @@ export const quickInventoryInbound = async ( * ์ž‘์—…์ง€์‹œ(wo_id) ๊ธฐ์ค€์œผ๋กœ ๋ชจ๋“  ์žฌ์ž‘์—… ์ฒด์ธ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. * ์›๋ณธ โ†’ ์žฌ์ž‘์—…1 โ†’ ์žฌ์ž‘์—…2 โ†’ ... ์ˆœ์„œ๋กœ ์ฒด์ธ ์ถ”์ . */ -export const getReworkHistory = async (req: AuthenticatedRequest, res: Response) => { - const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const woId = req.query.wo_id as string || req.params.woId; - if (!woId) { - return res.status(400).json({ success: false, message: "wo_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค." }); - } +export const getReworkHistory = async ( + req: AuthenticatedRequest, + res: Response, +) => { + const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const woId = (req.query.wo_id as string) || req.params.woId; + if (!woId) { + return res + .status(400) + .json({ success: false, message: "wo_id๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค." }); + } - const result = await pool.query( - `SELECT id, seq_no, process_code, process_name, status, + const result = await pool.query( + `SELECT id, seq_no, process_code, process_name, status, input_qty, good_qty, defect_qty, concession_qty, is_rework, rework_source_id, parent_process_id, accepted_by, accepted_at, started_at, completed_at, @@ -2749,195 +2978,240 @@ export const getReworkHistory = async (req: AuthenticatedRequest, res: Response) WHERE wo_id = $1 AND company_code = $2 AND (is_rework = 'Y' OR is_rework = '1' OR defect_qty::int > 0 OR parent_process_id IS NOT NULL) ORDER BY created_date ASC`, - [woId, companyCode] - ); + [woId, companyCode], + ); - // ์ฒด์ธ ๊ตฌ์„ฑ: rework_source_id๋ฅผ ๋”ฐ๋ผ ํŠธ๋ฆฌ ๊ตฌ์กฐ - const rows = result.rows; - const byId: Record = {}; - for (const r of rows) byId[r.id] = r; + // ์ฒด์ธ ๊ตฌ์„ฑ: rework_source_id๋ฅผ ๋”ฐ๋ผ ํŠธ๋ฆฌ ๊ตฌ์กฐ + const rows = result.rows; + const byId: Record = {}; + for (const r of rows) byId[r.id] = r; - const chains: Array<{ - source: typeof rows[0]; - reworks: typeof rows; - totalReworkCount: number; - }> = []; + const chains: Array<{ + source: (typeof rows)[0]; + reworks: typeof rows; + totalReworkCount: number; + }> = []; - // ์›๋ณธ ํ–‰(๋ถˆ๋Ÿ‰ ๋ฐœ์ƒํ•œ ๊ฒƒ) ์ฐพ๊ธฐ - const reworkSourceIds = new Set(rows.filter(r => r.rework_source_id).map(r => r.rework_source_id)); - const sources = rows.filter(r => reworkSourceIds.has(r.id) || (parseInt(r.defect_qty || "0", 10) > 0 && r.is_rework !== "Y")); + // ์›๋ณธ ํ–‰(๋ถˆ๋Ÿ‰ ๋ฐœ์ƒํ•œ ๊ฒƒ) ์ฐพ๊ธฐ + const reworkSourceIds = new Set( + rows.filter((r) => r.rework_source_id).map((r) => r.rework_source_id), + ); + const sources = rows.filter( + (r) => + reworkSourceIds.has(r.id) || + (parseInt(r.defect_qty || "0", 10) > 0 && r.is_rework !== "Y"), + ); - for (const src of sources) { - const chain: typeof rows = []; - const visited = new Set(); - // ์ด ์†Œ์Šค์—์„œ ์‹œ์ž‘ํ•˜๋Š” ์žฌ์ž‘์—… ์ฒด์ธ ์ถ”์  - const queue = rows.filter(r => r.rework_source_id === src.id); - while (queue.length > 0) { - const item = queue.shift()!; - if (visited.has(item.id)) continue; - visited.add(item.id); - chain.push(item); - // ์ด ์žฌ์ž‘์—…์—์„œ ๋˜ ์žฌ์ž‘์—…์ด ๋‚˜์˜จ ๊ฒƒ ์ฐพ๊ธฐ - const next = rows.filter(r => r.rework_source_id === item.id); - queue.push(...next); - } - chains.push({ - source: src, - reworks: chain, - totalReworkCount: chain.length, - }); - } + for (const src of sources) { + const chain: typeof rows = []; + const visited = new Set(); + // ์ด ์†Œ์Šค์—์„œ ์‹œ์ž‘ํ•˜๋Š” ์žฌ์ž‘์—… ์ฒด์ธ ์ถ”์  + const queue = rows.filter((r) => r.rework_source_id === src.id); + while (queue.length > 0) { + const item = queue.shift()!; + if (visited.has(item.id)) continue; + visited.add(item.id); + chain.push(item); + // ์ด ์žฌ์ž‘์—…์—์„œ ๋˜ ์žฌ์ž‘์—…์ด ๋‚˜์˜จ ๊ฒƒ ์ฐพ๊ธฐ + const next = rows.filter((r) => r.rework_source_id === item.id); + queue.push(...next); + } + chains.push({ + source: src, + reworks: chain, + totalReworkCount: chain.length, + }); + } - return res.json({ - success: true, - data: { - wo_id: woId, - total_rework_count: rows.filter(r => r.is_rework === "Y" || r.is_rework === "1").length, - chains, - all_records: rows, - }, - }); - } catch (error: any) { - logger.error("[pop/production] rework-history ์˜ค๋ฅ˜:", error); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ + success: true, + data: { + wo_id: woId, + total_rework_count: rows.filter( + (r) => r.is_rework === "Y" || r.is_rework === "1", + ).length, + chains, + all_records: rows, + }, + }); + } catch (error: any) { + logger.error("[pop/production] rework-history ์˜ค๋ฅ˜:", error); + return res.status(500).json({ success: false, message: error.message }); + } }; /** * ๊ณต์ •๋ณ„ BOM ์ž์žฌ ๋ชฉ๋ก + ์†Œ์š”๋Ÿ‰ ๊ณ„์‚ฐ * work_order_process_id โ†’ item_code โ†’ bom + bom_detail ์กฐํšŒ */ -export const getBomMaterials = async (req: AuthenticatedRequest, res: Response) => { - const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const { processId } = req.params; - if (!processId) { - return res.status(400).json({ success: false, message: "processId ํ•„์ˆ˜" }); - } +export const getBomMaterials = async ( + req: AuthenticatedRequest, + res: Response, +) => { + const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const { processId } = req.params; + if (!processId) { + return res + .status(400) + .json({ success: false, message: "processId ํ•„์ˆ˜" }); + } - // 1. work_order_process โ†’ work_instruction โ†’ item_code, plan_qty - const procResult = await pool.query( - `SELECT wop.wo_id, wop.process_code, wop.input_qty, wop.plan_qty, + // 1. work_order_process โ†’ work_instruction โ†’ item_code, plan_qty + const procResult = await pool.query( + `SELECT wop.wo_id, wop.process_code, wop.input_qty, wop.plan_qty, wi.item_id, wi.qty as instruction_qty FROM work_order_process wop JOIN work_instruction wi ON wop.wo_id = wi.id AND wop.company_code = wi.company_code WHERE wop.id = $1 AND wop.company_code = $2`, - [processId, companyCode] - ); - if (procResult.rowCount === 0) { - return res.json({ success: true, data: { materials: [], processQty: 0 } }); - } - const proc = procResult.rows[0]; - const processQty = parseInt(proc.input_qty || proc.plan_qty || proc.instruction_qty || "0", 10); + [processId, companyCode], + ); + if (procResult.rowCount === 0) { + return res.json({ + success: true, + data: { materials: [], processQty: 0 }, + }); + } + const proc = procResult.rows[0]; + const processQty = parseInt( + proc.input_qty || proc.plan_qty || proc.instruction_qty || "0", + 10, + ); - // 2. item_info โ†’ item_code (item_number) - const itemResult = await pool.query( - `SELECT item_number, item_name FROM item_info WHERE id = $1 AND company_code = $2`, - [proc.item_id, companyCode] - ); - if (itemResult.rowCount === 0) { - return res.json({ success: true, data: { materials: [], processQty } }); - } - const itemCode = itemResult.rows[0].item_number; + // 2. item_info โ†’ item_code (item_number) + const itemResult = await pool.query( + `SELECT item_number, item_name FROM item_info WHERE id = $1 AND company_code = $2`, + [proc.item_id, companyCode], + ); + if (itemResult.rowCount === 0) { + return res.json({ success: true, data: { materials: [], processQty } }); + } + const itemCode = itemResult.rows[0].item_number; - // 3. BOM ์กฐํšŒ - const bomResult = await pool.query( - `SELECT bd.id, bd.child_item_id, bd.quantity, bd.unit, bd.process_type, bd.loss_rate, + // 3. BOM ์กฐํšŒ + const bomResult = await pool.query( + `SELECT bd.id, bd.child_item_id, bd.quantity, bd.unit, bd.process_type, bd.loss_rate, i.item_name as child_item_name, i.item_number as child_item_code, i.unit as item_unit FROM bom b JOIN bom_detail bd ON b.id = bd.bom_id AND b.company_code = bd.company_code LEFT JOIN item_info i ON bd.child_item_id = i.id AND i.company_code = b.company_code WHERE (b.item_code = $1 OR b.item_id = $2) AND b.company_code = $3 ORDER BY bd.seq_no ASC`, - [itemCode, proc.item_id, companyCode] - ); + [itemCode, proc.item_id, companyCode], + ); - // 4. ์†Œ์š”๋Ÿ‰ ๊ณ„์‚ฐ - const bomBase = await pool.query( - `SELECT base_qty FROM bom WHERE (item_code = $1 OR item_id = $2) AND company_code = $3 LIMIT 1`, - [itemCode, proc.item_id, companyCode] - ); - const baseQty = parseFloat(bomBase.rows[0]?.base_qty || "1") || 1; + // 4. ์†Œ์š”๋Ÿ‰ ๊ณ„์‚ฐ + const bomBase = await pool.query( + `SELECT base_qty FROM bom WHERE (item_code = $1 OR item_id = $2) AND company_code = $3 LIMIT 1`, + [itemCode, proc.item_id, companyCode], + ); + const baseQty = parseFloat(bomBase.rows[0]?.base_qty || "1") || 1; - // ๊ธฐ์กด ํˆฌ์ž…๋Ÿ‰ ์กฐํšŒ (item_code๋ณ„ ํ•ฉ์‚ฐ โ€” detail_content์— item_code ์ €์žฅ๋จ) - const inputResult = await pool.query( - `SELECT detail_content as item_code, SUM(CAST(NULLIF(result_value, '') AS numeric)) as total_input + // ๊ธฐ์กด ํˆฌ์ž…๋Ÿ‰ ์กฐํšŒ (item_code๋ณ„ ํ•ฉ์‚ฐ โ€” detail_content์— item_code ์ €์žฅ๋จ) + const inputResult = await pool.query( + `SELECT detail_content as item_code, SUM(CAST(NULLIF(result_value, '') AS numeric)) as total_input FROM process_work_result WHERE work_order_process_id = $1 AND company_code = $2 AND detail_type = 'material_input' AND result_value IS NOT NULL AND result_value != '' GROUP BY detail_content`, - [processId, companyCode] - ); - const inputMap = new Map(); - for (const row of inputResult.rows) { - inputMap.set(String(row.item_code), parseFloat(row.total_input) || 0); - } + [processId, companyCode], + ); + const inputMap = new Map(); + for (const row of inputResult.rows) { + inputMap.set(String(row.item_code), parseFloat(row.total_input) || 0); + } - const materials = bomResult.rows.map((bd: Record) => { - const bomQty = parseFloat(String(bd.quantity || "0")) || 0; - const lossRate = parseFloat(String(bd.loss_rate || "0")) || 0; - const requiredQty = Math.ceil((processQty / baseQty) * bomQty * (1 + lossRate / 100)); - const childItemCode = String(bd.child_item_code || ""); - return { - id: bd.id, - child_item_id: bd.child_item_id, - child_item_code: childItemCode, - child_item_name: bd.child_item_name || "", - bom_qty: bomQty, - unit: bd.unit || bd.item_unit || "", - process_type: bd.process_type || "", - loss_rate: lossRate, - required_qty: requiredQty, - input_qty: inputMap.get(childItemCode) || 0, - }; - }); + const materials = bomResult.rows.map((bd: Record) => { + const bomQty = parseFloat(String(bd.quantity || "0")) || 0; + const lossRate = parseFloat(String(bd.loss_rate || "0")) || 0; + const requiredQty = Math.ceil( + (processQty / baseQty) * bomQty * (1 + lossRate / 100), + ); + const childItemCode = String(bd.child_item_code || ""); + return { + id: bd.id, + child_item_id: bd.child_item_id, + child_item_code: childItemCode, + child_item_name: bd.child_item_name || "", + bom_qty: bomQty, + unit: bd.unit || bd.item_unit || "", + process_type: bd.process_type || "", + loss_rate: lossRate, + required_qty: requiredQty, + input_qty: inputMap.get(childItemCode) || 0, + }; + }); - return res.json({ - success: true, - data: { materials, processQty, baseQty, itemCode, itemName: itemResult.rows[0].item_name }, - }); - } catch (error: any) { - logger.error("[pop/production] bom-materials ์˜ค๋ฅ˜:", error); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ + success: true, + data: { + materials, + processQty, + baseQty, + itemCode, + itemName: itemResult.rows[0].item_name, + }, + }); + } catch (error: any) { + logger.error("[pop/production] bom-materials ์˜ค๋ฅ˜:", error); + return res.status(500).json({ success: false, message: error.message }); + } }; /** * ์ž์žฌ ํˆฌ์ž… ๊ธฐ๋ก ์ €์žฅ * BOM ๊ธฐ์ค€๊ณผ ๋‹ค๋ฅธ ์ˆ˜๋Ÿ‰๋„ ํ—ˆ์šฉ (์œ ๋™ ํˆฌ์ž…) */ -export const saveMaterialInput = async (req: AuthenticatedRequest, res: Response) => { - const pool = getPool(); - const client = await pool.connect(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const { work_order_process_id, inputs } = req.body; +export const saveMaterialInput = async ( + req: AuthenticatedRequest, + res: Response, +) => { + const pool = getPool(); + const client = await pool.connect(); + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { work_order_process_id, inputs } = req.body; - if (!work_order_process_id || !inputs || !Array.isArray(inputs)) { - return res.status(400).json({ success: false, message: "work_order_process_id, inputs[] ํ•„์ˆ˜" }); - } + if (!work_order_process_id || !inputs || !Array.isArray(inputs)) { + return res.status(400).json({ + success: false, + message: "work_order_process_id, inputs[] ํ•„์ˆ˜", + }); + } - await client.query("BEGIN"); + await client.query("BEGIN"); - const results = []; - for (const input of inputs) { - const { child_item_id, child_item_code, child_item_name, input_qty, unit, bom_detail_id, required_qty, warehouse_code, location_code } = input; - // item_code/qty ๋“ฑ ๋Œ€์•ˆ ํ•„๋“œ๋ช…๋„ ํ—ˆ์šฉ - const effectiveItemId = child_item_id || input.item_id || input.item_code || child_item_code; - const effectiveItemCode = child_item_code || input.item_code || child_item_id; - const effectiveItemName = child_item_name || input.item_name || ""; - const effectiveQty = input_qty || input.qty || input.quantity; + const results = []; + for (const input of inputs) { + const { + child_item_id, + child_item_code, + child_item_name, + input_qty, + unit, + bom_detail_id, + required_qty, + warehouse_code, + location_code, + } = input; + // item_code/qty ๋“ฑ ๋Œ€์•ˆ ํ•„๋“œ๋ช…๋„ ํ—ˆ์šฉ + const effectiveItemId = + child_item_id || input.item_id || input.item_code || child_item_code; + const effectiveItemCode = + child_item_code || input.item_code || child_item_id; + const effectiveItemName = child_item_name || input.item_name || ""; + const effectiveQty = input_qty || input.qty || input.quantity; - if (!effectiveItemId || !effectiveQty) continue; + if (!effectiveItemId || !effectiveQty) continue; - const parsedQty = parseFloat(String(effectiveQty)); - if (isNaN(parsedQty) || parsedQty <= 0) continue; + const parsedQty = parseFloat(String(effectiveQty)); + if (isNaN(parsedQty) || parsedQty <= 0) continue; - // ํˆฌ์ž… ๊ธฐ๋ก INSERT (process_work_result์— material_input ํƒ€์ž…์œผ๋กœ) - const insertResult = await client.query( - `INSERT INTO process_work_result ( + // ํˆฌ์ž… ๊ธฐ๋ก INSERT (process_work_result์— material_input ํƒ€์ž…์œผ๋กœ) + const insertResult = await client.query( + `INSERT INTO process_work_result ( id, company_code, work_order_process_id, detail_type, detail_content, item_title, result_value, unit, is_passed, status, @@ -2948,74 +3222,113 @@ export const saveMaterialInput = async (req: AuthenticatedRequest, res: Response $5, $6, 'Y', 'completed', $7, $8, NOW()::text, $8 ) RETURNING id`, - [ - companyCode, work_order_process_id, - effectiveItemCode || effectiveItemId, effectiveItemName, - String(parsedQty), unit || "", - JSON.stringify({ bom_detail_id, required_qty: required_qty || 0, warehouse_code, location_code }), - userId, - ] - ); + [ + companyCode, + work_order_process_id, + effectiveItemCode || effectiveItemId, + effectiveItemName, + String(parsedQty), + unit || "", + JSON.stringify({ + bom_detail_id, + required_qty: required_qty || 0, + warehouse_code, + location_code, + }), + userId, + ], + ); - // ์žฌ๊ณ  ์ฐจ๊ฐ (warehouse_code๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ) - if (warehouse_code) { - const locCode = location_code || warehouse_code; - await client.query( - `UPDATE inventory_stock + // ์žฌ๊ณ  ์ฐจ๊ฐ: warehouse_code ์žˆ์œผ๋ฉด ๊ทธ ์ฐฝ๊ณ , ์—†์œผ๋ฉด ์ž๋™์œผ๋กœ ์žฌ๊ณ ๊ฐ€ ์žˆ๋Š” ์ฐฝ๊ณ  ํƒ์ƒ‰ + let effectiveWh = warehouse_code; + let effectiveLoc = location_code; + if (!effectiveWh) { + const autoStock = await client.query( + `SELECT warehouse_code, location_code FROM inventory_stock + WHERE company_code = $1 AND item_code = $2 + AND COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) > 0 + ORDER BY last_in_date DESC NULLS LAST LIMIT 1`, + [companyCode, effectiveItemCode], + ); + if (autoStock.rows.length > 0) { + effectiveWh = autoStock.rows[0].warehouse_code; + effectiveLoc = autoStock.rows[0].location_code || effectiveWh; + } + } + if (effectiveWh) { + const locCode = effectiveLoc || effectiveWh; + await client.query( + `UPDATE inventory_stock SET current_qty = (COALESCE(current_qty::numeric, 0) - $4::numeric)::text, updated_date = NOW(), writer = $5 WHERE company_code = $1 AND item_code = $2 AND warehouse_code = $3 AND location_code = $6`, - [companyCode, effectiveItemCode, warehouse_code, String(parsedQty), userId, locCode] - ); - } + [ + companyCode, + effectiveItemCode, + effectiveWh, + String(parsedQty), + userId, + locCode, + ], + ); + } - results.push({ id: insertResult.rows[0].id, child_item_code: effectiveItemCode, input_qty: parsedQty }); - } + results.push({ + id: insertResult.rows[0].id, + child_item_code: effectiveItemCode, + input_qty: parsedQty, + }); + } - await client.query("COMMIT"); + await client.query("COMMIT"); - return res.json({ - success: true, - message: `${results.length}๊ฑด ์ž์žฌ ํˆฌ์ž… ์™„๋ฃŒ`, - data: results, - }); - } catch (error: any) { - await client.query("ROLLBACK").catch(() => {}); - logger.error("[pop/production] material-input ์˜ค๋ฅ˜:", error); - return res.status(500).json({ success: false, message: error.message }); - } finally { - client.release(); - } + return res.json({ + success: true, + message: `${results.length}๊ฑด ์ž์žฌ ํˆฌ์ž… ์™„๋ฃŒ`, + data: results, + }); + } catch (error: any) { + await client.query("ROLLBACK").catch(() => {}); + logger.error("[pop/production] material-input ์˜ค๋ฅ˜:", error); + return res.status(500).json({ success: false, message: error.message }); + } finally { + client.release(); + } }; /** * ์ž์žฌ ํˆฌ์ž… ํ˜„ํ™ฉ ์กฐํšŒ */ -export const getMaterialInputs = async (req: AuthenticatedRequest, res: Response) => { - const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const { processId } = req.params; - if (!processId) { - return res.status(400).json({ success: false, message: "processId ํ•„์ˆ˜" }); - } +export const getMaterialInputs = async ( + req: AuthenticatedRequest, + res: Response, +) => { + const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const { processId } = req.params; + if (!processId) { + return res + .status(400) + .json({ success: false, message: "processId ํ•„์ˆ˜" }); + } - const result = await pool.query( - `SELECT id, detail_content as item_code, item_title as item_name, + const result = await pool.query( + `SELECT id, detail_content as item_code, item_title as item_name, result_value as input_qty, unit, remark, recorded_by, recorded_at FROM process_work_result WHERE work_order_process_id = $1 AND company_code = $2 AND detail_type = 'material_input' ORDER BY recorded_at ASC`, - [processId, companyCode] - ); + [processId, companyCode], + ); - return res.json({ success: true, data: result.rows }); - } catch (error: any) { - logger.error("[pop/production] material-inputs ์กฐํšŒ ์˜ค๋ฅ˜:", error); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("[pop/production] material-inputs ์กฐํšŒ ์˜ค๋ฅ˜:", error); + return res.status(500).json({ success: false, message: error.message }); + } }; /** @@ -3030,21 +3343,21 @@ export const getMaterialInputs = async (req: AuthenticatedRequest, res: Response * ์œผ๋กœ ์ž…๋ ฅ UI๋ฅผ ๊ฒฐ์ •ํ•œ๋‹ค. */ export const getChecklistItems = async ( - req: AuthenticatedRequest, - res: Response + req: AuthenticatedRequest, + res: Response, ) => { - const pool = getPool(); - try { - const companyCode = req.user!.companyCode; - const { processId } = req.params; - if (!processId) { - return res - .status(400) - .json({ success: false, message: "processId ํ•„์ˆ˜" }); - } + const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const { processId } = req.params; + if (!processId) { + return res + .status(400) + .json({ success: false, message: "processId ํ•„์ˆ˜" }); + } - const result = await pool.query( - `SELECT + const result = await pool.query( + `SELECT pwr.id, pwr.company_code, pwr.work_order_process_id, @@ -3087,14 +3400,12 @@ export const getChecklistItems = async ( ORDER BY COALESCE(NULLIF(pwr.item_sort_order, '')::int, 0), COALESCE(NULLIF(pwr.detail_sort_order, '')::int, 0)`, - [processId, companyCode] - ); + [processId, companyCode], + ); - return res.json({ success: true, data: result.rows }); - } catch (error: any) { - logger.error("[pop/production] checklist-items ์กฐํšŒ ์˜ค๋ฅ˜:", error); - return res - .status(500) - .json({ success: false, message: error.message }); - } + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("[pop/production] checklist-items ์กฐํšŒ ์˜ค๋ฅ˜:", error); + return res.status(500).json({ success: false, message: error.message }); + } }; diff --git a/backend-node/src/controllers/receivingController.ts b/backend-node/src/controllers/receivingController.ts index 80ab4c46..ef732ace 100644 --- a/backend-node/src/controllers/receivingController.ts +++ b/backend-node/src/controllers/receivingController.ts @@ -7,70 +7,65 @@ * - ๊ธฐํƒ€์ž…๊ณ  โ†’ item_info (ํ’ˆ๋ชฉ) */ -import { Response } from "express"; -import { AuthenticatedRequest } from "../types/auth"; +import type { Response } from "express"; import { getPool } from "../database/db"; +import type { AuthenticatedRequest } from "../types/auth"; import { logger } from "../utils/logger"; // ์ž…๊ณ  ๋ชฉ๋ก ์กฐํšŒ (ํ—ค๋”-๋””ํ…Œ์ผ JOIN, ๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜) export async function getList(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const { - inbound_type, - inbound_status, - search_keyword, - date_from, - date_to, - } = req.query; + try { + const companyCode = req.user!.companyCode; + const { inbound_type, inbound_status, search_keyword, date_from, date_to } = + req.query; - const conditions: string[] = []; - const params: any[] = []; - let paramIdx = 1; + const conditions: string[] = []; + const params: any[] = []; + let paramIdx = 1; - if (companyCode === "*") { - // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ์ „์ฒด ์กฐํšŒ - } else { - conditions.push(`im.company_code = $${paramIdx}`); - params.push(companyCode); - paramIdx++; - } + if (companyCode === "*") { + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ์ „์ฒด ์กฐํšŒ + } else { + conditions.push(`im.company_code = $${paramIdx}`); + params.push(companyCode); + paramIdx++; + } - if (inbound_type && inbound_type !== "all") { - conditions.push(`im.inbound_type = $${paramIdx}`); - params.push(inbound_type); - paramIdx++; - } + if (inbound_type && inbound_type !== "all") { + conditions.push(`im.inbound_type = $${paramIdx}`); + params.push(inbound_type); + paramIdx++; + } - if (inbound_status && inbound_status !== "all") { - conditions.push(`im.inbound_status = $${paramIdx}`); - params.push(inbound_status); - paramIdx++; - } + if (inbound_status && inbound_status !== "all") { + conditions.push(`im.inbound_status = $${paramIdx}`); + params.push(inbound_status); + paramIdx++; + } - if (search_keyword) { - conditions.push( - `(im.inbound_number ILIKE $${paramIdx} OR COALESCE(id.item_name, im.item_name) ILIKE $${paramIdx} OR COALESCE(id.item_number, im.item_number) ILIKE $${paramIdx} OR COALESCE(id.supplier_name, im.supplier_name) ILIKE $${paramIdx} OR COALESCE(id.reference_number, im.reference_number) ILIKE $${paramIdx})` - ); - params.push(`%${search_keyword}%`); - paramIdx++; - } + if (search_keyword) { + conditions.push( + `(im.inbound_number ILIKE $${paramIdx} OR COALESCE(id.item_name, im.item_name) ILIKE $${paramIdx} OR COALESCE(id.item_number, im.item_number) ILIKE $${paramIdx} OR COALESCE(id.supplier_name, im.supplier_name) ILIKE $${paramIdx} OR COALESCE(id.reference_number, im.reference_number) ILIKE $${paramIdx})`, + ); + params.push(`%${search_keyword}%`); + paramIdx++; + } - if (date_from) { - conditions.push(`im.inbound_date >= $${paramIdx}::date`); - params.push(date_from); - paramIdx++; - } - if (date_to) { - conditions.push(`im.inbound_date <= $${paramIdx}::date`); - params.push(date_to); - paramIdx++; - } + if (date_from) { + conditions.push(`im.inbound_date >= $${paramIdx}::date`); + params.push(date_from); + paramIdx++; + } + if (date_to) { + conditions.push(`im.inbound_date <= $${paramIdx}::date`); + params.push(date_to); + paramIdx++; + } - const whereClause = - conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + const whereClause = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; - const query = ` + const query = ` SELECT im.id, im.company_code, im.inbound_number, im.inbound_type, im.inbound_date, im.warehouse_code, im.location_code, im.inspector, im.manager, @@ -105,44 +100,55 @@ export async function getList(req: AuthenticatedRequest, res: Response) { ORDER BY im.created_date DESC, id.seq_no ASC `; - const pool = getPool(); - const result = await pool.query(query, params); + const pool = getPool(); + const result = await pool.query(query, params); - logger.info("์ž…๊ณ  ๋ชฉ๋ก ์กฐํšŒ", { - companyCode, - rowCount: result.rowCount, - }); + logger.info("์ž…๊ณ  ๋ชฉ๋ก ์กฐํšŒ", { + companyCode, + rowCount: result.rowCount, + }); - return res.json({ success: true, data: result.rows }); - } catch (error: any) { - logger.error("์ž…๊ณ  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("์ž…๊ณ  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ์ž…๊ณ  ๋“ฑ๋ก (ํ—ค๋” 1๊ฑด + ๋””ํ…Œ์ผ N๊ฑด) export async function create(req: AuthenticatedRequest, res: Response) { - const pool = getPool(); - const client = await pool.connect(); + const pool = getPool(); + const client = await pool.connect(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const { items, inbound_number, inbound_date, warehouse_code, location_code, inspector, manager, memo } = req.body; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { + items, + inbound_number, + inbound_date, + warehouse_code, + location_code, + inspector, + manager, + memo, + } = req.body; - if (!items || !Array.isArray(items) || items.length === 0) { - return res.status(400).json({ success: false, message: "์ž…๊ณ  ํ’ˆ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค." }); - } + if (!items || !Array.isArray(items) || items.length === 0) { + return res + .status(400) + .json({ success: false, message: "์ž…๊ณ  ํ’ˆ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค." }); + } - // ์ฒซ ๋ฒˆ์งธ ์•„์ดํ…œ์—์„œ inbound_type ์ถ”์ถœ (ํ—ค๋”์šฉ) - const inboundType = items[0].inbound_type || null; - const inboundNumber = inbound_number || items[0].inbound_number; + // ์ฒซ ๋ฒˆ์งธ ์•„์ดํ…œ์—์„œ inbound_type ์ถ”์ถœ (ํ—ค๋”์šฉ) + const inboundType = items[0].inbound_type || null; + const inboundNumber = inbound_number || items[0].inbound_number; - await client.query("BEGIN"); + await client.query("BEGIN"); - // 1. ํ—ค๋” INSERT (inbound_mng) โ€” ํ’ˆ๋ชฉ ์ปฌ๋Ÿผ์€ NULL - const headerResult = await client.query( - `INSERT INTO inbound_mng ( + // 1. ํ—ค๋” INSERT (inbound_mng) โ€” ํ’ˆ๋ชฉ ์ปฌ๋Ÿผ์€ NULL + const headerResult = await client.query( + `INSERT INTO inbound_mng ( id, company_code, inbound_number, inbound_type, inbound_date, warehouse_code, location_code, inbound_status, inspector, manager, memo, @@ -153,32 +159,32 @@ export async function create(req: AuthenticatedRequest, res: Response) { $7, $8, $9, $10, NOW(), $11, $11, '์ž…๊ณ ' ) RETURNING *`, - [ - companyCode, - inboundNumber, - inboundType, - inbound_date || items[0].inbound_date, - warehouse_code || items[0].warehouse_code || null, - location_code || items[0].location_code || null, - items[0].inbound_status || "๋Œ€๊ธฐ", - inspector || items[0].inspector || null, - manager || items[0].manager || null, - memo || items[0].memo || null, - userId, - ] - ); + [ + companyCode, + inboundNumber, + inboundType, + inbound_date || items[0].inbound_date, + warehouse_code || items[0].warehouse_code || null, + location_code || items[0].location_code || null, + items[0].inbound_status || "๋Œ€๊ธฐ", + inspector || items[0].inspector || null, + manager || items[0].manager || null, + memo || items[0].memo || null, + userId, + ], + ); - const headerRow = headerResult.rows[0]; - const insertedDetails: any[] = []; + const headerRow = headerResult.rows[0]; + const insertedDetails: any[] = []; - // 2. ๋””ํ…Œ์ผ INSERT (inbound_detail) + ์žฌ๊ณ /๋ฐœ์ฃผ ์—…๋ฐ์ดํŠธ - for (let i = 0; i < items.length; i++) { - const item = items[i]; - const seqNo = i + 1; + // 2. ๋””ํ…Œ์ผ INSERT (inbound_detail) + ์žฌ๊ณ /๋ฐœ์ฃผ ์—…๋ฐ์ดํŠธ + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const seqNo = i + 1; - // 2a. inbound_detail INSERT - const detailResult = await client.query( - `INSERT INTO inbound_detail ( + // 2a. inbound_detail INSERT + const detailResult = await client.query( + `INSERT INTO inbound_detail ( id, company_code, inbound_id, seq_no, inbound_type, item_number, item_name, spec, material, unit, inbound_qty, unit_price, total_amount, @@ -193,91 +199,104 @@ export async function create(req: AuthenticatedRequest, res: Response) { $17, $18, $19, NOW(), $20, $20, '์ž…๊ณ ' ) RETURNING *`, - [ - companyCode, - inboundNumber, - seqNo, - item.inbound_type || inboundType, - item.item_number || null, - item.item_name || null, - item.spec || null, - item.material || null, - item.unit || "EA", - item.inbound_qty || 0, - item.unit_price || 0, - item.total_amount || 0, - item.lot_number || null, - item.reference_number || null, - item.supplier_code || null, - item.supplier_name || null, - item.inspection_status || "๋Œ€๊ธฐ", - item.memo || null, - item.item_id || null, - userId, - ] - ); + [ + companyCode, + inboundNumber, + seqNo, + item.inbound_type || inboundType, + item.item_number || null, + item.item_name || null, + item.spec || null, + item.material || null, + item.unit || "EA", + item.inbound_qty || 0, + item.unit_price || 0, + item.total_amount || 0, + item.lot_number || null, + item.reference_number || null, + item.supplier_code || null, + item.supplier_name || null, + item.inspection_status || "๋Œ€๊ธฐ", + item.memo || null, + item.item_id || null, + userId, + ], + ); - insertedDetails.push(detailResult.rows[0]); + insertedDetails.push(detailResult.rows[0]); - // 2b. ์žฌ๊ณ  ์—…๋ฐ์ดํŠธ (inventory_stock): ์ž…๊ณ  ์ˆ˜๋Ÿ‰ ์ฆ๊ฐ€ โ€” ๊ธฐ์กด ๋กœ์ง ์œ ์ง€ - const itemCode = item.item_number || null; - const whCode = warehouse_code || item.warehouse_code || null; - const locCode = location_code || item.location_code || null; - const inQty = Number(item.inbound_qty) || 0; - if (itemCode && inQty > 0) { - const existingStock = await client.query( - `SELECT id FROM inventory_stock + // 2b. ์žฌ๊ณ  ์—…๋ฐ์ดํŠธ (inventory_stock): ์ž…๊ณ  ์ˆ˜๋Ÿ‰ ์ฆ๊ฐ€ โ€” ๊ธฐ์กด ๋กœ์ง ์œ ์ง€ + const itemCode = item.item_number || null; + const whCode = warehouse_code || item.warehouse_code || null; + const locCode = location_code || item.location_code || null; + const inQty = Number(item.inbound_qty) || 0; + if (itemCode && inQty > 0) { + const existingStock = await client.query( + `SELECT id FROM inventory_stock WHERE company_code = $1 AND item_code = $2 AND COALESCE(warehouse_code, '') = COALESCE($3, '') AND COALESCE(location_code, '') = COALESCE($4, '') LIMIT 1`, - [companyCode, itemCode, whCode || '', locCode || ''] - ); + [companyCode, itemCode, whCode || "", locCode || ""], + ); - if (existingStock.rows.length > 0) { - await client.query( - `UPDATE inventory_stock + if (existingStock.rows.length > 0) { + await client.query( + `UPDATE inventory_stock SET current_qty = CAST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) + $1 AS text), last_in_date = NOW(), updated_date = NOW() WHERE id = $2`, - [inQty, existingStock.rows[0].id] - ); - } else { - await client.query( - `INSERT INTO inventory_stock ( + [inQty, existingStock.rows[0].id], + ); + } else { + await client.query( + `INSERT INTO inventory_stock ( id, company_code, item_code, warehouse_code, location_code, current_qty, safety_qty, last_in_date, created_date, updated_date, writer ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, '0', NOW(), NOW(), NOW(), $6)`, - [companyCode, itemCode, whCode, locCode, String(inQty), userId] - ); - } + [companyCode, itemCode, whCode, locCode, String(inQty), userId], + ); + } - // 2b-2. ์žฌ๊ณ  ์ด๋ ฅ ๊ธฐ๋ก (inventory_history) - const afterStockRes = await client.query( - `SELECT current_qty FROM inventory_stock + // 2b-2. ์žฌ๊ณ  ์ด๋ ฅ ๊ธฐ๋ก (inventory_history) + const afterStockRes = await client.query( + `SELECT current_qty FROM inventory_stock WHERE company_code = $1 AND item_code = $2 AND COALESCE(warehouse_code, '') = COALESCE($3, '') AND COALESCE(location_code, '') = COALESCE($4, '') LIMIT 1`, - [companyCode, itemCode, whCode || '', locCode || ''] - ); - const afterQty = afterStockRes.rows[0]?.current_qty || String(inQty); - await client.query( - `INSERT INTO inventory_history ( + [companyCode, itemCode, whCode || "", locCode || ""], + ); + const afterQty = afterStockRes.rows[0]?.current_qty || String(inQty); + await client.query( + `INSERT INTO inventory_history ( id, company_code, item_code, warehouse_code, location_code, transaction_type, transaction_date, quantity, balance_qty, remark, writer, created_date ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '์ž…๊ณ ', NOW(), $5, $6, $7, $8, NOW())`, - [companyCode, itemCode, whCode, locCode, String(inQty), afterQty, item.inbound_type || '์ž…๊ณ ', userId] - ); - } + [ + companyCode, + itemCode, + whCode, + locCode, + String(inQty), + afterQty, + item.inbound_type || "์ž…๊ณ ", + userId, + ], + ); + } - // 2c. ๊ตฌ๋งค์ž…๊ณ ์ธ ๊ฒฝ์šฐ ๋ฐœ์ฃผ์˜ received_qty ์—…๋ฐ์ดํŠธ โ€” ๊ธฐ์กด ๋กœ์ง ์œ ์ง€ - if (item.inbound_type === "๊ตฌ๋งค์ž…๊ณ " && item.source_id && item.source_table === "purchase_order_mng") { - await client.query( - `UPDATE purchase_order_mng + // 2c. ๊ตฌ๋งค์ž…๊ณ ์ธ ๊ฒฝ์šฐ ๋ฐœ์ฃผ์˜ received_qty ์—…๋ฐ์ดํŠธ โ€” ๊ธฐ์กด ๋กœ์ง ์œ ์ง€ + if ( + item.inbound_type === "๊ตฌ๋งค์ž…๊ณ " && + item.source_id && + item.source_table === "purchase_order_mng" + ) { + await client.query( + `UPDATE purchase_order_mng SET received_qty = CAST( COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) + $1 AS text ), @@ -293,15 +312,19 @@ export async function create(req: AuthenticatedRequest, res: Response) { END, updated_date = NOW() WHERE id = $2 AND company_code = $3`, - [item.inbound_qty || 0, item.source_id, companyCode] - ); - } + [item.inbound_qty || 0, item.source_id, companyCode], + ); + } - // ๊ตฌ๋งค์ž…๊ณ ์ธ ๊ฒฝ์šฐ purchase_detail ํ’ˆ๋ชฉ๋ณ„ ์ž…๊ณ ์ˆ˜๋Ÿ‰ ์—…๋ฐ์ดํŠธ - if (item.inbound_type === "๊ตฌ๋งค์ž…๊ณ " && item.source_id && item.source_table === "purchase_detail") { - // 1. ํ•ด๋‹น purchase_detail์˜ received_qty ๋ˆ„์  ์—…๋ฐ์ดํŠธ - await client.query( - `UPDATE purchase_detail SET + // ๊ตฌ๋งค์ž…๊ณ ์ธ ๊ฒฝ์šฐ purchase_detail ํ’ˆ๋ชฉ๋ณ„ ์ž…๊ณ ์ˆ˜๋Ÿ‰ ์—…๋ฐ์ดํŠธ + if ( + item.inbound_type === "๊ตฌ๋งค์ž…๊ณ " && + item.source_id && + item.source_table === "purchase_detail" + ) { + // 1. ํ•ด๋‹น purchase_detail์˜ received_qty ๋ˆ„์  ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE purchase_detail SET received_qty = CAST( COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) + $1 AS text ), @@ -312,29 +335,29 @@ export async function create(req: AuthenticatedRequest, res: Response) { ), updated_date = NOW() WHERE id = $2 AND company_code = $3`, - [item.inbound_qty || 0, item.source_id, companyCode] - ); + [item.inbound_qty || 0, item.source_id, companyCode], + ); - // 2. ๋ฐœ์ฃผ ํ—ค๋” ์ƒํƒœ ์—…๋ฐ์ดํŠธ - const detailInfo = await client.query( - `SELECT purchase_no FROM purchase_detail WHERE id = $1 AND company_code = $2`, - [item.source_id, companyCode] - ); - if (detailInfo.rows.length > 0) { - const purchaseNo = detailInfo.rows[0].purchase_no; - // ์ž”๋Ÿ‰ ์žˆ๋Š” ๋””ํ…Œ์ผ์ด ์žˆ๋Š”์ง€ ํ™•์ธ - const unreceived = await client.query( - `SELECT id FROM purchase_detail + // 2. ๋ฐœ์ฃผ ํ—ค๋” ์ƒํƒœ ์—…๋ฐ์ดํŠธ + const detailInfo = await client.query( + `SELECT purchase_no FROM purchase_detail WHERE id = $1 AND company_code = $2`, + [item.source_id, companyCode], + ); + if (detailInfo.rows.length > 0) { + const purchaseNo = detailInfo.rows[0].purchase_no; + // ์ž”๋Ÿ‰ ์žˆ๋Š” ๋””ํ…Œ์ผ์ด ์žˆ๋Š”์ง€ ํ™•์ธ + const unreceived = await client.query( + `SELECT id FROM purchase_detail WHERE purchase_no = $1 AND company_code = $2 AND COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) - COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) > 0 LIMIT 1`, - [purchaseNo, companyCode] - ); - const newStatus = unreceived.rows.length === 0 ? '์ž…๊ณ ์™„๋ฃŒ' : '๋ถ€๋ถ„์ž…๊ณ '; - // ๋ฐœ์ฃผ ํ—ค๋”์˜ received_qty๋„ ๋””ํ…Œ์ผ ํ•ฉ๊ณ„๋กœ ๋™๊ธฐํ™” - await client.query( - `UPDATE purchase_order_mng SET + [purchaseNo, companyCode], + ); + const newStatus = + unreceived.rows.length === 0 ? "์ž…๊ณ ์™„๋ฃŒ" : "๋ถ€๋ถ„์ž…๊ณ "; + await client.query( + `UPDATE purchase_order_mng SET status = $1, received_qty = ( SELECT CAST(COALESCE(SUM(CAST(NULLIF(received_qty, '') AS numeric)), 0) AS text) @@ -351,58 +374,66 @@ export async function create(req: AuthenticatedRequest, res: Response) { ), updated_date = NOW() WHERE purchase_no = $2 AND company_code = $3`, - [newStatus, purchaseNo, companyCode] - ); - } - } - } + [newStatus, purchaseNo, companyCode], + ); + } + } + } - await client.query("COMMIT"); + await client.query("COMMIT"); - logger.info("์ž…๊ณ  ๋“ฑ๋ก ์™„๋ฃŒ", { - companyCode, - userId, - headerCount: 1, - detailCount: insertedDetails.length, - inbound_number: inboundNumber, - }); + logger.info("์ž…๊ณ  ๋“ฑ๋ก ์™„๋ฃŒ", { + companyCode, + userId, + headerCount: 1, + detailCount: insertedDetails.length, + inbound_number: inboundNumber, + }); - return res.json({ - success: true, - data: { header: headerRow, details: insertedDetails }, - message: `${insertedDetails.length}๊ฑด ์ž…๊ณ  ๋“ฑ๋ก ์™„๋ฃŒ`, - }); - } catch (error: any) { - await client.query("ROLLBACK"); - logger.error("์ž…๊ณ  ๋“ฑ๋ก ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } finally { - client.release(); - } + return res.json({ + success: true, + data: { header: headerRow, details: insertedDetails }, + message: `${insertedDetails.length}๊ฑด ์ž…๊ณ  ๋“ฑ๋ก ์™„๋ฃŒ`, + }); + } catch (error: any) { + await client.query("ROLLBACK"); + logger.error("์ž…๊ณ  ๋“ฑ๋ก ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } finally { + client.release(); + } } // ์ž…๊ณ  ์ˆ˜์ • (ํ—ค๋” + ๋””ํ…Œ์ผ ๋ถ„๋ฆฌ ์—…๋ฐ์ดํŠธ) export async function update(req: AuthenticatedRequest, res: Response) { - const pool = getPool(); - const client = await pool.connect(); + const pool = getPool(); + const client = await pool.connect(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const { id } = req.params; - const { - inbound_date, inbound_qty, unit_price, total_amount, - lot_number, warehouse_code, location_code, - inbound_status, inspection_status, - inspector, manager: mgr, memo, - detail_id, - } = req.body; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { id } = req.params; + const { + inbound_date, + inbound_qty, + unit_price, + total_amount, + lot_number, + warehouse_code, + location_code, + inbound_status, + inspection_status, + inspector, + manager: mgr, + memo, + detail_id, + } = req.body; - await client.query("BEGIN"); + await client.query("BEGIN"); - // ํ—ค๋” ์—…๋ฐ์ดํŠธ (inbound_mng) โ€” ํ—ค๋” ๋ ˆ๋ฒจ ํ•„๋“œ๋งŒ - const headerResult = await client.query( - `UPDATE inbound_mng SET + // ํ—ค๋” ์—…๋ฐ์ดํŠธ (inbound_mng) โ€” ํ—ค๋” ๋ ˆ๋ฒจ ํ•„๋“œ๋งŒ + const headerResult = await client.query( + `UPDATE inbound_mng SET inbound_date = COALESCE($1::date, inbound_date), warehouse_code = COALESCE($2, warehouse_code), location_code = COALESCE($3, location_code), @@ -414,23 +445,32 @@ export async function update(req: AuthenticatedRequest, res: Response) { updated_by = $8 WHERE id = $9 AND company_code = $10 RETURNING *`, - [ - inbound_date, warehouse_code, location_code, - inbound_status, inspector, mgr, memo, - userId, id, companyCode, - ] - ); + [ + inbound_date, + warehouse_code, + location_code, + inbound_status, + inspector, + mgr, + memo, + userId, + id, + companyCode, + ], + ); - if (headerResult.rowCount === 0) { - await client.query("ROLLBACK"); - return res.status(404).json({ success: false, message: "์ž…๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); - } + if (headerResult.rowCount === 0) { + await client.query("ROLLBACK"); + return res + .status(404) + .json({ success: false, message: "์ž…๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); + } - // ๋””ํ…Œ์ผ ์—…๋ฐ์ดํŠธ (inbound_detail) โ€” detail_id๊ฐ€ ์žˆ์œผ๋ฉด ๋””ํ…Œ์ผ ๋ ˆ๋ฒจ ํ•„๋“œ ์—…๋ฐ์ดํŠธ - let detailRow = null; - if (detail_id) { - const detailResult = await client.query( - `UPDATE inbound_detail SET + // ๋””ํ…Œ์ผ ์—…๋ฐ์ดํŠธ (inbound_detail) โ€” detail_id๊ฐ€ ์žˆ์œผ๋ฉด ๋””ํ…Œ์ผ ๋ ˆ๋ฒจ ํ•„๋“œ ์—…๋ฐ์ดํŠธ + let detailRow = null; + if (detail_id) { + const detailResult = await client.query( + `UPDATE inbound_detail SET inbound_qty = COALESCE($1, inbound_qty), unit_price = COALESCE($2, unit_price), total_amount = COALESCE($3, total_amount), @@ -441,108 +481,126 @@ export async function update(req: AuthenticatedRequest, res: Response) { updated_by = $7 WHERE id = $8 AND company_code = $9 RETURNING *`, - [ - inbound_qty, unit_price, total_amount, - lot_number, inspection_status, memo, - userId, detail_id, companyCode, - ] - ); - detailRow = detailResult.rows[0] || null; - } else { - // ๋ ˆ๊ฑฐ์‹œ ๋ฐ์ดํ„ฐ: detail_id ์—†์ด inbound_mng ์ž์ฒด์— ํ’ˆ๋ชฉ ์ •๋ณด ์—…๋ฐ์ดํŠธ - await client.query( - `UPDATE inbound_mng SET + [ + inbound_qty, + unit_price, + total_amount, + lot_number, + inspection_status, + memo, + userId, + detail_id, + companyCode, + ], + ); + detailRow = detailResult.rows[0] || null; + } else { + // ๋ ˆ๊ฑฐ์‹œ ๋ฐ์ดํ„ฐ: detail_id ์—†์ด inbound_mng ์ž์ฒด์— ํ’ˆ๋ชฉ ์ •๋ณด ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE inbound_mng SET inbound_qty = COALESCE($1, inbound_qty), unit_price = COALESCE($2, unit_price), total_amount = COALESCE($3, total_amount), lot_number = COALESCE($4, lot_number), inspection_status = COALESCE($5, inspection_status) WHERE id = $6 AND company_code = $7`, - [ - inbound_qty, unit_price, total_amount, - lot_number, inspection_status, - id, companyCode, - ] - ); - } + [ + inbound_qty, + unit_price, + total_amount, + lot_number, + inspection_status, + id, + companyCode, + ], + ); + } - await client.query("COMMIT"); + await client.query("COMMIT"); - logger.info("์ž…๊ณ  ์ˆ˜์ •", { companyCode, userId, id, detail_id }); + logger.info("์ž…๊ณ  ์ˆ˜์ •", { companyCode, userId, id, detail_id }); - return res.json({ - success: true, - data: { header: headerResult.rows[0], detail: detailRow }, - }); - } catch (error: any) { - await client.query("ROLLBACK"); - logger.error("์ž…๊ณ  ์ˆ˜์ • ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } finally { - client.release(); - } + return res.json({ + success: true, + data: { header: headerResult.rows[0], detail: detailRow }, + }); + } catch (error: any) { + await client.query("ROLLBACK"); + logger.error("์ž…๊ณ  ์ˆ˜์ • ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } finally { + client.release(); + } } // ์ž…๊ณ  ์‚ญ์ œ (ํ—ค๋” + ๋””ํ…Œ์ผ, ์žฌ๊ณ /๋ฐœ์ฃผ ๋กค๋ฐฑ ํฌํ•จ) -export async function deleteReceiving(req: AuthenticatedRequest, res: Response) { - const pool = getPool(); - const client = await pool.connect(); +export async function deleteReceiving( + req: AuthenticatedRequest, + res: Response, +) { + const pool = getPool(); + const client = await pool.connect(); - try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const { id } = req.params; + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + const { id } = req.params; - await client.query("BEGIN"); + await client.query("BEGIN"); - // ํ—ค๋” ์ •๋ณด ์กฐํšŒ (inbound_number, warehouse_code ๋“ฑ) - const headerResult = await client.query( - `SELECT * FROM inbound_mng WHERE id = $1 AND company_code = $2`, - [id, companyCode] - ); + // ํ—ค๋” ์ •๋ณด ์กฐํšŒ (inbound_number, warehouse_code ๋“ฑ) + const headerResult = await client.query( + `SELECT * FROM inbound_mng WHERE id = $1 AND company_code = $2`, + [id, companyCode], + ); - if (headerResult.rowCount === 0) { - await client.query("ROLLBACK"); - return res.status(404).json({ success: false, message: "๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); - } + if (headerResult.rowCount === 0) { + await client.query("ROLLBACK"); + return res + .status(404) + .json({ success: false, message: "๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." }); + } - const header = headerResult.rows[0]; - const inboundNumber = header.inbound_number; + const header = headerResult.rows[0]; + const inboundNumber = header.inbound_number; - // ๋””ํ…Œ์ผ ์กฐํšŒ (์žฌ๊ณ /๋ฐœ์ฃผ ๋กค๋ฐฑ์šฉ) - const detailResult = await client.query( - `SELECT * FROM inbound_detail WHERE inbound_id = $1 AND company_code = $2`, - [inboundNumber, companyCode] - ); + // ๋””ํ…Œ์ผ ์กฐํšŒ (์žฌ๊ณ /๋ฐœ์ฃผ ๋กค๋ฐฑ์šฉ) + const detailResult = await client.query( + `SELECT * FROM inbound_detail WHERE inbound_id = $1 AND company_code = $2`, + [inboundNumber, companyCode], + ); - // ๋””ํ…Œ์ผ์ด ์žˆ์œผ๋ฉด ๋””ํ…Œ์ผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋กค๋ฐฑ, ์—†์œผ๋ฉด ํ—ค๋”(๋ ˆ๊ฑฐ์‹œ) ๊ธฐ๋ฐ˜์œผ๋กœ ๋กค๋ฐฑ - const rollbackItems = detailResult.rows.length > 0 - ? detailResult.rows.map((d: any) => ({ - item_number: d.item_number, - inbound_qty: d.inbound_qty, - inbound_type: d.inbound_type || header.inbound_type, - source_table: header.source_table, - source_id: header.source_id, - })) - : [{ - item_number: header.item_number, - inbound_qty: header.inbound_qty, - inbound_type: header.inbound_type, - source_table: header.source_table, - source_id: header.source_id, - }]; + // ๋””ํ…Œ์ผ์ด ์žˆ์œผ๋ฉด ๋””ํ…Œ์ผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋กค๋ฐฑ, ์—†์œผ๋ฉด ํ—ค๋”(๋ ˆ๊ฑฐ์‹œ) ๊ธฐ๋ฐ˜์œผ๋กœ ๋กค๋ฐฑ + const rollbackItems = + detailResult.rows.length > 0 + ? detailResult.rows.map((d: any) => ({ + item_number: d.item_number, + inbound_qty: d.inbound_qty, + inbound_type: d.inbound_type || header.inbound_type, + source_table: header.source_table, + source_id: header.source_id, + })) + : [ + { + item_number: header.item_number, + inbound_qty: header.inbound_qty, + inbound_type: header.inbound_type, + source_table: header.source_table, + source_id: header.source_id, + }, + ]; - const whCode = header.warehouse_code || null; - const locCode = header.location_code || null; + const whCode = header.warehouse_code || null; + const locCode = header.location_code || null; - for (const item of rollbackItems) { - const itemCode = item.item_number || null; - const inQty = Number(item.inbound_qty) || 0; + for (const item of rollbackItems) { + const itemCode = item.item_number || null; + const inQty = Number(item.inbound_qty) || 0; - // ์žฌ๊ณ  ๋กค๋ฐฑ: ์ž…๊ณ  ์ˆ˜๋Ÿ‰๋งŒํผ ์ฐจ๊ฐ - if (itemCode && inQty > 0) { - await client.query( - `UPDATE inventory_stock + // ์žฌ๊ณ  ๋กค๋ฐฑ: ์ž…๊ณ  ์ˆ˜๋Ÿ‰๋งŒํผ ์ฐจ๊ฐ + if (itemCode && inQty > 0) { + await client.query( + `UPDATE inventory_stock SET current_qty = CAST( GREATEST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) - $1, 0) AS text ), @@ -550,33 +608,45 @@ export async function deleteReceiving(req: AuthenticatedRequest, res: Response) WHERE company_code = $2 AND item_code = $3 AND COALESCE(warehouse_code, '') = COALESCE($4, '') AND COALESCE(location_code, '') = COALESCE($5, '')`, - [inQty, companyCode, itemCode, whCode || '', locCode || ''] - ); + [inQty, companyCode, itemCode, whCode || "", locCode || ""], + ); - // ์ž…๊ณ ์ทจ์†Œ ์ด๋ ฅ ๊ธฐ๋ก - const afterStockRes = await client.query( - `SELECT current_qty FROM inventory_stock + // ์ž…๊ณ ์ทจ์†Œ ์ด๋ ฅ ๊ธฐ๋ก + const afterStockRes = await client.query( + `SELECT current_qty FROM inventory_stock WHERE company_code = $1 AND item_code = $2 AND COALESCE(warehouse_code, '') = COALESCE($3, '') AND COALESCE(location_code, '') = COALESCE($4, '') LIMIT 1`, - [companyCode, itemCode, whCode || '', locCode || ''] - ); - const afterQty = afterStockRes.rows[0]?.current_qty || '0'; - await client.query( - `INSERT INTO inventory_history ( + [companyCode, itemCode, whCode || "", locCode || ""], + ); + const afterQty = afterStockRes.rows[0]?.current_qty || "0"; + await client.query( + `INSERT INTO inventory_history ( id, company_code, item_code, warehouse_code, location_code, transaction_type, transaction_date, quantity, balance_qty, remark, writer, created_date ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '์ž…๊ณ ์ทจ์†Œ', NOW(), $5, $6, '์ž…๊ณ  ์‚ญ์ œ์— ์˜ํ•œ ๋กค๋ฐฑ', $7, NOW())`, - [companyCode, itemCode, whCode, locCode, String(-inQty), afterQty, userId] - ); - } + [ + companyCode, + itemCode, + whCode, + locCode, + String(-inQty), + afterQty, + userId, + ], + ); + } - // ๊ตฌ๋งค์ž…๊ณ  ๋ฐœ์ฃผ ๋กค๋ฐฑ: purchase_order_mng ๊ธฐ๋ฐ˜ - if (item.inbound_type === "๊ตฌ๋งค์ž…๊ณ " && item.source_id && item.source_table === "purchase_order_mng") { - await client.query( - `UPDATE purchase_order_mng + // ๊ตฌ๋งค์ž…๊ณ  ๋ฐœ์ฃผ ๋กค๋ฐฑ: purchase_order_mng ๊ธฐ๋ฐ˜ + if ( + item.inbound_type === "๊ตฌ๋งค์ž…๊ณ " && + item.source_id && + item.source_table === "purchase_order_mng" + ) { + await client.query( + `UPDATE purchase_order_mng SET received_qty = CAST( GREATEST(COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) - $1, 0) AS text ), @@ -591,21 +661,25 @@ export async function deleteReceiving(req: AuthenticatedRequest, res: Response) END, updated_date = NOW() WHERE id = $2 AND company_code = $3`, - [inQty, item.source_id, companyCode] - ); - } + [inQty, item.source_id, companyCode], + ); + } - // ๊ตฌ๋งค์ž…๊ณ  ๋ฐœ์ฃผ ๋กค๋ฐฑ: purchase_detail ๊ธฐ๋ฐ˜ - if (item.inbound_type === "๊ตฌ๋งค์ž…๊ณ " && item.source_id && item.source_table === "purchase_detail") { - const detailInfo = await client.query( - `SELECT purchase_no FROM purchase_detail WHERE id = $1 AND company_code = $2`, - [item.source_id, companyCode] - ); - if (detailInfo.rows.length > 0) { - const purchaseNo = detailInfo.rows[0].purchase_no; - // ์‚ญ์ œ ํ›„ ์žฌ๊ณ„์‚ฐ์„ ์œ„ํ•ด ํ˜„์žฌ ์ž…๊ณ  ๊ฑด ์ œ์™ธํ•œ ๋ฏธ์ž…๊ณ  ํ™•์ธ - const unreceived = await client.query( - `SELECT pd.id + // ๊ตฌ๋งค์ž…๊ณ  ๋ฐœ์ฃผ ๋กค๋ฐฑ: purchase_detail ๊ธฐ๋ฐ˜ + if ( + item.inbound_type === "๊ตฌ๋งค์ž…๊ณ " && + item.source_id && + item.source_table === "purchase_detail" + ) { + const detailInfo = await client.query( + `SELECT purchase_no FROM purchase_detail WHERE id = $1 AND company_code = $2`, + [item.source_id, companyCode], + ); + if (detailInfo.rows.length > 0) { + const purchaseNo = detailInfo.rows[0].purchase_no; + // ์‚ญ์ œ ํ›„ ์žฌ๊ณ„์‚ฐ์„ ์œ„ํ•ด ํ˜„์žฌ ์ž…๊ณ  ๊ฑด ์ œ์™ธํ•œ ๋ฏธ์ž…๊ณ  ํ™•์ธ + const unreceived = await client.query( + `SELECT pd.id FROM purchase_detail pd LEFT JOIN ( SELECT source_id, SUM(COALESCE(inbound_qty, 0)) AS total_received @@ -617,76 +691,82 @@ export async function deleteReceiving(req: AuthenticatedRequest, res: Response) WHERE pd.purchase_no = $2 AND pd.company_code = $1 AND COALESCE(CAST(NULLIF(pd.order_qty, '') AS numeric), 0) - COALESCE(r.total_received, 0) > 0 LIMIT 1`, - [companyCode, purchaseNo, inboundNumber] - ); - // ์ž”๋Ÿ‰ ์žˆ์œผ๋ฉด ๋ถ€๋ถ„์ž…๊ณ , ์ „๋Ÿ‰ ๋ฏธ์ž…๊ณ ๋ฉด ๋ฐœ์ฃผํ™•์ • - const hasAnyReceived = await client.query( - `SELECT 1 FROM inbound_mng + [companyCode, purchaseNo, inboundNumber], + ); + // ์ž”๋Ÿ‰ ์žˆ์œผ๋ฉด ๋ถ€๋ถ„์ž…๊ณ , ์ „๋Ÿ‰ ๋ฏธ์ž…๊ณ ๋ฉด ๋ฐœ์ฃผํ™•์ • + const hasAnyReceived = await client.query( + `SELECT 1 FROM inbound_mng WHERE source_table = 'purchase_detail' AND company_code = $1 AND inbound_number != $2 LIMIT 1`, - [companyCode, inboundNumber] - ); - const newStatus = hasAnyReceived.rows.length > 0 - ? (unreceived.rows.length === 0 ? '์ž…๊ณ ์™„๋ฃŒ' : '๋ถ€๋ถ„์ž…๊ณ ') - : '๋ฐœ์ฃผํ™•์ •'; - await client.query( - `UPDATE purchase_order_mng SET status = $1, updated_date = NOW() + [companyCode, inboundNumber], + ); + const newStatus = + hasAnyReceived.rows.length > 0 + ? unreceived.rows.length === 0 + ? "์ž…๊ณ ์™„๋ฃŒ" + : "๋ถ€๋ถ„์ž…๊ณ " + : "๋ฐœ์ฃผํ™•์ •"; + await client.query( + `UPDATE purchase_order_mng SET status = $1, updated_date = NOW() WHERE purchase_no = $2 AND company_code = $3`, - [newStatus, purchaseNo, companyCode] - ); - } - } - } + [newStatus, purchaseNo, companyCode], + ); + } + } + } - // ๋””ํ…Œ์ผ ์‚ญ์ œ - await client.query( - `DELETE FROM inbound_detail WHERE inbound_id = $1 AND company_code = $2`, - [inboundNumber, companyCode] - ); + // ๋””ํ…Œ์ผ ์‚ญ์ œ + await client.query( + `DELETE FROM inbound_detail WHERE inbound_id = $1 AND company_code = $2`, + [inboundNumber, companyCode], + ); - // ํ—ค๋” ์‚ญ์ œ - await client.query( - `DELETE FROM inbound_mng WHERE id = $1 AND company_code = $2`, - [id, companyCode] - ); + // ํ—ค๋” ์‚ญ์ œ + await client.query( + `DELETE FROM inbound_mng WHERE id = $1 AND company_code = $2`, + [id, companyCode], + ); - await client.query("COMMIT"); + await client.query("COMMIT"); - logger.info("์ž…๊ณ  ์‚ญ์ œ", { companyCode, id, inboundNumber }); + logger.info("์ž…๊ณ  ์‚ญ์ œ", { companyCode, id, inboundNumber }); - return res.json({ success: true, message: "์‚ญ์ œ ์™„๋ฃŒ" }); - } catch (error: any) { - await client.query("ROLLBACK"); - logger.error("์ž…๊ณ  ์‚ญ์ œ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } finally { - client.release(); - } + return res.json({ success: true, message: "์‚ญ์ œ ์™„๋ฃŒ" }); + } catch (error: any) { + await client.query("ROLLBACK"); + logger.error("์ž…๊ณ  ์‚ญ์ œ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } finally { + client.release(); + } } // ๊ตฌ๋งค์ž…๊ณ ์šฉ: ๋ฐœ์ฃผ ๋ฐ์ดํ„ฐ ์กฐํšŒ (๋ฏธ์ž…๊ณ ๋ถ„) - ์‹ ๊ทœ ํ—ค๋”-๋””ํ…Œ์ผ ๊ตฌ์กฐ + ๋ ˆ๊ฑฐ์‹œ ๋‹จ์ผ ํ…Œ์ด๋ธ” UNION ALL -export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const { keyword, page, pageSize } = req.query; - const currentPage = Math.max(1, Number(page) || 1); - const limit = Math.min(500, Math.max(1, Number(pageSize) || 20)); - const offset = (currentPage - 1) * limit; +export async function getPurchaseOrders( + req: AuthenticatedRequest, + res: Response, +) { + try { + const companyCode = req.user!.companyCode; + const { keyword, page, pageSize } = req.query; + const currentPage = Math.max(1, Number(page) || 1); + const limit = Math.min(500, Math.max(1, Number(pageSize) || 20)); + const offset = (currentPage - 1) * limit; - const params: any[] = [companyCode]; - let paramIdx = 2; + const params: any[] = [companyCode]; + let paramIdx = 2; - let keywordConditionDetail = ""; - let keywordConditionLegacy = ""; - if (keyword) { - keywordConditionDetail = `AND (pd.purchase_no ILIKE $${paramIdx} OR COALESCE(NULLIF(pd.item_name, ''), ii.item_name) ILIKE $${paramIdx} OR COALESCE(NULLIF(pd.item_code, ''), ii.item_number) ILIKE $${paramIdx} OR COALESCE(pd.supplier_name, po.supplier_name) ILIKE $${paramIdx})`; - keywordConditionLegacy = `AND (po.purchase_no ILIKE $${paramIdx} OR po.item_name ILIKE $${paramIdx} OR po.item_code ILIKE $${paramIdx} OR po.supplier_name ILIKE $${paramIdx})`; - params.push(`%${keyword}%`); - paramIdx++; - } + let keywordConditionDetail = ""; + let keywordConditionLegacy = ""; + if (keyword) { + keywordConditionDetail = `AND (pd.purchase_no ILIKE $${paramIdx} OR COALESCE(NULLIF(pd.item_name, ''), ii.item_name) ILIKE $${paramIdx} OR COALESCE(NULLIF(pd.item_code, ''), ii.item_number) ILIKE $${paramIdx} OR COALESCE(pd.supplier_name, po.supplier_name) ILIKE $${paramIdx})`; + keywordConditionLegacy = `AND (po.purchase_no ILIKE $${paramIdx} OR po.item_name ILIKE $${paramIdx} OR po.item_code ILIKE $${paramIdx} OR po.supplier_name ILIKE $${paramIdx})`; + params.push(`%${keyword}%`); + paramIdx++; + } - const baseQuery = ` + const baseQuery = ` WITH combined AS ( -- ๋””ํ…Œ์ผ ๊ธฐ๋ฐ˜ ๋ฐœ์ฃผ ๋ฐ์ดํ„ฐ (purchase_detail.received_qty๋กœ ์ž”๋Ÿ‰ ๊ณ„์‚ฐ) SELECT @@ -765,62 +845,62 @@ export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response ${keywordConditionLegacy} )`; - const pool = getPool(); + const pool = getPool(); - const countResult = await pool.query( - `${baseQuery} SELECT COUNT(*) AS total FROM combined`, - params - ); - const totalCount = parseInt(countResult.rows[0].total, 10); + const countResult = await pool.query( + `${baseQuery} SELECT COUNT(*) AS total FROM combined`, + params, + ); + const totalCount = parseInt(countResult.rows[0].total, 10); - const dataResult = await pool.query( - `${baseQuery} SELECT * FROM combined ORDER BY order_date DESC, purchase_no LIMIT ${limit} OFFSET ${offset}`, - params - ); + const dataResult = await pool.query( + `${baseQuery} SELECT * FROM combined ORDER BY order_date DESC, purchase_no LIMIT ${limit} OFFSET ${offset}`, + params, + ); - return res.json({ success: true, data: dataResult.rows, totalCount }); - } catch (error: any) { - logger.error("๋ฐœ์ฃผ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: dataResult.rows, totalCount }); + } catch (error: any) { + logger.error("๋ฐœ์ฃผ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ๋ฐ˜ํ’ˆ์ž…๊ณ ์šฉ: ์ถœํ•˜ ๋ฐ์ดํ„ฐ ์กฐํšŒ export async function getShipments(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const { keyword, page, pageSize } = req.query; - const currentPage = Math.max(1, Number(page) || 1); - const limit = Math.min(500, Math.max(1, Number(pageSize) || 20)); - const offset = (currentPage - 1) * limit; + try { + const companyCode = req.user!.companyCode; + const { keyword, page, pageSize } = req.query; + const currentPage = Math.max(1, Number(page) || 1); + const limit = Math.min(500, Math.max(1, Number(pageSize) || 20)); + const offset = (currentPage - 1) * limit; - const conditions: string[] = ["si.company_code = $1"]; - const params: any[] = [companyCode]; - let paramIdx = 2; + const conditions: string[] = ["si.company_code = $1"]; + const params: any[] = [companyCode]; + let paramIdx = 2; - if (keyword) { - conditions.push( - `(si.instruction_no ILIKE $${paramIdx} OR sid.item_name ILIKE $${paramIdx} OR sid.item_code ILIKE $${paramIdx})` - ); - params.push(`%${keyword}%`); - paramIdx++; - } + if (keyword) { + conditions.push( + `(si.instruction_no ILIKE $${paramIdx} OR sid.item_name ILIKE $${paramIdx} OR sid.item_code ILIKE $${paramIdx})`, + ); + params.push(`%${keyword}%`); + paramIdx++; + } - const whereClause = conditions.join(" AND "); - const pool = getPool(); + const whereClause = conditions.join(" AND "); + const pool = getPool(); - const countResult = await pool.query( - `SELECT COUNT(*) AS total + const countResult = await pool.query( + `SELECT COUNT(*) AS total FROM shipment_instruction si JOIN shipment_instruction_detail sid ON si.id = sid.instruction_id AND si.company_code = sid.company_code WHERE ${whereClause}`, - params - ); - const totalCount = parseInt(countResult.rows[0].total, 10); + params, + ); + const totalCount = parseInt(countResult.rows[0].total, 10); - const dataResult = await pool.query( - `SELECT + const dataResult = await pool.query( + `SELECT sid.id AS detail_id, si.id AS instruction_id, si.instruction_no, @@ -841,134 +921,143 @@ export async function getShipments(req: AuthenticatedRequest, res: Response) { WHERE ${whereClause} ORDER BY si.instruction_date DESC, si.instruction_no LIMIT ${limit} OFFSET ${offset}`, - params - ); + params, + ); - return res.json({ success: true, data: dataResult.rows, totalCount }); - } catch (error: any) { - logger.error("์ถœํ•˜ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: dataResult.rows, totalCount }); + } catch (error: any) { + logger.error("์ถœํ•˜ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ๊ธฐํƒ€์ž…๊ณ ์šฉ: ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ ์กฐํšŒ export async function getItems(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const { keyword, page, pageSize, division } = req.query; - const currentPage = Math.max(1, Number(page) || 1); - const limit = Math.min(500, Math.max(1, Number(pageSize) || 20)); - const offset = (currentPage - 1) * limit; + try { + const companyCode = req.user!.companyCode; + const { keyword, page, pageSize, division } = req.query; + const currentPage = Math.max(1, Number(page) || 1); + const limit = Math.min(500, Math.max(1, Number(pageSize) || 20)); + const offset = (currentPage - 1) * limit; - const conditions: string[] = ["company_code = $1"]; - const params: any[] = [companyCode]; - let paramIdx = 2; + const conditions: string[] = ["company_code = $1"]; + const params: any[] = [companyCode]; + let paramIdx = 2; - if (keyword) { - conditions.push( - `(item_number ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx})` - ); - params.push(`%${keyword}%`); - paramIdx++; - } + if (keyword) { + conditions.push( + `(item_number ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx})`, + ); + params.push(`%${keyword}%`); + paramIdx++; + } - if (division) { - conditions.push(`division ILIKE $${paramIdx}`); - params.push(`%${division}%`); - paramIdx++; - } + if (division) { + conditions.push(`division ILIKE $${paramIdx}`); + params.push(`%${division}%`); + paramIdx++; + } - const whereClause = conditions.join(" AND "); - const pool = getPool(); + const whereClause = conditions.join(" AND "); + const pool = getPool(); - const countResult = await pool.query( - `SELECT COUNT(*) AS total FROM item_info WHERE ${whereClause}`, - params - ); - const totalCount = parseInt(countResult.rows[0].total, 10); + const countResult = await pool.query( + `SELECT COUNT(*) AS total FROM item_info WHERE ${whereClause}`, + params, + ); + const totalCount = parseInt(countResult.rows[0].total, 10); - const dataResult = await pool.query( - `SELECT + const dataResult = await pool.query( + `SELECT id, item_number, item_name, size AS spec, material, unit, COALESCE(CAST(NULLIF(standard_price, '') AS numeric), 0) AS standard_price FROM item_info WHERE ${whereClause} ORDER BY item_name LIMIT ${limit} OFFSET ${offset}`, - params - ); + params, + ); - return res.json({ success: true, data: dataResult.rows, totalCount }); - } catch (error: any) { - logger.error("ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: dataResult.rows, totalCount }); + } catch (error: any) { + logger.error("ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ์ž…๊ณ ๋ฒˆํ˜ธ ์ž๋™์ƒ์„ฑ export async function generateNumber(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const ruleId = (req.query.ruleId as string) || (req.query.rule_id as string); + try { + const companyCode = req.user!.companyCode; + const ruleId = + (req.query.ruleId as string) || (req.query.rule_id as string); - // 1์ˆœ์œ„: POP ํ™”๋ฉด์„ค์ •์—์„œ ์„ ํƒํ•œ ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ - if (ruleId && ruleId !== "__none__") { - try { - const { numberingRuleService } = await import("../services/numberingRuleService"); - const newNumber = await numberingRuleService.allocateCode(ruleId, companyCode); - return res.json({ success: true, data: newNumber }); - } catch (e: any) { - logger.warn("์„ ํƒํ•œ ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ ์‹คํŒจ, ๊ธฐ๋ณธ ์ฑ„๋ฒˆ์œผ๋กœ ํด๋ฐฑ", { ruleId, error: e.message }); - // ํด๋ฐฑ - } - } + // 1์ˆœ์œ„: POP ํ™”๋ฉด์„ค์ •์—์„œ ์„ ํƒํ•œ ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ + if (ruleId && ruleId !== "__none__") { + try { + const { numberingRuleService } = await import( + "../services/numberingRuleService" + ); + const newNumber = await numberingRuleService.allocateCode( + ruleId, + companyCode, + ); + return res.json({ success: true, data: newNumber }); + } catch (e: any) { + logger.warn("์„ ํƒํ•œ ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ ์‹คํŒจ, ๊ธฐ๋ณธ ์ฑ„๋ฒˆ์œผ๋กœ ํด๋ฐฑ", { + ruleId, + error: e.message, + }); + // ํด๋ฐฑ + } + } - // 2์ˆœ์œ„: ๊ธฐ๋ณธ ํ•˜๋“œ์ฝ”๋”ฉ ์ฑ„๋ฒˆ (RCV-YYYY-XXXX) - const pool = getPool(); - const today = new Date(); - const yyyy = today.getFullYear(); - const prefix = `RCV-${yyyy}-`; + // 2์ˆœ์œ„: ๊ธฐ๋ณธ ํ•˜๋“œ์ฝ”๋”ฉ ์ฑ„๋ฒˆ (RCV-YYYY-XXXX) + const pool = getPool(); + const today = new Date(); + const yyyy = today.getFullYear(); + const prefix = `RCV-${yyyy}-`; - const result = await pool.query( - `SELECT inbound_number FROM inbound_mng + const result = await pool.query( + `SELECT inbound_number FROM inbound_mng WHERE company_code = $1 AND inbound_number LIKE $2 ORDER BY inbound_number DESC LIMIT 1`, - [companyCode, `${prefix}%`] - ); + [companyCode, `${prefix}%`], + ); - let seq = 1; - if (result.rows.length > 0) { - const lastNo = result.rows[0].inbound_number; - const lastSeq = parseInt(lastNo.replace(prefix, ""), 10); - if (!isNaN(lastSeq)) seq = lastSeq + 1; - } + let seq = 1; + if (result.rows.length > 0) { + const lastNo = result.rows[0].inbound_number; + const lastSeq = parseInt(lastNo.replace(prefix, ""), 10); + if (!isNaN(lastSeq)) seq = lastSeq + 1; + } - const newNumber = `${prefix}${String(seq).padStart(4, "0")}`; + const newNumber = `${prefix}${String(seq).padStart(4, "0")}`; - return res.json({ success: true, data: newNumber }); - } catch (error: any) { - logger.error("์ž…๊ณ ๋ฒˆํ˜ธ ์ƒ์„ฑ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: newNumber }); + } catch (error: any) { + logger.error("์ž…๊ณ ๋ฒˆํ˜ธ ์ƒ์„ฑ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } // ์ฐฝ๊ณ  ๋ชฉ๋ก ์กฐํšŒ export async function getWarehouses(req: AuthenticatedRequest, res: Response) { - try { - const companyCode = req.user!.companyCode; - const pool = getPool(); + try { + const companyCode = req.user!.companyCode; + const pool = getPool(); - const result = await pool.query( - `SELECT warehouse_code, warehouse_name, warehouse_type + const result = await pool.query( + `SELECT warehouse_code, warehouse_name, warehouse_type FROM warehouse_info WHERE company_code = $1 AND status != '์‚ญ์ œ' ORDER BY warehouse_name`, - [companyCode] - ); + [companyCode], + ); - return res.json({ success: true, data: result.rows }); - } catch (error: any) { - logger.error("์ฐฝ๊ณ  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); - return res.status(500).json({ success: false, message: error.message }); - } + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("์ฐฝ๊ณ  ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } } diff --git a/backend-node/src/routes/inspectionResultRoutes.ts b/backend-node/src/routes/inspectionResultRoutes.ts index d6e9375c..1e9d7960 100644 --- a/backend-node/src/routes/inspectionResultRoutes.ts +++ b/backend-node/src/routes/inspectionResultRoutes.ts @@ -1,4 +1,4 @@ -import { Router, Request, Response } from "express"; +import { type Request, type Response, Router } from "express"; import { getPool } from "../database/db"; import { authenticateToken } from "../middleware/authMiddleware"; @@ -9,32 +9,32 @@ router.use(authenticateToken); // ---- ๊ฒ€์‚ฌ ๊ธฐ์ค€ ์กฐํšŒ (item_inspection_info) ---- // GET /api/pop/inspection-result/info?itemCode=ITEM-001&inspectionType=์ž…๊ณ ๊ฒ€์‚ฌ router.get("/info", async (req: Request, res: Response) => { - const pool = getPool(); - const companyCode = (req as any).user?.companyCode; - const { itemCode, itemId, inspectionType } = req.query; + const pool = getPool(); + const companyCode = (req as any).user?.companyCode; + const { itemCode, itemId, inspectionType } = req.query; - if (!companyCode) { - return res.status(401).json({ success: false, message: "์ธ์ฆ ์ •๋ณด ์—†์Œ" }); - } + if (!companyCode) { + return res.status(401).json({ success: false, message: "์ธ์ฆ ์ •๋ณด ์—†์Œ" }); + } - const conditions: string[] = ["company_code = $1", "is_active = 'Y'"]; - const params: unknown[] = [companyCode]; - let idx = 2; + const conditions: string[] = ["company_code = $1", "is_active = 'Y'"]; + const params: unknown[] = [companyCode]; + let idx = 2; - if (itemCode) { - conditions.push(`item_code = $${idx++}`); - params.push(itemCode); - } - if (itemId) { - conditions.push(`item_id = $${idx++}`); - params.push(itemId); - } - if (inspectionType) { - conditions.push(`inspection_type = $${idx++}`); - params.push(inspectionType); - } + if (itemCode) { + conditions.push(`item_code = $${idx++}`); + params.push(itemCode); + } + if (itemId) { + conditions.push(`item_id = $${idx++}`); + params.push(itemId); + } + if (inspectionType) { + conditions.push(`inspection_type = $${idx++}`); + params.push(inspectionType); + } - const sql = ` + const sql = ` SELECT id, item_id, item_code, item_name, inspection_type, inspection_item_name, inspection_standard, inspection_method, pass_criteria, is_required, sort_order, memo @@ -43,174 +43,179 @@ router.get("/info", async (req: Request, res: Response) => { ORDER BY sort_order, inspection_item_name `; - try { - const result = await pool.query(sql, params); - return res.json({ success: true, data: result.rows }); - } catch (err: any) { - return res.status(500).json({ success: false, message: err.message }); - } + try { + const result = await pool.query(sql, params); + return res.json({ success: true, data: result.rows }); + } catch (err: any) { + return res.status(500).json({ success: false, message: err.message }); + } }); // ---- ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์กฐํšŒ ---- // GET /api/pop/inspection-result?referenceId=xxx&referenceTable=yyy&screenId=zzz router.get("/", async (req: Request, res: Response) => { - const pool = getPool(); - const companyCode = (req as any).user?.companyCode; - const { referenceId, referenceTable, screenId } = req.query; + const pool = getPool(); + const companyCode = (req as any).user?.companyCode; + const { referenceId, referenceTable, screenId } = req.query; - if (!companyCode) { - return res.status(401).json({ success: false, message: "์ธ์ฆ ์ •๋ณด ์—†์Œ" }); - } + if (!companyCode) { + return res.status(401).json({ success: false, message: "์ธ์ฆ ์ •๋ณด ์—†์Œ" }); + } - const conditions: string[] = ["company_code = $1"]; - const params: unknown[] = [companyCode]; - let idx = 2; + const conditions: string[] = ["company_code = $1"]; + const params: unknown[] = [companyCode]; + let idx = 2; - if (referenceId) { - conditions.push(`reference_id = $${idx++}`); - params.push(referenceId); - } - if (referenceTable) { - conditions.push(`reference_table = $${idx++}`); - params.push(referenceTable); - } - if (screenId) { - conditions.push(`screen_id = $${idx++}`); - params.push(screenId); - } + if (referenceId) { + conditions.push(`reference_id = $${idx++}`); + params.push(referenceId); + } + if (referenceTable) { + conditions.push(`reference_table = $${idx++}`); + params.push(referenceTable); + } + if (screenId) { + conditions.push(`screen_id = $${idx++}`); + params.push(screenId); + } - const sql = ` + const sql = ` SELECT * FROM inspection_result WHERE ${conditions.join(" AND ")} ORDER BY created_date DESC `; - try { - const result = await pool.query(sql, params); - return res.json({ success: true, data: result.rows }); - } catch (err: any) { - return res.status(500).json({ success: false, message: err.message }); - } + try { + const result = await pool.query(sql, params); + return res.json({ success: true, data: result.rows }); + } catch (err: any) { + return res.status(500).json({ success: false, message: err.message }); + } }); // ---- ๊ฒ€์‚ฌ๋ฒˆํ˜ธ ์ฑ„๋ฒˆ (PC numberingRuleService ํ™œ์šฉ) ---- async function generateInspectionNumber(companyCode: string): Promise { - // PC ์ฑ„๋ฒˆ ์„œ๋น„์Šค ๋™์  import (์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€) - const { numberingRuleService } = await import("../services/numberingRuleService"); + // PC ์ฑ„๋ฒˆ ์„œ๋น„์Šค ๋™์  import (์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€) + const { numberingRuleService } = await import( + "../services/numberingRuleService" + ); - // 1) inspection_result_mng / inspection_number ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ - const rule = await numberingRuleService.getNumberingRuleByColumn( - companyCode, - "inspection_result_mng", - "inspection_number" - ); + // 1) inspection_result_mng / inspection_number ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ + const rule = await numberingRuleService.getNumberingRuleByColumn( + companyCode, + "inspection_result_mng", + "inspection_number", + ); - if (rule && rule.ruleId) { - // 2) PC API์™€ ๋™์ผํ•œ allocateCode ํ˜ธ์ถœ โ†’ ์‹ค์ œ ์‹œํ€€์Šค +1 - return await numberingRuleService.allocateCode(rule.ruleId, companyCode); - } + if (rule && rule.ruleId) { + // 2) PC API์™€ ๋™์ผํ•œ allocateCode ํ˜ธ์ถœ โ†’ ์‹ค์ œ ์‹œํ€€์Šค +1 + return await numberingRuleService.allocateCode(rule.ruleId, companyCode); + } - // fallback: ์ฑ„๋ฒˆ ๊ทœ์น™ ์—†์œผ๋ฉด ๋‹จ์ˆœ SELECT MAX - const { getPool } = await import("../database/db"); - const pool = getPool(); - const year = new Date().getFullYear(); - const prefix = `QI-${year}-`; - const result = await pool.query( - `SELECT inspection_number FROM inspection_result_mng + // fallback: ์ฑ„๋ฒˆ ๊ทœ์น™ ์—†์œผ๋ฉด ๋‹จ์ˆœ SELECT MAX + const { getPool } = await import("../database/db"); + const pool = getPool(); + const year = new Date().getFullYear(); + const prefix = `QI-${year}-`; + const result = await pool.query( + `SELECT inspection_number FROM inspection_result_mng WHERE company_code = $1 AND inspection_number LIKE $2 ORDER BY inspection_number DESC LIMIT 1`, - [companyCode, `${prefix}%`] - ); - let nextSeq = 1; - if (result.rows.length > 0) { - const lastNumber = result.rows[0].inspection_number; - const match = lastNumber.match(/(\d+)$/); - if (match) nextSeq = parseInt(match[1], 10) + 1; - } - return `${prefix}${String(nextSeq).padStart(4, "0")}`; + [companyCode, `${prefix}%`], + ); + let nextSeq = 1; + if (result.rows.length > 0) { + const lastNumber = result.rows[0].inspection_number; + const match = lastNumber.match(/(\d+)$/); + if (match) nextSeq = parseInt(match[1], 10) + 1; + } + return `${prefix}${String(nextSeq).padStart(4, "0")}`; } // ---- ๊ฒ€์‚ฌ๋ฒˆํ˜ธ ์ฑ„๋ฒˆ ์ „์šฉ ์—”๋“œํฌ์ธํŠธ (๊ฒ€์‚ฌ ๋ชจ๋‹ฌ์—์„œ ๊ฒ€์‚ฌ ์™„๋ฃŒ ์‹œ) ---- // POST /api/pop/inspection-result/allocate-number router.post("/allocate-number", async (req: Request, res: Response) => { - const companyCode = (req as any).user?.companyCode; - if (!companyCode) { - return res.status(401).json({ success: false, message: "์ธ์ฆ ์ •๋ณด ์—†์Œ" }); - } - try { - const inspectionNumber = await generateInspectionNumber(companyCode); - return res.json({ success: true, data: { inspectionNumber } }); - } catch (err: any) { - return res.status(500).json({ success: false, message: err.message }); - } + const companyCode = (req as any).user?.companyCode; + if (!companyCode) { + return res.status(401).json({ success: false, message: "์ธ์ฆ ์ •๋ณด ์—†์Œ" }); + } + try { + const inspectionNumber = await generateInspectionNumber(companyCode); + return res.json({ success: true, data: { inspectionNumber } }); + } catch (err: any) { + return res.status(500).json({ success: false, message: err.message }); + } }); // ---- ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์ €์žฅ (๋งˆ์Šคํ„ฐ + ๋””ํ…Œ์ผ ํŠธ๋žœ์žญ์…˜) ---- // POST /api/pop/inspection-result router.post("/", async (req: Request, res: Response) => { - const pool = getPool(); - const companyCode = (req as any).user?.companyCode; - const writer = (req as any).user?.userId; + const pool = getPool(); + const companyCode = (req as any).user?.companyCode; + const writer = (req as any).user?.userId; - if (!companyCode) { - return res.status(401).json({ success: false, message: "์ธ์ฆ ์ •๋ณด ์—†์Œ" }); - } + if (!companyCode) { + return res.status(401).json({ success: false, message: "์ธ์ฆ ์ •๋ณด ์—†์Œ" }); + } - const { - inspectionNumber: providedNumber, // ํ”„๋ก ํŠธ์—์„œ ๋ฏธ๋ฆฌ ์ฑ„๋ฒˆํ•œ ๋ฒˆํ˜ธ (์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ) - referenceTable, - referenceId, - screenId, - itemId, - itemCode, - itemName, - inspectionType, - items, // ๊ฒ€์‚ฌ ํ•ญ๋ชฉ๋ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด - overallJudgment, - totalQty, - goodQty, - badQty, - defectDescription, - memo, - inspector, - supplierCode, - supplierName, - isCompleted, - } = req.body; + const { + inspectionNumber: providedNumber, // ํ”„๋ก ํŠธ์—์„œ ๋ฏธ๋ฆฌ ์ฑ„๋ฒˆํ•œ ๋ฒˆํ˜ธ (์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ) + referenceTable, + referenceId, + screenId, + itemId, + itemCode, + itemName, + inspectionType, + items, // ๊ฒ€์‚ฌ ํ•ญ๋ชฉ๋ณ„ ๊ฒฐ๊ณผ ๋ฐฐ์—ด + overallJudgment, + totalQty, + goodQty, + badQty, + defectDescription, + memo, + inspector, + supplierCode, + supplierName, + isCompleted, + } = req.body; - if (!items || !Array.isArray(items) || items.length === 0) { - return res.status(400).json({ success: false, message: "๊ฒ€์‚ฌ ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค" }); - } + if (!items || !Array.isArray(items) || items.length === 0) { + return res + .status(400) + .json({ success: false, message: "๊ฒ€์‚ฌ ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค" }); + } - const client = await pool.connect(); - try { - await client.query("BEGIN"); + const client = await pool.connect(); + try { + await client.query("BEGIN"); - // 1. ๋™์ผ referenceId + referenceTable ๊ธฐ์กด ๋งˆ์Šคํ„ฐ/๋””ํ…Œ์ผ ์‚ญ์ œ (๋ฎ์–ด์“ฐ๊ธฐ) - if (referenceId && referenceTable) { - await client.query( - `DELETE FROM inspection_result WHERE master_id IN ( + // 1. ๋™์ผ referenceId + referenceTable ๊ธฐ์กด ๋งˆ์Šคํ„ฐ/๋””ํ…Œ์ผ ์‚ญ์ œ (๋ฎ์–ด์“ฐ๊ธฐ) + if (referenceId && referenceTable) { + await client.query( + `DELETE FROM inspection_result WHERE master_id IN ( SELECT id FROM inspection_result_mng WHERE company_code = $1 AND reference_id = $2 AND reference_table = $3 )`, - [companyCode, referenceId, referenceTable] - ); - await client.query( - `DELETE FROM inspection_result_mng + [companyCode, referenceId, referenceTable], + ); + await client.query( + `DELETE FROM inspection_result_mng WHERE company_code = $1 AND reference_id = $2 AND reference_table = $3`, - [companyCode, referenceId, referenceTable] - ); - } + [companyCode, referenceId, referenceTable], + ); + } - // 2. ๊ฒ€์‚ฌ๋ฒˆํ˜ธ (ํ”„๋ก ํŠธ์—์„œ ๋ฏธ๋ฆฌ ๋ฐ›์•˜์œผ๋ฉด ์žฌ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ฑ„๋ฒˆ) - const inspectionNumber = providedNumber || await generateInspectionNumber(companyCode); + // 2. ๊ฒ€์‚ฌ๋ฒˆํ˜ธ (ํ”„๋ก ํŠธ์—์„œ ๋ฏธ๋ฆฌ ๋ฐ›์•˜์œผ๋ฉด ์žฌ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ฑ„๋ฒˆ) + const inspectionNumber = + providedNumber || (await generateInspectionNumber(companyCode)); - // 3. ๋งˆ์Šคํ„ฐ INSERT - const completedFlag = isCompleted ? "Y" : "N"; - const completedDate = isCompleted ? new Date() : null; - const masterResult = await client.query( - `INSERT INTO inspection_result_mng ( + // 3. ๋งˆ์Šคํ„ฐ INSERT + const completedFlag = isCompleted ? "Y" : "N"; + const completedDate = isCompleted ? new Date() : null; + const masterResult = await client.query( + `INSERT INTO inspection_result_mng ( company_code, writer, inspection_number, reference_table, reference_id, screen_id, item_id, item_code, item_name, @@ -222,37 +227,37 @@ router.post("/", async (req: Request, res: Response) => { ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, NOW(), $18, $19, $20, $21 ) RETURNING id, inspection_number`, - [ - companyCode, - writer, - inspectionNumber, - referenceTable || null, - referenceId || null, - screenId || null, - itemId || null, - itemCode || null, - itemName || null, - inspectionType || null, - totalQty != null ? Number(totalQty) : null, - goodQty != null ? Number(goodQty) : null, - badQty != null ? Number(badQty) : null, - overallJudgment || null, - defectDescription || null, - memo || null, - inspector || writer, - supplierCode || null, - supplierName || null, - completedFlag, - completedDate, - ] - ); - const masterId = masterResult.rows[0].id; + [ + companyCode, + writer, + inspectionNumber, + referenceTable || null, + referenceId || null, + screenId || null, + itemId || null, + itemCode || null, + itemName || null, + inspectionType || null, + totalQty != null ? Number(totalQty) : null, + goodQty != null ? Number(goodQty) : null, + badQty != null ? Number(badQty) : null, + overallJudgment || null, + defectDescription || null, + memo || null, + inspector || writer, + supplierCode || null, + supplierName || null, + completedFlag, + completedDate, + ], + ); + const masterId = masterResult.rows[0].id; - // 4. ๋””ํ…Œ์ผ N๊ฑด INSERT - const insertedDetailIds: string[] = []; - for (const item of items) { - const detailResult = await client.query( - `INSERT INTO inspection_result ( + // 4. ๋””ํ…Œ์ผ N๊ฑด INSERT + const insertedDetailIds: string[] = []; + for (const item of items) { + const detailResult = await client.query( + `INSERT INTO inspection_result ( company_code, writer, master_id, reference_table, reference_id, screen_id, inspection_info_id, item_id, item_code, item_name, @@ -262,48 +267,48 @@ router.post("/", async (req: Request, res: Response) => { ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21 ) RETURNING id`, - [ - companyCode, - writer, - masterId, - referenceTable || null, - referenceId || null, - screenId || null, - item.inspectionInfoId || null, - itemId || item.itemId || null, - itemCode || item.itemCode || null, - itemName || item.itemName || null, - inspectionType || item.inspectionType || null, - item.inspectionItemName || null, - item.inspectionStandard || null, - item.passCriteria || null, - item.isRequired || "Y", - item.measuredValue || null, - item.judgment || null, - overallJudgment || null, - memo || null, - completedFlag, - completedDate, - ] - ); - insertedDetailIds.push(detailResult.rows[0].id); - } + [ + companyCode, + writer, + masterId, + referenceTable || null, + referenceId || null, + screenId || null, + item.inspectionInfoId || null, + itemId || item.itemId || null, + itemCode || item.itemCode || null, + itemName || item.itemName || null, + inspectionType || item.inspectionType || null, + item.inspectionItemName || null, + item.inspectionStandard || null, + item.passCriteria || null, + item.isRequired || "Y", + item.measuredValue || null, + item.judgment || null, + overallJudgment || null, + memo || null, + completedFlag, + completedDate, + ], + ); + insertedDetailIds.push(detailResult.rows[0].id); + } - await client.query("COMMIT"); - return res.json({ - success: true, - data: { - masterId, - inspectionNumber, - detailIds: insertedDetailIds, - }, - }); - } catch (err: any) { - await client.query("ROLLBACK"); - return res.status(500).json({ success: false, message: err.message }); - } finally { - client.release(); - } + await client.query("COMMIT"); + return res.json({ + success: true, + data: { + masterId, + inspectionNumber, + detailIds: insertedDetailIds, + }, + }); + } catch (err: any) { + await client.query("ROLLBACK"); + return res.status(500).json({ success: false, message: err.message }); + } finally { + client.release(); + } }); export default router; diff --git a/frontend/app/(pop)/pop/inventory/history/page.tsx b/frontend/app/(pop)/pop/inventory/history/page.tsx index dafee006..d68d80c7 100644 --- a/frontend/app/(pop)/pop/inventory/history/page.tsx +++ b/frontend/app/(pop)/pop/inventory/history/page.tsx @@ -4,9 +4,9 @@ import { PopShell } from "@/components/pop/hardcoded"; import { InOutHistory } from "@/components/pop/hardcoded/inventory"; export default function InOutHistoryPage() { - return ( - - - - ); + return ( + + + + ); } diff --git a/frontend/app/(pop)/pop/inventory/page.tsx b/frontend/app/(pop)/pop/inventory/page.tsx index 25499758..9f60e883 100644 --- a/frontend/app/(pop)/pop/inventory/page.tsx +++ b/frontend/app/(pop)/pop/inventory/page.tsx @@ -4,9 +4,9 @@ import { PopShell } from "@/components/pop/hardcoded"; import { InventoryHome } from "@/components/pop/hardcoded/inventory"; export default function InventoryPage() { - return ( - - - - ); + return ( + + + + ); } diff --git a/frontend/app/(pop)/pop/quality/inspection/page.tsx b/frontend/app/(pop)/pop/quality/inspection/page.tsx index e682c963..3b12d4e9 100644 --- a/frontend/app/(pop)/pop/quality/inspection/page.tsx +++ b/frontend/app/(pop)/pop/quality/inspection/page.tsx @@ -4,9 +4,9 @@ import { PopShell } from "@/components/pop/hardcoded"; import { InspectionList } from "@/components/pop/hardcoded/quality"; export default function InspectionListPage() { - return ( - - - - ); + return ( + + + + ); } diff --git a/frontend/app/(pop)/pop/quality/page.tsx b/frontend/app/(pop)/pop/quality/page.tsx index 9f0bf528..e294e5a8 100644 --- a/frontend/app/(pop)/pop/quality/page.tsx +++ b/frontend/app/(pop)/pop/quality/page.tsx @@ -4,9 +4,9 @@ import { PopShell } from "@/components/pop/hardcoded"; import { QualityHome } from "@/components/pop/hardcoded/quality"; export default function QualityPage() { - return ( - - - - ); + return ( + + + + ); } diff --git a/frontend/components/pop/hardcoded/MenuIcons.tsx b/frontend/components/pop/hardcoded/MenuIcons.tsx index 15184777..4d2f93e2 100644 --- a/frontend/components/pop/hardcoded/MenuIcons.tsx +++ b/frontend/components/pop/hardcoded/MenuIcons.tsx @@ -1,143 +1,219 @@ "use client"; -import React from "react"; import { useRouter } from "next/navigation"; +import type React from "react"; interface MenuIconItem { - id: string; - title: string; - gradient: string; - shadowColor: string; - icon: React.ReactNode; - href: string; + id: string; + title: string; + gradient: string; + shadowColor: string; + icon: React.ReactNode; + href: string; } const MENU_ITEMS: MenuIconItem[] = [ - { - id: "incoming", - title: "์ž…๊ณ ", - gradient: "linear-gradient(135deg,#3b82f6,#1d4ed8)", - shadowColor: "rgba(59,130,246,.3)", - icon: ( - - - - ), - href: "/pop/inbound", - }, - { - id: "outgoing", - title: "์ถœ๊ณ ", - gradient: "linear-gradient(135deg,#22c55e,#15803d)", - shadowColor: "rgba(34,197,94,.3)", - icon: ( - - - - ), - href: "/pop/outbound", - }, - { - id: "production", - title: "์ƒ์‚ฐ", - gradient: "linear-gradient(135deg,#f59e0b,#d97706)", - shadowColor: "rgba(245,158,11,.3)", - icon: ( - - - - - ), - href: "/pop/production", - }, - { - id: "quality", - title: "ํ’ˆ์งˆ", - gradient: "linear-gradient(135deg,#ef4444,#b91c1c)", - shadowColor: "rgba(239,68,68,.3)", - icon: ( - - - - ), - href: "/pop/quality", - }, - { - id: "equipment", - title: "์„ค๋น„", - gradient: "linear-gradient(135deg,#8b5cf6,#6d28d9)", - shadowColor: "rgba(139,92,246,.3)", - icon: ( - - - - ), - href: "/pop/screens/equipment", - }, - { - id: "inventory", - title: "์žฌ๊ณ ", - gradient: "linear-gradient(135deg,#06b6d4,#0e7490)", - shadowColor: "rgba(6,182,212,.3)", - icon: ( - - - - ), - href: "/pop/inventory", - }, - // ์ž‘์—…์ง€์‹œ, ์ƒ์‚ฐ์‹ค์ ์€ ์ƒ์‚ฐ๊ด€๋ฆฌ(/pop/production) ๋ฉ”๋‰ด ์•ˆ์œผ๋กœ ์ด๋™ - { - id: "safety", - title: "์•ˆ์ „๊ด€๋ฆฌ", - gradient: "linear-gradient(135deg,#f97316,#c2410c)", - shadowColor: "rgba(249,115,22,.3)", - icon: ( - - - - ), - href: "/pop/screens/safety", - }, + { + id: "incoming", + title: "์ž…๊ณ ", + gradient: "linear-gradient(135deg,#3b82f6,#1d4ed8)", + shadowColor: "rgba(59,130,246,.3)", + icon: ( + + + + ), + href: "/pop/inbound", + }, + { + id: "outgoing", + title: "์ถœ๊ณ ", + gradient: "linear-gradient(135deg,#22c55e,#15803d)", + shadowColor: "rgba(34,197,94,.3)", + icon: ( + + + + ), + href: "/pop/outbound", + }, + { + id: "production", + title: "์ƒ์‚ฐ", + gradient: "linear-gradient(135deg,#f59e0b,#d97706)", + shadowColor: "rgba(245,158,11,.3)", + icon: ( + + + + + ), + href: "/pop/production", + }, + { + id: "quality", + title: "ํ’ˆ์งˆ", + gradient: "linear-gradient(135deg,#ef4444,#b91c1c)", + shadowColor: "rgba(239,68,68,.3)", + icon: ( + + + + ), + href: "/pop/quality", + }, + { + id: "equipment", + title: "์„ค๋น„", + gradient: "linear-gradient(135deg,#8b5cf6,#6d28d9)", + shadowColor: "rgba(139,92,246,.3)", + icon: ( + + + + ), + href: "/pop/screens/equipment", + }, + { + id: "inventory", + title: "์žฌ๊ณ ", + gradient: "linear-gradient(135deg,#06b6d4,#0e7490)", + shadowColor: "rgba(6,182,212,.3)", + icon: ( + + + + ), + href: "/pop/inventory", + }, + // ์ž‘์—…์ง€์‹œ, ์ƒ์‚ฐ์‹ค์ ์€ ์ƒ์‚ฐ๊ด€๋ฆฌ(/pop/production) ๋ฉ”๋‰ด ์•ˆ์œผ๋กœ ์ด๋™ + { + id: "safety", + title: "์•ˆ์ „๊ด€๋ฆฌ", + gradient: "linear-gradient(135deg,#f97316,#c2410c)", + shadowColor: "rgba(249,115,22,.3)", + icon: ( + + + + ), + href: "/pop/screens/safety", + }, ]; export function MenuIcons() { - const router = useRouter(); + const router = useRouter(); - const handleClick = (item: MenuIconItem) => { - if (item.href === "#") { - alert(`${item.title} ํ™”๋ฉด์€ ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค.`); - } else { - router.push(item.href); - } - }; + const handleClick = (item: MenuIconItem) => { + if (item.href === "#") { + alert(`${item.title} ํ™”๋ฉด์€ ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค.`); + } else { + router.push(item.href); + } + }; - return ( -
-

- ๋ฉ”๋‰ด -

-
- {MENU_ITEMS.map((item) => ( -
handleClick(item)} - > -
- {item.icon} -
- {item.title} -
- ))} -
-
- ); + return ( +
+

+ ๋ฉ”๋‰ด +

+
+ {MENU_ITEMS.map((item) => ( +
handleClick(item)} + > +
+ {item.icon} +
+ + {item.title} + +
+ ))} +
+
+ ); } diff --git a/frontend/components/pop/hardcoded/common/ConfirmModal.tsx b/frontend/components/pop/hardcoded/common/ConfirmModal.tsx index 8b5a5e41..34bfff4b 100644 --- a/frontend/components/pop/hardcoded/common/ConfirmModal.tsx +++ b/frontend/components/pop/hardcoded/common/ConfirmModal.tsx @@ -3,14 +3,14 @@ import React from "react"; export interface ConfirmModalProps { - open: boolean; - title?: string; - message: string; - confirmText?: string; - cancelText?: string; - variant?: "primary" | "danger" | "success"; - onConfirm: () => void; - onCancel: () => void; + open: boolean; + title?: string; + message: string; + confirmText?: string; + cancelText?: string; + variant?: "primary" | "danger" | "success"; + onConfirm: () => void; + onCancel: () => void; } /** @@ -18,65 +18,65 @@ export interface ConfirmModalProps { * ๋ชจ๋ฐ”์ผ ์นœํ™” ๋””์ž์ธ, bottom-sheet ์Šคํƒ€์ผ */ export function ConfirmModal({ - open, - title, - message, - confirmText = "ํ™•์ธ", - cancelText = "์ทจ์†Œ", - variant = "primary", - onConfirm, - onCancel, + open, + title, + message, + confirmText = "ํ™•์ธ", + cancelText = "์ทจ์†Œ", + variant = "primary", + onConfirm, + onCancel, }: ConfirmModalProps) { - if (!open) return null; + if (!open) return null; - const confirmBg = - variant === "danger" - ? "bg-gradient-to-b from-red-500 to-red-600 hover:from-red-600 hover:to-red-700" - : variant === "success" - ? "bg-gradient-to-b from-emerald-500 to-emerald-600 hover:from-emerald-600 hover:to-emerald-700" - : "bg-gradient-to-b from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700"; + const confirmBg = + variant === "danger" + ? "bg-gradient-to-b from-red-500 to-red-600 hover:from-red-600 hover:to-red-700" + : variant === "success" + ? "bg-gradient-to-b from-emerald-500 to-emerald-600 hover:from-emerald-600 hover:to-emerald-700" + : "bg-gradient-to-b from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700"; - return ( -
- {/* Overlay */} -
+ return ( +
+ {/* Overlay */} +
- {/* Center modal */} -
-
e.stopPropagation()} - > - {/* Body */} -
- {title && ( -

{title}

- )} -

- {message} -

-
+ {/* Center modal */} +
+
e.stopPropagation()} + > + {/* Body */} +
+ {title && ( +

{title}

+ )} +

+ {message} +

+
- {/* Buttons */} -
- -
- -
-
-
-
- ); + {/* Buttons */} +
+ +
+ +
+
+
+
+ ); } diff --git a/frontend/components/pop/hardcoded/inbound/InboundCart.tsx b/frontend/components/pop/hardcoded/inbound/InboundCart.tsx index e9649cc9..c6d7a585 100644 --- a/frontend/components/pop/hardcoded/inbound/InboundCart.tsx +++ b/frontend/components/pop/hardcoded/inbound/InboundCart.tsx @@ -1,7 +1,7 @@ "use client"; -import React, { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; +import React, { useCallback, useEffect, useState } from "react"; import { apiClient } from "@/lib/api/client"; import { InspectionModal, type InspectionResult } from "./InspectionModal"; import type { PackageEntry } from "./NumberPadModal"; @@ -11,9 +11,9 @@ import type { PackageEntry } from "./NumberPadModal"; /* ------------------------------------------------------------------ */ interface Warehouse { - warehouse_code: string; - warehouse_name: string; - warehouse_type?: string; + warehouse_code: string; + warehouse_name: string; + warehouse_type?: string; } /* ------------------------------------------------------------------ */ @@ -21,41 +21,41 @@ interface Warehouse { /* ------------------------------------------------------------------ */ export interface CartItem { - id: string; - /** cart_items ํ…Œ์ด๋ธ”์˜ PK (UUID) โ€” DB ์‚ญ์ œ์šฉ */ - dbId?: string; - /** purchase_detail or purchase_order_mng */ - source_table: string; - /** PK of the source row */ - source_id: string; - purchase_no: string; - item_code: string; - item_name: string; - spec: string; - material: string; - order_qty: number; - remain_qty: number; - /** User-entered quantity */ - inbound_qty: number; - unit_price: number; - supplier_code: string; - supplier_name: string; - order_date: string; - inspection_required?: boolean; - inspection_type?: "self" | "request" | null; - packages?: PackageEntry[]; - inspectionResult?: InspectionResult | null; + id: string; + /** cart_items ํ…Œ์ด๋ธ”์˜ PK (UUID) โ€” DB ์‚ญ์ œ์šฉ */ + dbId?: string; + /** purchase_detail or purchase_order_mng */ + source_table: string; + /** PK of the source row */ + source_id: string; + purchase_no: string; + item_code: string; + item_name: string; + spec: string; + material: string; + order_qty: number; + remain_qty: number; + /** User-entered quantity */ + inbound_qty: number; + unit_price: number; + supplier_code: string; + supplier_name: string; + order_date: string; + inspection_required?: boolean; + inspection_type?: "self" | "request" | null; + packages?: PackageEntry[]; + inspectionResult?: InspectionResult | null; } interface InboundCartProps { - open: boolean; - onClose: () => void; - items: CartItem[]; - onUpdateQty: (id: string, qty: number) => void; - onRemove: (id: string) => void; - onClear: () => void; - supplierName?: string; - onUpdateItems?: (items: CartItem[]) => void; + open: boolean; + onClose: () => void; + items: CartItem[]; + onUpdateQty: (id: string, qty: number) => void; + onRemove: (id: string) => void; + onClear: () => void; + supplierName?: string; + onUpdateItems?: (items: CartItem[]) => void; } /* ------------------------------------------------------------------ */ @@ -63,515 +63,673 @@ interface InboundCartProps { /* ------------------------------------------------------------------ */ export function InboundCart({ - open, - onClose, - items, - onUpdateQty, - onRemove, - onClear, - supplierName, - onUpdateItems, + open, + onClose, + items, + onUpdateQty, + onRemove, + onClear, + supplierName, + onUpdateItems, }: InboundCartProps) { - const router = useRouter(); - const [confirming, setConfirming] = useState(false); - const [resultMsg, setResultMsg] = useState(null); - const [selectedItems, setSelectedItems] = useState>(new Set()); - const [inspectionModalOpen, setInspectionModalOpen] = useState(false); - const [inspectionTarget, setInspectionTarget] = useState(null); + const router = useRouter(); + const [confirming, setConfirming] = useState(false); + const [resultMsg, setResultMsg] = useState(null); + const [selectedItems, setSelectedItems] = useState>(new Set()); + const [inspectionModalOpen, setInspectionModalOpen] = useState(false); + const [inspectionTarget, setInspectionTarget] = useState( + null, + ); - /* Warehouse state */ - const [warehouses, setWarehouses] = useState([]); - const [selectedWarehouse, setSelectedWarehouse] = useState(""); + /* Warehouse state */ + const [warehouses, setWarehouses] = useState([]); + const [selectedWarehouse, setSelectedWarehouse] = useState(""); - /* Fetch warehouses on mount */ - const fetchWarehouses = useCallback(async () => { - try { - const res = await apiClient.get("/receiving/warehouses"); - const data: Warehouse[] = res.data?.data ?? []; - setWarehouses(data); - if (data.length > 0 && !selectedWarehouse) { - setSelectedWarehouse(data[0].warehouse_code); - } - } catch { - // Keep empty - user can still confirm without warehouse - } - }, [selectedWarehouse]); + /* Fetch warehouses on mount */ + const fetchWarehouses = useCallback(async () => { + try { + const res = await apiClient.get("/receiving/warehouses"); + const data: Warehouse[] = res.data?.data ?? []; + setWarehouses(data); + if (data.length > 0 && !selectedWarehouse) { + setSelectedWarehouse(data[0].warehouse_code); + } + } catch { + // Keep empty - user can still confirm without warehouse + } + }, [selectedWarehouse]); - useEffect(() => { - if (open) { - fetchWarehouses(); - } - }, [open, fetchWarehouses]); + useEffect(() => { + if (open) { + fetchWarehouses(); + } + }, [open, fetchWarehouses]); - const totalQty = items.reduce((s, i) => s + i.inbound_qty, 0); - const totalAmount = items.reduce((s, i) => s + i.inbound_qty * i.unit_price, 0); + const totalQty = items.reduce((s, i) => s + i.inbound_qty, 0); + const totalAmount = items.reduce( + (s, i) => s + i.inbound_qty * i.unit_price, + 0, + ); - /* Toggle select */ - const toggleSelect = (id: string) => { - setSelectedItems((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }; + /* Toggle select */ + const toggleSelect = (id: string) => { + setSelectedItems((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; - const toggleSelectAll = () => { - if (selectedItems.size === items.length) { - setSelectedItems(new Set()); - } else { - setSelectedItems(new Set(items.map((i) => i.id))); - } - }; + const toggleSelectAll = () => { + if (selectedItems.size === items.length) { + setSelectedItems(new Set()); + } else { + setSelectedItems(new Set(items.map((i) => i.id))); + } + }; - /* Open inspection modal */ - const openInspection = (item: CartItem) => { - setInspectionTarget(item); - setInspectionModalOpen(true); - }; + /* Open inspection modal */ + const openInspection = (item: CartItem) => { + setInspectionTarget(item); + setInspectionModalOpen(true); + }; - /* Handle inspection complete */ - const handleInspectionComplete = (result: InspectionResult) => { - if (!inspectionTarget || !onUpdateItems) return; - const updated = items.map((item) => - item.id === inspectionTarget.id - ? { ...item, inspectionResult: result } - : item - ); - onUpdateItems(updated); - setInspectionTarget(null); - }; + /* Handle inspection complete */ + const handleInspectionComplete = (result: InspectionResult) => { + if (!inspectionTarget || !onUpdateItems) return; + const updated = items.map((item) => + item.id === inspectionTarget.id + ? { ...item, inspectionResult: result } + : item, + ); + onUpdateItems(updated); + setInspectionTarget(null); + }; - /* Confirm inbound โ€” PC receivingController.create ์™€ ๋™์ผํ•œ body ๊ตฌ์กฐ */ - const handleConfirm = async () => { - if (items.length === 0) return; - if (!selectedWarehouse) { - setResultMsg("์˜ค๋ฅ˜: ์ž…๊ณ  ์ฐฝ๊ณ ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); - return; - } - setConfirming(true); - setResultMsg(null); + /* Confirm inbound โ€” PC receivingController.create ์™€ ๋™์ผํ•œ body ๊ตฌ์กฐ */ + const handleConfirm = async () => { + if (items.length === 0) return; + if (!selectedWarehouse) { + setResultMsg("์˜ค๋ฅ˜: ์ž…๊ณ  ์ฐฝ๊ณ ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); + return; + } + setConfirming(true); + setResultMsg(null); - try { - // 1. ์ž…๊ณ ๋ฒˆํ˜ธ ์ฑ„๋ฒˆ (RCV-YYYY-XXXX) - let inboundNumber: string | undefined; - try { - const numRes = await apiClient.get("/receiving/generate-number"); - if (numRes.data?.success && numRes.data?.data) { - inboundNumber = numRes.data.data; - } - } catch { - // ์ฑ„๋ฒˆ ์‹คํŒจ ์‹œ ๋ฐฑ์—”๋“œ๊ฐ€ ์ฒ˜๋ฆฌ - } + try { + // 1. ์ž…๊ณ ๋ฒˆํ˜ธ ์ฑ„๋ฒˆ (RCV-YYYY-XXXX) + let inboundNumber: string | undefined; + try { + const numRes = await apiClient.get("/receiving/generate-number"); + if (numRes.data?.success && numRes.data?.data) { + inboundNumber = numRes.data.data; + } + } catch { + // ์ฑ„๋ฒˆ ์‹คํŒจ ์‹œ ๋ฐฑ์—”๋“œ๊ฐ€ ์ฒ˜๋ฆฌ + } - // 2. POST /api/receiving โ€” PC create ์™€ ๋™์ผํ•œ payload - const payload = { - inbound_number: inboundNumber, - inbound_date: new Date().toISOString().slice(0, 10), - warehouse_code: selectedWarehouse, - inbound_type: "๊ตฌ๋งค์ž…๊ณ ", - items: items.map((item, idx) => ({ - inbound_type: "๊ตฌ๋งค์ž…๊ณ ", - item_number: item.item_code, - item_name: item.item_name, - spec: item.spec || "", - material: item.material || "", - unit: "EA", - inbound_qty: String(item.inbound_qty), - unit_price: String(item.unit_price || 0), - total_amount: String((item.inbound_qty || 0) * (item.unit_price || 0)), - reference_number: item.purchase_no, - supplier_code: item.supplier_code, - supplier_name: item.supplier_name, - inspection_status: item.inspectionResult?.completed - ? "๊ฒ€์‚ฌ์™„๋ฃŒ" - : item.inspection_required - ? "๊ฒ€์‚ฌ๋Œ€๊ธฐ" - : "ํ•ฉ๊ฒฉ", - source_table: item.source_table, - source_id: item.source_id || item.id, - seq_no: idx + 1, - })), - }; + // 2. POST /api/receiving โ€” PC create ์™€ ๋™์ผํ•œ payload + const payload = { + inbound_number: inboundNumber, + inbound_date: new Date().toISOString().slice(0, 10), + warehouse_code: selectedWarehouse, + inbound_type: "๊ตฌ๋งค์ž…๊ณ ", + items: items.map((item, idx) => ({ + inbound_type: "๊ตฌ๋งค์ž…๊ณ ", + item_number: item.item_code, + item_name: item.item_name, + spec: item.spec || "", + material: item.material || "", + unit: "EA", + inbound_qty: String(item.inbound_qty), + unit_price: String(item.unit_price || 0), + total_amount: String( + (item.inbound_qty || 0) * (item.unit_price || 0), + ), + reference_number: item.purchase_no, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + inspection_status: item.inspectionResult?.completed + ? "๊ฒ€์‚ฌ์™„๋ฃŒ" + : item.inspection_required + ? "๊ฒ€์‚ฌ๋Œ€๊ธฐ" + : "ํ•ฉ๊ฒฉ", + source_table: item.source_table, + source_id: item.source_id || item.id, + seq_no: idx + 1, + })), + }; - const res = await apiClient.post("/receiving", payload); + const res = await apiClient.post("/receiving", payload); - if (res.data?.success) { - // 2-1. ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๊ฐ€ ์žˆ๋Š” ํ•ญ๋ชฉ โ†’ inspection_result์— ์ €์žฅ - const insertedDetails: any[] = res.data?.data?.details ?? res.data?.data?.items ?? []; - const inboundHeaderNo = res.data?.data?.header?.inbound_number || inboundNumber || ""; - const inspectionPromises = items - .map((item, idx) => { - if (!item.inspectionResult?.completed) return null; - const matchedDetail = insertedDetails[idx] ?? {}; - const referenceId = matchedDetail.id || matchedDetail.detail_id || `${inboundHeaderNo}-${idx + 1}`; - const goodQty = item.inspectionResult.goodQty || 0; - const badQty = item.inspectionResult.badQty || 0; - const totalQty = goodQty + badQty; - const overallJudgment = badQty === 0 ? "ํ•ฉ๊ฒฉ" : "๋ถˆํ•ฉ๊ฒฉ"; - return apiClient.post("/pop/inspection-result", { - inspectionNumber: item.inspectionResult.inspectionNumber, // ์นดํŠธ์—์„œ ๋ฐ›์€ ๊ฒ€์‚ฌ๋ฒˆํ˜ธ ์žฌ์‚ฌ์šฉ - referenceTable: "inbound_mng", - referenceId, - screenId: "pop_inbound_inspection", - itemId: item.item_id || null, - itemCode: item.item_code, - itemName: item.item_name, - inspectionType: "์ž…๊ณ ๊ฒ€์‚ฌ", - overallJudgment, - totalQty, - goodQty, - badQty, - defectDescription: badQty > 0 ? `๋ถˆ๋Ÿ‰ ${badQty}๊ฑด` : "", - memo: item.inspectionResult.remark || "", - supplierCode: item.supplier_code || null, - supplierName: item.supplier_name || null, - isCompleted: true, - items: item.inspectionResult.items.map((insp: any) => ({ - inspectionInfoId: insp.id || null, - inspectionItemName: insp.inspection_item_name, - inspectionStandard: insp.inspection_standard, - passCriteria: insp.pass_criteria, - isRequired: insp.is_required || "Y", - measuredValue: insp.measured_value || "", - judgment: insp.result || null, - })), - }).catch((err) => { - console.error("[inspection_result ์ €์žฅ ์‹คํŒจ]", item.item_code, err?.message); - }); - }) - .filter(Boolean); + if (res.data?.success) { + // 2-1. ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๊ฐ€ ์žˆ๋Š” ํ•ญ๋ชฉ โ†’ inspection_result์— ์ €์žฅ + const insertedDetails: any[] = + res.data?.data?.details ?? res.data?.data?.items ?? []; + const inboundHeaderNo = + res.data?.data?.header?.inbound_number || inboundNumber || ""; + const inspectionPromises = items + .map((item, idx) => { + if (!item.inspectionResult?.completed) return null; + const matchedDetail = insertedDetails[idx] ?? {}; + const referenceId = + matchedDetail.id || + matchedDetail.detail_id || + `${inboundHeaderNo}-${idx + 1}`; + const goodQty = item.inspectionResult.goodQty || 0; + const badQty = item.inspectionResult.badQty || 0; + const totalQty = goodQty + badQty; + const overallJudgment = badQty === 0 ? "ํ•ฉ๊ฒฉ" : "๋ถˆํ•ฉ๊ฒฉ"; + return apiClient + .post("/pop/inspection-result", { + inspectionNumber: item.inspectionResult.inspectionNumber, // ์นดํŠธ์—์„œ ๋ฐ›์€ ๊ฒ€์‚ฌ๋ฒˆํ˜ธ ์žฌ์‚ฌ์šฉ + referenceTable: "inbound_mng", + referenceId, + screenId: "pop_inbound_inspection", + itemId: item.item_id || null, + itemCode: item.item_code, + itemName: item.item_name, + inspectionType: "์ž…๊ณ ๊ฒ€์‚ฌ", + overallJudgment, + totalQty, + goodQty, + badQty, + defectDescription: badQty > 0 ? `๋ถˆ๋Ÿ‰ ${badQty}๊ฑด` : "", + memo: item.inspectionResult.remark || "", + supplierCode: item.supplier_code || null, + supplierName: item.supplier_name || null, + isCompleted: true, + items: item.inspectionResult.items.map((insp: any) => ({ + inspectionInfoId: insp.id || null, + inspectionItemName: insp.inspection_item_name, + inspectionStandard: insp.inspection_standard, + passCriteria: insp.pass_criteria, + isRequired: insp.is_required || "Y", + measuredValue: insp.measured_value || "", + judgment: insp.result || null, + })), + }) + .catch((err) => { + console.error( + "[inspection_result ์ €์žฅ ์‹คํŒจ]", + item.item_code, + err?.message, + ); + }); + }) + .filter(Boolean); - if (inspectionPromises.length > 0) { - await Promise.allSettled(inspectionPromises); - } + if (inspectionPromises.length > 0) { + await Promise.allSettled(inspectionPromises); + } - // 3. cart_items DB ์ •๋ฆฌ (๋ฐฑ๊ทธ๋ผ์šด๋“œ, ๋…ผ๋ธ”๋กœํ‚น) - // cart_items.row_key ๋กœ ์‚ญ์ œ (row_key = source_id ๋กœ ์ €์žฅ๋จ) - const rowKeys = items.map((item) => item.source_id || item.id).filter(Boolean); - if (rowKeys.length > 0) { - apiClient.post("/pop/execute-action", { - tasks: [{ type: "cart-save" }], - cartChanges: { - toDelete: rowKeys, - }, - }).catch(() => { - // cart cleanup ์‹คํŒจ ์‹œ ๋ฌด์‹œ - }); - } + // 3. cart_items DB ์ •๋ฆฌ (๋ฐฑ๊ทธ๋ผ์šด๋“œ, ๋…ผ๋ธ”๋กœํ‚น) + // cart_items.row_key ๋กœ ์‚ญ์ œ (row_key = source_id ๋กœ ์ €์žฅ๋จ) + const rowKeys = items + .map((item) => item.source_id || item.id) + .filter(Boolean); + if (rowKeys.length > 0) { + apiClient + .post("/pop/execute-action", { + tasks: [{ type: "cart-save" }], + cartChanges: { + toDelete: rowKeys, + }, + }) + .catch(() => { + // cart cleanup ์‹คํŒจ ์‹œ ๋ฌด์‹œ + }); + } - const inboundNo = res.data?.data?.header?.inbound_number || inboundNumber || ""; - setResultMsg(`${items.length}๊ฑด ์ž…๊ณ  ๋“ฑ๋ก ์™„๋ฃŒ! (${inboundNo})`); - setTimeout(() => { - onClear(); - onClose(); - router.push("/pop/inbound"); - }, 1500); - } else { - setResultMsg(`์˜ค๋ฅ˜: ${res.data?.message || "์ž…๊ณ  ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."}`); - } - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : "์ž…๊ณ  ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."; - setResultMsg(`์˜ค๋ฅ˜: ${msg}`); - } finally { - setConfirming(false); - } - }; + const inboundNo = + res.data?.data?.header?.inbound_number || inboundNumber || ""; + setResultMsg(`${items.length}๊ฑด ์ž…๊ณ  ๋“ฑ๋ก ์™„๋ฃŒ! (${inboundNo})`); + setTimeout(() => { + onClear(); + onClose(); + router.push("/pop/inbound"); + }, 1500); + } else { + setResultMsg( + `์˜ค๋ฅ˜: ${res.data?.message || "์ž…๊ณ  ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."}`, + ); + } + } catch (err: unknown) { + const msg = + err instanceof Error ? err.message : "์ž…๊ณ  ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."; + setResultMsg(`์˜ค๋ฅ˜: ${msg}`); + } finally { + setConfirming(false); + } + }; - if (!open) return null; + if (!open) return null; - return ( -
- {/* Overlay */} -
+ return ( +
+ {/* Overlay */} +
- {/* Panel */} -
- {/* Header */} -
-
-
- - - -
-
-

์ž…๊ณ  ์žฅ๋ฐ”๊ตฌ๋‹ˆ

- {supplierName && ( -

{supplierName}

- )} -
-
- -
+ {/* Panel */} +
+ {/* Header */} +
+
+
+ + + +
+
+

์ž…๊ณ  ์žฅ๋ฐ”๊ตฌ๋‹ˆ

+ {supplierName && ( +

{supplierName}

+ )} +
+
+ +
- {/* Select all bar */} - {items.length > 0 && ( -
- - - ์ „์ฒด ์„ ํƒ ({selectedItems.size}/{items.length}) - -
- )} + {/* Select all bar */} + {items.length > 0 && ( +
+ + + ์ „์ฒด ์„ ํƒ ({selectedItems.size}/{items.length}) + +
+ )} - {/* Items */} -
- {items.length === 0 ? ( -
- - - -

๋‹ด์€ ํ’ˆ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค

-
- ) : ( -
- {items.map((item) => ( -
- {/* Top row: checkbox + name + delete */} -
- {/* Checkbox */} - + {/* Items */} +
+ {items.length === 0 ? ( +
+ + + +

๋‹ด์€ ํ’ˆ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค

+
+ ) : ( +
+ {items.map((item) => ( +
+ {/* Top row: checkbox + name + delete */} +
+ {/* Checkbox */} + -
-

{item.item_name}

-

- {item.item_code} | {item.purchase_no} -

-
+
+

+ {item.item_name} +

+

+ {item.item_code} | {item.purchase_no} +

+
- {/* Delete button */} - -
+ {/* Delete button */} + +
- {/* Spec row */} - {(item.spec || item.material) && ( -

- {[item.spec, item.material].filter(Boolean).join(" | ")} -

- )} + {/* Spec row */} + {(item.spec || item.material) && ( +

+ {[item.spec, item.material].filter(Boolean).join(" | ")} +

+ )} - {/* Package info */} - {item.packages && item.packages.length > 0 && ( -
-
- - ํฌ์žฅ์™„๋ฃŒ - - - {"\uD83D\uDCE6"} {item.packages.map(p => - `${p.count}${p.unit.label} x ${p.qtyPerUnit.toLocaleString()} = ${(p.count * p.qtyPerUnit).toLocaleString()}EA` - ).join(", ")} - -
-
- )} + {/* Package info */} + {item.packages && item.packages.length > 0 && ( +
+
+ + ํฌ์žฅ์™„๋ฃŒ + + + {"\uD83D\uDCE6"}{" "} + {item.packages + .map( + (p) => + `${p.count}${p.unit.label} x ${p.qtyPerUnit.toLocaleString()} = ${(p.count * p.qtyPerUnit).toLocaleString()}EA`, + ) + .join(", ")} + +
+
+ )} - {/* Inspection row */} - {(item.inspection_type === "self" || item.inspection_type === "request") && ( -
- -
- )} + {/* Inspection row */} + {(item.inspection_type === "self" || + item.inspection_type === "request") && ( +
+ +
+ )} - {/* Qty controls */} -
-
- ๋ฏธ์ž…๊ณ : {item.remain_qty.toLocaleString()} -
-
- - { - const v = parseInt(e.target.value, 10); - if (!isNaN(v) && v >= 0) onUpdateQty(item.id, Math.min(v, item.remain_qty)); - }} - className="w-16 h-8 text-center text-sm font-semibold border border-gray-200 rounded-lg outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-100" - style={{ fontVariantNumeric: "tabular-nums" }} - /> - -
-
-
- ))} -
- )} -
+ {/* Qty controls */} +
+
+ + ๋ฏธ์ž…๊ณ : {item.remain_qty.toLocaleString()} + +
+
+ + { + const v = parseInt(e.target.value, 10); + if (!isNaN(v) && v >= 0) + onUpdateQty(item.id, Math.min(v, item.remain_qty)); + }} + className="w-16 h-8 text-center text-sm font-semibold border border-gray-200 rounded-lg outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-100" + style={{ fontVariantNumeric: "tabular-nums" }} + /> + +
+
+
+ ))} +
+ )} +
- {/* Footer summary + confirm */} - {items.length > 0 && ( -
- {/* Result message */} - {resultMsg && ( -
- {resultMsg} -
- )} + {/* Footer summary + confirm */} + {items.length > 0 && ( +
+ {/* Result message */} + {resultMsg && ( +
+ {resultMsg} +
+ )} - {/* Warehouse selection */} -
- - -
+ {/* Warehouse selection */} +
+ + +
- {/* Summary */} -
- - ์ด {items.length}๊ฑด - -
- - ํ•ฉ๊ณ„ ์ˆ˜๋Ÿ‰: {totalQty.toLocaleString()} - - {totalAmount > 0 && ( - - ({totalAmount.toLocaleString()}์›) - - )} -
-
+ {/* Summary */} +
+ + ์ด{" "} + {items.length} + ๊ฑด + +
+ + ํ•ฉ๊ณ„ ์ˆ˜๋Ÿ‰:{" "} + + {totalQty.toLocaleString()} + + + {totalAmount > 0 && ( + + ({totalAmount.toLocaleString()}์›) + + )} +
+
- {/* Buttons */} -
- - -
-
- )} -
+ {/* Buttons */} +
+ + +
+
+ )} +
- {/* Inspection Modal */} - {inspectionTarget && ( - { setInspectionModalOpen(false); setInspectionTarget(null); }} - onComplete={handleInspectionComplete} - itemCode={inspectionTarget.item_code} - itemName={inspectionTarget.item_name} - totalQty={inspectionTarget.inbound_qty} - initialResult={inspectionTarget.inspectionResult} - /> - )} -
- ); + {/* Inspection Modal */} + {inspectionTarget && ( + { + setInspectionModalOpen(false); + setInspectionTarget(null); + }} + onComplete={handleInspectionComplete} + itemCode={inspectionTarget.item_code} + itemName={inspectionTarget.item_name} + totalQty={inspectionTarget.inbound_qty} + initialResult={inspectionTarget.inspectionResult} + /> + )} +
+ ); } diff --git a/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx b/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx index c8b5ccfb..c0b3d5ba 100644 --- a/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx +++ b/frontend/components/pop/hardcoded/inbound/InboundCartPage.tsx @@ -1,80 +1,90 @@ "use client"; -import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { useRouter } from "next/navigation"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { apiClient } from "@/lib/api/client"; +import { type CartItemWithId, useCartSync } from "../common/useCartSync"; import { InspectionModal, type InspectionResult } from "./InspectionModal"; import { NumberPadModal, type PackageEntry } from "./NumberPadModal"; -import { useCartSync, type CartItemWithId } from "../common/useCartSync"; /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ interface Warehouse { - warehouse_code: string; - warehouse_name: string; - warehouse_type?: string; + warehouse_code: string; + warehouse_name: string; + warehouse_type?: string; } /** CartItemWithId -> ํ™”๋ฉด ํ‘œ์‹œ์šฉ ํŒŒ์‹ฑ ๊ฒฐ๊ณผ */ interface CartItemParsed { - id: string; - rowKey: string; - dbId: string; - source_table: string; - source_id: string; - purchase_no: string; - item_code: string; - item_name: string; - spec: string; - material: string; - order_qty: number; - remain_qty: number; - inbound_qty: number; - unit_price: number; - supplier_code: string; - supplier_name: string; - order_date?: string; - inspection_required?: boolean; - inspection_type?: "self" | "request" | null; - packages?: PackageEntry[]; - image?: string | null; + id: string; + rowKey: string; + dbId: string; + source_table: string; + source_id: string; + purchase_no: string; + item_code: string; + item_name: string; + spec: string; + material: string; + order_qty: number; + remain_qty: number; + inbound_qty: number; + unit_price: number; + supplier_code: string; + supplier_name: string; + order_date?: string; + inspection_required?: boolean; + inspection_type?: "self" | "request" | null; + packages?: PackageEntry[]; + image?: string | null; } /* ------------------------------------------------------------------ */ /* Helper: CartItemWithId -> CartItemParsed */ /* ------------------------------------------------------------------ */ function toCartItemParsed(item: CartItemWithId): CartItemParsed { - const data = item.row; - const inspType = data.inspection_type === "self" ? "self" - : data.inspection_type === "request" ? "request" - : null; + const data = item.row; + const inspType = + data.inspection_type === "self" + ? "self" + : data.inspection_type === "request" + ? "request" + : null; - return { - id: item.rowKey || String(data.id ?? ""), - rowKey: item.rowKey, - dbId: item.cartId || "", - source_table: item.sourceTable || String(data.source_table ?? "purchase_detail"), - source_id: item.rowKey || String(data.id ?? ""), - purchase_no: String(data.purchase_no ?? ""), - item_code: String(data.item_code ?? ""), - item_name: String(data.item_name ?? ""), - spec: String(data.spec ?? ""), - material: String(data.material ?? ""), - order_qty: Number(data.order_qty ?? 0), - remain_qty: Number(data.remain_qty ?? 0), - inbound_qty: item.quantity, - unit_price: Number(data.unit_price ?? 0), - supplier_code: String(data.supplier_code ?? ""), - supplier_name: String(data.supplier_name ?? ""), - order_date: data.order_date ? String(data.order_date) : undefined, - inspection_type: inspType, - inspection_required: inspType === "self", - // packageEntries์˜ ์‹ค์ œ ๋Ÿฐํƒ€์ž„ ํƒ€์ž…์€ NumberPadModal์˜ PackageEntry[] - packages: item.packageEntries as unknown as PackageEntry[] | undefined, - image: data.image ? String(data.image) : null, - }; + return { + id: item.rowKey || String(data.id ?? ""), + rowKey: item.rowKey, + dbId: item.cartId || "", + source_table: + item.sourceTable || String(data.source_table ?? "purchase_detail"), + source_id: item.rowKey || String(data.id ?? ""), + purchase_no: String(data.purchase_no ?? ""), + item_code: String(data.item_code ?? ""), + item_name: String(data.item_name ?? ""), + spec: String(data.spec ?? ""), + material: String(data.material ?? ""), + order_qty: Number(data.order_qty ?? 0), + remain_qty: Number(data.remain_qty ?? 0), + inbound_qty: item.quantity, + unit_price: Number(data.unit_price ?? 0), + supplier_code: String(data.supplier_code ?? ""), + supplier_name: String(data.supplier_name ?? ""), + order_date: data.order_date ? String(data.order_date) : undefined, + inspection_type: inspType, + inspection_required: inspType === "self", + // packageEntries์˜ ์‹ค์ œ ๋Ÿฐํƒ€์ž„ ํƒ€์ž…์€ NumberPadModal์˜ PackageEntry[] + packages: item.packageEntries as unknown as PackageEntry[] | undefined, + image: data.image ? String(data.image) : null, + }; } /* ------------------------------------------------------------------ */ @@ -82,1119 +92,1223 @@ function toCartItemParsed(item: CartItemWithId): CartItemParsed { /* ------------------------------------------------------------------ */ export function InboundCartPage() { - const router = useRouter(); - - /* Cart sync hook */ - const cart = useCartSync("pop-purchase-inbound", "purchase_detail"); - - /* Derived: parsed items from cart */ - const items = useMemo( - () => cart.cartItems.map(toCartItemParsed), - [cart.cartItems], - ); - - /* Inspection results (local overlay, keyed by rowKey) */ - const [inspectionResults, setInspectionResults] = useState< - Map - >(new Map()); - - /* Selection */ - const [selectedItems, setSelectedItems] = useState>(new Set()); - - /* Auto-select all when items change */ - useEffect(() => { - if (items.length > 0) { - setSelectedItems(new Set(items.map((i) => i.id))); - } - }, [items]); - - /* Sync inspectionResults with cart.row.inspectionResult - * ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ/์žฌ์ง„์ž… ์‹œ cart_items์— ์ €์žฅ๋œ inspectionResult๋ฅผ Map์œผ๋กœ ๋ณต์›. - * ์ฃผ์˜: delete๋Š” ๋ช…์‹œ์  ๊ฒ€์‚ฌ ์ทจ์†Œ(handleCancel)์—์„œ๋งŒ ์ฒ˜๋ฆฌ. - * (cart.saveToDb ํ›„ row JSON์ด staleํ•  ์ˆ˜ ์žˆ์–ด delete ๋กœ์ง์€ race condition ์œ ๋ฐœ) */ - useEffect(() => { - setInspectionResults((prev) => { - const next = new Map(prev); - let changed = false; - cart.cartItems.forEach((c) => { - const stored = (c.row as Record)?.inspectionResult; - if (stored && typeof stored === "object") { - // ์œ ํšจํ•œ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ โ†’ Map์— ์ถ”๊ฐ€ (๋ฎ์–ด์“ฐ์ง€ ์•Š์Œ, ๋กœ์ปฌ ์šฐ์„ ) - if (!next.has(c.rowKey)) { - next.set(c.rowKey, stored as InspectionResult); - changed = true; - } - } - // null/undefined์—ฌ๋„ Map์—์„œ ์ž๋™ ์ œ๊ฑฐํ•˜์ง€ ์•Š์Œ โ€” ๋ช…์‹œ์  cancel๋งŒ ์ฒ˜๋ฆฌ - }); - // ์นดํŠธ์—์„œ ์‚ฌ๋ผ์ง„ rowKey๋งŒ ์ •๋ฆฌ (์‹ค์ œ ์นดํŠธ ์‚ญ์ œ ์‹œ) - const cartKeys = new Set(cart.cartItems.map((c) => c.rowKey)); - Array.from(next.keys()).forEach((k) => { - if (!cartKeys.has(k)) { - next.delete(k); - changed = true; - } - }); - return changed ? next : prev; - }); - }, [cart.cartItems]); - - /* Warehouse */ - const [warehouses, setWarehouses] = useState([]); - const [selectedWarehouse, setSelectedWarehouse] = useState(""); - const [warehousePickerOpen, setWarehousePickerOpen] = useState(false); - - /* Inbound number */ - const [inboundNumber, setInboundNumber] = useState(""); - - /* Confirm result modal */ - const [confirmResult, setConfirmResult] = useState<{ - inboundNumber: string; - items: CartItemParsed[]; - warehouse: string; - date: string; - } | null>(null); - - /* Inbound date */ - const [inboundDate, setInboundDate] = useState( - new Date().toISOString().slice(0, 10) - ); - - /* Confirm state */ - const [confirming, setConfirming] = useState(false); - const [resultMsg, setResultMsg] = useState(null); - - /* Inspection modal */ - const [inspectionModalOpen, setInspectionModalOpen] = useState(false); - const [inspectionTarget, setInspectionTarget] = useState(null); - - /* Numpad modal (for qty edit) */ - const [numpadOpen, setNumpadOpen] = useState(false); - const [numpadTarget, setNumpadTarget] = useState(null); - - /* Derived: supplier name (all items should be same supplier) */ - const supplierName = items.length > 0 ? items[0].supplier_name : ""; - - /* ------------------------------------------------------------------ */ - /* Fetch warehouses */ - /* ------------------------------------------------------------------ */ - const fetchedRef = useRef(false); - - const fetchWarehouses = useCallback(async () => { - try { - const res = await apiClient.get("/receiving/warehouses"); - const data: Warehouse[] = res.data?.data ?? []; - setWarehouses(data); - if (data.length > 0) { - setSelectedWarehouse(data[0].warehouse_code); - } - } catch { - /* keep empty */ - } - }, []); - - useEffect(() => { - if (fetchedRef.current) return; - fetchedRef.current = true; - fetchWarehouses(); - }, [fetchWarehouses]); - - /* ------------------------------------------------------------------ */ - /* Selection */ - /* ------------------------------------------------------------------ */ - const toggleSelect = (id: string) => { - setSelectedItems((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }; - - const toggleSelectAll = () => { - if (selectedItems.size === items.length) { - setSelectedItems(new Set()); - } else { - setSelectedItems(new Set(items.map((i) => i.id))); - } - }; - - /* ------------------------------------------------------------------ */ - /* Qty edit via numpad */ - /* ------------------------------------------------------------------ */ - const openNumpad = (item: CartItemParsed) => { - setNumpadTarget(item); - setNumpadOpen(true); - }; - - const handleNumpadConfirm = (qty: number, packages: PackageEntry[]) => { - if (!numpadTarget) return; - const finalQty = Math.min(qty, numpadTarget.remain_qty); - - cart.updateItemQuantity( - numpadTarget.rowKey, - finalQty, - undefined, - // PackageEntry ํƒ€์ž…์ด registry vs NumberPadModal์—์„œ ๋‹ค๋ฅด๋ฏ€๋กœ any ์บ์ŠคํŒ… - // eslint-disable-next-line @typescript-eslint/no-explicit-any - packages.length > 0 ? packages as any : undefined, - ); - setNumpadTarget(null); - // Auto-save effect below will persist change to DB - }; - - /* ------------------------------------------------------------------ */ - /* Remove item */ - /* ------------------------------------------------------------------ */ - const handleRemove = (rowKey: string) => { - cart.removeItem(rowKey); - setSelectedItems((prev) => { - const next = new Set(prev); - next.delete(rowKey); - return next; - }); - // Auto-save effect below will persist change to DB - }; - - /* Auto-save: persist dirty changes to DB after a short debounce */ - const autoSaveTimerRef = useRef | null>(null); - useEffect(() => { - if (!cart.isDirty || cart.syncStatus === "saving") return; - if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); - autoSaveTimerRef.current = setTimeout(() => { - cart.saveToDb().catch(() => {}); - }, 500); - return () => { - if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); - }; - }, [cart.isDirty, cart.syncStatus, cart]); - - /* ------------------------------------------------------------------ */ - /* Inspection */ - /* ------------------------------------------------------------------ */ - const openInspection = (item: CartItemParsed) => { - setInspectionTarget(item); - setInspectionModalOpen(true); - }; - - const handleInspectionComplete = (result: InspectionResult) => { - if (!inspectionTarget) return; - const targetRowKey = inspectionTarget.rowKey; - setInspectionResults((prev) => { - const next = new Map(prev); - next.set(targetRowKey, result); - return next; - }); - // cart_items.row_data์— ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์ €์žฅ (ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์œ ์ง€) - cart.updateItemRow(targetRowKey, { inspectionResult: result }); - setInspectionTarget(null); - // ์ฆ‰์‹œ DB ์ €์žฅ (์ž๋™์ €์žฅ ๋””๋ฐ”์šด์Šค๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์Œ) - setTimeout(() => { - cart.saveToDb().catch((err) => console.error("[๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์ €์žฅ ์‹คํŒจ]", err)); - }, 100); - }; - - /* Pass inspection (non-required only) */ - const handlePassInspection = (rowKey: string) => { - const item = items.find((i) => i.rowKey === rowKey); - if (!item) return; - const result: InspectionResult = { - items: [], - goodQty: item.inbound_qty, - badQty: 0, - remark: "pass", - completed: true, - }; - setInspectionResults((prev) => { - const next = new Map(prev); - next.set(rowKey, result); - return next; - }); - cart.updateItemRow(rowKey, { inspectionResult: result }); - }; - - const getInspectionResult = (rowKey: string): InspectionResult | null => { - return inspectionResults.get(rowKey) || null; - }; - - /* ------------------------------------------------------------------ */ - /* Validation: required inspections */ - /* ------------------------------------------------------------------ */ - const selectedItemsList = items.filter((i) => selectedItems.has(i.id)); - - // CEO ์ •์ฑ… (2026-04-09 ์‹œ์—ฐ ๊ฒฐ์ •): ๊ฒ€์‚ฌ ํ•„์ˆ˜ ํ•ญ๋ชฉ ๋ฏธ์™„๋ฃŒ ์‹œ ํ™•์ • ์ฐจ๋‹จ - // ๊ฒ€์‚ฌ ๋น ์ง„ ์ž…๊ณ ๊ฐ€ ๊ฒ€์‚ฌ๊ด€๋ฆฌ์—์„œ ์ถ”์  ์•ˆ ๋˜๋ฏ€๋กœ, ์ž…๋ ฅ ์‹œ์ ์— ๋ง‰์Œ - const hasUnfinishedRequiredInspection = selectedItemsList.some( - (item) => - item.inspection_required && - item.inspection_type === "self" && - !getInspectionResult(item.rowKey)?.completed - ); - - /* ------------------------------------------------------------------ */ - /* Confirm inbound */ - /* ------------------------------------------------------------------ */ - const handleConfirm = async () => { - if (selectedItemsList.length === 0) return; - - if (!selectedWarehouse) { - setResultMsg("์˜ค๋ฅ˜: ์ž…๊ณ  ์ฐฝ๊ณ ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); - return; - } - - // ๊ฒ€์‚ฌ ๋ฏธ์™„๋ฃŒ์—ฌ๋„ ํ™•์ • ๊ฐ€๋Šฅ. ๋‹จ์ง€ inspection_result์— ์•ˆ ๋“ค์–ด๊ฐ€๊ฑฐ๋‚˜ "๋Œ€๊ธฐ" ์ƒํƒœ๋กœ ๊ธฐ๋ก. - // (CEO ์ •์ฑ…: ์ž…๊ณ  ์ž์ฒด๋Š” ์ง„ํ–‰, ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋งŒ ๋ˆ„๋ฝ/๋Œ€๊ธฐ ์ƒํƒœ๋กœ ํ‘œ์‹œ) - - setConfirming(true); - setResultMsg(null); - - try { - // ํ™•์ • ์‹œ์ ์— ์ฑ„๋ฒˆ (๋™์‹œ์ ‘์† ์ถฉ๋Œ ๋ฐฉ์ง€) - // POP ํ™”๋ฉด์„ค์ •์—์„œ ์„ ํƒํ•œ ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ (์—†์œผ๋ฉด ๊ธฐ๋ณธ) - let finalNumber = ""; - try { - const settingsRes: any = await apiClient.get("/screen-management/screens/6527/layout-pop").catch(() => null); - const ruleId = settingsRes?.data?.data?.settings?.popConfig?.inbound?.numberingRuleId; - const url = ruleId && ruleId !== "__none__" - ? `/receiving/generate-number?ruleId=${encodeURIComponent(ruleId)}` - : "/receiving/generate-number"; - const numRes = await apiClient.get(url); - if (numRes.data?.success && numRes.data?.data) { - finalNumber = numRes.data.data; - setInboundNumber(finalNumber); - } - } catch { - /* backend will handle */ - } - - // POST /api/receiving -- same payload structure as PC - const payload = { - inbound_number: finalNumber, - inbound_date: inboundDate, - warehouse_code: selectedWarehouse, - inbound_type: "๊ตฌ๋งค์ž…๊ณ ", - items: selectedItemsList.map((item, idx) => { - const inspResult = getInspectionResult(item.rowKey); - return { - inbound_type: "๊ตฌ๋งค์ž…๊ณ ", - item_number: item.item_code, - item_name: item.item_name, - spec: item.spec || "", - material: item.material || "", - unit: "EA", - inbound_qty: String(item.inbound_qty), - unit_price: String(item.unit_price || 0), - total_amount: String( - (item.inbound_qty || 0) * (item.unit_price || 0) - ), - reference_number: item.purchase_no, - supplier_code: item.supplier_code, - supplier_name: item.supplier_name, - inbound_status: "์ž…๊ณ ์™„๋ฃŒ", - inspection_status: inspResult?.completed - ? "๊ฒ€์‚ฌ์™„๋ฃŒ" - : item.inspection_required - ? "๊ฒ€์‚ฌ๋Œ€๊ธฐ" - : "ํ•ฉ๊ฒฉ", - source_table: item.source_table, - source_id: item.source_id || item.id, - seq_no: idx + 1, - }; - }), - }; - - const res = await apiClient.post("/receiving", payload); - - if (res.data?.success) { - // ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋ฅผ inspection_result_mng + inspection_result์— ์ €์žฅ - const insertedDetails: Array> = - (res.data?.data?.details as Array>) ?? - (res.data?.data?.items as Array>) ?? - []; - const inboundHeaderNo: string = - (res.data?.data?.header as { inbound_number?: string } | undefined)?.inbound_number || - finalNumber || ""; - const inspectionPromises = selectedItemsList - .map((item, idx) => { - const inspResult = getInspectionResult(item.rowKey); - if (!inspResult?.completed) return null; - const matchedDetail = insertedDetails[idx] ?? {}; - const referenceId = - (matchedDetail.id as string) || - (matchedDetail.detail_id as string) || - `${inboundHeaderNo}-${idx + 1}`; - const goodQty = inspResult.goodQty || 0; - const badQty = inspResult.badQty || 0; - const totalQty = goodQty + badQty; - const overallJudgment = badQty === 0 ? "ํ•ฉ๊ฒฉ" : "๋ถˆํ•ฉ๊ฒฉ"; - return apiClient - .post("/pop/inspection-result", { - inspectionNumber: inspResult.inspectionNumber, - referenceTable: "inbound_mng", - referenceId, - screenId: "pop_inbound_inspection", - itemId: item.item_id || null, - itemCode: item.item_code, - itemName: item.item_name, - inspectionType: "์ž…๊ณ ๊ฒ€์‚ฌ", - overallJudgment, - totalQty, - goodQty, - badQty, - defectDescription: badQty > 0 ? `๋ถˆ๋Ÿ‰ ${badQty}๊ฑด` : "", - memo: inspResult.remark || "", - supplierCode: item.supplier_code || null, - supplierName: item.supplier_name || null, - isCompleted: true, - items: inspResult.items.map((insp) => ({ - inspectionInfoId: insp.id || null, - inspectionItemName: insp.inspection_item_name, - inspectionStandard: insp.inspection_standard, - passCriteria: insp.pass_criteria, - isRequired: insp.is_required || "Y", - measuredValue: insp.measured_value || "", - judgment: insp.result || null, - })), - }) - .catch((err: unknown) => { - const e = err as { message?: string }; - console.error("[inspection_result ์ €์žฅ ์‹คํŒจ]", item.item_code, e?.message); - }); - }) - .filter(Boolean); - if (inspectionPromises.length > 0) { - await Promise.all(inspectionPromises); - } - - // Remove confirmed items from cart - direct DB delete for reliability - const confirmedItems = [...selectedItemsList]; - const { dataApi } = await import("@/lib/api/data"); - const confirmPromises = confirmedItems - .filter((item) => item.dbId) - .map((item) => dataApi.updateRecord("cart_items", item.dbId, { status: "confirmed" }).catch(() => {})); - await Promise.all(confirmPromises); - - // Also clean up local state via useCartSync - for (const item of confirmedItems) { - cart.removeItem(item.rowKey); - } - // Reload from DB to sync state - await cart.loadFromDb(); - - const inboundNo = - res.data?.data?.header?.inbound_number || finalNumber || ""; - - // ๊ฒฐ๊ณผ ๋ชจ๋‹ฌ ํ‘œ์‹œ (๋ฐ”๋กœ ์ด๋™ํ•˜์ง€ ์•Š์Œ) - setConfirmResult({ - inboundNumber: inboundNo, - items: confirmedItems, - warehouse: warehouses.find(w => w.warehouse_code === selectedWarehouse)?.warehouse_name || selectedWarehouse, - date: inboundDate, - }); - setResultMsg(null); - } else { - setResultMsg( - `์˜ค๋ฅ˜: ${res.data?.message || "์ž…๊ณ  ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."}` - ); - } - } catch (err: unknown) { - const msg = - err instanceof Error ? err.message : "์ž…๊ณ  ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."; - setResultMsg(`์˜ค๋ฅ˜: ${msg}`); - } finally { - setConfirming(false); - } - }; - - /* ------------------------------------------------------------------ */ - /* Helpers */ - /* ------------------------------------------------------------------ */ - const selectedWarehouseName = - warehouses.find((w) => w.warehouse_code === selectedWarehouse) - ?.warehouse_name || selectedWarehouse; - - const totalQty = selectedItemsList.reduce((s, i) => s + i.inbound_qty, 0); - - /* ------------------------------------------------------------------ */ - /* Render */ - /* ------------------------------------------------------------------ */ - return ( -
- {/* ===== Header ===== */} -
-
- -
-

- ์ž…๊ณ  ์žฅ๋ฐ”๊ตฌ๋‹ˆ -

- {supplierName && ( -

{supplierName}

- )} -
-
- - {/* Confirm button (header only) */} - -
- - {/* ===== Info banner ===== */} -
-
- {supplierName && ( - - {supplierName} - - )} - - {inboundDate} - - {selectedWarehouseName && ( - - | {selectedWarehouseName} - - )} - - {inboundNumber || "ํ™•์ • ์‹œ ์ž๋™์ƒ์„ฑ"} - -
- - {/* Info fields: 3 columns */} -
- {/* Inbound date */} -
- - setInboundDate(e.target.value)} - className="w-full px-3 py-2.5 border border-gray-200 rounded-lg text-sm outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-100 bg-white" - /> -
- - {/* Warehouse selector - card-style touch button */} -
- - -
- - {/* Inbound number (readonly -- ํ™•์ • ์‹œ์ ์— ์ฑ„๋ฒˆ) */} -
- -
- {inboundNumber ? ( - {inboundNumber} - ) : ( - ํ™•์ • ์‹œ ์ž๋™์ƒ์„ฑ - )} -
-
-
-
- - {/* ===== Select all bar ===== */} - {items.length > 0 && ( -
-
- - - ๋‹ด์€ ํ’ˆ๋ชฉ{" "} - {items.length} - -
- - -
- )} - - {/* ===== Items list ===== */} - {cart.loading ? ( -
- - - - - ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘... -
- ) : items.length === 0 ? ( -
- - - -

- ๋‹ด์€ ํ’ˆ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค -

-

- ๊ตฌ๋งค์ž…๊ณ  ํ™”๋ฉด์—์„œ ํ’ˆ๋ชฉ์„ ๋‹ด์•„์ฃผ์„ธ์š” -

- -
- ) : ( -
- {items.map((item) => { - const inspResult = getInspectionResult(item.rowKey); - return ( -
- {/* Blue left bar for selected items */} - {selectedItems.has(item.id) && ( -
- )} - - {/* === Header row: checkbox + item code + item name + inspection badge === */} -
- {/* Checkbox */} - - {item.item_code} - {item.item_name} - {item.inspection_type === "self" && ( - - ๊ฒ€์‚ฌ ํ•„์ˆ˜ - - )} - {item.inspection_type === "request" && ( - - ๊ฒ€์‚ฌ์˜๋ขฐ ์„ ํƒ - - )} -
- - {/* === Body row: image + info + action === */} -
- {/* Product image */} -
- {item.image ? ( - {item.item_name} - ) : ( - {"\uD83D\uDCE6"} - )} -
- - {/* Info columns */} -
-
- ๋ฐœ์ฃผ์ผ - {item.order_date || "-"} -
-
- ๋ฐœ์ฃผ๋ฒˆํ˜ธ - {item.purchase_no || "-"} -
-
- ๋ฐœ์ฃผ์ˆ˜๋Ÿ‰ - {item.order_qty.toLocaleString()} -
-
- ๋ฏธ์ž…๊ณ  - {item.remain_qty.toLocaleString()} -
-
- - {/* Action column: qty display + delete button */} -
- {/* Qty display - clickable to open numpad */} - - - {/* Delete button */} - -
-
- - {/* === Package info === */} - {item.packages && item.packages.length > 0 && ( -
-
- - ํฌ์žฅ์™„๋ฃŒ - - - {item.packages.reduce((s, p) => s + p.count * p.qtyPerUnit, 0).toLocaleString()} EA - -
- {item.packages.map((pkg, idx) => ( -
- {pkg.unit.icon} - {pkg.count}{pkg.unit.label} x {pkg.qtyPerUnit.toLocaleString()}EA = {(pkg.count * pkg.qtyPerUnit).toLocaleString()}EA -
- ))} -
- )} - - {/* === Inspection row === */} - {(item.inspection_type === "self" || - item.inspection_type === "request") && ( -
-
- - - {/* Pass button for non-required */} - {!item.inspection_required && - !inspResult?.completed && ( - - )} -
-
- )} -
- ); - })} -
- )} - - {/* ===== Result toast (only when message exists) ===== */} - {resultMsg && ( -
-
- {resultMsg} -
-
- )} - - {/* ===== Warehouse picker modal ===== */} - {warehousePickerOpen && ( -
-
setWarehousePickerOpen(false)} - /> -
- {/* Header */} -
-

์ฐฝ๊ณ  ์„ ํƒ

- -
- - {/* Warehouse list */} -
- {warehouses.length === 0 ? ( -

- ๋“ฑ๋ก๋œ ์ฐฝ๊ณ ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค -

- ) : ( -
- {warehouses.map((wh) => ( - - ))} -
- )} -
-
-
- )} - - {/* ===== Inspection Modal ===== */} - {inspectionTarget && ( - { - setInspectionModalOpen(false); - setInspectionTarget(null); - }} - onComplete={handleInspectionComplete} - onCancel={() => { - // ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ๋ฌดํšจํ™” (์™„๋ฃŒ โ†’ ๋Œ€๊ธฐ ํ’€๋ฆผ) - const targetRowKey = inspectionTarget.rowKey; - setInspectionResults((prev) => { - const next = new Map(prev); - next.delete(targetRowKey); - return next; - }); - cart.updateItemRow(targetRowKey, { inspectionResult: null }); - setTimeout(() => cart.saveToDb().catch(() => {}), 100); - }} - itemCode={inspectionTarget.item_code} - itemName={inspectionTarget.item_name} - totalQty={inspectionTarget.inbound_qty} - initialResult={getInspectionResult(inspectionTarget.rowKey)} - /> - )} - - {/* ===== NumberPad Modal (qty edit) ===== */} - {numpadTarget && ( - { - setNumpadOpen(false); - setNumpadTarget(null); - }} - onConfirm={handleNumpadConfirm} - maxQty={numpadTarget.remain_qty} - itemName={numpadTarget.item_name} - initialQty={numpadTarget.inbound_qty} - initialPackages={numpadTarget.packages} - /> - )} - - {/* ===== ์ž…๊ณ  ์™„๋ฃŒ ๊ฒฐ๊ณผ ๋ชจ๋‹ฌ ===== */} - {confirmResult && ( -
-
-
- {/* ํ—ค๋” */} -
-
- - - -
-

์ž…๊ณ  ์ฒ˜๋ฆฌ ์™„๋ฃŒ

-

{confirmResult.inboundNumber}

-
- - {/* ์ฒ˜๋ฆฌ ๋‚ด์—ญ */} -
-
- ์ฐฝ๊ณ : {confirmResult.warehouse} - {confirmResult.date} -
- -
์ฒ˜๋ฆฌ๋œ ํ’ˆ๋ชฉ ({confirmResult.items.length}๊ฑด)
-
- {confirmResult.items.map((item) => ( -
-
-

{item.item_name}

-

{item.item_code}

-
- {item.inbound_qty?.toLocaleString()} EA -
- ))} -
-
- - {/* ํ™•์ธ ๋ฒ„ํŠผ */} -
- -
-
-
- )} -
- ); + const router = useRouter(); + + /* Cart sync hook */ + const cart = useCartSync("pop-purchase-inbound", "purchase_detail"); + + /* Derived: parsed items from cart */ + const items = useMemo( + () => cart.cartItems.map(toCartItemParsed), + [cart.cartItems], + ); + + /* Inspection results (local overlay, keyed by rowKey) */ + const [inspectionResults, setInspectionResults] = useState< + Map + >(new Map()); + + /* Selection */ + const [selectedItems, setSelectedItems] = useState>(new Set()); + + /* Auto-select all when items change */ + useEffect(() => { + if (items.length > 0) { + setSelectedItems(new Set(items.map((i) => i.id))); + } + }, [items]); + + /* Sync inspectionResults with cart.row.inspectionResult + * ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ/์žฌ์ง„์ž… ์‹œ cart_items์— ์ €์žฅ๋œ inspectionResult๋ฅผ Map์œผ๋กœ ๋ณต์›. + * ์ฃผ์˜: delete๋Š” ๋ช…์‹œ์  ๊ฒ€์‚ฌ ์ทจ์†Œ(handleCancel)์—์„œ๋งŒ ์ฒ˜๋ฆฌ. + * (cart.saveToDb ํ›„ row JSON์ด staleํ•  ์ˆ˜ ์žˆ์–ด delete ๋กœ์ง์€ race condition ์œ ๋ฐœ) */ + useEffect(() => { + setInspectionResults((prev) => { + const next = new Map(prev); + let changed = false; + cart.cartItems.forEach((c) => { + const stored = (c.row as Record)?.inspectionResult; + if (stored && typeof stored === "object") { + // ์œ ํšจํ•œ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ โ†’ Map์— ์ถ”๊ฐ€ (๋ฎ์–ด์“ฐ์ง€ ์•Š์Œ, ๋กœ์ปฌ ์šฐ์„ ) + if (!next.has(c.rowKey)) { + next.set(c.rowKey, stored as InspectionResult); + changed = true; + } + } + // null/undefined์—ฌ๋„ Map์—์„œ ์ž๋™ ์ œ๊ฑฐํ•˜์ง€ ์•Š์Œ โ€” ๋ช…์‹œ์  cancel๋งŒ ์ฒ˜๋ฆฌ + }); + // ์นดํŠธ์—์„œ ์‚ฌ๋ผ์ง„ rowKey๋งŒ ์ •๋ฆฌ (์‹ค์ œ ์นดํŠธ ์‚ญ์ œ ์‹œ) + const cartKeys = new Set(cart.cartItems.map((c) => c.rowKey)); + Array.from(next.keys()).forEach((k) => { + if (!cartKeys.has(k)) { + next.delete(k); + changed = true; + } + }); + return changed ? next : prev; + }); + }, [cart.cartItems]); + + /* Warehouse */ + const [warehouses, setWarehouses] = useState([]); + const [selectedWarehouse, setSelectedWarehouse] = useState(""); + const [warehousePickerOpen, setWarehousePickerOpen] = useState(false); + + /* Inbound number */ + const [inboundNumber, setInboundNumber] = useState(""); + + /* Confirm result modal */ + const [confirmResult, setConfirmResult] = useState<{ + inboundNumber: string; + items: CartItemParsed[]; + warehouse: string; + date: string; + } | null>(null); + + /* Inbound date */ + const [inboundDate, setInboundDate] = useState( + new Date().toISOString().slice(0, 10), + ); + + /* Confirm state */ + const [confirming, setConfirming] = useState(false); + const [resultMsg, setResultMsg] = useState(null); + + /* Inspection modal */ + const [inspectionModalOpen, setInspectionModalOpen] = useState(false); + const [inspectionTarget, setInspectionTarget] = + useState(null); + + /* Numpad modal (for qty edit) */ + const [numpadOpen, setNumpadOpen] = useState(false); + const [numpadTarget, setNumpadTarget] = useState(null); + + /* Derived: supplier name (all items should be same supplier) */ + const supplierName = items.length > 0 ? items[0].supplier_name : ""; + + /* ------------------------------------------------------------------ */ + /* Fetch warehouses */ + /* ------------------------------------------------------------------ */ + const fetchedRef = useRef(false); + + const fetchWarehouses = useCallback(async () => { + try { + const res = await apiClient.get("/receiving/warehouses"); + const data: Warehouse[] = res.data?.data ?? []; + setWarehouses(data); + if (data.length > 0) { + setSelectedWarehouse(data[0].warehouse_code); + } + } catch { + /* keep empty */ + } + }, []); + + useEffect(() => { + if (fetchedRef.current) return; + fetchedRef.current = true; + fetchWarehouses(); + }, [fetchWarehouses]); + + /* ------------------------------------------------------------------ */ + /* Selection */ + /* ------------------------------------------------------------------ */ + const toggleSelect = (id: string) => { + setSelectedItems((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + const toggleSelectAll = () => { + if (selectedItems.size === items.length) { + setSelectedItems(new Set()); + } else { + setSelectedItems(new Set(items.map((i) => i.id))); + } + }; + + /* ------------------------------------------------------------------ */ + /* Qty edit via numpad */ + /* ------------------------------------------------------------------ */ + const openNumpad = (item: CartItemParsed) => { + setNumpadTarget(item); + setNumpadOpen(true); + }; + + const handleNumpadConfirm = (qty: number, packages: PackageEntry[]) => { + if (!numpadTarget) return; + const finalQty = Math.min(qty, numpadTarget.remain_qty); + + cart.updateItemQuantity( + numpadTarget.rowKey, + finalQty, + undefined, + // PackageEntry ํƒ€์ž…์ด registry vs NumberPadModal์—์„œ ๋‹ค๋ฅด๋ฏ€๋กœ any ์บ์ŠคํŒ… + // eslint-disable-next-line @typescript-eslint/no-explicit-any + packages.length > 0 ? (packages as any) : undefined, + ); + setNumpadTarget(null); + // Auto-save effect below will persist change to DB + }; + + /* ------------------------------------------------------------------ */ + /* Remove item */ + /* ------------------------------------------------------------------ */ + const handleRemove = (rowKey: string) => { + cart.removeItem(rowKey); + setSelectedItems((prev) => { + const next = new Set(prev); + next.delete(rowKey); + return next; + }); + // Auto-save effect below will persist change to DB + }; + + /* Auto-save: persist dirty changes to DB after a short debounce */ + const autoSaveTimerRef = useRef | null>(null); + useEffect(() => { + if (!cart.isDirty || cart.syncStatus === "saving") return; + if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); + autoSaveTimerRef.current = setTimeout(() => { + cart.saveToDb().catch(() => {}); + }, 500); + return () => { + if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); + }; + }, [cart.isDirty, cart.syncStatus, cart]); + + /* ------------------------------------------------------------------ */ + /* Inspection */ + /* ------------------------------------------------------------------ */ + const openInspection = (item: CartItemParsed) => { + setInspectionTarget(item); + setInspectionModalOpen(true); + }; + + const handleInspectionComplete = (result: InspectionResult) => { + if (!inspectionTarget) return; + const targetRowKey = inspectionTarget.rowKey; + setInspectionResults((prev) => { + const next = new Map(prev); + next.set(targetRowKey, result); + return next; + }); + // cart_items.row_data์— ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์ €์žฅ (ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์œ ์ง€) + cart.updateItemRow(targetRowKey, { inspectionResult: result }); + setInspectionTarget(null); + // ์ฆ‰์‹œ DB ์ €์žฅ (์ž๋™์ €์žฅ ๋””๋ฐ”์šด์Šค๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์Œ) + setTimeout(() => { + cart + .saveToDb() + .catch((err) => console.error("[๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์ €์žฅ ์‹คํŒจ]", err)); + }, 100); + }; + + /* Pass inspection (non-required only) */ + const handlePassInspection = (rowKey: string) => { + const item = items.find((i) => i.rowKey === rowKey); + if (!item) return; + const result: InspectionResult = { + items: [], + goodQty: item.inbound_qty, + badQty: 0, + remark: "pass", + completed: true, + }; + setInspectionResults((prev) => { + const next = new Map(prev); + next.set(rowKey, result); + return next; + }); + cart.updateItemRow(rowKey, { inspectionResult: result }); + }; + + const getInspectionResult = (rowKey: string): InspectionResult | null => { + return inspectionResults.get(rowKey) || null; + }; + + /* ------------------------------------------------------------------ */ + /* Validation: required inspections */ + /* ------------------------------------------------------------------ */ + const selectedItemsList = items.filter((i) => selectedItems.has(i.id)); + + // CEO ์ •์ฑ… (2026-04-09 ์‹œ์—ฐ ๊ฒฐ์ •): ๊ฒ€์‚ฌ ํ•„์ˆ˜ ํ•ญ๋ชฉ ๋ฏธ์™„๋ฃŒ ์‹œ ํ™•์ • ์ฐจ๋‹จ + // ๊ฒ€์‚ฌ ๋น ์ง„ ์ž…๊ณ ๊ฐ€ ๊ฒ€์‚ฌ๊ด€๋ฆฌ์—์„œ ์ถ”์  ์•ˆ ๋˜๋ฏ€๋กœ, ์ž…๋ ฅ ์‹œ์ ์— ๋ง‰์Œ + const hasUnfinishedRequiredInspection = selectedItemsList.some( + (item) => + item.inspection_required && + item.inspection_type === "self" && + !getInspectionResult(item.rowKey)?.completed, + ); + + /* ------------------------------------------------------------------ */ + /* Confirm inbound */ + /* ------------------------------------------------------------------ */ + const handleConfirm = async () => { + if (selectedItemsList.length === 0) return; + + if (!selectedWarehouse) { + setResultMsg("์˜ค๋ฅ˜: ์ž…๊ณ  ์ฐฝ๊ณ ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); + return; + } + + // ๊ฒ€์‚ฌ ๋ฏธ์™„๋ฃŒ์—ฌ๋„ ํ™•์ • ๊ฐ€๋Šฅ. ๋‹จ์ง€ inspection_result์— ์•ˆ ๋“ค์–ด๊ฐ€๊ฑฐ๋‚˜ "๋Œ€๊ธฐ" ์ƒํƒœ๋กœ ๊ธฐ๋ก. + // (CEO ์ •์ฑ…: ์ž…๊ณ  ์ž์ฒด๋Š” ์ง„ํ–‰, ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋งŒ ๋ˆ„๋ฝ/๋Œ€๊ธฐ ์ƒํƒœ๋กœ ํ‘œ์‹œ) + + setConfirming(true); + setResultMsg(null); + + try { + // ํ™•์ • ์‹œ์ ์— ์ฑ„๋ฒˆ (๋™์‹œ์ ‘์† ์ถฉ๋Œ ๋ฐฉ์ง€) + // POP ํ™”๋ฉด์„ค์ •์—์„œ ์„ ํƒํ•œ ์ฑ„๋ฒˆ๊ทœ์น™ ์‚ฌ์šฉ (์—†์œผ๋ฉด ๊ธฐ๋ณธ) + let finalNumber = ""; + try { + const settingsRes: any = await apiClient + .get("/screen-management/screens/6527/layout-pop") + .catch(() => null); + const ruleId = + settingsRes?.data?.data?.settings?.popConfig?.inbound + ?.numberingRuleId; + const url = + ruleId && ruleId !== "__none__" + ? `/receiving/generate-number?ruleId=${encodeURIComponent(ruleId)}` + : "/receiving/generate-number"; + const numRes = await apiClient.get(url); + if (numRes.data?.success && numRes.data?.data) { + finalNumber = numRes.data.data; + setInboundNumber(finalNumber); + } + } catch { + /* backend will handle */ + } + + // POST /api/receiving -- same payload structure as PC + const payload = { + inbound_number: finalNumber, + inbound_date: inboundDate, + warehouse_code: selectedWarehouse, + inbound_type: "๊ตฌ๋งค์ž…๊ณ ", + items: selectedItemsList.map((item, idx) => { + const inspResult = getInspectionResult(item.rowKey); + return { + inbound_type: "๊ตฌ๋งค์ž…๊ณ ", + item_number: item.item_code, + item_name: item.item_name, + spec: item.spec || "", + material: item.material || "", + unit: "EA", + inbound_qty: String(item.inbound_qty), + unit_price: String(item.unit_price || 0), + total_amount: String( + (item.inbound_qty || 0) * (item.unit_price || 0), + ), + reference_number: item.purchase_no, + supplier_code: item.supplier_code, + supplier_name: item.supplier_name, + inbound_status: "์ž…๊ณ ์™„๋ฃŒ", + inspection_status: inspResult?.completed + ? "๊ฒ€์‚ฌ์™„๋ฃŒ" + : item.inspection_required + ? "๊ฒ€์‚ฌ๋Œ€๊ธฐ" + : "ํ•ฉ๊ฒฉ", + source_table: item.source_table, + source_id: item.source_id || item.id, + seq_no: idx + 1, + }; + }), + }; + + const res = await apiClient.post("/receiving", payload); + + if (res.data?.success) { + // ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋ฅผ inspection_result_mng + inspection_result์— ์ €์žฅ + const insertedDetails: Array> = + (res.data?.data?.details as Array>) ?? + (res.data?.data?.items as Array>) ?? + []; + const inboundHeaderNo: string = + (res.data?.data?.header as { inbound_number?: string } | undefined) + ?.inbound_number || + finalNumber || + ""; + const inspectionPromises = selectedItemsList + .map((item, idx) => { + const inspResult = getInspectionResult(item.rowKey); + if (!inspResult?.completed) return null; + const matchedDetail = insertedDetails[idx] ?? {}; + const referenceId = + (matchedDetail.id as string) || + (matchedDetail.detail_id as string) || + `${inboundHeaderNo}-${idx + 1}`; + const goodQty = inspResult.goodQty || 0; + const badQty = inspResult.badQty || 0; + const totalQty = goodQty + badQty; + const overallJudgment = badQty === 0 ? "ํ•ฉ๊ฒฉ" : "๋ถˆํ•ฉ๊ฒฉ"; + return apiClient + .post("/pop/inspection-result", { + inspectionNumber: inspResult.inspectionNumber, + referenceTable: "inbound_mng", + referenceId, + screenId: "pop_inbound_inspection", + itemId: item.item_id || null, + itemCode: item.item_code, + itemName: item.item_name, + inspectionType: "์ž…๊ณ ๊ฒ€์‚ฌ", + overallJudgment, + totalQty, + goodQty, + badQty, + defectDescription: badQty > 0 ? `๋ถˆ๋Ÿ‰ ${badQty}๊ฑด` : "", + memo: inspResult.remark || "", + supplierCode: item.supplier_code || null, + supplierName: item.supplier_name || null, + isCompleted: true, + items: inspResult.items.map((insp) => ({ + inspectionInfoId: insp.id || null, + inspectionItemName: insp.inspection_item_name, + inspectionStandard: insp.inspection_standard, + passCriteria: insp.pass_criteria, + isRequired: insp.is_required || "Y", + measuredValue: insp.measured_value || "", + judgment: insp.result || null, + })), + }) + .catch((err: unknown) => { + const e = err as { message?: string }; + console.error( + "[inspection_result ์ €์žฅ ์‹คํŒจ]", + item.item_code, + e?.message, + ); + }); + }) + .filter(Boolean); + if (inspectionPromises.length > 0) { + await Promise.all(inspectionPromises); + } + + // Remove confirmed items from cart - direct DB delete for reliability + const confirmedItems = [...selectedItemsList]; + const { dataApi } = await import("@/lib/api/data"); + const confirmPromises = confirmedItems + .filter((item) => item.dbId) + .map((item) => + dataApi + .updateRecord("cart_items", item.dbId, { status: "confirmed" }) + .catch(() => {}), + ); + await Promise.all(confirmPromises); + + // Also clean up local state via useCartSync + for (const item of confirmedItems) { + cart.removeItem(item.rowKey); + } + // Reload from DB to sync state + await cart.loadFromDb(); + + const inboundNo = + res.data?.data?.header?.inbound_number || finalNumber || ""; + + // ๊ฒฐ๊ณผ ๋ชจ๋‹ฌ ํ‘œ์‹œ (๋ฐ”๋กœ ์ด๋™ํ•˜์ง€ ์•Š์Œ) + setConfirmResult({ + inboundNumber: inboundNo, + items: confirmedItems, + warehouse: + warehouses.find((w) => w.warehouse_code === selectedWarehouse) + ?.warehouse_name || selectedWarehouse, + date: inboundDate, + }); + setResultMsg(null); + } else { + setResultMsg( + `์˜ค๋ฅ˜: ${res.data?.message || "์ž…๊ณ  ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."}`, + ); + } + } catch (err: unknown) { + const msg = + err instanceof Error ? err.message : "์ž…๊ณ  ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."; + setResultMsg(`์˜ค๋ฅ˜: ${msg}`); + } finally { + setConfirming(false); + } + }; + + /* ------------------------------------------------------------------ */ + /* Helpers */ + /* ------------------------------------------------------------------ */ + const selectedWarehouseName = + warehouses.find((w) => w.warehouse_code === selectedWarehouse) + ?.warehouse_name || selectedWarehouse; + + const totalQty = selectedItemsList.reduce((s, i) => s + i.inbound_qty, 0); + + /* ------------------------------------------------------------------ */ + /* Render */ + /* ------------------------------------------------------------------ */ + return ( +
+ {/* ===== Header ===== */} +
+
+ +
+

+ ์ž…๊ณ  ์žฅ๋ฐ”๊ตฌ๋‹ˆ +

+ {supplierName && ( +

{supplierName}

+ )} +
+
+ + {/* Confirm button (header only) */} + +
+ + {/* ===== Info banner ===== */} +
+
+ {supplierName && ( + + {supplierName} + + )} + {inboundDate} + {selectedWarehouseName && ( + + | {selectedWarehouseName} + + )} + + {inboundNumber || "ํ™•์ • ์‹œ ์ž๋™์ƒ์„ฑ"} + +
+ + {/* Info fields: 3 columns */} +
+ {/* Inbound date */} +
+ + setInboundDate(e.target.value)} + className="w-full px-3 py-2.5 border border-gray-200 rounded-lg text-sm outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-100 bg-white" + /> +
+ + {/* Warehouse selector - card-style touch button */} +
+ + +
+ + {/* Inbound number (readonly -- ํ™•์ • ์‹œ์ ์— ์ฑ„๋ฒˆ) */} +
+ +
+ {inboundNumber ? ( + {inboundNumber} + ) : ( + ํ™•์ • ์‹œ ์ž๋™์ƒ์„ฑ + )} +
+
+
+
+ + {/* ===== Select all bar ===== */} + {items.length > 0 && ( +
+
+ + + ๋‹ด์€ ํ’ˆ๋ชฉ {items.length} + +
+ + +
+ )} + + {/* ===== Items list ===== */} + {cart.loading ? ( +
+ + + + + ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘... +
+ ) : items.length === 0 ? ( +
+ + + +

+ ๋‹ด์€ ํ’ˆ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค +

+

+ ๊ตฌ๋งค์ž…๊ณ  ํ™”๋ฉด์—์„œ ํ’ˆ๋ชฉ์„ ๋‹ด์•„์ฃผ์„ธ์š” +

+ +
+ ) : ( +
+ {items.map((item) => { + const inspResult = getInspectionResult(item.rowKey); + return ( +
+ {/* Blue left bar for selected items */} + {selectedItems.has(item.id) && ( +
+ )} + + {/* === Header row: checkbox + item code + item name + inspection badge === */} +
+ {/* Checkbox */} + + + {item.item_code} + + + {item.item_name} + + {item.inspection_type === "self" && ( + + ๊ฒ€์‚ฌ ํ•„์ˆ˜ + + )} + {item.inspection_type === "request" && ( + + ๊ฒ€์‚ฌ์˜๋ขฐ ์„ ํƒ + + )} +
+ + {/* === Body row: image + info + action === */} +
+ {/* Product image */} +
+ {item.image ? ( + {item.item_name} + ) : ( + + {"\uD83D\uDCE6"} + + )} +
+ + {/* Info columns */} +
+
+ + ๋ฐœ์ฃผ์ผ + + + {item.order_date || "-"} + +
+
+ + ๋ฐœ์ฃผ๋ฒˆํ˜ธ + + + {item.purchase_no || "-"} + +
+
+ + ๋ฐœ์ฃผ์ˆ˜๋Ÿ‰ + + + {item.order_qty.toLocaleString()} + +
+
+ + ๋ฏธ์ž…๊ณ  + + + {item.remain_qty.toLocaleString()} + +
+
+ + {/* Action column: qty display + delete button */} +
+ {/* Qty display - clickable to open numpad */} + + + {/* Delete button */} + +
+
+ + {/* === Package info === */} + {item.packages && item.packages.length > 0 && ( +
+
+ + ํฌ์žฅ์™„๋ฃŒ + + + {item.packages + .reduce((s, p) => s + p.count * p.qtyPerUnit, 0) + .toLocaleString()}{" "} + EA + +
+ {item.packages.map((pkg, idx) => ( +
+ {pkg.unit.icon} + + {pkg.count} + {pkg.unit.label} x {pkg.qtyPerUnit.toLocaleString()}EA + = {(pkg.count * pkg.qtyPerUnit).toLocaleString()}EA + +
+ ))} +
+ )} + + {/* === Inspection row === */} + {(item.inspection_type === "self" || + item.inspection_type === "request") && ( +
+
+ + + {/* Pass button for non-required */} + {!item.inspection_required && !inspResult?.completed && ( + + )} +
+
+ )} +
+ ); + })} +
+ )} + + {/* ===== Result toast (only when message exists) ===== */} + {resultMsg && ( +
+
+ {resultMsg} +
+
+ )} + + {/* ===== Warehouse picker modal ===== */} + {warehousePickerOpen && ( +
+
setWarehousePickerOpen(false)} + /> +
+ {/* Header */} +
+

์ฐฝ๊ณ  ์„ ํƒ

+ +
+ + {/* Warehouse list */} +
+ {warehouses.length === 0 ? ( +

+ ๋“ฑ๋ก๋œ ์ฐฝ๊ณ ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค +

+ ) : ( +
+ {warehouses.map((wh) => ( + + ))} +
+ )} +
+
+
+ )} + + {/* ===== Inspection Modal ===== */} + {inspectionTarget && ( + { + setInspectionModalOpen(false); + setInspectionTarget(null); + }} + onComplete={handleInspectionComplete} + onCancel={() => { + // ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ๋ฌดํšจํ™” (์™„๋ฃŒ โ†’ ๋Œ€๊ธฐ ํ’€๋ฆผ) + const targetRowKey = inspectionTarget.rowKey; + setInspectionResults((prev) => { + const next = new Map(prev); + next.delete(targetRowKey); + return next; + }); + cart.updateItemRow(targetRowKey, { inspectionResult: null }); + setTimeout(() => cart.saveToDb().catch(() => {}), 100); + }} + itemCode={inspectionTarget.item_code} + itemName={inspectionTarget.item_name} + totalQty={inspectionTarget.inbound_qty} + initialResult={getInspectionResult(inspectionTarget.rowKey)} + /> + )} + + {/* ===== NumberPad Modal (qty edit) ===== */} + {numpadTarget && ( + { + setNumpadOpen(false); + setNumpadTarget(null); + }} + onConfirm={handleNumpadConfirm} + maxQty={numpadTarget.remain_qty} + itemName={numpadTarget.item_name} + initialQty={numpadTarget.inbound_qty} + initialPackages={numpadTarget.packages} + /> + )} + + {/* ===== ์ž…๊ณ  ์™„๋ฃŒ ๊ฒฐ๊ณผ ๋ชจ๋‹ฌ ===== */} + {confirmResult && ( +
+
+
+ {/* ํ—ค๋” */} +
+
+ + + +
+

์ž…๊ณ  ์ฒ˜๋ฆฌ ์™„๋ฃŒ

+

+ {confirmResult.inboundNumber} +

+
+ + {/* ์ฒ˜๋ฆฌ ๋‚ด์—ญ */} +
+
+ + ์ฐฝ๊ณ :{" "} + + {confirmResult.warehouse} + + + {confirmResult.date} +
+ +
+ ์ฒ˜๋ฆฌ๋œ ํ’ˆ๋ชฉ ({confirmResult.items.length}๊ฑด) +
+
+ {confirmResult.items.map((item) => ( +
+
+

+ {item.item_name} +

+

+ {item.item_code} +

+
+ + {item.inbound_qty?.toLocaleString()} EA + +
+ ))} +
+
+ + {/* ํ™•์ธ ๋ฒ„ํŠผ */} +
+ +
+
+
+ )} +
+ ); } diff --git a/frontend/components/pop/hardcoded/inbound/InspectionModal.tsx b/frontend/components/pop/hardcoded/inbound/InspectionModal.tsx index b85254fa..1bf9079f 100644 --- a/frontend/components/pop/hardcoded/inbound/InspectionModal.tsx +++ b/frontend/components/pop/hardcoded/inbound/InspectionModal.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useCallback } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { apiClient } from "@/lib/api/client"; /* ------------------------------------------------------------------ */ @@ -8,36 +8,36 @@ import { apiClient } from "@/lib/api/client"; /* ------------------------------------------------------------------ */ export interface InspectionItem { - id: string; - inspection_item_name: string; - inspection_standard: string; - inspection_method: string; - pass_criteria: string; - is_required: string; - /** User-entered measured value */ - measured_value: string; - /** "pass" | "fail" | null */ - result: "pass" | "fail" | null; + id: string; + inspection_item_name: string; + inspection_standard: string; + inspection_method: string; + pass_criteria: string; + is_required: string; + /** User-entered measured value */ + measured_value: string; + /** "pass" | "fail" | null */ + result: "pass" | "fail" | null; } export interface InspectionResult { - items: InspectionItem[]; - goodQty: number; - badQty: number; - remark: string; - completed: boolean; - inspectionNumber?: string; // ๊ฒ€์‚ฌ ์™„๋ฃŒ ์‹œ ์ฑ„๋ฒˆ ๋ฐ›์Œ (์žฌ์‚ฌ์šฉ) + items: InspectionItem[]; + goodQty: number; + badQty: number; + remark: string; + completed: boolean; + inspectionNumber?: string; // ๊ฒ€์‚ฌ ์™„๋ฃŒ ์‹œ ์ฑ„๋ฒˆ ๋ฐ›์Œ (์žฌ์‚ฌ์šฉ) } interface InspectionModalProps { - open: boolean; - onClose: () => void; - onComplete: (result: InspectionResult) => void; - onCancel?: () => void; // ์ทจ์†Œ = ๊ฒ€์‚ฌ ๋ฌดํšจํ™” (์™„๋ฃŒ โ†’ ๋Œ€๊ธฐ) - itemCode: string; - itemName: string; - totalQty: number; - initialResult?: InspectionResult | null; + open: boolean; + onClose: () => void; + onComplete: (result: InspectionResult) => void; + onCancel?: () => void; // ์ทจ์†Œ = ๊ฒ€์‚ฌ ๋ฌดํšจํ™” (์™„๋ฃŒ โ†’ ๋Œ€๊ธฐ) + itemCode: string; + itemName: string; + totalQty: number; + initialResult?: InspectionResult | null; } /* ------------------------------------------------------------------ */ @@ -45,46 +45,46 @@ interface InspectionModalProps { /* ------------------------------------------------------------------ */ const DUMMY_INSPECTION_ITEMS: InspectionItem[] = [ - { - id: "dummy-1", - inspection_item_name: "์™ธ๊ด€ ๊ฒ€์‚ฌ", - inspection_standard: "์Šคํฌ๋ž˜์น˜, ๋ณ€์ƒ‰, ์ฐํž˜ ์—†์Œ", - inspection_method: "์œก์•ˆ ๊ฒ€์‚ฌ", - pass_criteria: "์ด์ƒ ์—†์Œ", - is_required: "Y", - measured_value: "", - result: null, - }, - { - id: "dummy-2", - inspection_item_name: "์น˜์ˆ˜ ๊ฒ€์‚ฌ", - inspection_standard: "๊ทœ๊ฒฉ +-0.5mm", - inspection_method: "์บ˜๋ฆฌํผ์Šค ์ธก์ •", - pass_criteria: "ํ—ˆ์šฉ ์˜ค์ฐจ ์ด๋‚ด", - is_required: "Y", - measured_value: "", - result: null, - }, - { - id: "dummy-3", - inspection_item_name: "์ˆ˜๋Ÿ‰ ๊ฒ€์‚ฌ", - inspection_standard: "๋ฐœ์ฃผ ์ˆ˜๋Ÿ‰๊ณผ ์ผ์น˜", - inspection_method: "์ „์ˆ˜ ๊ฒ€์‚ฌ", - pass_criteria: "์ˆ˜๋Ÿ‰ ์ผ์น˜", - is_required: "Y", - measured_value: "", - result: null, - }, - { - id: "dummy-4", - inspection_item_name: "ํฌ์žฅ ์ƒํƒœ", - inspection_standard: "ํฌ์žฅ ์†์ƒ ์—†์Œ", - inspection_method: "์œก์•ˆ ๊ฒ€์‚ฌ", - pass_criteria: "์ด์ƒ ์—†์Œ", - is_required: "", - measured_value: "", - result: null, - }, + { + id: "dummy-1", + inspection_item_name: "์™ธ๊ด€ ๊ฒ€์‚ฌ", + inspection_standard: "์Šคํฌ๋ž˜์น˜, ๋ณ€์ƒ‰, ์ฐํž˜ ์—†์Œ", + inspection_method: "์œก์•ˆ ๊ฒ€์‚ฌ", + pass_criteria: "์ด์ƒ ์—†์Œ", + is_required: "Y", + measured_value: "", + result: null, + }, + { + id: "dummy-2", + inspection_item_name: "์น˜์ˆ˜ ๊ฒ€์‚ฌ", + inspection_standard: "๊ทœ๊ฒฉ +-0.5mm", + inspection_method: "์บ˜๋ฆฌํผ์Šค ์ธก์ •", + pass_criteria: "ํ—ˆ์šฉ ์˜ค์ฐจ ์ด๋‚ด", + is_required: "Y", + measured_value: "", + result: null, + }, + { + id: "dummy-3", + inspection_item_name: "์ˆ˜๋Ÿ‰ ๊ฒ€์‚ฌ", + inspection_standard: "๋ฐœ์ฃผ ์ˆ˜๋Ÿ‰๊ณผ ์ผ์น˜", + inspection_method: "์ „์ˆ˜ ๊ฒ€์‚ฌ", + pass_criteria: "์ˆ˜๋Ÿ‰ ์ผ์น˜", + is_required: "Y", + measured_value: "", + result: null, + }, + { + id: "dummy-4", + inspection_item_name: "ํฌ์žฅ ์ƒํƒœ", + inspection_standard: "ํฌ์žฅ ์†์ƒ ์—†์Œ", + inspection_method: "์œก์•ˆ ๊ฒ€์‚ฌ", + pass_criteria: "์ด์ƒ ์—†์Œ", + is_required: "", + measured_value: "", + result: null, + }, ]; /* ------------------------------------------------------------------ */ @@ -92,501 +92,636 @@ const DUMMY_INSPECTION_ITEMS: InspectionItem[] = [ /* ------------------------------------------------------------------ */ export function InspectionModal({ - open, - onClose, - onComplete, - onCancel, - itemCode, - itemName, - totalQty, - initialResult, + open, + onClose, + onComplete, + onCancel, + itemCode, + itemName, + totalQty, + initialResult, }: InspectionModalProps) { - const [inspItems, setInspItems] = useState([]); - const [loading, setLoading] = useState(false); - const [goodQty, setGoodQty] = useState(0); - const [badQty, setBadQty] = useState(0); - /* NumPad state */ - const [numpadOpen, setNumpadOpen] = useState(false); - const [numpadTitle, setNumpadTitle] = useState(""); - const [numpadValue, setNumpadValue] = useState(""); - const [numpadMax, setNumpadMax] = useState(undefined); - const numpadCallbackRef = React.useRef<((val: string) => void) | null>(null); + const [inspItems, setInspItems] = useState([]); + const [loading, setLoading] = useState(false); + const [goodQty, setGoodQty] = useState(0); + const [badQty, setBadQty] = useState(0); + /* NumPad state */ + const [numpadOpen, setNumpadOpen] = useState(false); + const [numpadTitle, setNumpadTitle] = useState(""); + const [numpadValue, setNumpadValue] = useState(""); + const [numpadMax, setNumpadMax] = useState(undefined); + const numpadCallbackRef = React.useRef<((val: string) => void) | null>(null); - const openNumpad = (title: string, currentValue: string | number, onConfirm: (v: string) => void, max?: number) => { - setNumpadTitle(title); - setNumpadValue(String(currentValue || "")); - setNumpadMax(max); - numpadCallbackRef.current = onConfirm; - setNumpadOpen(true); - }; - const [remark, setRemark] = useState(""); + const openNumpad = ( + title: string, + currentValue: string | number, + onConfirm: (v: string) => void, + max?: number, + ) => { + setNumpadTitle(title); + setNumpadValue(String(currentValue || "")); + setNumpadMax(max); + numpadCallbackRef.current = onConfirm; + setNumpadOpen(true); + }; + const [remark, setRemark] = useState(""); - /* Fetch inspection items from DB */ - const fetchInspectionItems = useCallback(async () => { - setLoading(true); - try { - const res = await apiClient.get("/pop/inspection-result/info", { - params: { itemCode }, - }); - const data = res.data?.data; - if (Array.isArray(data) && data.length > 0) { - setInspItems( - data.map((r: Record) => ({ - id: String(r.id ?? ""), - inspection_item_name: String(r.inspection_item_name ?? ""), - inspection_standard: String(r.inspection_standard ?? ""), - inspection_method: String(r.inspection_method ?? ""), - pass_criteria: String(r.pass_criteria ?? ""), - is_required: String(r.is_required ?? "Y"), - measured_value: "", - result: null, - })) - ); - } else { - setInspItems(DUMMY_INSPECTION_ITEMS.map((i) => ({ ...i }))); - } - } catch { - setInspItems(DUMMY_INSPECTION_ITEMS.map((i) => ({ ...i }))); - } finally { - setLoading(false); - } - }, [itemCode]); + /* Fetch inspection items from DB */ + const fetchInspectionItems = useCallback(async () => { + setLoading(true); + try { + const res = await apiClient.get("/pop/inspection-result/info", { + params: { itemCode }, + }); + const data = res.data?.data; + if (Array.isArray(data) && data.length > 0) { + setInspItems( + data.map((r: Record) => ({ + id: String(r.id ?? ""), + inspection_item_name: String(r.inspection_item_name ?? ""), + inspection_standard: String(r.inspection_standard ?? ""), + inspection_method: String(r.inspection_method ?? ""), + pass_criteria: String(r.pass_criteria ?? ""), + is_required: String(r.is_required ?? "Y"), + measured_value: "", + result: null, + })), + ); + } else { + setInspItems(DUMMY_INSPECTION_ITEMS.map((i) => ({ ...i }))); + } + } catch { + setInspItems(DUMMY_INSPECTION_ITEMS.map((i) => ({ ...i }))); + } finally { + setLoading(false); + } + }, [itemCode]); - /* Init on open */ - useEffect(() => { - if (!open) return; - if (initialResult) { - setInspItems(initialResult.items.map((i) => ({ ...i }))); - setGoodQty(initialResult.goodQty); - setBadQty(initialResult.badQty); - setRemark(initialResult.remark); - } else { - fetchInspectionItems(); - setGoodQty(totalQty); - setBadQty(0); - setRemark(""); - } - }, [open, initialResult, fetchInspectionItems, totalQty]); + /* Init on open */ + useEffect(() => { + if (!open) return; + if (initialResult) { + setInspItems(initialResult.items.map((i) => ({ ...i }))); + setGoodQty(initialResult.goodQty); + setBadQty(initialResult.badQty); + setRemark(initialResult.remark); + } else { + fetchInspectionItems(); + setGoodQty(totalQty); + setBadQty(0); + setRemark(""); + } + }, [open, initialResult, fetchInspectionItems, totalQty]); - /* Update item */ - const updateItem = (id: string, field: "measured_value" | "result", value: string) => { - setInspItems((prev) => - prev.map((item) => - item.id === id - ? { ...item, [field]: field === "result" ? (item.result === value ? null : value) : value } - : item - ) - ); - }; + /* Update item */ + const updateItem = ( + id: string, + field: "measured_value" | "result", + value: string, + ) => { + setInspItems((prev) => + prev.map((item) => + item.id === id + ? { + ...item, + [field]: + field === "result" + ? item.result === value + ? null + : value + : value, + } + : item, + ), + ); + }; - /* Handle good/bad qty sync */ - const handleGoodQtyChange = (val: number) => { - const v = Math.max(0, Math.min(val, totalQty)); - setGoodQty(v); - setBadQty(totalQty - v); - }; + /* Handle good/bad qty sync */ + const handleGoodQtyChange = (val: number) => { + const v = Math.max(0, Math.min(val, totalQty)); + setGoodQty(v); + setBadQty(totalQty - v); + }; - const handleBadQtyChange = (val: number) => { - const v = Math.max(0, Math.min(val, totalQty)); - setBadQty(v); - setGoodQty(totalQty - v); - }; + const handleBadQtyChange = (val: number) => { + const v = Math.max(0, Math.min(val, totalQty)); + setBadQty(v); + setGoodQty(totalQty - v); + }; - /* ๊ฒ€์‚ฌ ์™„๋ฃŒ ๊ฐ€๋Šฅ ์—ฌ๋ถ€: ํ•„์ˆ˜ ํ•ญ๋ชฉ์ด ๋ชจ๋‘ pass */ - const canComplete = inspItems - .filter((it) => it.is_required === "Y") - .every((it) => it.result === "pass"); + /* ๊ฒ€์‚ฌ ์™„๋ฃŒ ๊ฐ€๋Šฅ ์—ฌ๋ถ€: ํ•„์ˆ˜ ํ•ญ๋ชฉ์ด ๋ชจ๋‘ pass */ + const canComplete = inspItems + .filter((it) => it.is_required === "Y") + .every((it) => it.result === "pass"); - /* Complete */ - const [allocating, setAllocating] = useState(false); - const handleComplete = async () => { - if (!canComplete) return; - setAllocating(true); - try { - // 1. ๊ธฐ์กด inspectionNumber ์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ฑ„๋ฒˆ ํ˜ธ์ถœ - let inspectionNumber = initialResult?.inspectionNumber; - if (!inspectionNumber) { - try { - const res = await apiClient.post("/pop/inspection-result/allocate-number"); - inspectionNumber = res.data?.data?.inspectionNumber; - } catch (err) { - console.error("[๊ฒ€์‚ฌ๋ฒˆํ˜ธ ์ฑ„๋ฒˆ ์‹คํŒจ]", err); - } - } - // 2. ๊ฒฐ๊ณผ ์ „๋‹ฌ (์ฑ„๋ฒˆ ํฌํ•จ) - onComplete({ - items: inspItems, - goodQty, - badQty, - remark, - completed: true, - inspectionNumber, - }); - onClose(); - } finally { - setAllocating(false); - } - }; + /* Complete */ + const [allocating, setAllocating] = useState(false); + const handleComplete = async () => { + if (!canComplete) return; + setAllocating(true); + try { + // 1. ๊ธฐ์กด inspectionNumber ์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ฑ„๋ฒˆ ํ˜ธ์ถœ + let inspectionNumber = initialResult?.inspectionNumber; + if (!inspectionNumber) { + try { + const res = await apiClient.post( + "/pop/inspection-result/allocate-number", + ); + inspectionNumber = res.data?.data?.inspectionNumber; + } catch (err) { + console.error("[๊ฒ€์‚ฌ๋ฒˆํ˜ธ ์ฑ„๋ฒˆ ์‹คํŒจ]", err); + } + } + // 2. ๊ฒฐ๊ณผ ์ „๋‹ฌ (์ฑ„๋ฒˆ ํฌํ•จ) + onComplete({ + items: inspItems, + goodQty, + badQty, + remark, + completed: true, + inspectionNumber, + }); + onClose(); + } finally { + setAllocating(false); + } + }; - if (!open) return null; + if (!open) return null; - return ( -
- {/* Overlay */} -
+ return ( +
+ {/* Overlay */} +
- {/* Bottom sheet */} -
e.stopPropagation()} - > - {/* Handle bar */} -
-
-
+ {/* Bottom sheet */} +
e.stopPropagation()} + > + {/* Handle bar */} +
+
+
- {/* Header */} -
-

์ž์ฃผ๊ฒ€์‚ฌ

- -
+ {/* Header */} +
+

์ž์ฃผ๊ฒ€์‚ฌ

+ +
- {/* Scrollable body */} -
- {/* Item summary */} -
- - {itemCode} - - - {itemName} - - - {totalQty.toLocaleString()} EA - -
+ {/* Scrollable body */} +
+ {/* Item summary */} +
+ + {itemCode} + + + {itemName} + + + {totalQty.toLocaleString()} EA + +
- {/* Inspection items section */} -
-
-

๊ฒ€์‚ฌ ํ•ญ๋ชฉ

- - {inspItems.length}๊ฐœ - -
+ {/* Inspection items section */} +
+
+

๊ฒ€์‚ฌ ํ•ญ๋ชฉ

+ + {inspItems.length}๊ฐœ + +
- {loading ? ( -
- - - - - ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘... -
- ) : inspItems.length === 0 ? ( -
- ๋“ฑ๋ก๋œ ๊ฒ€์‚ฌ ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค -
- ) : ( -
- {inspItems.map((item) => ( -
- {/* Item header */} -
- - {item.inspection_item_name} - - {item.is_required === "Y" && ( - - ํ•„์ˆ˜ - - )} -
+ {loading ? ( +
+ + + + + ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘... +
+ ) : inspItems.length === 0 ? ( +
+ ๋“ฑ๋ก๋œ ๊ฒ€์‚ฌ ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค +
+ ) : ( +
+ {inspItems.map((item) => ( +
+ {/* Item header */} +
+ + {item.inspection_item_name} + + {item.is_required === "Y" && ( + + ํ•„์ˆ˜ + + )} +
- {/* Info grid */} -
- {item.inspection_standard && ( - <> - ๊ธฐ์ค€ - {item.inspection_standard} - - )} - {item.inspection_method && ( - <> - ๋ฐฉ๋ฒ• - {item.inspection_method} - - )} - {item.pass_criteria && ( - <> - ํŒ์ • - {item.pass_criteria} - - )} -
+ {/* Info grid */} +
+ {item.inspection_standard && ( + <> + ๊ธฐ์ค€ + + {item.inspection_standard} + + + )} + {item.inspection_method && ( + <> + ๋ฐฉ๋ฒ• + + {item.inspection_method} + + + )} + {item.pass_criteria && ( + <> + ํŒ์ • + + {item.pass_criteria} + + + )} +
- {/* Input + result buttons */} -
- -
- - -
- {/* Camera placeholder */} - -
-
- ))} -
- )} -
+ {/* Input + result buttons */} +
+ +
+ + +
+ {/* Camera placeholder */} + +
+
+ ))} +
+ )} +
- {/* Final judgment section */} -
-

์ข…ํ•ฉ ํŒ์ •

+ {/* Final judgment section */} +
+

+ ์ข…ํ•ฉ ํŒ์ • +

- {/* Good / Bad qty */} -
-
- -
- - EA -
-
-
- -
- - EA -
-
-
+ {/* Good / Bad qty */} +
+
+ +
+ + EA +
+
+
+ +
+ + EA +
+
+
- {/* Total summary */} -
- ์ „์ฒด ์ˆ˜๋Ÿ‰ - - {totalQty.toLocaleString()} EA - -
-
+ {/* Total summary */} +
+ ์ „์ฒด ์ˆ˜๋Ÿ‰ + + {totalQty.toLocaleString()} EA + +
+
- {/* Remark */} -
-

๋น„๊ณ 

-