feat(WEB): 중간검사 입력 모달 템플릿 기반 렌더링 개선
- measurement_type 기반 항목별 렌더링 (OK/NG 체크 vs 수치 입력 분리) - 섹션 구분선 간소화, isNumericItem/resolveDesignValue 헬퍼 추가
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user