Files
sam-react-prod/src/components/material/ReceivingManagement/ImportInspectionInputModal.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

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>
);
}