2026-02-05 21:43:28 +09:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 중간검사 입력 모달
|
|
|
|
|
*
|
|
|
|
|
* 공정별로 다른 검사 항목 표시:
|
|
|
|
|
* - screen: 스크린 중간검사
|
|
|
|
|
* - slat: 슬랫 중간검사
|
|
|
|
|
* - slat_jointbar: 조인트바 중간검사
|
|
|
|
|
* - bending: 절곡 중간검사
|
|
|
|
|
* - bending_wip: 재고생산(재공품) 중간검사
|
|
|
|
|
*/
|
|
|
|
|
|
2026-02-09 21:31:07 +09:00
|
|
|
import { useState, useEffect, useMemo } from 'react';
|
2026-02-05 21:43:28 +09:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
} from '@/components/ui/dialog';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
2026-02-10 08:36:12 +09:00
|
|
|
import type { InspectionTemplateData } from './actions';
|
2026-02-05 21:43:28 +09:00
|
|
|
|
|
|
|
|
// 중간검사 공정 타입
|
|
|
|
|
export type InspectionProcessType =
|
|
|
|
|
| 'screen'
|
|
|
|
|
| 'slat'
|
|
|
|
|
| 'slat_jointbar'
|
|
|
|
|
| 'bending'
|
|
|
|
|
| 'bending_wip';
|
|
|
|
|
|
|
|
|
|
// 검사 결과 데이터 타입
|
|
|
|
|
export interface InspectionData {
|
|
|
|
|
productName: string;
|
|
|
|
|
specification: string;
|
|
|
|
|
// 겉모양 상태
|
|
|
|
|
bendingStatus?: 'good' | 'bad' | null; // 절곡상태
|
|
|
|
|
processingStatus?: 'good' | 'bad' | null; // 가공상태
|
|
|
|
|
sewingStatus?: 'good' | 'bad' | null; // 재봉상태
|
|
|
|
|
assemblyStatus?: 'good' | 'bad' | null; // 조립상태
|
|
|
|
|
// 치수
|
|
|
|
|
length?: number | null;
|
|
|
|
|
width?: number | null;
|
|
|
|
|
height1?: number | null;
|
|
|
|
|
height2?: number | null;
|
|
|
|
|
length3?: number | null;
|
|
|
|
|
gap4?: number | null;
|
|
|
|
|
gapStatus?: 'ok' | 'ng' | null;
|
|
|
|
|
// 간격 포인트들 (절곡용)
|
|
|
|
|
gapPoints?: { left: number | null; right: number | null }[];
|
|
|
|
|
// 판정
|
|
|
|
|
judgment: 'pass' | 'fail' | null;
|
|
|
|
|
nonConformingContent: string;
|
2026-02-10 08:36:12 +09:00
|
|
|
// 동적 폼 값 (템플릿 기반 검사 시)
|
|
|
|
|
templateValues?: Record<string, unknown>;
|
2026-02-05 21:43:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface InspectionInputModalProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (open: boolean) => void;
|
|
|
|
|
processType: InspectionProcessType;
|
|
|
|
|
productName?: string;
|
|
|
|
|
specification?: string;
|
2026-02-09 10:33:02 +09:00
|
|
|
initialData?: InspectionData;
|
2026-02-05 21:43:28 +09:00
|
|
|
onComplete: (data: InspectionData) => void;
|
2026-02-10 08:36:12 +09:00
|
|
|
/** 문서 템플릿 데이터 (있으면 동적 폼 모드) */
|
|
|
|
|
templateData?: InspectionTemplateData;
|
2026-02-05 21:43:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PROCESS_TITLES: Record<InspectionProcessType, string> = {
|
|
|
|
|
screen: '# 스크린 중간검사',
|
|
|
|
|
slat: '# 슬랫 중간검사',
|
|
|
|
|
slat_jointbar: '# 조인트바 중간검사',
|
|
|
|
|
bending: '# 절곡 중간검사',
|
|
|
|
|
bending_wip: '# 재고생산 중간검사',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 양호/불량 버튼 컴포넌트
|
|
|
|
|
function StatusToggle({
|
|
|
|
|
value,
|
|
|
|
|
onChange,
|
|
|
|
|
}: {
|
|
|
|
|
value: 'good' | 'bad' | null;
|
|
|
|
|
onChange: (v: 'good' | 'bad') => void;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="flex gap-2 flex-1">
|
2026-02-05 21:43:28 +09:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onChange('good')}
|
|
|
|
|
className={cn(
|
2026-02-07 05:10:29 +09:00
|
|
|
'flex-1 py-2.5 rounded-lg text-sm font-bold transition-colors',
|
2026-02-05 21:43:28 +09:00
|
|
|
value === 'good'
|
2026-02-07 05:10:29 +09:00
|
|
|
? 'bg-black text-white'
|
|
|
|
|
: 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'
|
2026-02-05 21:43:28 +09:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
양호
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onChange('bad')}
|
|
|
|
|
className={cn(
|
2026-02-07 05:10:29 +09:00
|
|
|
'flex-1 py-2.5 rounded-lg text-sm font-bold transition-colors',
|
2026-02-05 21:43:28 +09:00
|
|
|
value === 'bad'
|
2026-02-07 05:10:29 +09:00
|
|
|
? 'bg-black text-white'
|
|
|
|
|
: 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'
|
2026-02-05 21:43:28 +09:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
불량
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OK/NG 버튼 컴포넌트
|
|
|
|
|
function OkNgToggle({
|
|
|
|
|
value,
|
|
|
|
|
onChange,
|
|
|
|
|
}: {
|
|
|
|
|
value: 'ok' | 'ng' | null;
|
|
|
|
|
onChange: (v: 'ok' | 'ng') => void;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="flex gap-2 flex-1">
|
2026-02-05 21:43:28 +09:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onChange('ok')}
|
|
|
|
|
className={cn(
|
2026-02-07 05:10:29 +09:00
|
|
|
'flex-1 py-2.5 rounded-lg text-sm font-bold transition-colors',
|
2026-02-05 21:43:28 +09:00
|
|
|
value === 'ok'
|
2026-02-07 05:10:29 +09:00
|
|
|
? 'bg-black text-white'
|
|
|
|
|
: 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'
|
2026-02-05 21:43:28 +09:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
OK
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onChange('ng')}
|
|
|
|
|
className={cn(
|
2026-02-07 05:10:29 +09:00
|
|
|
'flex-1 py-2.5 rounded-lg text-sm font-bold transition-colors',
|
2026-02-05 21:43:28 +09:00
|
|
|
value === 'ng'
|
2026-02-07 05:10:29 +09:00
|
|
|
? 'bg-black text-white'
|
|
|
|
|
: 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'
|
2026-02-05 21:43:28 +09:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
NG
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 21:31:07 +09:00
|
|
|
// 자동 판정 표시 컴포넌트
|
|
|
|
|
function JudgmentDisplay({ value }: { value: 'pass' | 'fail' | null }) {
|
2026-02-05 21:43:28 +09:00
|
|
|
return (
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="flex gap-2 flex-1">
|
2026-02-09 21:31:07 +09:00
|
|
|
<div
|
2026-02-05 21:43:28 +09:00
|
|
|
className={cn(
|
2026-02-09 21:31:07 +09:00
|
|
|
'flex-1 py-2.5 rounded-lg text-sm font-bold text-center transition-colors',
|
2026-02-05 21:43:28 +09:00
|
|
|
value === 'pass'
|
2026-02-07 05:10:29 +09:00
|
|
|
? 'bg-orange-600 text-white'
|
2026-02-09 21:31:07 +09:00
|
|
|
: 'bg-gray-100 text-gray-400 border border-gray-300'
|
2026-02-05 21:43:28 +09:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
적합
|
2026-02-09 21:31:07 +09:00
|
|
|
</div>
|
|
|
|
|
<div
|
2026-02-05 21:43:28 +09:00
|
|
|
className={cn(
|
2026-02-09 21:31:07 +09:00
|
|
|
'flex-1 py-2.5 rounded-lg text-sm font-bold text-center transition-colors',
|
2026-02-05 21:43:28 +09:00
|
|
|
value === 'fail'
|
2026-02-07 05:10:29 +09:00
|
|
|
? 'bg-black text-white'
|
2026-02-09 21:31:07 +09:00
|
|
|
: 'bg-gray-100 text-gray-400 border border-gray-300'
|
2026-02-05 21:43:28 +09:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
부적합
|
2026-02-09 21:31:07 +09:00
|
|
|
</div>
|
2026-02-05 21:43:28 +09:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 21:31:07 +09:00
|
|
|
// 공정별 자동 판정 계산
|
|
|
|
|
function hasMeasurement(v: number | null | undefined): boolean {
|
|
|
|
|
return v != null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function computeJudgment(processType: InspectionProcessType, data: InspectionData): 'pass' | 'fail' | null {
|
|
|
|
|
switch (processType) {
|
|
|
|
|
case 'screen': {
|
|
|
|
|
const { processingStatus, sewingStatus, assemblyStatus, gapStatus, length, width } = data;
|
|
|
|
|
// 불량이 하나라도 있으면 즉시 부적합
|
|
|
|
|
if (processingStatus === 'bad' || sewingStatus === 'bad' || assemblyStatus === 'bad' || gapStatus === 'ng') return 'fail';
|
|
|
|
|
// 모든 상태 양호 + 측정값 입력 완료 시 적합
|
|
|
|
|
if (processingStatus === 'good' && sewingStatus === 'good' && assemblyStatus === 'good' && gapStatus === 'ok'
|
|
|
|
|
&& hasMeasurement(length) && hasMeasurement(width)) return 'pass';
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
case 'slat': {
|
|
|
|
|
const { processingStatus, assemblyStatus, height1, height2, length } = data;
|
|
|
|
|
if (processingStatus === 'bad' || assemblyStatus === 'bad') return 'fail';
|
|
|
|
|
if (processingStatus === 'good' && assemblyStatus === 'good'
|
|
|
|
|
&& hasMeasurement(height1) && hasMeasurement(height2) && hasMeasurement(length)) return 'pass';
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
case 'slat_jointbar': {
|
|
|
|
|
const { processingStatus, assemblyStatus, height1, height2, length3, gap4 } = data;
|
|
|
|
|
if (processingStatus === 'bad' || assemblyStatus === 'bad') return 'fail';
|
|
|
|
|
if (processingStatus === 'good' && assemblyStatus === 'good'
|
|
|
|
|
&& hasMeasurement(height1) && hasMeasurement(height2) && hasMeasurement(length3) && hasMeasurement(gap4)) return 'pass';
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
case 'bending': {
|
|
|
|
|
const { bendingStatus, length } = data;
|
|
|
|
|
if (bendingStatus === 'bad') return 'fail';
|
|
|
|
|
if (bendingStatus === 'good' && hasMeasurement(length)) return 'pass';
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
case 'bending_wip': {
|
|
|
|
|
const { bendingStatus, length, width } = data;
|
|
|
|
|
if (bendingStatus === 'bad') return 'fail';
|
|
|
|
|
if (bendingStatus === 'good' && hasMeasurement(length) && hasMeasurement(width)) return 'pass';
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 08:36:12 +09:00
|
|
|
// ===== Tolerance 기반 판정 유틸 =====
|
|
|
|
|
type ToleranceConfig = NonNullable<NonNullable<InspectionTemplateData['template']>['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<InspectionTemplateData['template']>;
|
|
|
|
|
formValues: Record<string, unknown>;
|
|
|
|
|
onValueChange: (key: string, value: unknown) => void;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-5">
|
|
|
|
|
{template.sections.map((section) => (
|
|
|
|
|
<div key={section.id} className="space-y-3">
|
|
|
|
|
<span className="text-sm font-bold text-gray-800">{section.name}</span>
|
|
|
|
|
{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 (
|
|
|
|
|
<div key={item.id} className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">{item.name}</span>
|
|
|
|
|
<div className="flex gap-2 flex-1">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onValueChange(fieldKey, 'good')}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex-1 py-2.5 rounded-lg text-sm font-bold transition-colors',
|
|
|
|
|
value === 'good'
|
|
|
|
|
? 'bg-black text-white'
|
|
|
|
|
: 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
양호
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onValueChange(fieldKey, 'bad')}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex-1 py-2.5 rounded-lg text-sm font-bold transition-colors',
|
|
|
|
|
value === 'bad'
|
|
|
|
|
? 'bg-black text-white'
|
|
|
|
|
: 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
불량
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 숫자 입력 (치수 등)
|
|
|
|
|
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 (
|
|
|
|
|
<div key={item.id} className="space-y-1.5">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="text-sm font-bold">
|
|
|
|
|
{item.name}{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={item.standard_criteria || '입력'}
|
|
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 동적 폼의 자동 판정 계산
|
|
|
|
|
function computeDynamicJudgment(
|
|
|
|
|
template: NonNullable<InspectionTemplateData['template']>,
|
|
|
|
|
formValues: Record<string, unknown>
|
|
|
|
|
): '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';
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 21:43:28 +09:00
|
|
|
export function InspectionInputModal({
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
processType,
|
|
|
|
|
productName = '',
|
|
|
|
|
specification = '',
|
2026-02-09 10:33:02 +09:00
|
|
|
initialData,
|
2026-02-05 21:43:28 +09:00
|
|
|
onComplete,
|
2026-02-10 08:36:12 +09:00
|
|
|
templateData,
|
2026-02-05 21:43:28 +09:00
|
|
|
}: InspectionInputModalProps) {
|
2026-02-10 08:36:12 +09:00
|
|
|
// 템플릿 모드 여부
|
|
|
|
|
const useTemplateMode = !!(templateData?.has_template && templateData.template);
|
|
|
|
|
|
2026-02-05 21:43:28 +09:00
|
|
|
const [formData, setFormData] = useState<InspectionData>({
|
|
|
|
|
productName,
|
|
|
|
|
specification,
|
|
|
|
|
judgment: null,
|
|
|
|
|
nonConformingContent: '',
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-10 08:36:12 +09:00
|
|
|
// 동적 폼 값 (템플릿 모드용)
|
|
|
|
|
const [dynamicFormValues, setDynamicFormValues] = useState<Record<string, unknown>>({});
|
|
|
|
|
|
2026-02-05 21:43:28 +09:00
|
|
|
// 절곡용 간격 포인트 초기화
|
|
|
|
|
const [gapPoints, setGapPoints] = useState<{ left: number | null; right: number | null }[]>(
|
|
|
|
|
Array(5).fill(null).map(() => ({ left: null, right: null }))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open) {
|
2026-02-09 10:33:02 +09:00
|
|
|
// initialData가 있으면 기존 저장 데이터로 복원
|
|
|
|
|
if (initialData) {
|
|
|
|
|
setFormData({
|
|
|
|
|
...initialData,
|
|
|
|
|
productName: initialData.productName || productName,
|
|
|
|
|
specification: initialData.specification || specification,
|
|
|
|
|
});
|
|
|
|
|
if (initialData.gapPoints) {
|
|
|
|
|
setGapPoints(initialData.gapPoints);
|
|
|
|
|
} else {
|
|
|
|
|
setGapPoints(Array(5).fill(null).map(() => ({ left: null, right: null })));
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 17:37:49 +09:00
|
|
|
// 공정별 기본값 설정 - 모두 미선택(null) 상태로 초기화
|
2026-02-05 21:43:28 +09:00
|
|
|
const baseData: InspectionData = {
|
|
|
|
|
productName,
|
|
|
|
|
specification,
|
2026-02-09 17:37:49 +09:00
|
|
|
judgment: null,
|
2026-02-05 21:43:28 +09:00
|
|
|
nonConformingContent: '',
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-09 17:37:49 +09:00
|
|
|
// 공정별 추가 기본값 설정 (모두 null)
|
2026-02-05 21:43:28 +09:00
|
|
|
switch (processType) {
|
|
|
|
|
case 'screen':
|
|
|
|
|
setFormData({
|
|
|
|
|
...baseData,
|
2026-02-09 17:37:49 +09:00
|
|
|
processingStatus: null,
|
|
|
|
|
sewingStatus: null,
|
|
|
|
|
assemblyStatus: null,
|
|
|
|
|
gapStatus: null,
|
2026-02-05 21:43:28 +09:00
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'slat':
|
|
|
|
|
setFormData({
|
|
|
|
|
...baseData,
|
2026-02-09 17:37:49 +09:00
|
|
|
processingStatus: null,
|
|
|
|
|
assemblyStatus: null,
|
2026-02-05 21:43:28 +09:00
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'slat_jointbar':
|
|
|
|
|
setFormData({
|
|
|
|
|
...baseData,
|
2026-02-09 17:37:49 +09:00
|
|
|
processingStatus: null,
|
|
|
|
|
assemblyStatus: null,
|
2026-02-05 21:43:28 +09:00
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'bending':
|
|
|
|
|
setFormData({
|
|
|
|
|
...baseData,
|
2026-02-09 17:37:49 +09:00
|
|
|
bendingStatus: null,
|
2026-02-05 21:43:28 +09:00
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'bending_wip':
|
|
|
|
|
setFormData({
|
|
|
|
|
...baseData,
|
2026-02-09 17:37:49 +09:00
|
|
|
bendingStatus: null,
|
2026-02-05 21:43:28 +09:00
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
setFormData(baseData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setGapPoints(Array(5).fill(null).map(() => ({ left: null, right: null })));
|
2026-02-10 08:36:12 +09:00
|
|
|
setDynamicFormValues({});
|
2026-02-05 21:43:28 +09:00
|
|
|
}
|
2026-02-09 10:33:02 +09:00
|
|
|
}, [open, productName, specification, processType, initialData]);
|
2026-02-05 21:43:28 +09:00
|
|
|
|
2026-02-10 08:36:12 +09:00
|
|
|
// 자동 판정 계산 (템플릿 모드 vs 레거시 모드)
|
|
|
|
|
const autoJudgment = useMemo(() => {
|
|
|
|
|
if (useTemplateMode && templateData?.template) {
|
|
|
|
|
return computeDynamicJudgment(templateData.template, dynamicFormValues);
|
|
|
|
|
}
|
|
|
|
|
return computeJudgment(processType, formData);
|
|
|
|
|
}, [useTemplateMode, templateData, dynamicFormValues, processType, formData]);
|
2026-02-09 21:31:07 +09:00
|
|
|
|
|
|
|
|
// 판정값 자동 동기화
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setFormData((prev) => {
|
|
|
|
|
if (prev.judgment === autoJudgment) return prev;
|
|
|
|
|
return { ...prev, judgment: autoJudgment };
|
|
|
|
|
});
|
|
|
|
|
}, [autoJudgment]);
|
|
|
|
|
|
2026-02-05 21:43:28 +09:00
|
|
|
const handleComplete = () => {
|
|
|
|
|
const data: InspectionData = {
|
|
|
|
|
...formData,
|
|
|
|
|
gapPoints: processType === 'bending' ? gapPoints : undefined,
|
2026-02-10 08:36:12 +09:00
|
|
|
// 동적 폼 값을 templateValues로 병합
|
|
|
|
|
...(useTemplateMode ? { templateValues: dynamicFormValues } : {}),
|
2026-02-05 21:43:28 +09:00
|
|
|
};
|
|
|
|
|
onComplete(data);
|
|
|
|
|
onOpenChange(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
onOpenChange(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 숫자 입력 핸들러
|
|
|
|
|
const handleNumberChange = (
|
|
|
|
|
key: keyof InspectionData,
|
|
|
|
|
value: string
|
|
|
|
|
) => {
|
|
|
|
|
const num = value === '' ? null : parseFloat(value);
|
|
|
|
|
setFormData((prev) => ({ ...prev, [key]: num }));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
2026-02-07 05:10:29 +09:00
|
|
|
<DialogContent className="w-[95vw] max-w-[500px] sm:max-w-[500px] max-h-[90vh] flex flex-col p-0">
|
|
|
|
|
<DialogHeader className="px-6 pt-6 pb-0 shrink-0">
|
2026-02-06 09:41:27 +09:00
|
|
|
<DialogTitle className="text-lg font-bold">
|
2026-02-05 21:43:28 +09:00
|
|
|
{PROCESS_TITLES[processType]}
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="flex-1 min-h-0 overflow-y-auto px-5 py-4 space-y-5">
|
|
|
|
|
{/* 기본 정보 (읽기전용) */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">제품명</span>
|
|
|
|
|
<Input
|
|
|
|
|
value={formData.productName}
|
|
|
|
|
readOnly
|
|
|
|
|
className="h-11 bg-gray-100 border-gray-300 rounded-lg"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">규격</span>
|
|
|
|
|
<Input
|
|
|
|
|
value={formData.specification}
|
|
|
|
|
readOnly
|
|
|
|
|
className="h-11 bg-gray-100 border-gray-300 rounded-lg"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-05 21:43:28 +09:00
|
|
|
</div>
|
|
|
|
|
|
2026-02-10 08:36:12 +09:00
|
|
|
{/* ===== 동적 폼 (템플릿 기반) ===== */}
|
|
|
|
|
{useTemplateMode && templateData?.template && (
|
|
|
|
|
<DynamicInspectionForm
|
|
|
|
|
template={templateData.template}
|
|
|
|
|
formValues={dynamicFormValues}
|
|
|
|
|
onValueChange={(key, value) =>
|
|
|
|
|
setDynamicFormValues((prev) => ({ ...prev, [key]: value }))
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* ===== 레거시: 공정별 하드코딩 검사 항목 (템플릿 없을 때만 표시) ===== */}
|
|
|
|
|
|
2026-02-05 21:43:28 +09:00
|
|
|
{/* ===== 재고생산 (bending_wip) 검사 항목 ===== */}
|
2026-02-10 08:36:12 +09:00
|
|
|
{!useTemplateMode && processType === 'bending_wip' && (
|
2026-02-05 21:43:28 +09:00
|
|
|
<>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">검모양 절곡상태</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<StatusToggle
|
|
|
|
|
value={formData.bendingStatus || null}
|
|
|
|
|
onChange={(v) => setFormData((prev) => ({ ...prev, bendingStatus: v }))}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">길이 (1,000)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="1,000"
|
|
|
|
|
value={formData.length ?? ''}
|
|
|
|
|
onChange={(e) => handleNumberChange('length', e.target.value)}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">너비 (1,000)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="1,000"
|
|
|
|
|
value={formData.width ?? ''}
|
|
|
|
|
onChange={(e) => handleNumberChange('width', e.target.value)}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">간격 (1,000)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="①"
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="1,000"
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* ===== 스크린 검사 항목 ===== */}
|
2026-02-10 08:36:12 +09:00
|
|
|
{!useTemplateMode && processType === 'screen' && (
|
2026-02-05 21:43:28 +09:00
|
|
|
<>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">검모양 가공상태</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<StatusToggle
|
|
|
|
|
value={formData.processingStatus || null}
|
|
|
|
|
onChange={(v) => setFormData((prev) => ({ ...prev, processingStatus: v }))}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">검모양 재봉상태</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<StatusToggle
|
|
|
|
|
value={formData.sewingStatus || null}
|
|
|
|
|
onChange={(v) => setFormData((prev) => ({ ...prev, sewingStatus: v }))}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">검모양 조립상태</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<StatusToggle
|
|
|
|
|
value={formData.assemblyStatus || null}
|
|
|
|
|
onChange={(v) => setFormData((prev) => ({ ...prev, assemblyStatus: v }))}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">길이 (1,000)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="1,000"
|
|
|
|
|
value={formData.length ?? ''}
|
|
|
|
|
onChange={(e) => handleNumberChange('length', e.target.value)}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">너비 (1,000)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="1,000"
|
|
|
|
|
value={formData.width ?? ''}
|
|
|
|
|
onChange={(e) => handleNumberChange('width', e.target.value)}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">간격 (400 이하)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<OkNgToggle
|
|
|
|
|
value={formData.gapStatus || null}
|
|
|
|
|
onChange={(v) => setFormData((prev) => ({ ...prev, gapStatus: v }))}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* ===== 슬랫 검사 항목 ===== */}
|
2026-02-10 08:36:12 +09:00
|
|
|
{!useTemplateMode && processType === 'slat' && (
|
2026-02-05 21:43:28 +09:00
|
|
|
<>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">검모양 가공상태</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<StatusToggle
|
|
|
|
|
value={formData.processingStatus || null}
|
|
|
|
|
onChange={(v) => setFormData((prev) => ({ ...prev, processingStatus: v }))}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">검모양 조립상태</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<StatusToggle
|
|
|
|
|
value={formData.assemblyStatus || null}
|
|
|
|
|
onChange={(v) => setFormData((prev) => ({ ...prev, assemblyStatus: v }))}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">① 높이 (16.5 ± 1)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="16.5"
|
|
|
|
|
value={formData.height1 ?? ''}
|
|
|
|
|
onChange={(e) => handleNumberChange('height1', e.target.value)}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">② 높이 (14.5 ± 1)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="14.5"
|
|
|
|
|
value={formData.height2 ?? ''}
|
|
|
|
|
onChange={(e) => handleNumberChange('height2', e.target.value)}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">길이 (0)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="0"
|
|
|
|
|
value={formData.length ?? ''}
|
|
|
|
|
onChange={(e) => handleNumberChange('length', e.target.value)}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* ===== 조인트바 검사 항목 ===== */}
|
2026-02-10 08:36:12 +09:00
|
|
|
{!useTemplateMode && processType === 'slat_jointbar' && (
|
2026-02-05 21:43:28 +09:00
|
|
|
<>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">검모양 가공상태</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<StatusToggle
|
|
|
|
|
value={formData.processingStatus || null}
|
|
|
|
|
onChange={(v) => setFormData((prev) => ({ ...prev, processingStatus: v }))}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">검모양 조립상태</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<StatusToggle
|
|
|
|
|
value={formData.assemblyStatus || null}
|
|
|
|
|
onChange={(v) => setFormData((prev) => ({ ...prev, assemblyStatus: v }))}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">① 높이 (16.5 ± 1)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="16.5"
|
|
|
|
|
value={formData.height1 ?? ''}
|
|
|
|
|
onChange={(e) => handleNumberChange('height1', e.target.value)}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">② 높이 (14.5 ± 1)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="14.5"
|
|
|
|
|
value={formData.height2 ?? ''}
|
|
|
|
|
onChange={(e) => handleNumberChange('height2', e.target.value)}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">③ 길이 (300 ± 1)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="300"
|
|
|
|
|
value={formData.length3 ?? ''}
|
|
|
|
|
onChange={(e) => handleNumberChange('length3', e.target.value)}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">④ 간격 (150 ± 1)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="150"
|
|
|
|
|
value={formData.gap4 ?? ''}
|
|
|
|
|
onChange={(e) => handleNumberChange('gap4', e.target.value)}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* ===== 절곡 검사 항목 ===== */}
|
2026-02-10 08:36:12 +09:00
|
|
|
{!useTemplateMode && processType === 'bending' && (
|
2026-02-05 21:43:28 +09:00
|
|
|
<>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">검모양 절곡상태</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<StatusToggle
|
|
|
|
|
value={formData.bendingStatus || null}
|
|
|
|
|
onChange={(v) => setFormData((prev) => ({ ...prev, bendingStatus: v }))}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">길이 (1,000)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="1,000"
|
|
|
|
|
value={formData.length ?? ''}
|
|
|
|
|
onChange={(e) => handleNumberChange('length', e.target.value)}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-07 05:10:29 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">너비 (N/A)</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="N/A"
|
|
|
|
|
value={formData.width ?? 'N/A'}
|
|
|
|
|
readOnly
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 bg-gray-100 border-gray-300 rounded-lg"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-3">
|
2026-02-07 05:10:29 +09:00
|
|
|
<span className="text-sm font-bold">간격</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
{gapPoints.map((point, index) => (
|
|
|
|
|
<div key={index} className="grid grid-cols-3 gap-2 items-center">
|
2026-02-07 05:10:29 +09:00
|
|
|
<span className="text-gray-500 text-sm font-medium">⑤{index + 1}</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder={String(30 + index * 10)}
|
|
|
|
|
value={point.left ?? ''}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const newPoints = [...gapPoints];
|
|
|
|
|
newPoints[index] = {
|
|
|
|
|
...newPoints[index],
|
|
|
|
|
left: e.target.value === '' ? null : parseFloat(e.target.value),
|
|
|
|
|
};
|
|
|
|
|
setGapPoints(newPoints);
|
|
|
|
|
}}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder={String(30 + index * 10)}
|
|
|
|
|
value={point.right ?? ''}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const newPoints = [...gapPoints];
|
|
|
|
|
newPoints[index] = {
|
|
|
|
|
...newPoints[index],
|
|
|
|
|
right: e.target.value === '' ? null : parseFloat(e.target.value),
|
|
|
|
|
};
|
|
|
|
|
setGapPoints(newPoints);
|
|
|
|
|
}}
|
2026-02-07 05:10:29 +09:00
|
|
|
className="h-11 rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-07 05:10:29 +09:00
|
|
|
{/* 부적합 내용 */}
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<span className="text-sm font-bold">부적합 내용</span>
|
2026-02-05 21:43:28 +09:00
|
|
|
<Textarea
|
|
|
|
|
value={formData.nonConformingContent}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setFormData((prev) => ({ ...prev, nonConformingContent: e.target.value }))
|
|
|
|
|
}
|
|
|
|
|
placeholder="입력 시 '일련번호: 내용' 형태로 취합되어 표시"
|
2026-02-07 05:10:29 +09:00
|
|
|
className="min-h-[80px] rounded-lg border-gray-300"
|
2026-02-05 21:43:28 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-07 05:10:29 +09:00
|
|
|
{/* 하단 고정: 판정 + 버튼 */}
|
|
|
|
|
<div className="shrink-0 border-t bg-white px-5 pb-5 pt-3 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
|
|
|
|
|
<div className="space-y-2 mb-4">
|
2026-02-09 21:31:07 +09:00
|
|
|
<span className="text-sm font-bold">판정 (자동)</span>
|
|
|
|
|
<JudgmentDisplay value={autoJudgment} />
|
2026-02-07 05:10:29 +09:00
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={handleCancel}
|
|
|
|
|
className="flex-1 h-11 rounded-lg border-gray-300 font-bold"
|
|
|
|
|
>
|
|
|
|
|
취소
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={handleComplete}
|
|
|
|
|
className="flex-1 h-11 rounded-lg bg-black hover:bg-gray-800 text-white font-bold"
|
|
|
|
|
>
|
|
|
|
|
검사 완료
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-02-05 21:43:28 +09:00
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|