From 973c3a901857bc904dbfc06e2502c893c58b3441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 11 Feb 2026 09:51:38 +0900 Subject: [PATCH] =?UTF-8?q?feat(WEB):=20=EC=9E=91=EC=97=85=EC=9D=BC?= =?UTF-8?q?=EC=A7=80/=EA=B2=80=EC=82=AC=EC=84=B1=EC=A0=81=EC=84=9C=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EA=B3=B5=EC=A0=95=20=EB=A0=88=EB=B2=A8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Process 타입에 documentTemplateId, needsWorkLog 필드 추가 (ProcessStep에서 이동) - ProcessForm에 중간검사 양식/작업일지 설정 UI 추가 - StepForm에서 해당 UI 제거 - WorkerScreen 하단 버튼 조건부 렌더링: needsWorkLog/documentTemplateId 기반 --- .../process-management/ProcessForm.tsx | 93 +++++++++++++- .../process-management/StepForm.tsx | 52 +------- src/components/process-management/actions.ts | 23 ++-- .../production/WorkerScreen/index.tsx | 114 +++++++++++++----- src/types/process.ts | 14 ++- 5 files changed, 200 insertions(+), 96 deletions(-) diff --git a/src/components/process-management/ProcessForm.tsx b/src/components/process-management/ProcessForm.tsx index 99dcfd1b..82883f6b 100644 --- a/src/components/process-management/ProcessForm.tsx +++ b/src/components/process-management/ProcessForm.tsx @@ -37,7 +37,9 @@ import { updateProcess, getDepartmentOptions, getProcessSteps, + getDocumentTemplates, type DepartmentOption, + type DocumentTemplateOption, } from './actions'; interface ProcessFormProps { @@ -67,6 +69,18 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) { ); const [isLoading, setIsLoading] = useState(false); + // 중간검사/작업일지 설정 (Process 레벨) + const [documentTemplateId, setDocumentTemplateId] = useState( + initialData?.documentTemplateId + ); + const [needsWorkLog, setNeedsWorkLog] = useState( + initialData?.needsWorkLog ?? false + ); + const [workLogTemplateId, setWorkLogTemplateId] = useState( + initialData?.workLogTemplateId + ); + const [documentTemplates, setDocumentTemplates] = useState([]); + // 품목 분류 규칙 (기존 로직 유지) const [classificationRules, setClassificationRules] = useState( initialData?.classificationRules || [] @@ -124,15 +138,21 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) { ); }, []); - // 부서 목록 + 단계 목록 로드 + // 부서 목록 + 문서양식 목록 로드 useEffect(() => { - const loadDepartments = async () => { + const loadInitialData = async () => { setIsDepartmentsLoading(true); - const departments = await getDepartmentOptions(); + const [departments, templates] = await Promise.all([ + getDepartmentOptions(), + getDocumentTemplates(), + ]); setDepartmentOptions(departments); + if (templates.success && templates.data) { + setDocumentTemplates(templates.data); + } setIsDepartmentsLoading(false); }; - loadDepartments(); + loadInitialData(); }, []); useEffect(() => { @@ -300,6 +320,9 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) { processType, processCategory: processCategory || undefined, department, + documentTemplateId: documentTemplateId || undefined, + needsWorkLog, + workLogTemplateId: needsWorkLog ? workLogTemplateId : undefined, classificationRules: classificationRules.map((rule) => ({ registrationType: rule.registrationType, ruleType: rule.ruleType, @@ -463,6 +486,64 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) { + {/* Row 3: 중간검사양식 | 작업일지여부 | 작업일지양식 */} +
+
+ + +
+
+ + +
+ {needsWorkLog && ( +
+ + +
+ )} +
@@ -679,6 +760,10 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) { manager, useProductionDate, isActive, + documentTemplateId, + needsWorkLog, + workLogTemplateId, + documentTemplates, classificationRules, steps, isStepsLoading, diff --git a/src/components/process-management/StepForm.tsx b/src/components/process-management/StepForm.tsx index e9c9267e..7f69e8ff 100644 --- a/src/components/process-management/StepForm.tsx +++ b/src/components/process-management/StepForm.tsx @@ -9,7 +9,7 @@ * - 완료 정보: 유형(선택 완료 시 완료/클릭 시 완료) */ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; @@ -37,8 +37,7 @@ import { STEP_CONNECTION_TARGET_OPTIONS, DEFAULT_INSPECTION_SETTING, } from '@/types/process'; -import { createProcessStep, updateProcessStep, getDocumentTemplates } from './actions'; -import type { DocumentTemplateOption } from './actions'; +import { createProcessStep, updateProcessStep } from './actions'; import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types'; import { InspectionSettingModal } from './InspectionSettingModal'; import { InspectionPreviewModal } from './InspectionPreviewModal'; @@ -105,12 +104,6 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { initialData?.completionType || '클릭 시 완료' ); - // 문서양식 선택 - const [documentTemplateId, setDocumentTemplateId] = useState( - initialData?.documentTemplateId - ); - const [documentTemplates, setDocumentTemplates] = useState([]); - // 검사 설정 const [inspectionSetting, setInspectionSetting] = useState( initialData?.inspectionSetting || DEFAULT_INSPECTION_SETTING @@ -125,17 +118,6 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { // 검사여부가 "사용"인지 확인 const isInspectionEnabled = needsInspection === '사용'; - // 검사여부가 "사용"이면 문서양식 목록 조회 - useEffect(() => { - if (isInspectionEnabled && documentTemplates.length === 0) { - getDocumentTemplates().then((result) => { - if (result.success && result.data) { - setDocumentTemplates(result.data); - } - }); - } - }, [isInspectionEnabled, documentTemplates.length]); - // 제출 const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => { if (!stepName.trim()) { @@ -149,7 +131,6 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { isRequired: isRequired === '필수', needsApproval: needsApproval === '필요', needsInspection: needsInspection === '사용', - documentTemplateId: isInspectionEnabled ? documentTemplateId : undefined, isActive: isActive === '사용', order: initialData?.order || 0, connectionType, @@ -269,33 +250,6 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { - {/* 문서양식 선택 (검사여부가 "사용"일 때만 표시) */} - {isInspectionEnabled && ( -
-
- - -

- 중간검사 시 사용할 문서양식을 선택합니다. MNG에서 등록한 양식이 표시됩니다. -

-
-
- )} @@ -384,8 +338,6 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { completionType, initialData?.stepCode, isInspectionEnabled, - documentTemplateId, - documentTemplates, ] ); diff --git a/src/components/process-management/actions.ts b/src/components/process-management/actions.ts index 64845f0b..212e7137 100644 --- a/src/components/process-management/actions.ts +++ b/src/components/process-management/actions.ts @@ -21,6 +21,11 @@ interface ApiProcess { process_category: string | null; use_production_date: boolean; work_log_template: string | null; + document_template_id: number | null; + document_template?: { id: number; name: string; category: string } | null; + needs_work_log: boolean; + work_log_template_id: number | null; + work_log_template_relation?: { id: number; name: string; category: string } | null; required_workers: number; equipment_info: string | null; work_steps: string[] | null; @@ -88,6 +93,11 @@ function transformApiToFrontend(apiData: ApiProcess): Process { processCategory: apiData.process_category ?? undefined, useProductionDate: apiData.use_production_date ?? false, workLogTemplate: apiData.work_log_template ?? undefined, + documentTemplateId: apiData.document_template_id ?? undefined, + documentTemplateName: apiData.document_template?.name ?? undefined, + needsWorkLog: apiData.needs_work_log ?? false, + workLogTemplateId: apiData.work_log_template_id ?? undefined, + workLogTemplateName: apiData.work_log_template_relation?.name ?? undefined, classificationRules: [...patternRules, ...individualRules], requiredWorkers: apiData.required_workers, equipmentInfo: apiData.equipment_info ?? undefined, @@ -179,6 +189,9 @@ function transformFrontendToApi(data: ProcessFormData): Record process_category: data.processCategory || null, use_production_date: data.useProductionDate ?? false, work_log_template: data.workLogTemplate || null, + document_template_id: data.documentTemplateId || null, + needs_work_log: data.needsWorkLog ?? false, + work_log_template_id: data.workLogTemplateId || null, required_workers: data.requiredWorkers, equipment_info: data.equipmentInfo || null, work_steps: data.workSteps ? data.workSteps.split(',').map((s) => s.trim()).filter(Boolean) : [], @@ -567,12 +580,6 @@ interface ApiProcessStep { is_required: boolean; needs_approval: boolean; needs_inspection: boolean; - document_template_id: number | null; - document_template?: { - id: number; - name: string; - category: string; - } | null; is_active: boolean; sort_order: number; connection_type: string | null; @@ -590,8 +597,6 @@ function transformStepApiToFrontend(apiStep: ApiProcessStep): ProcessStep { isRequired: apiStep.is_required, needsApproval: apiStep.needs_approval, needsInspection: apiStep.needs_inspection, - documentTemplateId: apiStep.document_template_id ?? undefined, - documentTemplateName: apiStep.document_template?.name ?? undefined, isActive: apiStep.is_active, order: apiStep.sort_order, connectionType: (apiStep.connection_type as ProcessStep['connectionType']) || '없음', @@ -647,7 +652,6 @@ export async function createProcessStep( is_required: data.isRequired, needs_approval: data.needsApproval, needs_inspection: data.needsInspection, - document_template_id: data.documentTemplateId || null, is_active: data.isActive, connection_type: data.connectionType || null, connection_target: data.connectionTarget || null, @@ -672,7 +676,6 @@ export async function updateProcessStep( if (data.isRequired !== undefined) apiData.is_required = data.isRequired; if (data.needsApproval !== undefined) apiData.needs_approval = data.needsApproval; if (data.needsInspection !== undefined) apiData.needs_inspection = data.needsInspection; - if (data.documentTemplateId !== undefined) apiData.document_template_id = data.documentTemplateId || null; if (data.isActive !== undefined) apiData.is_active = data.isActive; if (data.connectionType !== undefined) apiData.connection_type = data.connectionType || null; if (data.connectionTarget !== undefined) apiData.connection_target = data.connectionTarget || null; diff --git a/src/components/production/WorkerScreen/index.tsx b/src/components/production/WorkerScreen/index.tsx index 96c5d655..d7f1430a 100644 --- a/src/components/production/WorkerScreen/index.tsx +++ b/src/components/production/WorkerScreen/index.tsx @@ -366,6 +366,7 @@ export default function WorkerScreen() { const [currentInspectionSetting, setCurrentInspectionSetting] = useState(); // 문서 템플릿 데이터 (document_template 기반 동적 검사용) const [inspectionTemplateData, setInspectionTemplateData] = useState(); + const [inspectionDimensions, setInspectionDimensions] = useState<{ width?: number; height?: number }>({}); // 중간검사 체크 상태 관리: { [itemId]: boolean } const [inspectionCheckedMap, setInspectionCheckedMap] = useState>({}); @@ -432,6 +433,15 @@ export default function WorkerScreen() { return 'screen'; }, [activeTab, processListCache]); + // 선택된 공정의 작업일지/검사성적서 설정 + const activeProcessSettings = useMemo(() => { + const process = processListCache.find((p) => p.id === activeTab); + return { + needsWorkLog: process?.needsWorkLog ?? false, + hasDocumentTemplate: !!process?.documentTemplateId, + }; + }, [activeTab, processListCache]); + // activeTab 변경 시 해당 공정의 중간검사 설정 조회 useEffect(() => { if (processListCache.length === 0 || !activeTab) return; @@ -570,22 +580,60 @@ export default function WorkerScreen() { const itemNames = group.items.map((it) => it.itemName).filter(Boolean); const itemSummary = itemNames.length > 0 ? itemNames.join(', ') : '-'; - apiItems.push({ + // 첫 번째 아이템의 options에서 사이즈/공정별 정보 추출 + const firstItem = group.items[0]; + const opts = (firstItem?.options || {}) as Record; + + const workItem: WorkItemData = { id: `${selectedOrder.id}-node-${nodeKey}`, - apiItemId: group.items[0]?.id as number | undefined, + apiItemId: firstItem?.id as number | undefined, workOrderId: selectedOrder.id, itemNo: index + 1, itemCode: selectedOrder.orderNo || '-', itemName: `${group.nodeName} : ${itemSummary}`, - floor: '-', - code: '-', - width: 0, - height: 0, + floor: (opts.floor as string) || '-', + code: (opts.code as string) || '-', + width: (opts.width as number) || 0, + height: (opts.height as number) || 0, quantity: group.totalQuantity, processType: activeProcessTabKey, steps, materialInputs: [], - }); + }; + + // 공정별 추가 정보 추출 + if (opts.cutting_info) { + const ci = opts.cutting_info as { width: number; sheets: number }; + workItem.cuttingInfo = { width: ci.width, sheets: ci.sheets }; + } + if (opts.slat_info) { + const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number }; + workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar }; + } + if (opts.bending_info) { + const bi = opts.bending_info as { + common: { kind: string; type: string; length_quantities: { length: number; quantity: number }[] }; + detail_parts: { part_name: string; material: string; barcy_info: string }[]; + }; + workItem.bendingInfo = { + common: { kind: bi.common.kind, type: bi.common.type, lengthQuantities: bi.common.length_quantities || [] }, + detailParts: (bi.detail_parts || []).map(dp => ({ partName: dp.part_name, material: dp.material, barcyInfo: dp.barcy_info })), + }; + } + if (opts.is_wip) { + workItem.isWip = true; + const wi = opts.wip_info as { specification: string; length_quantity: string; drawing_url?: string } | undefined; + if (wi) { + workItem.wipInfo = { specification: wi.specification, lengthQuantity: wi.length_quantity, drawingUrl: wi.drawing_url }; + } + } + if (opts.is_joint_bar) { + workItem.isJointBar = true; + const jb = opts.slat_joint_bar_info as { specification: string; length: number; quantity: number } | undefined; + if (jb) workItem.slatJointBarInfo = jb; + } + + apiItems.push(workItem); }); } else if (selectedOrder) { // nodeGroups가 없는 경우 폴백 (단일 항목) @@ -868,6 +916,7 @@ export default function WorkerScreen() { createdAt: '', }; setSelectedOrder(syntheticOrder); + setInspectionDimensions({ width: item.width, height: item.height }); // 실제 API 아이템인 경우 검사 템플릿 로딩 시도 if (item.workOrderId && !item.id.startsWith('mock-')) { @@ -1238,34 +1287,40 @@ export default function WorkerScreen() { {/* 하단 고정 버튼 */} -
-
- {hasWipItems ? ( - - ) : ( - <> - + {(hasWipItems || activeProcessSettings.needsWorkLog || activeProcessSettings.hasDocumentTemplate) && ( +
+
+ {hasWipItems ? ( - - )} + ) : ( + <> + {activeProcessSettings.needsWorkLog && ( + + )} + {activeProcessSettings.hasDocumentTemplate && ( + + )} + + )} +
-
+ )} {/* 모달/다이얼로그 */} ); diff --git a/src/types/process.ts b/src/types/process.ts index 426f9c72..87e56e2d 100644 --- a/src/types/process.ts +++ b/src/types/process.ts @@ -57,7 +57,14 @@ export interface Process { description?: string; // 공정 설명 (테이블에 표시) processType: ProcessType; // 생산, 검사 등 department: string; // 담당부서 - workLogTemplate?: string; // 작업일지 양식 + workLogTemplate?: string; // 작업일지 양식 (레거시 string) + + // 중간검사/작업일지 설정 (Process 레벨) + documentTemplateId?: number; // 중간검사 양식 ID + documentTemplateName?: string; // 중간검사 양식명 (표시용) + needsWorkLog: boolean; // 작업일지 여부 + workLogTemplateId?: number; // 작업일지 양식 ID + workLogTemplateName?: string; // 작업일지 양식명 (표시용) // 자동 분류 규칙 classificationRules: ClassificationRule[]; @@ -99,6 +106,9 @@ export interface ProcessFormData { processCategory?: string; useProductionDate?: boolean; workLogTemplate?: string; + documentTemplateId?: number; + needsWorkLog: boolean; + workLogTemplateId?: number; classificationRules: ClassificationRuleInput[]; requiredWorkers: number; equipmentInfo?: string; @@ -169,8 +179,6 @@ export interface ProcessStep { isRequired: boolean; // 필수여부 needsApproval: boolean; // 승인여부 needsInspection: boolean; // 검사여부 - documentTemplateId?: number; // 문서양식 ID (검사 시 사용할 템플릿) - documentTemplateName?: string; // 문서양식명 (표시용) isActive: boolean; // 사용여부 order: number; // 순서 (드래그&드롭) // 연결 정보