From 911b6ca31ae0ffedcb2d594598c46c93265fc164 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 15:58:49 +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=EC=9E=85=EB=A0=A5=20=EB=AA=A8=EB=8B=AC=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20=EA=B8=B0=EB=B0=98=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - measurement_type 기반 항목별 렌더링 (OK/NG 체크 vs 수치 입력 분리) - 섹션 구분선 간소화, isNumericItem/resolveDesignValue 헬퍼 추가 --- .../WorkerScreen/InspectionInputModal.tsx | 249 ++++++++---------- 1 file changed, 114 insertions(+), 135 deletions(-) diff --git a/src/components/production/WorkerScreen/InspectionInputModal.tsx b/src/components/production/WorkerScreen/InspectionInputModal.tsx index 18945c45..3d3d2602 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 './types'; +import type { InspectionTemplateData, InspectionTemplateSectionItem } from './types'; // 중간검사 공정 타입 export type InspectionProcessType = @@ -281,6 +281,29 @@ function formatDimension(val: number | undefined): string { return val.toLocaleString(); } +// ===== 항목별 입력 유형 판별 ===== +// measurement_type이 'numeric' 또는 'measurement'이면 측정치수 입력, 나머지는 OK/NG 체크 +function isNumericItem(item: InspectionTemplateSectionItem): boolean { + const mt = item.measurement_type?.toLowerCase(); + return mt === 'numeric' || mt === 'measurement'; +} + +// 기준치수 resolve (standard_criteria → reference_attribute fallback) +function resolveDesignValue( + item: InspectionTemplateSectionItem, + workItemDimensions?: { width?: number; height?: number } +): number | undefined { + if (item.standard_criteria) { + const designStr = typeof item.standard_criteria === 'object' + ? String((item.standard_criteria as Record).nominal ?? '') + : String(item.standard_criteria); + const parsed = parseFloat(designStr); + if (!isNaN(parsed)) return parsed; + } + const refVal = resolveRefValue(item.field_values, workItemDimensions); + return refVal !== null ? refVal : undefined; +} + // ===== 동적 폼 (템플릿 기반) ===== function DynamicInspectionForm({ template, @@ -293,115 +316,86 @@ function DynamicInspectionForm({ onValueChange: (key: string, value: unknown) => void; workItemDimensions?: { width?: number; height?: number }; }) { - // 템플릿 컬럼에서 check 타입 컬럼 추출 - const checkColumns = template.columns?.filter(c => c.column_type === 'check') ?? []; - const hasCheckColumns = checkColumns.length > 0; - + // 모든 섹션의 아이템을 플랫하게 렌더링 (섹션 구분은 가벼운 구분선으로) return ( -
- {template.sections.map((section) => ( +
+ {template.sections.map((section, sectionIdx) => (
- {section.name} + {/* 섹션 구분: 2번째 섹션부터 구분선 표시 */} + {sectionIdx > 0 && ( +
+ {section.name} +
+ )} + {section.items.map((item) => { - const itemLabel = item.item || item.name || ''; - - // ── check 컬럼이 있으면 OK/NG 토글 렌더링 ── - if (hasCheckColumns) { - return ( -
-
- {itemLabel} - {item.standard && ( -

{item.standard}

- )} -
- {checkColumns.map((col) => { - const fieldKey = `section_${section.id}_item_${item.id}_col_${col.id}`; - const value = formValues[fieldKey] as 'ok' | 'ng' | null | undefined; - return ( -
- {checkColumns.length > 1 && ( - {col.label} - )} - onValueChange(fieldKey, v)} - /> -
- ); - })} -
- ); - } - - // ── check 컬럼 없음: 기존 로직 (binary → 양호/불량, else → 숫자 입력) ── const fieldKey = `section_${section.id}_item_${item.id}`; - const value = formValues[fieldKey]; + const itemLabel = item.item || item.name || ''; + const designValue = resolveDesignValue(item, workItemDimensions); + + // ── 측정치수 입력 (길이, 높이 등) ── + if (isNumericItem(item)) { + const numValue = formValues[fieldKey] as number | null | undefined; + + const designLabel = designValue !== undefined ? designValue.toLocaleString() : ''; + const toleranceLabel = item.tolerance + ? ` (${designLabel ? designLabel + ' ' : ''}${formatToleranceLabel(item.tolerance)})` + : designLabel ? ` (${designLabel})` : ''; + + let itemJudgment: 'pass' | 'fail' | null = null; + if (item.tolerance && numValue != null && designValue !== undefined) { + itemJudgment = evaluateTolerance(numValue, designValue, item.tolerance); + } - if (item.measurement_type === 'binary' || item.type === 'boolean') { return (
- {itemLabel} - onValueChange(fieldKey, v)} +
+ + {itemLabel}{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" />
); } - // 숫자 입력 (치수 등) - const numValue = value as number | null | undefined; - - let designValue: number | undefined; - if (item.standard_criteria) { - const designStr = typeof item.standard_criteria === 'object' - ? String((item.standard_criteria as Record).nominal ?? '') - : String(item.standard_criteria); - const parsed = parseFloat(designStr); - if (!isNaN(parsed)) designValue = parsed; - } - if (designValue === undefined) { - const refVal = resolveRefValue(item.field_values, workItemDimensions); - if (refVal !== null) designValue = refVal; - } - - const designLabel = designValue !== undefined ? designValue.toLocaleString() : ''; - const toleranceLabel = item.tolerance - ? ` (${designLabel ? designLabel + ' ' : ''}${formatToleranceLabel(item.tolerance)})` - : designLabel ? ` (${designLabel})` : ''; - - let itemJudgment: 'pass' | 'fail' | null = null; - if (item.tolerance && numValue != null && designValue !== undefined) { - itemJudgment = evaluateTolerance(numValue, designValue, item.tolerance); - } - - const placeholderStr = designValue !== undefined ? String(designValue) : '입력'; + // ── OK/NG 체크 (기준치수 표시 있으면 함께 표시) ── + const value = formValues[fieldKey] as 'ok' | 'ng' | null | undefined; + const hasStandard = designValue !== undefined || item.standard; + const standardDisplay = designValue !== undefined + ? (item.tolerance + ? `${designValue.toLocaleString()} ${formatToleranceLabel(item.tolerance)}` + : String(designValue.toLocaleString())) + : item.standard; return (
-
- - {itemLabel}{toleranceLabel} - - {itemJudgment && ( - - {itemJudgment === 'pass' ? '적합' : '부적합'} - +
+ {itemLabel} + {hasStandard && standardDisplay && ( +

기준: {standardDisplay}

)}
- { - const v = e.target.value === '' ? null : parseFloat(e.target.value); - onValueChange(fieldKey, v); - }} - className="h-11 rounded-lg border-gray-300" + onValueChange(fieldKey, v)} />
); @@ -418,63 +412,42 @@ function computeDynamicJudgment( formValues: Record, workItemDimensions?: { width?: number; height?: number } ): 'pass' | 'fail' | null { - let hasAnyValue = false; + let totalItems = 0; + let filledItems = 0; let hasFail = false; - const checkColumns = template.columns?.filter(c => c.column_type === 'check') ?? []; - const hasCheckColumns = checkColumns.length > 0; - for (const section of template.sections) { for (const item of section.items) { - // ── check 컬럼 기반 판정 ── - if (hasCheckColumns) { - for (const col of checkColumns) { - const key = `section_${section.id}_item_${item.id}_col_${col.id}`; - const val = formValues[key]; - if (val != null) { - hasAnyValue = true; - if (val === 'ng') hasFail = true; - } - } - continue; - } - - // ── 기존 로직: measurement_type 기반 판정 ── + totalItems++; 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) { + if (isNumericItem(item)) { const numValue = value as number | null | undefined; if (numValue != null) { - hasAnyValue = true; - let design: number | undefined; - if (item.standard_criteria) { - const designStr = typeof item.standard_criteria === 'object' - ? String((item.standard_criteria as Record).nominal ?? '') - : String(item.standard_criteria); - const parsed = parseFloat(designStr); - if (!isNaN(parsed)) design = parsed; - } - if (design === undefined) { - const refVal = resolveRefValue(item.field_values, workItemDimensions); - if (refVal !== null) design = refVal; - } - if (design !== undefined) { - const result = evaluateTolerance(numValue, design, item.tolerance); - if (result === 'fail') hasFail = true; + filledItems++; + if (item.tolerance) { + const design = resolveDesignValue(item, workItemDimensions); + if (design !== undefined) { + const result = evaluateTolerance(numValue, design, item.tolerance); + if (result === 'fail') hasFail = true; + } } } - } else if (value != null) { - hasAnyValue = true; + } else { + if (value != null) { + filledItems++; + if (value === 'ng') hasFail = true; + } } } } - if (!hasAnyValue) return null; - return hasFail ? 'fail' : 'pass'; + if (filledItems === 0) return null; + if (hasFail) return 'fail'; + // 모든 항목이 입력되어야 적합 판정 + if (filledItems < totalItems) return null; + return 'pass'; } export function InspectionInputModal({ @@ -520,6 +493,12 @@ export function InspectionInputModal({ } else { setGapPoints(Array(5).fill(null).map(() => ({ left: null, right: null }))); } + // 동적 폼 값 복원 (템플릿 기반 검사 데이터) + if (initialData.templateValues) { + setDynamicFormValues(initialData.templateValues); + } else { + setDynamicFormValues({}); + } return; }