From e508014224053f06c6a5ecfe2bde2606177e0b40 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 19:27:45 +0900 Subject: [PATCH] =?UTF-8?q?fix(WEB):=20Turbopack=20use=20server=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EA=B0=84=20export=20type=20=EB=9F=B0?= =?UTF-8?q?=ED=83=80=EC=9E=84=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 검사 템플릿 타입(InspectionTemplateData 등)을 WorkerScreen/types.ts로 분리 - use server 파일에서 export type 제거 (Turbopack 모듈 평가 시 값으로 처리되는 문제) - 모든 타입 import를 types.ts 직접 참조로 변경 --- .../production/WorkOrders/actions.ts | 102 ++++ .../documents/InspectionReportModal.tsx | 206 +++----- .../documents/TemplateInspectionContent.tsx | 482 ++++++++++++++++++ .../WorkerScreen/InspectionInputModal.tsx | 22 +- .../production/WorkerScreen/actions.ts | 65 +-- .../production/WorkerScreen/index.tsx | 2 +- .../production/WorkerScreen/types.ts | 115 +++++ 7 files changed, 785 insertions(+), 209 deletions(-) create mode 100644 src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx diff --git a/src/components/production/WorkOrders/actions.ts b/src/components/production/WorkOrders/actions.ts index 57b3a18a..2eb6aeef 100644 --- a/src/components/production/WorkOrders/actions.ts +++ b/src/components/production/WorkOrders/actions.ts @@ -734,6 +734,108 @@ export async function saveInspectionData( } } +// ===== 검사 문서 템플릿 조회 (document_template 기반) ===== +import type { InspectionTemplateData } from '@/components/production/WorkerScreen/types'; + +export async function getInspectionTemplate( + workOrderId: string +): Promise<{ success: boolean; data?: InspectionTemplateData; error?: string }> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-template`, + { method: 'GET' } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + if (!response.ok || !result.success) { + return { success: false, error: result.message || '검사 템플릿을 불러올 수 없습니다.' }; + } + + return { success: true, data: result.data }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] getInspectionTemplate error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +// ===== 검사 문서 저장 (Document + DocumentData) ===== +export async function saveInspectionDocument( + workOrderId: string, + data: { + step_id?: number; + 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; +}> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-document`, + { + method: 'POST', + body: JSON.stringify(data), + } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + if (!response.ok || !result.success) { + return { success: false, error: result.message || '검사 문서 저장에 실패했습니다.' }; + } + + return { success: true, data: result.data }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] saveInspectionDocument error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +// ===== 검사 문서 resolve (기존 문서/템플릿 조회) ===== +export async function resolveInspectionDocument( + workOrderId: string, + params?: { step_id?: number } +): Promise<{ + success: boolean; + data?: { mode: 'existing' | 'new'; document?: Record; template?: Record }; + error?: string; +}> { + try { + const query = params?.step_id ? `?step_id=${params.step_id}` : ''; + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-resolve${query}`, + { method: 'GET' } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + if (!response.ok || !result.success) { + return { success: false, error: result.message || '검사 문서 조회에 실패했습니다.' }; + } + + return { success: true, data: result.data }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] resolveInspectionDocument error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + // ===== 수주 목록 조회 (작업지시 생성용) ===== export interface SalesOrderForWorkOrder { id: number; diff --git a/src/components/production/WorkOrders/documents/InspectionReportModal.tsx b/src/components/production/WorkOrders/documents/InspectionReportModal.tsx index 4d3423d3..c2758426 100644 --- a/src/components/production/WorkOrders/documents/InspectionReportModal.tsx +++ b/src/components/production/WorkOrders/documents/InspectionReportModal.tsx @@ -16,7 +16,13 @@ import { Loader2, Save } from 'lucide-react'; import { DocumentViewer } from '@/components/document-system'; import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; -import { getWorkOrderById, saveInspectionData, getInspectionReport } from '../actions'; +import { + getWorkOrderById, + saveInspectionData, + getInspectionReport, + getInspectionTemplate, + saveInspectionDocument, +} from '../actions'; import type { WorkOrder, ProcessType } from '../types'; import type { InspectionReportData } from '../actions'; import { ScreenInspectionContent } from './ScreenInspectionContent'; @@ -24,11 +30,12 @@ import { SlatInspectionContent } from './SlatInspectionContent'; import { BendingInspectionContent } from './BendingInspectionContent'; import { BendingWipInspectionContent } from './BendingWipInspectionContent'; import { SlatJointBarInspectionContent } from './SlatJointBarInspectionContent'; +import { TemplateInspectionContent } from './TemplateInspectionContent'; 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'; +import type { InspectionTemplateData } from '@/components/production/WorkerScreen/types'; const PROCESS_LABELS: Record = { screen: '스크린', @@ -124,6 +131,8 @@ export function InspectionReportModal({ const [apiWorkItems, setApiWorkItems] = useState(null); const [apiInspectionDataMap, setApiInspectionDataMap] = useState(null); const [reportSummary, setReportSummary] = useState(null); + // 자체 로딩한 템플릿 데이터 (prop으로 안 넘어올 때) + const [selfTemplateData, setSelfTemplateData] = useState(null); // props에서 목업 제외한 실제 개소만 사용 (WorkerScreen에서 apiItems + mockItems가 합쳐져 전달됨) // React에서는 개소 미등록 시 성적서 버튼 자체가 노출되지 않으므로 API fallback 불필요 @@ -180,12 +189,14 @@ export function InspectionReportModal({ setIsLoading(true); setError(null); - // 작업지시 기본정보 + 검사 성적서 데이터 동시 로딩 + // 작업지시 기본정보 + 검사 성적서 + 템플릿 동시 로딩 Promise.all([ getWorkOrderById(workOrderId), getInspectionReport(workOrderId), + // prop으로 템플릿이 안 넘어왔으면 자체 로딩 + !templateData ? getInspectionTemplate(workOrderId) : Promise.resolve(null), ]) - .then(([orderResult, reportResult]) => { + .then(([orderResult, reportResult, templateResult]) => { // 1) WorkOrder 기본정보 if (orderResult.success && orderResult.data) { const orderData = orderResult.data; @@ -217,6 +228,11 @@ export function InspectionReportModal({ setApiInspectionDataMap(null); setReportSummary(null); } + + // 3) 템플릿 데이터 (자체 로딩한 경우) + if (templateResult && templateResult.success && templateResult.data) { + setSelfTemplateData(templateResult.data); + } }) .catch(() => { setError('서버 오류가 발생했습니다.'); @@ -229,9 +245,15 @@ export function InspectionReportModal({ setApiWorkItems(null); setApiInspectionDataMap(null); setReportSummary(null); + setSelfTemplateData(null); setError(null); } - }, [open, workOrderId, processType]); + }, [open, workOrderId, processType, templateData]); + + // 템플릿 결정: prop 우선, 없으면 자체 로딩 결과 사용 + const resolvedTemplateData = templateData || selfTemplateData; + const activeTemplate = resolvedTemplateData?.has_template ? resolvedTemplateData.template : null; + const activeStepId = resolvedTemplateData?.templates?.[0]?.step_id ?? null; const handleSave = useCallback(async () => { if (!workOrderId || !contentRef.current) return; @@ -239,18 +261,43 @@ export function InspectionReportModal({ const data = contentRef.current.getInspectionData(); setIsSaving(true); try { - const result = await saveInspectionData(workOrderId, processType, data); - if (result.success) { - toast.success('검사 데이터가 저장되었습니다.'); + // 템플릿 모드: Document 기반 저장 + if (activeTemplate) { + const inspData = data as { + template_id: number; + items: { id: string; apiItemId?: number; judgment: string | null; values: Record }[]; + inadequateContent: string; + overall_result: string | null; + }; + const result = await saveInspectionDocument(workOrderId, { + step_id: activeStepId ?? undefined, + title: activeTemplate.title || activeTemplate.name, + data: inspData.items.map(item => ({ + item_id: item.apiItemId || item.id, + judgment: item.judgment, + values: item.values, + })), + }); + if (result.success) { + toast.success('검사 문서가 저장되었습니다.'); + } else { + toast.error(result.error || '저장에 실패했습니다.'); + } } else { - toast.error(result.error || '저장에 실패했습니다.'); + // 레거시 모드: 기존 저장 + const result = await saveInspectionData(workOrderId, processType, data); + if (result.success) { + toast.success('검사 데이터가 저장되었습니다.'); + } else { + toast.error(result.error || '저장에 실패했습니다.'); + } } } catch { toast.error('저장 중 오류가 발생했습니다.'); } finally { setIsSaving(false); } - }, [workOrderId, processType]); + }, [workOrderId, processType, activeTemplate, activeStepId]); if (!workOrderId) return null; @@ -262,139 +309,20 @@ export function InspectionReportModal({ ? '중간검사성적서 (조인트바)' : '중간검사 성적서'; - // 템플릿 기반 동적 렌더링 여부 - const useTemplateMode = !!(templateData?.has_template && templateData.template); - const renderContent = () => { if (!order) return null; - // 템플릿 모드: 동적 렌더링 - if (useTemplateMode && templateData?.template) { - const tpl = templateData.template; + // 템플릿 모드: TemplateInspectionContent 사용 + if (activeTemplate) { 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}
- (서명) -
-
- )} -
+ ); } diff --git a/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx b/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx new file mode 100644 index 00000000..276f32d0 --- /dev/null +++ b/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx @@ -0,0 +1,482 @@ +'use client'; + +/** + * 템플릿 기반 중간검사 성적서 콘텐츠 + * + * DocumentTemplate의 sections/items에서 measurement_type별 입력 셀을 자동 생성: + * - checkbox → 양호/불량 토글 + * - numeric → 기준값 표시 + 측정값 입력 + * - single_value → 단일값 입력 + * - substitute → 성적서 대체 배지 + * - text → 자유 텍스트 입력 + */ + +import { useState, forwardRef, useImperativeHandle, useEffect, Fragment } from 'react'; +import type { WorkOrder } from '../types'; +import type { WorkItemData } from '@/components/production/WorkerScreen/types'; +import type { InspectionDataMap } from './InspectionReportModal'; +import type { + InspectionTemplateFormat, + InspectionTemplateSectionItem, + InspectionTolerance, +} from '@/components/production/WorkerScreen/types'; +import { + type InspectionContentRef, + InspectionCheckbox, + InspectionLayout, + InspectionFooter, + JudgmentCell, + calculateOverallResult, + getFullDate, + getOrderInfo, + INPUT_CLASS, +} from './inspection-shared'; + +export type { InspectionContentRef }; + +// ===== 셀 값 타입 ===== +interface CellValue { + status?: 'good' | 'bad' | null; + measurements?: [string, string, string]; + value?: string; + text?: string; +} + +// ===== Props ===== +interface TemplateInspectionContentProps { + data: WorkOrder; + template: InspectionTemplateFormat; + readOnly?: boolean; + workItems?: WorkItemData[]; + inspectionDataMap?: InspectionDataMap; +} + +// ===== 유틸 ===== + +function formatTolerance(tol: InspectionTolerance | null): string { + if (!tol) return '-'; + if (tol.type === 'symmetric') return `± ${tol.value}`; + if (tol.type === 'asymmetric') return `+${tol.plus} / -${tol.minus}`; + if (tol.type === 'range') return `${tol.min} ~ ${tol.max}`; + return '-'; +} + +function formatStandard(item: InspectionTemplateSectionItem): string { + const sc = item.standard_criteria; + if (!sc) return item.standard || '-'; + if (typeof sc === 'object') { + if ('nominal' in sc) return String(sc.nominal); + if ('min' in sc && 'max' in sc) return `${sc.min} ~ ${sc.max}`; + if ('max' in sc) return `≤ ${sc.max}`; + if ('min' in sc) return `≥ ${sc.min}`; + } + return String(sc); +} + +function getNominalValue(item: InspectionTemplateSectionItem): number | null { + const sc = item.standard_criteria; + if (!sc || typeof sc !== 'object') { + if (typeof sc === 'string') { + const v = parseFloat(sc); + return isNaN(v) ? null : v; + } + return null; + } + if ('nominal' in sc) return sc.nominal; + return null; +} + +function formatFrequency(item: InspectionTemplateSectionItem): string { + if (item.frequency_n && item.frequency_c) return `n=${item.frequency_n}, c=${item.frequency_c}`; + if (item.frequency) return item.frequency; + return '-'; +} + +function getMeasurementLabel(type: string | null): string { + switch (type) { + case 'checkbox': return 'OK/NG'; + case 'numeric': return '수치(3회)'; + case 'single_value': return '단일값'; + case 'substitute': return '대체'; + case 'text': return '자유입력'; + default: return type || '-'; + } +} + +/** 측정값이 공차 범위 내인지 판정 */ +function isWithinTolerance(measured: number, item: InspectionTemplateSectionItem): boolean { + const nominal = getNominalValue(item); + const tol = item.tolerance; + if (nominal === null || !tol) return true; // 기준 없으면 pass + + switch (tol.type) { + case 'symmetric': + return Math.abs(measured - nominal) <= (tol.value ?? 0); + case 'asymmetric': + return measured >= nominal - (tol.minus ?? 0) && measured <= nominal + (tol.plus ?? 0); + case 'range': + return measured >= (tol.min ?? -Infinity) && measured <= (tol.max ?? Infinity); + default: + return true; + } +} + +// ===== 컴포넌트 ===== + +export const TemplateInspectionContent = forwardRef( + function TemplateInspectionContent({ data: order, template, readOnly = false, workItems, inspectionDataMap }, ref) { + const fullDate = getFullDate(); + const { documentNo, primaryAssignee } = getOrderInfo(order); + + // 모든 섹션의 아이템을 평탄화 (DATA 테이블 컬럼용) + const allItems = template.sections.flatMap(s => s.items); + + // 셀 값 상태: key = `${rowIdx}-${itemId}` + const [cellValues, setCellValues] = useState>({}); + const [inadequateContent, setInadequateContent] = useState(''); + + // inspectionDataMap에서 초기값 복원 + useEffect(() => { + if (!inspectionDataMap || !workItems) return; + const initial: Record = {}; + workItems.forEach((wi, rowIdx) => { + const itemData = inspectionDataMap.get(wi.id); + if (!itemData?.templateValues) return; + allItems.forEach(sectionItem => { + const key = `${rowIdx}-${sectionItem.id}`; + const val = itemData.templateValues?.[`item_${sectionItem.id}`]; + if (val && typeof val === 'object') { + initial[key] = val as CellValue; + } + }); + }); + if (Object.keys(initial).length > 0) setCellValues(initial); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inspectionDataMap, workItems]); + + // ref로 데이터 수집 노출 + useImperativeHandle(ref, () => ({ + getInspectionData: () => { + const items = effectiveWorkItems.map((wi, idx) => ({ + id: wi.id, + apiItemId: wi.apiItemId, + judgment: getRowJudgment(idx), + values: allItems.reduce((acc, sItem) => { + const key = `${idx}-${sItem.id}`; + acc[`item_${sItem.id}`] = cellValues[key] || null; + return acc; + }, {} as Record), + })); + + return { + template_id: template.id, + items, + inadequateContent, + overall_result: overallResult, + }; + }, + })); + + const updateCell = (key: string, update: Partial) => { + setCellValues(prev => ({ + ...prev, + [key]: { ...prev[key], ...update }, + })); + }; + + // 행별 판정 계산 + const getRowJudgment = (rowIdx: number): '적' | '부' | null => { + let hasAnyValue = false; + let hasFail = false; + + for (const item of allItems) { + const key = `${rowIdx}-${item.id}`; + const cell = cellValues[key]; + if (!cell) continue; + + if (item.measurement_type === 'checkbox') { + if (cell.status === 'bad') hasFail = true; + if (cell.status) hasAnyValue = true; + } else if (item.measurement_type === 'numeric') { + const measurements = cell.measurements || ['', '', '']; + for (const m of measurements) { + if (m) { + hasAnyValue = true; + const val = parseFloat(m); + if (!isNaN(val) && !isWithinTolerance(val, item)) hasFail = true; + } + } + } else if (item.measurement_type === 'single_value') { + if (cell.value) { + hasAnyValue = true; + const val = parseFloat(cell.value); + if (!isNaN(val) && !isWithinTolerance(val, item)) hasFail = true; + } + } else if (item.measurement_type === 'substitute') { + // 성적서 대체는 항상 적합 취급 + hasAnyValue = true; + } else if (cell.value || cell.text) { + hasAnyValue = true; + } + } + + if (!hasAnyValue) return null; + return hasFail ? '부' : '적'; + }; + + const effectiveWorkItems = workItems || []; + + // 종합판정 + const judgments = effectiveWorkItems.map((_, idx) => getRowJudgment(idx)); + const overallResult = calculateOverallResult(judgments); + + // numeric 아이템의 DATA 열 colspan 계산 + const getItemColSpan = (item: InspectionTemplateSectionItem) => { + if (item.measurement_type === 'numeric') return 2; // 기준 + 측정 + return 1; + }; + const totalDataCols = allItems.reduce((sum, item) => sum + getItemColSpan(item), 0); + + return ( + + {/* 기본 정보 */} + {template.basic_fields?.length > 0 && ( + + + {template.basic_fields.map(field => ( + + + + + ))} + +
{field.label} + {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) : + field.field_key === 'inspection_date' ? fullDate : + field.default_value || '-'} +
+ )} + + {/* 검사 기준서 - 섹션별 */} + {template.sections.map(section => ( +
+
■ {section.name}
+ + + + + + + + + + + + + + {section.items.map((item, idx) => ( + + + + + + + + + + ))} + +
No검사항목검사기준허용오차검사방식측정유형빈도
{idx + 1}{item.item || item.category || '-'}{formatStandard(item)}{formatTolerance(item.tolerance)}{item.method_name || item.method || '-'}{getMeasurementLabel(item.measurement_type)}{formatFrequency(item)}
+
+ ))} + + {/* 검사 DATA 테이블 */} + {allItems.length > 0 && effectiveWorkItems.length > 0 && ( +
+
■ 검사 DATA
+
+ + + {/* 상위 헤더: 항목 그룹 */} + + + + {allItems.map(item => ( + + ))} + + + {/* 하위 헤더: numeric 아이템만 기준/측정 분할 */} + + {allItems.map(item => { + if (item.measurement_type === 'numeric') { + return ( + + + + + ); + } + // checkbox, single_value, text, substitute: 단일 열 + return ( + + ); + })} + + + + {effectiveWorkItems.map((wi, rowIdx) => ( + + + + {allItems.map(item => { + const key = `${rowIdx}-${item.id}`; + const cell = cellValues[key]; + + // checkbox → 양호/불량 토글 + if (item.measurement_type === 'checkbox') { + return ( + + ); + } + + // numeric → 기준값 + 측정값 입력 + if (item.measurement_type === 'numeric') { + return ( + + + + + ); + } + + // substitute → 성적서 대체 표시 + if (item.measurement_type === 'substitute') { + return ( + + ); + } + + // single_value, text, default → 입력 필드 + return ( + + ); + })} + + + ))} + +
No품명 + {item.item || item.category || '-'} + 판정
기준측정 + {item.measurement_type === 'checkbox' ? '양호/불량' : + item.measurement_type === 'substitute' ? '대체' : + '입력'} +
{rowIdx + 1}{wi.itemName || '-'} +
+ + +
+
+ {formatStandard(item)} + + { + const m: [string, string, string] = [ + ...(cell?.measurements || ['', '', '']), + ] as [string, string, string]; + m[0] = e.target.value; + updateCell(key, { measurements: m }); + }} + readOnly={readOnly} + placeholder="측정값" + /> + + + 대체 + + + updateCell(key, { value: e.target.value })} + readOnly={readOnly} + placeholder="-" + /> +
+
+
+ )} + + {/* 부적합 내용 + 종합판정 */} + + + {/* 결재라인 */} + {template.approval_lines?.length > 0 && ( +
+ + + + + {template.approval_lines.map(line => ( + + ))} + + + {template.approval_lines.map(line => ( + + ))} + + + {template.approval_lines.map(line => ( + + ))} + + +

+ {line.role || line.name} +
+ {line.name || '이름'} +
+ {line.dept || '부서명'} +
+
+ )} +
+ ); + } +); diff --git a/src/components/production/WorkerScreen/InspectionInputModal.tsx b/src/components/production/WorkerScreen/InspectionInputModal.tsx index 0a6872d3..648fbfa5 100644 --- a/src/components/production/WorkerScreen/InspectionInputModal.tsx +++ b/src/components/production/WorkerScreen/InspectionInputModal.tsx @@ -22,7 +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'; +import type { InspectionTemplateData } from './types'; // 중간검사 공정 타입 export type InspectionProcessType = @@ -281,7 +281,7 @@ function DynamicInspectionForm({ // 양호/불량 토글 return (
- {item.name} + {item.item || item.name}