Files
sam-react-prod/src/components/production/WorkerScreen/InspectionInputModal.tsx
유병철 881f4668da fix(WEB): 검사입력 모달 UI/UX 개선
- ImportInspectionInputModal 수입검사 입력 모달 개선
- InspectionInputModal 작업자화면 검사입력 개선
- ProductInspectionInputModal 제품검사 입력 개선
- WipProductionModal 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 09:41:27 +09:00

654 lines
22 KiB
TypeScript

'use client';
/**
* 중간검사 입력 모달
*
* 공정별로 다른 검사 항목 표시:
* - screen: 스크린 중간검사
* - slat: 슬랫 중간검사
* - slat_jointbar: 조인트바 중간검사
* - bending: 절곡 중간검사
* - bending_wip: 재고생산(재공품) 중간검사
*/
import { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
// 중간검사 공정 타입
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;
}
interface InspectionInputModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
processType: InspectionProcessType;
productName?: string;
specification?: string;
onComplete: (data: InspectionData) => void;
}
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 (
<div className="flex gap-2">
<button
type="button"
onClick={() => onChange('good')}
className={cn(
'px-4 py-2 rounded-lg text-sm font-medium transition-colors',
value === 'good'
? 'bg-orange-500 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
)}
>
</button>
<button
type="button"
onClick={() => onChange('bad')}
className={cn(
'px-4 py-2 rounded-lg text-sm font-medium transition-colors',
value === 'bad'
? 'bg-gray-700 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
)}
>
</button>
</div>
);
}
// OK/NG 버튼 컴포넌트
function OkNgToggle({
value,
onChange,
}: {
value: 'ok' | 'ng' | null;
onChange: (v: 'ok' | 'ng') => void;
}) {
return (
<div className="flex gap-2">
<button
type="button"
onClick={() => onChange('ok')}
className={cn(
'px-6 py-2 rounded-lg text-sm font-medium transition-colors',
value === 'ok'
? 'bg-gray-700 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
)}
>
OK
</button>
<button
type="button"
onClick={() => onChange('ng')}
className={cn(
'px-6 py-2 rounded-lg text-sm font-medium transition-colors',
value === 'ng'
? 'bg-gray-700 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
)}
>
NG
</button>
</div>
);
}
// 적합/부적합 버튼 컴포넌트
function JudgmentToggle({
value,
onChange,
}: {
value: 'pass' | 'fail' | null;
onChange: (v: 'pass' | 'fail') => void;
}) {
return (
<div className="flex gap-2">
<button
type="button"
onClick={() => onChange('pass')}
className={cn(
'px-4 py-2 rounded-lg text-sm font-medium transition-colors',
value === 'pass'
? 'bg-orange-500 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
)}
>
</button>
<button
type="button"
onClick={() => onChange('fail')}
className={cn(
'px-4 py-2 rounded-lg text-sm font-medium transition-colors',
value === 'fail'
? 'bg-gray-700 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
)}
>
</button>
</div>
);
}
export function InspectionInputModal({
open,
onOpenChange,
processType,
productName = '',
specification = '',
onComplete,
}: InspectionInputModalProps) {
const [formData, setFormData] = useState<InspectionData>({
productName,
specification,
judgment: null,
nonConformingContent: '',
});
// 절곡용 간격 포인트 초기화
const [gapPoints, setGapPoints] = useState<{ left: number | null; right: number | null }[]>(
Array(5).fill(null).map(() => ({ left: null, right: null }))
);
useEffect(() => {
if (open) {
// 공정별 기본값 설정 - 모두 양호/OK/적합 상태로 초기화
const baseData: InspectionData = {
productName,
specification,
judgment: 'pass', // 기본값: 적합
nonConformingContent: '',
};
// 공정별 추가 기본값 설정
switch (processType) {
case 'screen':
setFormData({
...baseData,
processingStatus: 'good', // 가공상태: 양호
sewingStatus: 'good', // 재봉상태: 양호
assemblyStatus: 'good', // 조립상태: 양호
gapStatus: 'ok', // 간격: OK
});
break;
case 'slat':
setFormData({
...baseData,
processingStatus: 'good', // 가공상태: 양호
assemblyStatus: 'good', // 조립상태: 양호
});
break;
case 'slat_jointbar':
setFormData({
...baseData,
processingStatus: 'good', // 가공상태: 양호
assemblyStatus: 'good', // 조립상태: 양호
});
break;
case 'bending':
setFormData({
...baseData,
bendingStatus: 'good', // 절곡상태: 양호
});
break;
case 'bending_wip':
setFormData({
...baseData,
bendingStatus: 'good', // 절곡상태: 양호
});
break;
default:
setFormData(baseData);
}
setGapPoints(Array(5).fill(null).map(() => ({ left: null, right: null })));
}
}, [open, productName, specification, processType]);
const handleComplete = () => {
const data: InspectionData = {
...formData,
gapPoints: processType === 'bending' ? gapPoints : undefined,
};
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}>
<DialogContent className="w-[95vw] max-w-[500px] sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="text-lg font-bold">
{PROCESS_TITLES[processType]}
</DialogTitle>
</DialogHeader>
<div className="space-y-6 mt-4 max-h-[70vh] overflow-y-auto pr-2">
{/* 제품명 */}
<div className="space-y-2">
<Label className="text-muted-foreground"></Label>
<Input
value={formData.productName}
readOnly
className=""
/>
</div>
{/* 규격 */}
<div className="space-y-2">
<Label className="text-muted-foreground"></Label>
<Input
value={formData.specification}
readOnly
className=""
/>
</div>
{/* ===== 재고생산 (bending_wip) 검사 항목 ===== */}
{processType === 'bending_wip' && (
<>
<div className="space-y-2">
<Label className="text-muted-foreground"> </Label>
<StatusToggle
value={formData.bendingStatus || null}
onChange={(v) => setFormData((prev) => ({ ...prev, bendingStatus: v }))}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-muted-foreground"> (1,000)</Label>
<Input
type="number"
placeholder="1,000"
value={formData.length ?? ''}
onChange={(e) => handleNumberChange('length', e.target.value)}
className=""
/>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground"> (1,000)</Label>
<Input
type="number"
placeholder="1,000"
value={formData.width ?? ''}
onChange={(e) => handleNumberChange('width', e.target.value)}
className=""
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-muted-foreground"> (1,000)</Label>
<div className="flex gap-2">
<Input
type="number"
placeholder="①"
className=""
/>
<Input
type="number"
placeholder="1,000"
className=""
/>
</div>
</div>
</div>
</>
)}
{/* ===== 스크린 검사 항목 ===== */}
{processType === 'screen' && (
<>
<div className="space-y-2">
<Label className="text-muted-foreground"> </Label>
<StatusToggle
value={formData.processingStatus || null}
onChange={(v) => setFormData((prev) => ({ ...prev, processingStatus: v }))}
/>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground"> </Label>
<StatusToggle
value={formData.sewingStatus || null}
onChange={(v) => setFormData((prev) => ({ ...prev, sewingStatus: v }))}
/>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground"> </Label>
<StatusToggle
value={formData.assemblyStatus || null}
onChange={(v) => setFormData((prev) => ({ ...prev, assemblyStatus: v }))}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-muted-foreground"> (1,000)</Label>
<Input
type="number"
placeholder="1,000"
value={formData.length ?? ''}
onChange={(e) => handleNumberChange('length', e.target.value)}
className=""
/>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground"> (1,000)</Label>
<Input
type="number"
placeholder="1,000"
value={formData.width ?? ''}
onChange={(e) => handleNumberChange('width', e.target.value)}
className=""
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground"> (400 )</Label>
<OkNgToggle
value={formData.gapStatus || null}
onChange={(v) => setFormData((prev) => ({ ...prev, gapStatus: v }))}
/>
</div>
</>
)}
{/* ===== 슬랫 검사 항목 ===== */}
{processType === 'slat' && (
<>
<div className="space-y-2">
<Label className="text-muted-foreground"> </Label>
<StatusToggle
value={formData.processingStatus || null}
onChange={(v) => setFormData((prev) => ({ ...prev, processingStatus: v }))}
/>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground"> </Label>
<StatusToggle
value={formData.assemblyStatus || null}
onChange={(v) => setFormData((prev) => ({ ...prev, assemblyStatus: v }))}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-muted-foreground"> (16.5 ± 1)</Label>
<Input
type="number"
placeholder="16.5"
value={formData.height1 ?? ''}
onChange={(e) => handleNumberChange('height1', e.target.value)}
className=""
/>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground"> (14.5 ± 1)</Label>
<Input
type="number"
placeholder="14.5"
value={formData.height2 ?? ''}
onChange={(e) => handleNumberChange('height2', e.target.value)}
className=""
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground"> (0)</Label>
<Input
type="number"
placeholder="0"
value={formData.length ?? ''}
onChange={(e) => handleNumberChange('length', e.target.value)}
className=""
/>
</div>
</>
)}
{/* ===== 조인트바 검사 항목 ===== */}
{processType === 'slat_jointbar' && (
<>
<div className="space-y-2">
<Label className="text-muted-foreground"> </Label>
<StatusToggle
value={formData.processingStatus || null}
onChange={(v) => setFormData((prev) => ({ ...prev, processingStatus: v }))}
/>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground"> </Label>
<StatusToggle
value={formData.assemblyStatus || null}
onChange={(v) => setFormData((prev) => ({ ...prev, assemblyStatus: v }))}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-muted-foreground"> (16.5 ± 1)</Label>
<Input
type="number"
placeholder="16.5"
value={formData.height1 ?? ''}
onChange={(e) => handleNumberChange('height1', e.target.value)}
className=""
/>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground"> (14.5 ± 1)</Label>
<Input
type="number"
placeholder="14.5"
value={formData.height2 ?? ''}
onChange={(e) => handleNumberChange('height2', e.target.value)}
className=""
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-muted-foreground"> (300 ± 1)</Label>
<Input
type="number"
placeholder="300"
value={formData.length3 ?? ''}
onChange={(e) => handleNumberChange('length3', e.target.value)}
className=""
/>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground"> (150 ± 1)</Label>
<Input
type="number"
placeholder="150"
value={formData.gap4 ?? ''}
onChange={(e) => handleNumberChange('gap4', e.target.value)}
className=""
/>
</div>
</div>
</>
)}
{/* ===== 절곡 검사 항목 ===== */}
{processType === 'bending' && (
<>
<div className="space-y-2">
<Label className="text-muted-foreground"> </Label>
<StatusToggle
value={formData.bendingStatus || null}
onChange={(v) => setFormData((prev) => ({ ...prev, bendingStatus: v }))}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-muted-foreground"> (1,000)</Label>
<Input
type="number"
placeholder="1,000"
value={formData.length ?? ''}
onChange={(e) => handleNumberChange('length', e.target.value)}
className=""
/>
</div>
<div className="space-y-2">
<Label className="text-muted-foreground"> (N/A)</Label>
<Input
type="text"
placeholder="N/A"
value={formData.width ?? 'N/A'}
readOnly
className=""
/>
</div>
</div>
<div className="space-y-3">
<Label className="text-muted-foreground"></Label>
{gapPoints.map((point, index) => (
<div key={index} className="grid grid-cols-3 gap-2 items-center">
<span className="text-muted-foreground text-sm">{index + 1}</span>
<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);
}}
className=""
/>
<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);
}}
className=""
/>
</div>
))}
</div>
</>
)}
{/* 공통: 판정 */}
<div className="space-y-2">
<Label className="text-muted-foreground"></Label>
<JudgmentToggle
value={formData.judgment}
onChange={(v) => setFormData((prev) => ({ ...prev, judgment: v }))}
/>
</div>
{/* 공통: 부적합 내용 */}
<div className="space-y-2">
<Label className="text-muted-foreground"> </Label>
<Textarea
value={formData.nonConformingContent}
onChange={(e) =>
setFormData((prev) => ({ ...prev, nonConformingContent: e.target.value }))
}
placeholder="입력 시 '일련번호: 내용' 형태로 취합되어 표시"
className="min-h-[80px]"
/>
</div>
</div>
{/* 버튼 영역 */}
<div className="flex gap-3 mt-6">
<Button
variant="outline"
onClick={handleCancel}
className="flex-1"
>
</Button>
<Button
onClick={handleComplete}
className="flex-1 bg-orange-500 hover:bg-orange-600 text-white"
>
</Button>
</div>
</DialogContent>
</Dialog>
);
}