- ImportInspectionInputModal 수입검사 입력 모달 개선 - InspectionInputModal 작업자화면 검사입력 개선 - ProductInspectionInputModal 제품검사 입력 개선 - WipProductionModal 수정 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
366 lines
10 KiB
TypeScript
366 lines
10 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 수입검사 입력 모달
|
|
*
|
|
* 작업자 화면 중간검사 모달 양식 참고
|
|
* 기획서: 스크린샷 2026-02-05 오후 9.58.16
|
|
*/
|
|
|
|
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';
|
|
|
|
// 시료 탭 타입
|
|
type SampleTab = 'N1' | 'N2' | 'N3';
|
|
|
|
// 검사 결과 데이터 타입
|
|
export interface ImportInspectionData {
|
|
sampleTab: SampleTab;
|
|
productName: string;
|
|
specification: string;
|
|
// 겉모양
|
|
appearanceStatus: 'ok' | 'ng' | null;
|
|
// 치수
|
|
thickness: number | null;
|
|
width: number | null;
|
|
length: number | null;
|
|
// 판정
|
|
judgment: 'pass' | 'fail' | null;
|
|
// 물성치
|
|
tensileStrength: number | null; // 인장강도 (270 이상)
|
|
elongation: number | null; // 연신율 (36 이상)
|
|
zincCoating: number | null; // 아연의 최소 부착량 (17 이상)
|
|
// 내용
|
|
content: string;
|
|
}
|
|
|
|
interface ImportInspectionInputModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
productName?: string;
|
|
specification?: string;
|
|
onComplete: (data: ImportInspectionData) => void;
|
|
}
|
|
|
|
// 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 ImportInspectionInputModal({
|
|
open,
|
|
onOpenChange,
|
|
productName = '',
|
|
specification = '',
|
|
onComplete,
|
|
}: ImportInspectionInputModalProps) {
|
|
const [formData, setFormData] = useState<ImportInspectionData>({
|
|
sampleTab: 'N1',
|
|
productName,
|
|
specification,
|
|
appearanceStatus: 'ok',
|
|
thickness: 1.55,
|
|
width: 1219,
|
|
length: 480,
|
|
judgment: 'pass',
|
|
tensileStrength: null,
|
|
elongation: null,
|
|
zincCoating: null,
|
|
content: '',
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
// 모달 열릴 때 초기화 - 기본값 적합 상태
|
|
setFormData({
|
|
sampleTab: 'N1',
|
|
productName,
|
|
specification,
|
|
appearanceStatus: 'ok',
|
|
thickness: 1.55,
|
|
width: 1219,
|
|
length: 480,
|
|
judgment: 'pass',
|
|
tensileStrength: null,
|
|
elongation: null,
|
|
zincCoating: null,
|
|
content: '',
|
|
});
|
|
}
|
|
}, [open, productName, specification]);
|
|
|
|
const handleComplete = () => {
|
|
onComplete(formData);
|
|
onOpenChange(false);
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
onOpenChange(false);
|
|
};
|
|
|
|
// 숫자 입력 핸들러
|
|
const handleNumberChange = (
|
|
key: keyof ImportInspectionData,
|
|
value: string
|
|
) => {
|
|
const num = value === '' ? null : parseFloat(value);
|
|
setFormData((prev) => ({ ...prev, [key]: num }));
|
|
};
|
|
|
|
const sampleTabs: SampleTab[] = ['N1', 'N2', 'N3'];
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="w-[95vw] max-w-[500px] sm:max-w-[500px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-lg font-bold">
|
|
수입검사
|
|
</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>
|
|
|
|
{/* 시료 탭: N1, N2, N3 */}
|
|
<div className="flex gap-2">
|
|
{sampleTabs.map((tab) => (
|
|
<button
|
|
key={tab}
|
|
type="button"
|
|
onClick={() => setFormData((prev) => ({ ...prev, sampleTab: tab }))}
|
|
className={cn(
|
|
'px-6 py-2 rounded-lg text-sm font-medium transition-colors',
|
|
formData.sampleTab === tab
|
|
? 'bg-orange-500 text-white'
|
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
)}
|
|
>
|
|
{tab}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* 겉모양: OK/NG */}
|
|
<div className="space-y-2">
|
|
<Label className="text-muted-foreground">겉모양</Label>
|
|
<OkNgToggle
|
|
value={formData.appearanceStatus}
|
|
onChange={(v) => setFormData((prev) => ({ ...prev, appearanceStatus: v }))}
|
|
/>
|
|
</div>
|
|
|
|
{/* 두께 / 너비 */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label className="text-muted-foreground">두께 (1.55)</Label>
|
|
<Input
|
|
type="number"
|
|
step="0.01"
|
|
placeholder="1.55"
|
|
value={formData.thickness ?? ''}
|
|
onChange={(e) => handleNumberChange('thickness', e.target.value)}
|
|
className=""
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label className="text-muted-foreground">너비 (1219)</Label>
|
|
<Input
|
|
type="number"
|
|
placeholder="1219"
|
|
value={formData.width ?? ''}
|
|
onChange={(e) => handleNumberChange('width', e.target.value)}
|
|
className=""
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 길이 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-muted-foreground">길이 (480)</Label>
|
|
<Input
|
|
type="number"
|
|
placeholder="480"
|
|
value={formData.length ?? ''}
|
|
onChange={(e) => handleNumberChange('length', e.target.value)}
|
|
className=""
|
|
/>
|
|
</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="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label className="text-muted-foreground">인장강도 (270 이상)</Label>
|
|
<Input
|
|
type="number"
|
|
placeholder=""
|
|
value={formData.tensileStrength ?? ''}
|
|
onChange={(e) => handleNumberChange('tensileStrength', e.target.value)}
|
|
className=""
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label className="text-muted-foreground">연신율 (36 이상)</Label>
|
|
<Input
|
|
type="number"
|
|
placeholder=""
|
|
value={formData.elongation ?? ''}
|
|
onChange={(e) => handleNumberChange('elongation', e.target.value)}
|
|
className=""
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 아연의 최소 부착량 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-muted-foreground">아연의 최소 부착량 (17 이상)</Label>
|
|
<Input
|
|
type="number"
|
|
placeholder=""
|
|
value={formData.zincCoating ?? ''}
|
|
onChange={(e) => handleNumberChange('zincCoating', e.target.value)}
|
|
className=""
|
|
/>
|
|
</div>
|
|
|
|
{/* 내용 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-muted-foreground">내용</Label>
|
|
<Textarea
|
|
value={formData.content}
|
|
onChange={(e) =>
|
|
setFormData((prev) => ({ ...prev, content: 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>
|
|
);
|
|
} |