feat(WEB): 중간검사 입력 모달 템플릿 기반 렌더링 개선

- measurement_type 기반 항목별 렌더링 (OK/NG 체크 vs 수치 입력 분리)
- 섹션 구분선 간소화, isNumericItem/resolveDesignValue 헬퍼 추가
This commit is contained in:
2026-02-11 15:58:49 +09:00
parent 0ba7ec25df
commit 911b6ca31a

View File

@@ -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<string, number>).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 (
<div className="space-y-5">
{template.sections.map((section) => (
<div className="space-y-4">
{template.sections.map((section, sectionIdx) => (
<div key={section.id} className="space-y-3">
<span className="text-sm font-bold text-gray-800">{section.name}</span>
{/* 섹션 구분: 2번째 섹션부터 구분선 표시 */}
{sectionIdx > 0 && (
<div className="border-t border-gray-200 pt-3">
<span className="text-xs text-gray-400 font-medium">{section.name}</span>
</div>
)}
{section.items.map((item) => {
const itemLabel = item.item || item.name || '';
// ── check 컬럼이 있으면 OK/NG 토글 렌더링 ──
if (hasCheckColumns) {
return (
<div key={item.id} className="space-y-2">
<div className="space-y-0.5">
<span className="text-sm font-bold">{itemLabel}</span>
{item.standard && (
<p className="text-xs text-gray-500">{item.standard}</p>
)}
</div>
{checkColumns.map((col) => {
const fieldKey = `section_${section.id}_item_${item.id}_col_${col.id}`;
const value = formValues[fieldKey] as 'ok' | 'ng' | null | undefined;
return (
<div key={col.id} className="space-y-1">
{checkColumns.length > 1 && (
<span className="text-xs text-gray-500">{col.label}</span>
)}
<OkNgToggle
value={value ?? null}
onChange={(v) => onValueChange(fieldKey, v)}
/>
</div>
);
})}
</div>
);
}
// ── 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 (
<div key={item.id} className="space-y-1.5">
<span className="text-sm font-bold">{itemLabel}</span>
<StatusToggle
value={(value as 'good' | 'bad' | null) ?? null}
onChange={(v) => onValueChange(fieldKey, v)}
<div className="flex items-center gap-2">
<span className="text-sm font-bold">
{itemLabel}{toleranceLabel}
</span>
{itemJudgment && (
<span className={cn(
'text-xs font-bold px-2 py-0.5 rounded',
itemJudgment === 'pass' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
)}>
{itemJudgment === 'pass' ? '적합' : '부적합'}
</span>
)}
</div>
<Input
type="number"
placeholder={designValue !== undefined ? String(designValue) : '입력'}
value={numValue ?? ''}
onChange={(e) => {
const v = e.target.value === '' ? null : parseFloat(e.target.value);
onValueChange(fieldKey, v);
}}
className="h-11 rounded-lg border-gray-300"
/>
</div>
);
}
// 숫자 입력 (치수 등)
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<string, number>).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 (
<div key={item.id} className="space-y-1.5">
<div className="flex items-center gap-2">
<span className="text-sm font-bold">
{itemLabel}{toleranceLabel}
</span>
{itemJudgment && (
<span className={cn(
'text-xs font-bold px-2 py-0.5 rounded',
itemJudgment === 'pass' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
)}>
{itemJudgment === 'pass' ? '적합' : '부적합'}
</span>
<div className="space-y-0.5">
<span className="text-sm font-bold">{itemLabel}</span>
{hasStandard && standardDisplay && (
<p className="text-xs text-gray-500">: {standardDisplay}</p>
)}
</div>
<Input
type="number"
placeholder={placeholderStr}
value={numValue ?? ''}
onChange={(e) => {
const v = e.target.value === '' ? null : parseFloat(e.target.value);
onValueChange(fieldKey, v);
}}
className="h-11 rounded-lg border-gray-300"
<OkNgToggle
value={value ?? null}
onChange={(v) => onValueChange(fieldKey, v)}
/>
</div>
);
@@ -418,63 +412,42 @@ function computeDynamicJudgment(
formValues: Record<string, unknown>,
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<string, number>).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;
}