From 7aaf264661bd068c2ac91f1dbdcb8f4f4c05db4c Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 14 Apr 2026 17:51:47 +0900 Subject: [PATCH] feat: Enhance work item detail management with additional inspection fields - Updated the processWorkStandardController and workInstructionController to include new fields for process_inspection_apply and equip_inspection_apply in SQL queries and data handling. - Modified the DetailFormModal and WorkItemDetailList components to support individual registration of inspection items and equipment inspections, improving the flexibility of the inspection process. - Implemented logic to handle automatic content generation for inspection and equipment inspection types, enhancing user experience and data accuracy. - These changes aim to improve the management of work item details and streamline the inspection process across multiple company implementations. --- .../processWorkStandardController.ts | 17 +++++++---- .../controllers/workInstructionController.ts | 18 +++++++----- .../components/DetailFormModal.tsx | 28 +++++++++++++++++- .../components/WorkItemDetailList.tsx | 29 +++++++++++++++---- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/backend-node/src/controllers/processWorkStandardController.ts b/backend-node/src/controllers/processWorkStandardController.ts index d745bab4..622d8f89 100644 --- a/backend-node/src/controllers/processWorkStandardController.ts +++ b/backend-node/src/controllers/processWorkStandardController.ts @@ -463,7 +463,7 @@ export async function getWorkItemDetails(req: AuthenticatedRequest, res: Respons SELECT id, work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, - selected_bom_items, created_date + selected_bom_items, process_inspection_apply, equip_inspection_apply, created_date FROM process_work_item_detail WHERE work_item_id = $1 AND company_code = $2 ORDER BY sort_order, created_date @@ -492,7 +492,7 @@ export async function createWorkItemDetail(req: AuthenticatedRequest, res: Respo work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, - selected_bom_items, + selected_bom_items, process_inspection_apply, equip_inspection_apply, } = req.body; if (!work_item_id || !content) { @@ -515,8 +515,9 @@ export async function createWorkItemDetail(req: AuthenticatedRequest, res: Respo INSERT INTO process_work_item_detail (company_code, work_item_id, detail_type, content, is_required, sort_order, remark, writer, inspection_code, inspection_method, unit, lower_limit, upper_limit, - duration_minutes, input_type, lookup_target, display_fields, selected_bom_items) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) + duration_minutes, input_type, lookup_target, display_fields, selected_bom_items, + process_inspection_apply, equip_inspection_apply) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) RETURNING * `; @@ -542,6 +543,8 @@ export async function createWorkItemDetail(req: AuthenticatedRequest, res: Respo lookup_target || null, display_fields || null, bomItemsJson, + process_inspection_apply || null, + equip_inspection_apply || null, ]); logger.info("작업 항목 상세 생성", { companyCode, id: result.rows[0].id }); @@ -567,7 +570,7 @@ export async function updateWorkItemDetail(req: AuthenticatedRequest, res: Respo detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, - selected_bom_items, + selected_bom_items, process_inspection_apply, equip_inspection_apply, } = req.body; const bomItemsJson = Array.isArray(selected_bom_items) ? JSON.stringify(selected_bom_items) : selected_bom_items ?? null; @@ -589,6 +592,8 @@ export async function updateWorkItemDetail(req: AuthenticatedRequest, res: Respo lookup_target = $15, display_fields = $16, selected_bom_items = $17, + process_inspection_apply = $18, + equip_inspection_apply = $19, updated_date = NOW() WHERE id = $6 AND company_code = $7 RETURNING * @@ -612,6 +617,8 @@ export async function updateWorkItemDetail(req: AuthenticatedRequest, res: Respo lookup_target || null, display_fields || null, bomItemsJson, + process_inspection_apply || null, + equip_inspection_apply || null, ]); if (result.rowCount === 0) { diff --git a/backend-node/src/controllers/workInstructionController.ts b/backend-node/src/controllers/workInstructionController.ts index 9f855147..b8960006 100644 --- a/backend-node/src/controllers/workInstructionController.ts +++ b/backend-node/src/controllers/workInstructionController.ts @@ -443,7 +443,8 @@ export async function getWorkStandard(req: AuthenticatedRequest, res: Response) const detailsResult = await pool.query( `SELECT id, wi_work_item_id AS work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, - duration_minutes, input_type, lookup_target, display_fields + duration_minutes, input_type, lookup_target, display_fields, + process_inspection_apply, equip_inspection_apply FROM wi_process_work_item_detail WHERE wi_work_item_id = $1 AND company_code = $2 ORDER BY sort_order`, @@ -467,7 +468,8 @@ export async function getWorkStandard(req: AuthenticatedRequest, res: Response) const detailsResult = await pool.query( `SELECT id, work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, - duration_minutes, input_type, lookup_target, display_fields + duration_minutes, input_type, lookup_target, display_fields, + process_inspection_apply, equip_inspection_apply FROM process_work_item_detail WHERE work_item_id = $1 AND company_code = $2 ORDER BY sort_order`, @@ -548,9 +550,9 @@ export async function copyWorkStandard(req: AuthenticatedRequest, res: Response) for (const origDetail of origDetails.rows) { await client.query( - `INSERT INTO wi_process_work_item_detail (company_code, wi_work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, writer) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`, - [companyCode, newItemId, origDetail.detail_type, origDetail.content, origDetail.is_required, origDetail.sort_order, origDetail.remark, origDetail.inspection_code, origDetail.inspection_method, origDetail.unit, origDetail.lower_limit, origDetail.upper_limit, origDetail.duration_minutes, origDetail.input_type, origDetail.lookup_target, origDetail.display_fields, userId] + `INSERT INTO wi_process_work_item_detail (company_code, wi_work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, process_inspection_apply, equip_inspection_apply, writer) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`, + [companyCode, newItemId, origDetail.detail_type, origDetail.content, origDetail.is_required, origDetail.sort_order, origDetail.remark, origDetail.inspection_code, origDetail.inspection_method, origDetail.unit, origDetail.lower_limit, origDetail.upper_limit, origDetail.duration_minutes, origDetail.input_type, origDetail.lookup_target, origDetail.display_fields, origDetail.process_inspection_apply || null, origDetail.equip_inspection_apply || null, userId] ); } } @@ -612,9 +614,9 @@ export async function saveWorkStandard(req: AuthenticatedRequest, res: Response) if (wi.details && Array.isArray(wi.details)) { for (const d of wi.details) { await client.query( - `INSERT INTO wi_process_work_item_detail (company_code, wi_work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, writer) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`, - [companyCode, newId, d.detail_type, d.content, d.is_required, d.sort_order, d.remark || null, d.inspection_code || null, d.inspection_method || null, d.unit || null, d.lower_limit || null, d.upper_limit || null, d.duration_minutes || null, d.input_type || null, d.lookup_target || null, d.display_fields || null, userId] + `INSERT INTO wi_process_work_item_detail (company_code, wi_work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, process_inspection_apply, equip_inspection_apply, writer) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`, + [companyCode, newId, d.detail_type, d.content, d.is_required, d.sort_order, d.remark || null, d.inspection_code || null, d.inspection_method || null, d.unit || null, d.lower_limit || null, d.upper_limit || null, d.duration_minutes || null, d.input_type || null, d.lookup_target || null, d.display_fields || null, d.process_inspection_apply || null, d.equip_inspection_apply || null, userId] ); } } diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/DetailFormModal.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/DetailFormModal.tsx index db38946f..68e23e2a 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/components/DetailFormModal.tsx +++ b/frontend/lib/registry/components/v2-process-work-standard/components/DetailFormModal.tsx @@ -270,14 +270,40 @@ export function DetailFormModal({ const submitData = { ...formData }; - // content 자동 설정 (UI에서 직접 입력이 없는 유형들) + // 검사항목 적용 → 품목검사정보 각각 개별 등록 if (type === "inspection" && submitData.process_inspection_apply === "apply") { + if (itemInspections.length > 0) { + for (const insp of itemInspections) { + onSubmit({ + ...submitData, + detail_type: "inspection", + content: `${insp.inspection_item_name || insp.inspection_standard || "-"} | ${insp.pass_criteria || ""}`.trim(), + is_required: submitData.is_required || "Y", + }); + } + onClose(); + return; + } submitData.content = submitData.content || "품목별 검사정보 (자동 연동)"; } if (type === "lookup") { submitData.content = submitData.content || "품목 등록 문서 (자동 연동)"; } + // 설비점검 적용 → 점검항목 각각 개별 등록 if (type === "equip_inspection" && submitData.equip_inspection_apply === "apply") { + if (equipInspItems.length > 0) { + for (const item of equipInspItems) { + const range = (item.lower_limit || item.upper_limit) ? `${item.lower_limit || ""} ~ ${item.upper_limit || ""}${item.unit ? ` ${item.unit}` : ""}` : ""; + onSubmit({ + ...submitData, + detail_type: "equip_inspection", + content: `${item.inspection_item || "-"}${range ? ` | ${range}` : ""}`.trim(), + is_required: submitData.is_required || "Y", + }); + } + onClose(); + return; + } submitData.content = submitData.content || "설비 점검항목 (설비정보 연동)"; } if (type === "production_result") { diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemDetailList.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemDetailList.tsx index de5b6ec1..ce869bf0 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemDetailList.tsx +++ b/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemDetailList.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useRef } from "react"; import { Plus, Pencil, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -34,6 +34,7 @@ export function WorkItemDetailList({ const [modalOpen, setModalOpen] = useState(false); const [modalMode, setModalMode] = useState<"add" | "edit">("add"); const [editTarget, setEditTarget] = useState(null); + const editFirstRef = useRef(false); if (!workItem) { return ( @@ -57,6 +58,7 @@ export function WorkItemDetailList({ const handleOpenEdit = (detail: WorkItemDetail) => { setModalMode("edit"); setEditTarget(detail); + editFirstRef.current = false; setModalOpen(true); }; @@ -64,14 +66,31 @@ export function WorkItemDetailList({ if (modalMode === "add") { onCreateDetail({ ...data, sort_order: details.length + 1 }); } else if (editTarget) { - onUpdateDetail(editTarget.id, data); + const isApplyGroup = editTarget.detail_type === "inspection" && editTarget.process_inspection_apply === "apply" + || editTarget.detail_type === "equip_inspection" && editTarget.equip_inspection_apply === "apply" + || editTarget.detail_type === "material_input"; + + if (isApplyGroup && data.is_required !== undefined) { + // 같은 유형의 apply 그룹 전체 일괄 업데이트 (필수여부 등) + const siblings = details.filter(d => + d.detail_type === editTarget.detail_type && + (d.detail_type === "inspection" ? d.process_inspection_apply === "apply" : + d.detail_type === "equip_inspection" ? d.equip_inspection_apply === "apply" : + d.detail_type === "material_input") + ); + for (const sib of siblings) { + onUpdateDetail(sib.id, { ...sib, is_required: data.is_required }); + } + } else { + onUpdateDetail(editTarget.id, data); + } } }; const getContentSummary = (detail: WorkItemDetail): string => { const type = detail.detail_type; if (type === "inspection") { - if (detail.process_inspection_apply === "apply") return "품목별 검사정보 (자동 연동)"; + if (detail.process_inspection_apply === "apply") return detail.content || "품목별 검사정보 (자동 연동)"; const parts = [detail.content]; if (detail.inspection_method) parts.push(`[${detail.inspection_method}]`); if (detail.base_value) { @@ -93,9 +112,7 @@ export function WorkItemDetailList({ } if (type === "lookup") return "품목 등록 문서 (자동 연동)"; if (type === "equip_inspection") { - return detail.equip_inspection_apply === "apply" - ? "설비 점검항목 (설비정보 연동)" - : detail.content || "설비점검"; + return detail.content || (detail.equip_inspection_apply === "apply" ? "설비 점검항목 (설비정보 연동)" : "설비점검"); } if (type === "equip_condition") { const parts = [detail.content];