From 5d12bef5e5db7501af7b3b26f45faafa4835834a Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 18 Mar 2026 18:26:54 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20MES=20=EC=B2=B4=ED=81=AC=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=90=EB=8F=99=20=EB=B3=B5=EC=82=AC=20?= =?UTF-8?q?+=20=EA=B5=AC=EC=A1=B0=EC=A0=81=20=EB=B2=84=EA=B7=B8=203?= =?UTF-8?q?=EA=B1=B4=20=EC=88=98=EC=A0=95=20=EB=B6=84=ED=95=A0=20=EC=A0=91?= =?UTF-8?q?=EC=88=98/=EC=9E=AC=EC=9E=91=EC=97=85=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EC=B2=B4=ED=81=AC=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EA=B0=80=20=EB=B3=B5=EC=82=AC=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=AC=B8=EC=A0=9C=EB=A5=BC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=ED=95=98=EA=B3=A0,=20=EA=B3=B5=EC=A0=95=20?= =?UTF-8?q?=ED=9D=90=EB=A6=84=20=EB=AA=A8=EB=8B=AC=EA=B3=BC=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=ED=81=B4=EB=A6=AD=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B2=A9=EB=A6=AC=20=EB=B2=84=EA=B7=B8=EB=A5=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=9C=EB=8B=A4.=20[=EC=B2=B4=ED=81=AC=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=90=EB=8F=99=20=EB=B3=B5=EC=82=AC]?= =?UTF-8?q?=20-=20copyChecklistToSplit=20=EA=B3=B5=ED=86=B5=20=ED=97=AC?= =?UTF-8?q?=ED=8D=BC=20=ED=95=A8=EC=88=98=20=EC=B6=94=EC=B6=9C=20=20=20(cr?= =?UTF-8?q?eateWorkProcesses/acceptProcess/saveResult=203=EA=B3=B3?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=86=B5=ED=95=A9=20=EC=82=AC=EC=9A=A9)?= =?UTF-8?q?=20-=20routing=5Fdetail=5Fid=20=EC=A1=B4=EC=9E=AC=20=EC=8B=9C:?= =?UTF-8?q?=20process=5Fwork=5Fitem=20=ED=85=9C=ED=94=8C=EB=A6=BF=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B3=B5=EC=82=AC=20-=20routing=5Fdetail=5Fid=20?= =?UTF-8?q?=EB=B6=80=EC=9E=AC=20=EC=8B=9C:=20=EB=A7=88=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=20process=5Fwork=5Fresult=EC=97=90=EC=84=9C=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=A7=8C=20=EB=B3=B5=EC=82=AC=20[ChecklistItem=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=88=98=EC=A0=95]=20-=20?= =?UTF-8?q?=EB=A0=88=EA=B1=B0=EC=8B=9C=20detail=5Ftype(inspect=5Fnumeric/i?= =?UTF-8?q?nspect=5Fox=20=EB=93=B1)=EC=9D=84=20=EC=A0=95=EA=B7=9C=ED=99=94?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=20=20detail=5Ftype=3Dinspect=20+=20input?= =?UTF-8?q?=5Ftype=20=EC=9E=90=EB=8F=99=20=ED=8C=8C=EC=8B=B1=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20InspectRouter=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20-=20i?= =?UTF-8?q?nfo=20=ED=83=80=EC=9E=85=20=EB=A0=8C=EB=8D=94=EB=9F=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20[=EA=B3=B5=EC=A0=95=20=ED=9D=90=EB=A6=84?= =?UTF-8?q?=20=EB=AA=A8=EB=8B=AC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EA=B2=A9?= =?UTF-8?q?=EB=A6=AC]=20-=20Dialog=20=EB=9E=98=ED=8D=BC=EC=97=90=20onClick?= =?UTF-8?q?/onPointerDown=20stopPropagation=20=EC=B6=94=EA=B0=80=20=20=20(?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EB=8B=AB=EA=B8=B0=20=EC=8B=9C=20=EB=B6=80?= =?UTF-8?q?=EB=AA=A8=20=EC=B9=B4=EB=93=9C=20onClick=20=EC=A0=84=ED=8C=8C?= =?UTF-8?q?=20=EC=B0=A8=EB=8B=A8)=20[=EC=B9=B4=EB=93=9C=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=A7=84=ED=96=89=20=ED=83=AD=20=EC=A0=9C=ED=95=9C?= =?UTF-8?q?]=20-=20handleCardSelect=EC=97=90=20VIRTUAL=5FSUB=5FSTATUS=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B0=80=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=20=20(in=5Fprogress=EA=B0=80=20=EC=95=84=EB=8B=8C=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=EB=8A=94=20=EC=9E=91=EC=97=85=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=97=B4=EB=A6=BC=20=EC=B0=A8=EB=8B=A8)?= =?UTF-8?q?=20-=20=EC=9E=AC=EC=9E=91=EC=97=85=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=20=EC=A0=91=EC=88=98=EA=B0=80=EB=8A=A5=20?= =?UTF-8?q?=ED=83=AD=EC=97=90=EC=84=9C=20=EC=83=81=EC=84=B8=20=EC=A7=84?= =?UTF-8?q?=EC=9E=85=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/popProductionController.ts | 133 +++++++++++++----- .../PopCardListV2Component.tsx | 13 +- .../pop-card-list-v2/cell-renderers.tsx | 3 + .../PopWorkDetailComponent.tsx | 22 ++- 4 files changed, 128 insertions(+), 43 deletions(-) diff --git a/backend-node/src/controllers/popProductionController.ts b/backend-node/src/controllers/popProductionController.ts index 0192aa35..205c6a59 100644 --- a/backend-node/src/controllers/popProductionController.ts +++ b/backend-node/src/controllers/popProductionController.ts @@ -11,6 +11,79 @@ interface DefectDetailItem { disposition: string; } +/** + * 체크리스트 복사 공통 함수 + * 분할 행/재작업 카드 생성 시 마스터의 체크리스트를 새 행에 복사한다. + * + * 전략: routingDetailId가 있으면 원본 템플릿에서, 없으면 마스터의 기존 결과에서 복사 + */ +async function copyChecklistToSplit( + 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 ( + company_code, work_order_process_id, + source_work_item_id, source_detail_id, + work_phase, item_title, item_sort_order, + detail_content, detail_type, detail_sort_order, is_required, + inspection_code, inspection_method, unit, lower_limit, upper_limit, + input_type, lookup_target, display_fields, duration_minutes, + status, writer + ) + SELECT + pwi.company_code, $1, + pwi.id, pwd.id, + pwi.work_phase, pwi.title, pwi.sort_order::text, + pwd.content, pwd.detail_type, pwd.sort_order::text, pwd.is_required, + pwd.inspection_code, pwd.inspection_method, pwd.unit, pwd.lower_limit, pwd.upper_limit, + pwd.input_type, pwd.lookup_target, pwd.display_fields, pwd.duration_minutes::text, + 'pending', $2 + FROM process_work_item pwi + JOIN process_work_item_detail pwd ON pwd.work_item_id = pwi.id + AND pwd.company_code = pwi.company_code + WHERE pwi.routing_detail_id = $3 + AND pwi.company_code = $4 + ORDER BY pwi.sort_order, pwd.sort_order`, + [newProcessId, userId, routingDetailId, companyCode] + ); + return result.rowCount ?? 0; + } + + // B. routing_detail_id가 없으면 마스터 행의 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, + detail_content, detail_type, detail_sort_order, is_required, + inspection_code, inspection_method, unit, lower_limit, upper_limit, + input_type, lookup_target, display_fields, duration_minutes, + status, writer + ) + SELECT + company_code, $1, + source_work_item_id, source_detail_id, + work_phase, item_title, item_sort_order, + detail_content, detail_type, detail_sort_order, is_required, + inspection_code, inspection_method, unit, lower_limit, upper_limit, + input_type, lookup_target, display_fields, duration_minutes, + 'pending', $2 + FROM process_work_result + 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; +} + /** * D-BE1: 작업지시 공정 일괄 생성 * PC에서 작업지시 생성 후 호출. 1 트랜잭션으로 work_order_process + process_work_result 일괄 생성. @@ -117,36 +190,10 @@ export const createWorkProcesses = async ( ); const wopId = wopResult.rows[0].id; - // 3. process_work_result INSERT (스냅샷 복사) - // process_work_item + process_work_item_detail에서 해당 routing_detail의 항목 조회 후 복사 - const snapshotResult = 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, - detail_content, detail_type, detail_sort_order, is_required, - inspection_code, inspection_method, unit, lower_limit, upper_limit, - input_type, lookup_target, display_fields, duration_minutes, - status, writer - ) - SELECT - pwi.company_code, $1, - pwi.id, pwd.id, - pwi.work_phase, pwi.title, pwi.sort_order::text, - pwd.content, pwd.detail_type, pwd.sort_order::text, pwd.is_required, - pwd.inspection_code, pwd.inspection_method, pwd.unit, pwd.lower_limit, pwd.upper_limit, - pwd.input_type, pwd.lookup_target, pwd.display_fields, pwd.duration_minutes::text, - 'pending', $2 - FROM process_work_item pwi - JOIN process_work_item_detail pwd ON pwd.work_item_id = pwi.id - AND pwd.company_code = pwi.company_code - WHERE pwi.routing_detail_id = $3 - AND pwi.company_code = $4 - ORDER BY pwi.sort_order, pwd.sort_order`, - [wopId, userId, rd.id, companyCode] + // 3. process_work_result INSERT (공통 함수로 체크리스트 복사) + const checklistCount = await copyChecklistToSplit( + client, wopId, wopId, rd.id, companyCode, userId ); - - const checklistCount = snapshotResult.rowCount ?? 0; totalChecklists += checklistCount; processes.push({ @@ -750,11 +797,19 @@ export const saveResult = async ( masterId, companyCode, userId, ] ); - logger.info("[pop/production] 재작업 카드 자동 생성", { - reworkId: reworkInsert.rows[0]?.id, - sourceId: work_order_process_id, - reworkQty: totalReworkQty, - }); + // 재작업 카드에 체크리스트 복사 + const reworkId = reworkInsert.rows[0]?.id; + if (reworkId) { + const reworkChecklistCount = await copyChecklistToSplit( + pool, masterId, reworkId, proc.routing_detail_id, companyCode, userId + ); + logger.info("[pop/production] 재작업 카드 자동 생성", { + reworkId, + sourceId: work_order_process_id, + reworkQty: totalReworkQty, + checklistCount: reworkChecklistCount, + }); + } } } @@ -1406,16 +1461,22 @@ export const acceptProcess = async (req: AuthenticatedRequest, res: Response) => ] ); + // 분할 행에 체크리스트 복사 (마스터의 routing_detail_id 또는 마스터의 기존 체크리스트에서) + const splitId = result.rows[0].id; + const checklistCount = await copyChecklistToSplit( + pool, masterId, splitId, row.routing_detail_id, companyCode, userId + ); + // 마스터는 항상 acceptable 유지 (completed 전환은 saveResult에서 처리) - // availableQty=0이면 프론트에서 접수 버튼이 숨겨지므로 상태 변경 불필요 const newTotalInput = currentTotalInput + qty; logger.info("[pop/production] accept-process 분할 접수 완료", { companyCode, userId, masterId, - splitId: result.rows[0].id, + splitId, acceptedQty: qty, totalAccepted: newTotalInput, prevGoodQty, + checklistCount, }); return res.json({ diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx index f3349cbb..8a4e37bc 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx @@ -278,17 +278,22 @@ export function PopCardListV2Component({ }, [publish, setSharedData]); const handleCardSelect = useCallback((row: RowData) => { - // 복제 카드(접수가능 가상)는 클릭 시 모달을 열지 않음 - 접수 버튼으로만 동작 if (row.__isAcceptClone) return; if (effectiveConfig?.cardClickAction === "modal-open" && effectiveConfig?.cardClickModalConfig?.screenId) { const mc = effectiveConfig.cardClickModalConfig; + + // 작업상세는 "진행(in_progress)" 탭 카드만 열 수 있음 + const subStatus = row[VIRTUAL_SUB_STATUS] as string | undefined; + if (subStatus && subStatus !== "in_progress") return; + if (mc.condition && mc.condition.type !== "always") { - const processFlow = row.__processFlow__ as { isCurrent: boolean; status?: string }[] | undefined; - const currentProcess = processFlow?.find((s) => s.isCurrent); if (mc.condition.type === "timeline-status") { const condVal = mc.condition.value; - const curStatus = currentProcess?.status; + const curStatus = subStatus || (() => { + const pf = row.__processFlow__ as { isCurrent: boolean; status?: string }[] | undefined; + return pf?.find((s) => s.isCurrent)?.status; + })(); if (Array.isArray(condVal)) { if (!curStatus || !condVal.includes(curStatus)) return; } else { diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx index da8889cb..909b1811 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx @@ -522,6 +522,8 @@ function TimelineCell({ cell, row }: CellRendererProps) { })} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} +
e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}> @@ -595,6 +597,7 @@ function TimelineCell({ cell, row }: CellRendererProps) {
+ ); } diff --git a/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx b/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx index 116ac3eb..2bf9a517 100644 --- a/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx +++ b/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx @@ -1690,12 +1690,22 @@ interface ChecklistItemProps { function ChecklistItem({ item, saving, disabled, onSave }: ChecklistItemProps) { const isDisabled = disabled || saving; + const dt = item.detail_type ?? ""; - switch (item.detail_type) { + // "inspect_numeric" 등 레거시 형식 → "inspect"로 정규화, 접미사를 input_type 폴백으로 사용 + if (dt.startsWith("inspect")) { + const normalized = { ...item, detail_type: "inspect" } as WorkResultRow; + if (!normalized.input_type && dt.includes("_")) { + const suffix = dt.split("_").slice(1).join("_"); + const typeMap: Record = { numeric: "numeric_range", ox: "ox", text: "text", select: "select" }; + normalized.input_type = typeMap[suffix] ?? suffix; + } + return ; + } + + switch (dt) { case "check": return ; - case "inspect": - return ; case "input": return ; case "procedure": @@ -1704,6 +1714,12 @@ function ChecklistItem({ item, saving, disabled, onSave }: ChecklistItemProps) { return ; case "result": return ; + case "info": + return ( +
+ {item.detail_label || item.detail_content} +
+ ); default: return (