From d9ced89a95c6a8e49920a46dd9c28c673fad6a91 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 22 Apr 2026 14:49:40 +0900 Subject: [PATCH] feat: Enhance packaging and work instruction functionality - Updated SQL queries in the packaging and work instruction controllers to include additional fields such as `inventory_unit` and `material`, improving data retrieval for packaging items. - Implemented new columns in the `work_instruction_detail` table for better tracking of item schedules, equipment, and personnel involved in work instructions. - Enhanced frontend components to utilize the new data structure, including category options for inventory units and materials, improving user experience in the packaging and subcontractor item pages. - Refactored item inspection display logic to format pass criteria more clearly, enhancing readability for inspection data. --- .../src/controllers/packagingController.ts | 6 +- .../controllers/workInstructionController.ts | 39 ++- .../COMPANY_10/logistics/packaging/page.tsx | 46 ++- .../outsourcing/subcontractor-item/page.tsx | 64 +++- .../production/work-instruction/page.tsx | 12 +- .../quality/item-inspection/page.tsx | 11 +- .../COMPANY_16/logistics/packaging/page.tsx | 46 ++- .../outsourcing/subcontractor-item/page.tsx | 64 +++- .../quality/item-inspection/page.tsx | 11 +- .../COMPANY_29/logistics/packaging/page.tsx | 46 ++- .../outsourcing/subcontractor-item/page.tsx | 64 +++- .../production/work-instruction/page.tsx | 12 +- .../quality/item-inspection/page.tsx | 11 +- .../COMPANY_30/logistics/packaging/page.tsx | 46 ++- .../outsourcing/subcontractor-item/page.tsx | 65 +++- .../production/work-instruction/page.tsx | 12 +- .../quality/item-inspection/page.tsx | 11 +- .../COMPANY_7/logistics/packaging/page.tsx | 46 ++- .../outsourcing/subcontractor-item/page.tsx | 64 +++- .../production/work-instruction/page.tsx | 305 +++++++++++++++--- .../quality/item-inspection/page.tsx | 11 +- .../COMPANY_8/logistics/packaging/page.tsx | 46 ++- .../outsourcing/subcontractor-item/page.tsx | 64 +++- .../production/work-instruction/page.tsx | 12 +- .../quality/item-inspection/page.tsx | 11 +- .../COMPANY_9/logistics/packaging/page.tsx | 46 ++- .../outsourcing/subcontractor-item/page.tsx | 64 +++- .../production/work-instruction/page.tsx | 12 +- .../quality/item-inspection/page.tsx | 11 +- frontend/lib/api/packaging.ts | 3 + 30 files changed, 1079 insertions(+), 182 deletions(-) diff --git a/backend-node/src/controllers/packagingController.ts b/backend-node/src/controllers/packagingController.ts index 037ab46a..1b195258 100644 --- a/backend-node/src/controllers/packagingController.ts +++ b/backend-node/src/controllers/packagingController.ts @@ -175,7 +175,7 @@ export async function getPkgUnitItems( const pool = getPool(); const result = await pool.query( - `SELECT pui.*, ii.item_name, ii.size AS spec, ii.unit + `SELECT pui.*, ii.item_name, ii.size AS spec, ii.unit, ii.inventory_unit, ii.material FROM pkg_unit_item pui LEFT JOIN item_info ii ON pui.item_number = ii.item_number AND pui.company_code = ii.company_code WHERE pui.pkg_code=$1 AND pui.company_code=$2 @@ -532,7 +532,7 @@ export async function getItemsByDivision( } const result = await pool.query( - `SELECT id, item_number, item_name, size, material, unit, division + `SELECT id, item_number, item_name, size, material, unit, inventory_unit, division FROM item_info WHERE ${conditions.join(" AND ")} ORDER BY item_name`, @@ -585,7 +585,7 @@ export async function getGeneralItems( } const result = await pool.query( - `SELECT id, item_number, item_name, size AS spec, material, unit, division + `SELECT id, item_number, item_name, size AS spec, material, unit, inventory_unit, division FROM item_info WHERE ${conditions.join(" AND ")} ORDER BY item_name diff --git a/backend-node/src/controllers/workInstructionController.ts b/backend-node/src/controllers/workInstructionController.ts index 6fccb78b..c6b9e667 100644 --- a/backend-node/src/controllers/workInstructionController.ts +++ b/backend-node/src/controllers/workInstructionController.ts @@ -7,13 +7,19 @@ import { getPool } from "../database/db"; import { logger } from "../utils/logger"; import { numberingRuleService } from "../services/numberingRuleService"; -// 자동 마이그레이션: work_instruction_detail에 routing_version_id 컬럼 추가 +// 자동 마이그레이션: work_instruction_detail에 routing_version_id + 품목별 일정/설비/작업조/작업자 컬럼 추가 let _migrationDone = false; async function ensureDetailRoutingColumn() { if (_migrationDone) return; try { const pool = getPool(); await pool.query("ALTER TABLE work_instruction_detail ADD COLUMN IF NOT EXISTS routing_version_id VARCHAR(500)"); + // 품목별 일정/설비/작업조/작업자 컬럼 (옵션 A — 다중선택 지원) + await pool.query("ALTER TABLE work_instruction_detail ADD COLUMN IF NOT EXISTS start_date VARCHAR(500)"); + await pool.query("ALTER TABLE work_instruction_detail ADD COLUMN IF NOT EXISTS end_date VARCHAR(500)"); + await pool.query("ALTER TABLE work_instruction_detail ADD COLUMN IF NOT EXISTS equipment_ids VARCHAR(1000)"); + await pool.query("ALTER TABLE work_instruction_detail ADD COLUMN IF NOT EXISTS work_teams VARCHAR(200)"); + await pool.query("ALTER TABLE work_instruction_detail ADD COLUMN IF NOT EXISTS workers VARCHAR(1000)"); _migrationDone = true; } catch { /* 이미 존재하거나 권한 문제 시 무시 */ } } @@ -130,6 +136,11 @@ export async function getList(req: AuthenticatedRequest, res: Response) { d.source_table, d.source_id, d.routing_version_id AS detail_routing_version_id, + d.start_date AS detail_start_date, + d.end_date AS detail_end_date, + d.equipment_ids AS detail_equipment_ids, + d.work_teams AS detail_work_teams, + d.workers AS detail_workers, COALESCE(itm.item_name, '') AS item_name, COALESCE(itm.type, '') AS item_type, COALESCE(itm.size, '') AS item_spec, @@ -186,6 +197,11 @@ export async function getList(req: AuthenticatedRequest, res: Response) { d.source_table, d.source_id, d.routing_version_id AS detail_routing_version_id, + d.start_date AS detail_start_date, + d.end_date AS detail_end_date, + d.equipment_ids AS detail_equipment_ids, + d.work_teams AS detail_work_teams, + d.workers AS detail_workers, COALESCE(itm.item_name, '') AS item_name, COALESCE(itm.type, '') AS item_type, COALESCE(itm.size, '') AS item_spec, @@ -293,8 +309,25 @@ export async function save(req: AuthenticatedRequest, res: Response) { if (!firstRouting && itemRouting) firstRouting = itemRouting; totalQty += Number(item.qty || 0); await client.query( - `INSERT INTO work_instruction_detail (id,company_code,work_instruction_no,work_instruction_id,item_number,qty,remark,source_table,source_id,part_code,routing_version_id,created_date,writer) VALUES (gen_random_uuid()::text,$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,NOW(),$11)`, - [companyCode, wiNo, wiId, item.itemNumber||item.itemCode||"", item.qty||"0", item.remark||"", item.sourceTable||"", item.sourceId||"", item.partCode||item.itemNumber||item.itemCode||"", itemRouting, userId] + `INSERT INTO work_instruction_detail (id,company_code,work_instruction_no,work_instruction_id,item_number,qty,remark,source_table,source_id,part_code,routing_version_id,start_date,end_date,equipment_ids,work_teams,workers,created_date,writer) VALUES (gen_random_uuid()::text,$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,NOW(),$16)`, + [ + companyCode, + wiNo, + wiId, + item.itemNumber||item.itemCode||"", + item.qty||"0", + item.remark||"", + item.sourceTable||"", + item.sourceId||"", + item.partCode||item.itemNumber||item.itemCode||"", + itemRouting, + item.startDate||"", + item.endDate||"", + item.equipmentIds||"", + item.workTeams||"", + item.workers||"", + userId, + ] ); } diff --git a/frontend/app/(main)/COMPANY_10/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_10/logistics/packaging/page.tsx index 74585bb8..66b467bb 100644 --- a/frontend/app/(main)/COMPANY_10/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_10/logistics/packaging/page.tsx @@ -27,6 +27,7 @@ import { getItemsByDivision, getGeneralItems, type PkgUnit, type PkgUnitItem, type LoadingUnit, type LoadingUnitPkg, type ItemInfoForPkg, } from "@/lib/api/packaging"; +import { apiClient } from "@/lib/api/client"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; @@ -118,6 +119,45 @@ export default function PackagingPage() { const [saving, setSaving] = useState(false); + // 카테고리 옵션 (inventory_unit / material) — 코드 → 라벨 변환 + const [categoryOptions, setCategoryOptions] = useState< + Record + >({}); + + useEffect(() => { + const load = async () => { + const flatten = (vals: any[]): { code: string; label: string }[] => { + const out: { code: string; label: string }[] = []; + for (const v of vals) { + out.push({ + code: v.valueCode || v.value_code || v.code, + label: v.valueLabel || v.value_label || v.label, + }); + if (v.children?.length) out.push(...flatten(v.children)); + } + return out; + }; + const optMap: Record = {}; + for (const col of ["inventory_unit", "material"]) { + try { + const res = await apiClient.get( + `/table-categories/item_info/${col}/values` + ); + if (res.data?.success) optMap[col] = flatten(res.data.data || []); + } catch { + /* skip */ + } + } + setCategoryOptions(optMap); + }; + load(); + }, []); + + const resolveCat = (col: string, code: string | null | undefined) => { + if (!code) return ""; + return categoryOptions[col]?.find((o) => o.code === code)?.label || code; + }; + // --- 데이터 로드 (item_info 기반 + pkg_unit/loading_unit LEFT JOIN 방식) --- const fetchPkgUnits = useCallback(async () => { setPkgLoading(true); @@ -622,7 +662,7 @@ export default function PackagingPage() { {item.item_number} {item.item_name || "-"} {item.spec || "-"} - {item.unit || "EA"} + {resolveCat("inventory_unit", item.inventory_unit) || "EA"} {Number(item.pkg_qty).toLocaleString()} + + + {searchable && ( +
+ setKeyword(e.target.value)} className="h-7 text-xs" /> +
+ )} +
+ {filtered.length === 0 ? ( +
{emptyMessage}
+ ) : filtered.map(opt => ( + + ))} +
+ {value.length > 0 && ( +
+ {value.length}개 선택됨 + +
+ )} +
+ + ); } export default function WorkInstructionPage() { @@ -197,12 +281,23 @@ export default function WorkInstructionPage() { const applyRegistration = () => { if (regCheckedIds.size === 0) { alert("품목을 선택해주세요."); return; } + const today = new Date().toISOString().split("T")[0]; const items: SelectedItem[] = []; for (const item of regSourceData) { if (!regCheckedIds.has(getRegId(item))) continue; - if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code }); - else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id }); - else items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: Number(item.plan_qty || 1), remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id }); + const baseExtra = { startDate: today, endDate: "", equipmentIds: [], workTeams: [], workers: [] } as Pick; + if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code, ...baseExtra }); + else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id, ...baseExtra }); + else { + // 생산계획: 잔량(remain_qty)이 있으면 잔량 기반으로 기본 수량 제안 (0/음수 허용 — 계획 초과 가능) + const defaultQty = item.remain_qty !== undefined && item.remain_qty !== null + ? Number(item.remain_qty) + : Number(item.plan_qty || 1); + // 생산계획: 일정이 있으면 기본값으로 전달 + const planStart = item.start_date ? String(item.start_date).split("T")[0] : today; + const planEnd = item.end_date ? String(item.end_date).split("T")[0] : ""; + items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: defaultQty, remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id, startDate: planStart, endDate: planEnd, equipmentIds: [], workTeams: [], workers: [] }); + } } // 동일품목 합산 @@ -250,6 +345,9 @@ export default function WorkInstructionPage() { itemCode: firstItem?.itemCode || "", itemName: firstItem?.itemName || "", spec: firstItem?.spec || "", qty: Number(confirmAddQty), remark: "", sourceType: "item" as SourceType, sourceTable: "item_info", sourceId: firstItem?.itemCode || "", + startDate: firstItem?.startDate || new Date().toISOString().split("T")[0], + endDate: firstItem?.endDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setConfirmAddQty(""); }; @@ -259,11 +357,29 @@ export default function WorkInstructionPage() { if (confirmItems.length === 0) { alert("품목이 없습니다."); return; } setSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값으로 (하위 호환 유지 — 조회 화면이 헤더값으로 표시되는 레거시 대비) + const first = confirmItems[0]; + const headerStart = first?.startDate || ""; + const headerEnd = first?.endDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || ""; + const headerWorkTeam = first?.workTeams?.[0] || ""; + const headerWorker = first?.workers?.[0] || ""; const payload = { - status: confirmStatus, startDate: confirmStartDate, endDate: confirmEndDate, - equipmentId: confirmEquipmentId, workTeam: confirmWorkTeam, worker: confirmWorker, + status: confirmStatus, + startDate: headerStart, endDate: headerEnd, + equipmentId: headerEquipment, workTeam: headerWorkTeam, worker: headerWorker, routing: confirmRouting || null, - items: confirmItems.map(i => ({ itemNumber: i.itemCode, itemCode: i.itemCode, qty: String(i.qty), remark: i.remark, sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode, routing: i.routing || null })), + items: confirmItems.map(i => ({ + itemNumber: i.itemCode, itemCode: i.itemCode, qty: String(i.qty), remark: i.remark, + sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode, + routing: i.routing || null, + // 품목별 일정/설비/작업조/작업자 (옵션 A — 다중값 쉼표 구분) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsConfirmModalOpen(false); fetchOrders(); alert("작업지시가 등록되었습니다."); } @@ -286,6 +402,12 @@ export default function WorkInstructionPage() { sourceTable: d.source_table || "item_info", sourceId: d.source_id || "", routing: d.detail_routing_version_id || order.routing_version_id || "", routingOptions: [], + // 품목별 일정/설비/작업조/작업자 (detail 값 우선, 없으면 헤더값 폴백) + startDate: d.detail_start_date || d.start_date || "", + endDate: d.detail_end_date || d.end_date || "", + equipmentIds: (d.detail_equipment_ids || "").split(",").filter(Boolean), + workTeams: (d.detail_work_teams || "").split(",").filter(Boolean), + workers: (d.detail_workers || "").split(",").filter(Boolean), })); setEditItems(items); setAddQty(""); setAddEquipment(""); setAddWorkTeam(""); setAddWorker(""); @@ -316,9 +438,13 @@ export default function WorkInstructionPage() { const addEditItem = () => { if (!addQty || Number(addQty) <= 0) { alert("수량을 입력해주세요."); return; } + const firstItem = editItems[0]; setEditItems(prev => [...prev, { itemCode: editOrder?.item_number || "", itemName: editOrder?.item_name || "", spec: editOrder?.item_spec || "", qty: Number(addQty), remark: "", sourceType: "item", sourceTable: "item_info", sourceId: editOrder?.item_number || "", + startDate: firstItem?.startDate || editStartDate || "", + endDate: firstItem?.endDate || editEndDate || "", + equipmentIds: [], workTeams: [], workers: [], }]); setAddQty(""); }; @@ -327,11 +453,30 @@ export default function WorkInstructionPage() { if (!editOrder || editItems.length === 0) { alert("품목이 없습니다."); return; } setEditSaving(true); try { + // 헤더 대표값: 첫 번째 품목의 첫 번째 값 사용 (하위 호환 — 등록 모달과 동일 패턴) + const first = editItems[0]; + const headerStart = first?.startDate || editStartDate || ""; + const headerEnd = first?.endDate || editEndDate || ""; + const headerEquipment = first?.equipmentIds?.[0] || editEquipmentId || ""; + const headerWorkTeam = first?.workTeams?.[0] || editWorkTeam || ""; + const headerWorker = first?.workers?.[0] || editWorker || ""; const payload = { - id: editOrder.wi_id, status: editStatus, startDate: editStartDate, endDate: editEndDate, - equipmentId: editEquipmentId, workTeam: editWorkTeam, worker: editWorker, remark: editRemark, + id: editOrder.wi_id, status: editStatus, + startDate: headerStart, endDate: headerEnd, + equipmentId: headerEquipment, workTeam: headerWorkTeam, worker: headerWorker, + remark: editRemark, routing: editRouting || null, - items: editItems.map(i => ({ itemNumber: i.itemCode, itemCode: i.itemCode, qty: String(i.qty), remark: i.remark, sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode, routing: i.routing || null })), + items: editItems.map(i => ({ + itemNumber: i.itemCode, itemCode: i.itemCode, qty: String(i.qty), remark: i.remark, + sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode, + routing: i.routing || null, + // 품목별 일정/설비/작업조/작업자 (다중값 쉼표 구분 — 등록 모달과 동일) + startDate: i.startDate || "", + endDate: i.endDate || "", + equipmentIds: (i.equipmentIds || []).join(","), + workTeams: (i.workTeams || []).join(","), + workers: (i.workers || []).join(","), + })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsEditModalOpen(false); fetchOrders(); alert("수정되었습니다."); } @@ -578,7 +723,7 @@ export default function WorkInstructionPage() { 0 && regCheckedIds.size === regSourceData.length} onCheckedChange={toggleRegAll} /> {regSourceType === "item" && <>품목코드품목명규격} {regSourceType === "order" && <>수주번호품번품목명규격수량납기일} - {regSourceType === "production" && <>계획번호품번품목명계획수량시작일완료일설비} + {regSourceType === "production" && <>계획번호품번품목명계획수량적용수량잔량시작일완료일설비} @@ -590,7 +735,7 @@ export default function WorkInstructionPage() { e.stopPropagation()}> toggleRegItem(id)} /> {regSourceType === "item" && <>{item.item_code}{item.item_name}{item.spec || "-"}} {regSourceType === "order" && <>{item.order_no}{item.item_code}{item.item_name}{item.spec || "-"}{Number(item.qty || 0).toLocaleString()}{item.due_date || "-"}} - {regSourceType === "production" && <>{item.plan_no}{item.item_code}{item.item_name}{Number(item.plan_qty || 0).toLocaleString()}{item.start_date ? String(item.start_date).split("T")[0] : "-"}{item.end_date ? String(item.end_date).split("T")[0] : "-"}{item.equipment_name || "-"}} + {regSourceType === "production" && <>{item.plan_no}{item.item_code}{item.item_name}{Number(item.plan_qty || 0).toLocaleString()}{Number(item.applied_qty || 0).toLocaleString()}{Number(item.remain_qty ?? item.plan_qty ?? 0).toLocaleString()}{item.start_date ? String(item.start_date).split("T")[0] : "-"}{item.end_date ? String(item.end_date).split("T")[0] : "-"}{item.equipment_name || "-"}} ); })} @@ -619,7 +764,7 @@ export default function WorkInstructionPage() { {/* ── 2단계: 확인 모달 ── */} - + 작업지시 적용 확인 기본 정보를 입력하고 '최종 적용' 버튼을 눌러주세요. @@ -628,38 +773,33 @@ export default function WorkInstructionPage() {

작업지시 기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setConfirmStartDate(e.target.value)} className="h-9" />
-
setConfirmEndDate(e.target.value)} className="h-9" />
-
- -
-
- -
-
- -

품목 목록

- +
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 비고 + 수량 + 라우팅 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 @@ -668,7 +808,7 @@ export default function WorkInstructionPage() { {idx + 1} {item.itemCode} - {item.itemName || item.itemCode} + {item.itemName || item.itemCode} {item.spec || "-"} setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -690,6 +830,40 @@ export default function WorkInstructionPage() { + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> @@ -710,7 +884,7 @@ export default function WorkInstructionPage() { {/* ── 수정 모달 ── */} { if (!v && wsModalOpen) return; setIsEditModalOpen(v); }}> - + {`작업지시 관리 - ${editOrder?.work_instruction_no || ""}`} 품목을 추가/삭제하고 정보를 수정해주세요. @@ -719,48 +893,47 @@ export default function WorkInstructionPage() {

기본 정보

-
+

시작일·완료예정일·설비·작업조·작업자는 아래 품목별로 지정해주세요.

+
-
setEditStartDate(e.target.value)} className="h-9" />
-
setEditEndDate(e.target.value)} className="h-9" />
-
-
-
- -
-
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
+
setEditRemark(e.target.value)} className="h-9" placeholder="비고를 입력해주세요" />
- {/* 품목 테이블 — 라우팅/공정작업기준을 품목별로 표시 */} + {/* 품목 테이블 — 품목별 일정/설비/작업조/작업자 + 라우팅/공정작업기준 */}
작업지시 항목 {editItems.length}건
-
+
순번 품목코드 - 품목명 + 품목명 규격 - 수량 - 라우팅 - 공정작업기준 - 비고 + 수량 + 라우팅 + 공정작업기준 + 시작일 + 완료예정일 + 설비 + 작업조 + 작업자 + 비고 {editItems.length === 0 ? ( - 등록된 품목이 없어요 + 등록된 품목이 없어요 ) : editItems.map((item, idx) => ( {idx + 1} {item.itemCode} - {item.itemName || "-"} + {item.itemName || "-"} {item.spec || "-"} setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> @@ -803,6 +976,40 @@ export default function WorkInstructionPage() { 수정 + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, startDate: e.target.value } : it))} /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, endDate: e.target.value } : it))} /> + + + ({ value: eq.id, label: eq.equipment_name || eq.equipment_code, sub: eq.equipment_code }))} + value={item.equipmentIds || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, equipmentIds: next } : it))} + placeholder="설비 선택" + searchable + emptyMessage="설비가 없어요" + /> + + + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workTeams: next } : it))} + placeholder="작업조 선택" + /> + + + ({ value: emp.user_id, label: emp.user_name, sub: emp.dept_name || undefined }))} + value={item.workers || []} + onChange={next => setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, workers: next } : it))} + placeholder="작업자 선택" + searchable + emptyMessage="사원을 찾을 수 없어요" + /> + setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> diff --git a/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx index 842e81dd..fb5cc751 100644 --- a/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx @@ -1189,7 +1189,16 @@ export default function ItemInspectionInfoPage() { return jcLabel ? {jcLabel} : "-"; })()} - {row.pass_criteria || "-"} + {(() => { + const pc = row.pass_criteria; + if (!pc) return "-"; + if (pc.includes("|")) { + const [s, t] = pc.split("|"); + if (!t || !t.trim()) return s || "-"; + return `${s} ± ${t}`; + } + return pc; + })()} {row.is_required === "true" || row.is_required === true ? ( 필수 diff --git a/frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx b/frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx index 74585bb8..66b467bb 100644 --- a/frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx +++ b/frontend/app/(main)/COMPANY_8/logistics/packaging/page.tsx @@ -27,6 +27,7 @@ import { getItemsByDivision, getGeneralItems, type PkgUnit, type PkgUnitItem, type LoadingUnit, type LoadingUnitPkg, type ItemInfoForPkg, } from "@/lib/api/packaging"; +import { apiClient } from "@/lib/api/client"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; @@ -118,6 +119,45 @@ export default function PackagingPage() { const [saving, setSaving] = useState(false); + // 카테고리 옵션 (inventory_unit / material) — 코드 → 라벨 변환 + const [categoryOptions, setCategoryOptions] = useState< + Record + >({}); + + useEffect(() => { + const load = async () => { + const flatten = (vals: any[]): { code: string; label: string }[] => { + const out: { code: string; label: string }[] = []; + for (const v of vals) { + out.push({ + code: v.valueCode || v.value_code || v.code, + label: v.valueLabel || v.value_label || v.label, + }); + if (v.children?.length) out.push(...flatten(v.children)); + } + return out; + }; + const optMap: Record = {}; + for (const col of ["inventory_unit", "material"]) { + try { + const res = await apiClient.get( + `/table-categories/item_info/${col}/values` + ); + if (res.data?.success) optMap[col] = flatten(res.data.data || []); + } catch { + /* skip */ + } + } + setCategoryOptions(optMap); + }; + load(); + }, []); + + const resolveCat = (col: string, code: string | null | undefined) => { + if (!code) return ""; + return categoryOptions[col]?.find((o) => o.code === code)?.label || code; + }; + // --- 데이터 로드 (item_info 기반 + pkg_unit/loading_unit LEFT JOIN 방식) --- const fetchPkgUnits = useCallback(async () => { setPkgLoading(true); @@ -622,7 +662,7 @@ export default function PackagingPage() { {item.item_number} {item.item_name || "-"} {item.spec || "-"} - {item.unit || "EA"} + {resolveCat("inventory_unit", item.inventory_unit) || "EA"} {Number(item.pkg_qty).toLocaleString()}