From 12a423051a2200cb54570e1c48cf8f97f0a78d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Tue, 10 Feb 2026 08:36:12 +0900 Subject: [PATCH] =?UTF-8?q?feat(WEB):=20=EC=A4=91=EA=B0=84=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=EB=AC=B8=EC=84=9C=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EB=8F=99=EC=A0=81=20=EC=97=B0=EB=8F=99=20-=20=EA=B3=B5?= =?UTF-8?q?=EC=A0=95=EA=B4=80=EB=A6=AC=20=EC=84=A0=ED=83=9D=EA=B8=B0=20+?= =?UTF-8?q?=20Worker=20Screen=20=EB=8F=99=EC=A0=81=20=ED=8F=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProcessStep 타입에 documentTemplateId/documentTemplateName 추가 - 공정관리 actions.ts: document_template_id 매핑 + getDocumentTemplates 서버 액션 - StepForm: 검사여부 사용 시 문서양식 선택 드롭다운 추가 - WorkerScreen actions.ts: getInspectionTemplate, saveInspectionDocument 서버 액션 추가 - InspectionInputModal: tolerance 기반 자동 판정 + 동적 폼(DynamicInspectionForm) 추가 - evaluateTolerance: symmetric/asymmetric/range 3가지 tolerance 판정 - 기존 공정별 하드코딩은 템플릿 없을 때 레거시 모드로 유지 - InspectionReportModal: 템플릿 모드 동적 렌더링 (기준서/DATA/결재라인) - WorkerScreen index: handleInspectionComplete에서 Document 저장 호출 추가 --- .../process-management/StepForm.tsx | 52 ++++- src/components/process-management/actions.ts | 44 ++++ .../documents/InspectionReportModal.tsx | 139 +++++++++++- .../WorkerScreen/InspectionInputModal.tsx | 212 +++++++++++++++++- .../production/WorkerScreen/actions.ts | 100 +++++++++ .../production/WorkerScreen/index.tsx | 38 +++- src/types/process.ts | 2 + 7 files changed, 574 insertions(+), 13 deletions(-) diff --git a/src/components/process-management/StepForm.tsx b/src/components/process-management/StepForm.tsx index 7f69e8ff..e9c9267e 100644 --- a/src/components/process-management/StepForm.tsx +++ b/src/components/process-management/StepForm.tsx @@ -9,7 +9,7 @@ * - 완료 정보: 유형(선택 완료 시 완료/클릭 시 완료) */ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; @@ -37,7 +37,8 @@ import { STEP_CONNECTION_TARGET_OPTIONS, DEFAULT_INSPECTION_SETTING, } from '@/types/process'; -import { createProcessStep, updateProcessStep } from './actions'; +import { createProcessStep, updateProcessStep, getDocumentTemplates } from './actions'; +import type { DocumentTemplateOption } from './actions'; import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types'; import { InspectionSettingModal } from './InspectionSettingModal'; import { InspectionPreviewModal } from './InspectionPreviewModal'; @@ -104,6 +105,12 @@ 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 @@ -118,6 +125,17 @@ 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()) { @@ -131,6 +149,7 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { isRequired: isRequired === '필수', needsApproval: needsApproval === '필요', needsInspection: needsInspection === '사용', + documentTemplateId: isInspectionEnabled ? documentTemplateId : undefined, isActive: isActive === '사용', order: initialData?.order || 0, connectionType, @@ -250,6 +269,33 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { + {/* 문서양식 선택 (검사여부가 "사용"일 때만 표시) */} + {isInspectionEnabled && ( +
+
+ + +

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

+
+
+ )} @@ -338,6 +384,8 @@ 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 ba1645e3..28d33987 100644 --- a/src/components/process-management/actions.ts +++ b/src/components/process-management/actions.ts @@ -519,6 +519,40 @@ export async function getItemTypeOptions(): Promise ({ value: item.code, label: item.name })); } +// ============================================================================ +// 문서 양식 (Document Template) API +// ============================================================================ + +export interface DocumentTemplateOption { + id: number; + name: string; + category: string; +} + +/** + * 문서 양식 목록 조회 (드롭다운용) + */ +export async function getDocumentTemplates(): Promise<{ + success: boolean; + data?: DocumentTemplateOption[]; + error?: string; +}> { + interface ApiTemplateItem { id: number; name: string; category: string } + const result = await executeServerAction<{ data: ApiTemplateItem[] }>({ + url: `${API_URL}/api/v1/document-templates?is_active=1&per_page=100`, + errorMessage: '문서 양식 목록 조회에 실패했습니다.', + }); + if (!result.success || !result.data?.data) return { success: false, error: result.error }; + return { + success: true, + data: result.data.data.map((item) => ({ + id: item.id, + name: item.name, + category: item.category, + })), + }; +} + // ============================================================================ // 공정 단계 (Process Step) API // ============================================================================ @@ -531,6 +565,12 @@ 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; @@ -548,6 +588,8 @@ 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']) || '없음', @@ -603,6 +645,7 @@ 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, @@ -627,6 +670,7 @@ 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/WorkOrders/documents/InspectionReportModal.tsx b/src/components/production/WorkOrders/documents/InspectionReportModal.tsx index 1051f0cc..4d3423d3 100644 --- a/src/components/production/WorkOrders/documents/InspectionReportModal.tsx +++ b/src/components/production/WorkOrders/documents/InspectionReportModal.tsx @@ -28,6 +28,7 @@ import type { InspectionContentRef } from './ScreenInspectionContent'; import type { InspectionData } from '@/components/production/WorkerScreen/InspectionInputModal'; import type { WorkItemData } from '@/components/production/WorkerScreen/types'; import type { InspectionSetting } from '@/types/process'; +import type { InspectionTemplateData } from '@/components/production/WorkerScreen/actions'; const PROCESS_LABELS: Record = { screen: '스크린', @@ -53,6 +54,8 @@ interface InspectionReportModalProps { inspectionDataMap?: InspectionDataMap; /** 중간검사 설정 - 도해/검사기준 이미지 표시용 */ inspectionSetting?: InspectionSetting; + /** 문서 템플릿 데이터 (있으면 동적 렌더링 모드) */ + templateData?: InspectionTemplateData; } /** @@ -109,6 +112,7 @@ export function InspectionReportModal({ workItems: propWorkItems, inspectionDataMap: propInspectionDataMap, inspectionSetting, + templateData, }: InspectionReportModalProps) { const [order, setOrder] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -258,10 +262,143 @@ export function InspectionReportModal({ ? '중간검사성적서 (조인트바)' : '중간검사 성적서'; + // 템플릿 기반 동적 렌더링 여부 + const useTemplateMode = !!(templateData?.has_template && templateData.template); + const renderContent = () => { if (!order) return null; - // 공통 props + // 템플릿 모드: 동적 렌더링 + if (useTemplateMode && templateData?.template) { + const tpl = templateData.template; + return ( +
+ {/* 기본 정보 */} +
+

기본 정보

+ + + {tpl.basic_fields?.map((field) => ( + + + + + ))} + +
{field.name} + {field.field_key === 'product_name' ? order.items?.[0]?.productName || '-' : + field.field_key === 'lot_no' ? (order.lotNo || '-') : + field.field_key === 'quantity' ? String(order.items?.reduce((sum, i) => sum + (i.quantity || 0), 0) || 0) : + '-'} +
+
+ + {/* 검사 기준서 (sections) */} + {tpl.sections?.map((section) => ( +
+

{section.name}

+ + + + + + + + + + + + {section.items?.map((item, idx) => ( + + + + + + + + ))} + +
No검사항목기준허용오차방법
{idx + 1}{item.name}{item.standard_criteria || '-'} + {item.tolerance ? ( + item.tolerance.type === 'symmetric' ? `± ${item.tolerance.value}` : + item.tolerance.type === 'asymmetric' ? `+${item.tolerance.plus} / -${item.tolerance.minus}` : + item.tolerance.type === 'range' ? `${item.tolerance.min} ~ ${item.tolerance.max}` : '-' + ) : '-'} + {item.measurement_type || '-'}
+
+ ))} + + {/* 검사 DATA (columns) */} + {tpl.columns && tpl.columns.length > 0 && ( +
+

검사 DATA

+ + + + + {tpl.columns.map((col) => ( + + ))} + + + + + {(effectiveWorkItems || []).map((item, idx) => { + const itemData = effectiveInspectionDataMap?.get(item.id); + return ( + + + {tpl.columns.map((col) => ( + + ))} + + + ); + })} + +
No{col.name}판정
{idx + 1} + {itemData?.templateValues + ? String(itemData.templateValues[`col_${col.id}`] ?? '-') + : '-'} + + {itemData?.judgment === 'pass' ? ( + 적합 + ) : itemData?.judgment === 'fail' ? ( + 부적합 + ) : '-'} +
+
+ )} + + {/* 결재라인 */} + {tpl.approval_lines && tpl.approval_lines.length > 0 && ( +
+

결재

+ + + + {tpl.approval_lines.map((line) => ( + + ))} + + + + + {tpl.approval_lines.map((line) => ( + + ))} + + +
{line.role_name}
+ (서명) +
+
+ )} +
+ ); + } + + // 레거시 모드: 공정별 하드코딩 컴포넌트 const commonProps = { ref: contentRef, data: order, diff --git a/src/components/production/WorkerScreen/InspectionInputModal.tsx b/src/components/production/WorkerScreen/InspectionInputModal.tsx index 73d3d02b..0a6872d3 100644 --- a/src/components/production/WorkerScreen/InspectionInputModal.tsx +++ b/src/components/production/WorkerScreen/InspectionInputModal.tsx @@ -22,6 +22,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { cn } from '@/lib/utils'; +import type { InspectionTemplateData } from './actions'; // 중간검사 공정 타입 export type InspectionProcessType = @@ -53,6 +54,8 @@ export interface InspectionData { // 판정 judgment: 'pass' | 'fail' | null; nonConformingContent: string; + // 동적 폼 값 (템플릿 기반 검사 시) + templateValues?: Record; } interface InspectionInputModalProps { @@ -63,6 +66,8 @@ interface InspectionInputModalProps { specification?: string; initialData?: InspectionData; onComplete: (data: InspectionData) => void; + /** 문서 템플릿 데이터 (있으면 동적 폼 모드) */ + templateData?: InspectionTemplateData; } const PROCESS_TITLES: Record = { @@ -224,6 +229,171 @@ function computeJudgment(processType: InspectionProcessType, data: InspectionDat } } +// ===== Tolerance 기반 판정 유틸 ===== +type ToleranceConfig = NonNullable['sections'][number]['items'][number]['tolerance']>; + +function evaluateTolerance(measured: number, design: number, tolerance: ToleranceConfig): 'pass' | 'fail' { + switch (tolerance.type) { + case 'symmetric': + return Math.abs(measured - design) <= (tolerance.value ?? 0) ? 'pass' : 'fail'; + case 'asymmetric': + return (measured >= design - (tolerance.minus ?? 0) && measured <= design + (tolerance.plus ?? 0)) ? 'pass' : 'fail'; + case 'range': + return (measured >= (tolerance.min ?? -Infinity) && measured <= (tolerance.max ?? Infinity)) ? 'pass' : 'fail'; + default: + return 'pass'; + } +} + +function formatToleranceLabel(tolerance: ToleranceConfig): string { + switch (tolerance.type) { + case 'symmetric': + return `± ${tolerance.value}`; + case 'asymmetric': + return `+${tolerance.plus} / -${tolerance.minus}`; + case 'range': + return `${tolerance.min} ~ ${tolerance.max}`; + default: + return ''; + } +} + +// ===== 동적 폼 (템플릿 기반) ===== +function DynamicInspectionForm({ + template, + formValues, + onValueChange, +}: { + template: NonNullable; + formValues: Record; + onValueChange: (key: string, value: unknown) => void; +}) { + return ( +
+ {template.sections.map((section) => ( +
+ {section.name} + {section.items.map((item) => { + const fieldKey = `section_${section.id}_item_${item.id}`; + const value = formValues[fieldKey]; + + if (item.measurement_type === 'binary' || item.type === 'boolean') { + // 양호/불량 토글 + return ( +
+ {item.name} +
+ + +
+
+ ); + } + + // 숫자 입력 (치수 등) + const toleranceLabel = item.tolerance ? ` (${formatToleranceLabel(item.tolerance)})` : ''; + const numValue = value as number | null | undefined; + // 판정 표시 + let itemJudgment: 'pass' | 'fail' | null = null; + if (item.tolerance && numValue != null && item.standard_criteria) { + const design = parseFloat(item.standard_criteria); + if (!isNaN(design)) { + itemJudgment = evaluateTolerance(numValue, design, item.tolerance); + } + } + + return ( +
+
+ + {item.name}{toleranceLabel} + + {itemJudgment && ( + + {itemJudgment === 'pass' ? '적합' : '부적합'} + + )} +
+ { + const v = e.target.value === '' ? null : parseFloat(e.target.value); + onValueChange(fieldKey, v); + }} + className="h-11 rounded-lg border-gray-300" + /> +
+ ); + })} +
+ ))} +
+ ); +} + +// 동적 폼의 자동 판정 계산 +function computeDynamicJudgment( + template: NonNullable, + formValues: Record +): 'pass' | 'fail' | null { + let hasAnyValue = false; + let hasFail = false; + + for (const section of template.sections) { + for (const item of section.items) { + const fieldKey = `section_${section.id}_item_${item.id}`; + const value = formValues[fieldKey]; + + if (item.measurement_type === 'binary' || item.type === 'boolean') { + if (value === 'bad') hasFail = true; + if (value != null) hasAnyValue = true; + } else if (item.tolerance && item.standard_criteria) { + const numValue = value as number | null | undefined; + if (numValue != null) { + hasAnyValue = true; + const design = parseFloat(item.standard_criteria); + if (!isNaN(design)) { + const result = evaluateTolerance(numValue, design, item.tolerance); + if (result === 'fail') hasFail = true; + } + } + } else if (value != null) { + hasAnyValue = true; + } + } + } + + if (!hasAnyValue) return null; + return hasFail ? 'fail' : 'pass'; +} + export function InspectionInputModal({ open, onOpenChange, @@ -232,7 +402,11 @@ export function InspectionInputModal({ specification = '', initialData, onComplete, + templateData, }: InspectionInputModalProps) { + // 템플릿 모드 여부 + const useTemplateMode = !!(templateData?.has_template && templateData.template); + const [formData, setFormData] = useState({ productName, specification, @@ -240,6 +414,9 @@ export function InspectionInputModal({ nonConformingContent: '', }); + // 동적 폼 값 (템플릿 모드용) + const [dynamicFormValues, setDynamicFormValues] = useState>({}); + // 절곡용 간격 포인트 초기화 const [gapPoints, setGapPoints] = useState<{ left: number | null; right: number | null }[]>( Array(5).fill(null).map(() => ({ left: null, right: null })) @@ -312,11 +489,17 @@ export function InspectionInputModal({ } setGapPoints(Array(5).fill(null).map(() => ({ left: null, right: null }))); + setDynamicFormValues({}); } }, [open, productName, specification, processType, initialData]); - // 자동 판정 계산 - const autoJudgment = useMemo(() => computeJudgment(processType, formData), [processType, formData]); + // 자동 판정 계산 (템플릿 모드 vs 레거시 모드) + const autoJudgment = useMemo(() => { + if (useTemplateMode && templateData?.template) { + return computeDynamicJudgment(templateData.template, dynamicFormValues); + } + return computeJudgment(processType, formData); + }, [useTemplateMode, templateData, dynamicFormValues, processType, formData]); // 판정값 자동 동기화 useEffect(() => { @@ -330,6 +513,8 @@ export function InspectionInputModal({ const data: InspectionData = { ...formData, gapPoints: processType === 'bending' ? gapPoints : undefined, + // 동적 폼 값을 templateValues로 병합 + ...(useTemplateMode ? { templateValues: dynamicFormValues } : {}), }; onComplete(data); onOpenChange(false); @@ -378,8 +563,21 @@ export function InspectionInputModal({ + {/* ===== 동적 폼 (템플릿 기반) ===== */} + {useTemplateMode && templateData?.template && ( + + setDynamicFormValues((prev) => ({ ...prev, [key]: value })) + } + /> + )} + + {/* ===== 레거시: 공정별 하드코딩 검사 항목 (템플릿 없을 때만 표시) ===== */} + {/* ===== 재고생산 (bending_wip) 검사 항목 ===== */} - {processType === 'bending_wip' && ( + {!useTemplateMode && processType === 'bending_wip' && ( <>
검모양 절곡상태 @@ -431,7 +629,7 @@ export function InspectionInputModal({ )} {/* ===== 스크린 검사 항목 ===== */} - {processType === 'screen' && ( + {!useTemplateMode && processType === 'screen' && ( <>
검모양 가공상태 @@ -487,7 +685,7 @@ export function InspectionInputModal({ )} {/* ===== 슬랫 검사 항목 ===== */} - {processType === 'slat' && ( + {!useTemplateMode && processType === 'slat' && ( <>
검모양 가공상태 @@ -539,7 +737,7 @@ export function InspectionInputModal({ )} {/* ===== 조인트바 검사 항목 ===== */} - {processType === 'slat_jointbar' && ( + {!useTemplateMode && processType === 'slat_jointbar' && ( <>
검모양 가공상태 @@ -603,7 +801,7 @@ export function InspectionInputModal({ )} {/* ===== 절곡 검사 항목 ===== */} - {processType === 'bending' && ( + {!useTemplateMode && processType === 'bending' && ( <>
검모양 절곡상태 diff --git a/src/components/production/WorkerScreen/actions.ts b/src/components/production/WorkerScreen/actions.ts index e6c45770..3e008695 100644 --- a/src/components/production/WorkerScreen/actions.ts +++ b/src/components/production/WorkerScreen/actions.ts @@ -513,4 +513,104 @@ export async function getWorkOrderInspectionData( errorMessage: '검사 데이터 조회에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; +} + +// ===== 검사 문서 템플릿 조회 (document_template 기반) ===== +export interface InspectionTemplateData { + work_order_id: number; + has_template: boolean; + template?: { + id: number; + name: string; + category: string; + description: string | null; + sections: { + id: number; + name: string; + sort_order: number; + items: { + id: number; + name: string; + type: string; + is_required: boolean; + sort_order: number; + options: Record | null; + tolerance: { + type: 'symmetric' | 'asymmetric' | 'range'; + value?: number; + plus?: number; + minus?: number; + min?: number; + max?: number; + } | null; + standard_criteria: string | null; + measurement_type: string | null; + }[]; + }[]; + columns: { + id: number; + name: string; + type: string; + sort_order: number; + is_required: boolean; + options: Record | null; + }[]; + approval_lines: { + id: number; + role_name: string; + sort_order: number; + is_required: boolean; + }[]; + basic_fields: { + id: number; + name: string; + field_key: string; + field_type: string; + is_required: boolean; + sort_order: number; + options: Record | null; + }[]; + }; + work_order_info?: { + work_order_no: string; + project_name: string | null; + process_name: string | null; + scheduled_date: string | null; + }; +} + +export async function getInspectionTemplate( + workOrderId: string +): Promise<{ + success: boolean; + data?: InspectionTemplateData; + error?: string; +}> { + const result = await executeServerAction({ + url: `${API_URL}/api/v1/work-orders/${workOrderId}/inspection-template`, + errorMessage: '검사 템플릿 조회에 실패했습니다.', + }); + return { success: result.success, data: result.data, error: result.error }; +} + +// ===== 검사 문서 저장 (Document + DocumentData) ===== +export async function saveInspectionDocument( + workOrderId: string, + data: { + title?: string; + data: Record[]; + approvers?: { role_name: string; user_id?: number }[]; + } +): Promise<{ + success: boolean; + data?: { document_id: number; document_no: string; status: string }; + error?: string; +}> { + const result = await executeServerAction<{ document_id: number; document_no: string; status: string }>({ + url: `${API_URL}/api/v1/work-orders/${workOrderId}/inspection-document`, + method: 'POST', + body: data, + errorMessage: '검사 문서 저장에 실패했습니다.', + }); + return { success: result.success, data: result.data, error: result.error }; } \ No newline at end of file diff --git a/src/components/production/WorkerScreen/index.tsx b/src/components/production/WorkerScreen/index.tsx index 519ec224..fc9fe95c 100644 --- a/src/components/production/WorkerScreen/index.tsx +++ b/src/components/production/WorkerScreen/index.tsx @@ -33,7 +33,8 @@ import { Button } from '@/components/ui/button'; import { PageLayout } from '@/components/organisms/PageLayout'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; -import { getMyWorkOrders, completeWorkOrder, saveItemInspection, getWorkOrderInspectionData } from './actions'; +import { getMyWorkOrders, completeWorkOrder, saveItemInspection, getWorkOrderInspectionData, saveInspectionDocument, getInspectionTemplate } from './actions'; +import type { InspectionTemplateData } from './actions'; import { getProcessList } from '@/components/process-management/actions'; import type { InspectionSetting, Process } from '@/types/process'; import type { WorkOrder } from '../ProductionDashboard/types'; @@ -363,6 +364,8 @@ export default function WorkerScreen() { const [isInspectionInputModalOpen, setIsInspectionInputModalOpen] = useState(false); // 공정의 중간검사 설정 const [currentInspectionSetting, setCurrentInspectionSetting] = useState(); + // 문서 템플릿 데이터 (document_template 기반 동적 검사용) + const [inspectionTemplateData, setInspectionTemplateData] = useState(); // 중간검사 체크 상태 관리: { [itemId]: boolean } const [inspectionCheckedMap, setInspectionCheckedMap] = useState>({}); @@ -842,8 +845,8 @@ export default function WorkerScreen() { }; }, [filteredWorkOrders, workItems]); - // 중간검사 버튼 클릭 핸들러 - 바로 모달 열기 - const handleInspectionClick = useCallback((itemId: string) => { + // 중간검사 버튼 클릭 핸들러 - 템플릿 로드 후 모달 열기 + const handleInspectionClick = useCallback(async (itemId: string) => { // 해당 아이템 찾기 const item = workItems.find((w) => w.id === itemId); if (item) { @@ -867,6 +870,23 @@ export default function WorkerScreen() { createdAt: '', }; setSelectedOrder(syntheticOrder); + + // 실제 API 아이템인 경우 검사 템플릿 로딩 시도 + if (item.workOrderId && !item.id.startsWith('mock-')) { + try { + const tplResult = await getInspectionTemplate(item.workOrderId); + if (tplResult.success && tplResult.data?.has_template) { + setInspectionTemplateData(tplResult.data); + } else { + setInspectionTemplateData(undefined); + } + } catch { + setInspectionTemplateData(undefined); + } + } else { + setInspectionTemplateData(undefined); + } + setIsInspectionInputModalOpen(true); } }, [workItems]); @@ -922,6 +942,7 @@ export default function WorkerScreen() { const targetItem = workItems.find((w) => w.id === selectedOrder.id); if (targetItem?.apiItemId && targetItem?.workOrderId) { try { + // 1. 기존: work_order_items.options에 저장 const result = await saveItemInspection( targetItem.workOrderId, targetItem.apiItemId, @@ -933,6 +954,15 @@ export default function WorkerScreen() { } else { toast.error(result.error || '검사 데이터 저장에 실패했습니다.'); } + + // 2. 추가: Document + DocumentData로 저장 (document_template 연결된 경우) + try { + await saveInspectionDocument(targetItem.workOrderId, { + data: [data as unknown as Record], + }); + } catch { + // Document 저장 실패는 무시 (template 미연결 시 404 가능) + } } catch { toast.error('검사 데이터 저장 중 오류가 발생했습니다.'); } @@ -1275,6 +1305,7 @@ export default function WorkerScreen() { workItems={workItems} inspectionDataMap={inspectionDataMap} inspectionSetting={currentInspectionSetting} + templateData={inspectionTemplateData} /> ); diff --git a/src/types/process.ts b/src/types/process.ts index f6acfdab..d488a450 100644 --- a/src/types/process.ts +++ b/src/types/process.ts @@ -166,6 +166,8 @@ export interface ProcessStep { isRequired: boolean; // 필수여부 needsApproval: boolean; // 승인여부 needsInspection: boolean; // 검사여부 + documentTemplateId?: number; // 문서양식 ID (검사 시 사용할 템플릿) + documentTemplateName?: string; // 문서양식명 (표시용) isActive: boolean; // 사용여부 order: number; // 순서 (드래그&드롭) // 연결 정보