320 lines
12 KiB
TypeScript
320 lines
12 KiB
TypeScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 검사 등록 페이지
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { useState, useCallback } from 'react';
|
|||
|
|
import { useRouter } from 'next/navigation';
|
|||
|
|
import { ClipboardCheck, ImageIcon } from 'lucide-react';
|
|||
|
|
import { Button } from '@/components/ui/button';
|
|||
|
|
import { Input } from '@/components/ui/input';
|
|||
|
|
import { Label } from '@/components/ui/label';
|
|||
|
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|||
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|||
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|||
|
|
import { PageLayout } from '@/components/organisms/PageLayout';
|
|||
|
|
import { inspectionItemsTemplate, judgeMeasurement } from './mockData';
|
|||
|
|
import type { InspectionItem, QualityCheckItem, MeasurementItem } from './types';
|
|||
|
|
|
|||
|
|
export function InspectionCreate() {
|
|||
|
|
const router = useRouter();
|
|||
|
|
|
|||
|
|
// 폼 상태
|
|||
|
|
const [formData, setFormData] = useState({
|
|||
|
|
lotNo: 'WO-251219-05', // 자동 (예시)
|
|||
|
|
itemName: '조인트바', // 자동 (예시)
|
|||
|
|
processName: '조립 공정', // 자동 (예시)
|
|||
|
|
quantity: 50,
|
|||
|
|
inspector: '',
|
|||
|
|
remarks: '',
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 검사 항목 상태
|
|||
|
|
const [inspectionItems, setInspectionItems] = useState<InspectionItem[]>(
|
|||
|
|
inspectionItemsTemplate.map(item => ({ ...item }))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// validation 에러 상태
|
|||
|
|
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
|||
|
|
|
|||
|
|
// 폼 입력 핸들러
|
|||
|
|
const handleInputChange = (field: string, value: string | number) => {
|
|||
|
|
setFormData(prev => ({ ...prev, [field]: value }));
|
|||
|
|
// 입력 시 에러 클리어
|
|||
|
|
if (validationErrors.length > 0) {
|
|||
|
|
setValidationErrors([]);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 품질 검사 항목 결과 변경 (양호/불량)
|
|||
|
|
const handleQualityResultChange = useCallback((itemId: string, result: '양호' | '불량') => {
|
|||
|
|
setInspectionItems(prev => prev.map(item => {
|
|||
|
|
if (item.id === itemId && item.type === 'quality') {
|
|||
|
|
return {
|
|||
|
|
...item,
|
|||
|
|
result,
|
|||
|
|
judgment: result === '양호' ? '적합' : '부적합',
|
|||
|
|
} as QualityCheckItem;
|
|||
|
|
}
|
|||
|
|
return item;
|
|||
|
|
}));
|
|||
|
|
// 입력 시 에러 클리어
|
|||
|
|
setValidationErrors([]);
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
// 측정 항목 값 변경
|
|||
|
|
const handleMeasurementChange = useCallback((itemId: string, value: string) => {
|
|||
|
|
setInspectionItems(prev => prev.map(item => {
|
|||
|
|
if (item.id === itemId && item.type === 'measurement') {
|
|||
|
|
const measuredValue = parseFloat(value) || 0;
|
|||
|
|
const judgment = judgeMeasurement(item.spec, measuredValue);
|
|||
|
|
return {
|
|||
|
|
...item,
|
|||
|
|
measuredValue,
|
|||
|
|
judgment,
|
|||
|
|
} as MeasurementItem;
|
|||
|
|
}
|
|||
|
|
return item;
|
|||
|
|
}));
|
|||
|
|
// 입력 시 에러 클리어
|
|||
|
|
setValidationErrors([]);
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
// 취소
|
|||
|
|
const handleCancel = () => {
|
|||
|
|
router.push('/quality/inspections');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// validation 체크
|
|||
|
|
const validateForm = (): boolean => {
|
|||
|
|
const errors: string[] = [];
|
|||
|
|
|
|||
|
|
// 필수 필드: 작업자
|
|||
|
|
if (!formData.inspector.trim()) {
|
|||
|
|
errors.push('작업자는 필수 입력 항목입니다.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 검사 항목 validation
|
|||
|
|
inspectionItems.forEach((item, index) => {
|
|||
|
|
if (item.type === 'quality') {
|
|||
|
|
const qualityItem = item as QualityCheckItem;
|
|||
|
|
if (!qualityItem.result) {
|
|||
|
|
errors.push(`${index + 1}. ${item.name}: 결과를 선택해주세요.`);
|
|||
|
|
}
|
|||
|
|
} else if (item.type === 'measurement') {
|
|||
|
|
const measurementItem = item as MeasurementItem;
|
|||
|
|
if (measurementItem.measuredValue === undefined || measurementItem.measuredValue === null) {
|
|||
|
|
errors.push(`${index + 1}. ${item.name}: 측정값을 입력해주세요.`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
setValidationErrors(errors);
|
|||
|
|
return errors.length === 0;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 검사완료
|
|||
|
|
const handleSubmit = () => {
|
|||
|
|
// validation 체크
|
|||
|
|
if (!validateForm()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TODO: API 호출
|
|||
|
|
console.log('Submit:', { ...formData, items: inspectionItems });
|
|||
|
|
router.push('/quality/inspections');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<PageLayout>
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
{/* 헤더 */}
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
<ClipboardCheck className="w-6 h-6" />
|
|||
|
|
<h1 className="text-xl font-semibold">검사 등록</h1>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<Button variant="outline" onClick={handleCancel}>
|
|||
|
|
취소
|
|||
|
|
</Button>
|
|||
|
|
<Button onClick={handleSubmit}>
|
|||
|
|
검사완료
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Validation 에러 표시 */}
|
|||
|
|
{validationErrors.length > 0 && (
|
|||
|
|
<Alert className="bg-red-50 border-red-200">
|
|||
|
|
<AlertDescription className="text-red-900">
|
|||
|
|
<div className="flex items-start gap-2">
|
|||
|
|
<span className="text-lg">⚠️</span>
|
|||
|
|
<div className="flex-1">
|
|||
|
|
<strong className="block mb-2">
|
|||
|
|
입력 내용을 확인해주세요 ({validationErrors.length}개 오류)
|
|||
|
|
</strong>
|
|||
|
|
<ul className="space-y-1 text-sm">
|
|||
|
|
{validationErrors.map((error, index) => (
|
|||
|
|
<li key={index} className="flex items-start gap-1">
|
|||
|
|
<span>•</span>
|
|||
|
|
<span>{error}</span>
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</AlertDescription>
|
|||
|
|
</Alert>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 검사 개요 */}
|
|||
|
|
<Card>
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="text-base">검사 개요</CardTitle>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent>
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="text-muted-foreground">LOT NO (자동)</Label>
|
|||
|
|
<Input
|
|||
|
|
value={formData.lotNo}
|
|||
|
|
disabled
|
|||
|
|
className="bg-muted"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="text-muted-foreground">품목명 (자동)</Label>
|
|||
|
|
<Input
|
|||
|
|
value={formData.itemName}
|
|||
|
|
disabled
|
|||
|
|
className="bg-muted"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="text-muted-foreground">공정명 (자동)</Label>
|
|||
|
|
<Input
|
|||
|
|
value={formData.processName}
|
|||
|
|
disabled
|
|||
|
|
className="bg-muted"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label>수량</Label>
|
|||
|
|
<Input
|
|||
|
|
type="number"
|
|||
|
|
value={formData.quantity}
|
|||
|
|
onChange={(e) => handleInputChange('quantity', parseInt(e.target.value) || 0)}
|
|||
|
|
placeholder="수량 입력"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label>작업자 *</Label>
|
|||
|
|
<Input
|
|||
|
|
value={formData.inspector}
|
|||
|
|
onChange={(e) => handleInputChange('inspector', e.target.value)}
|
|||
|
|
placeholder="작업자 입력"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div className="space-y-2 md:col-span-3">
|
|||
|
|
<Label>특이사항</Label>
|
|||
|
|
<Input
|
|||
|
|
value={formData.remarks}
|
|||
|
|
onChange={(e) => handleInputChange('remarks', e.target.value)}
|
|||
|
|
placeholder="특이사항 입력"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* 검사 기준 및 도해 */}
|
|||
|
|
<Card>
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="text-base">검사 기준 및 도해</CardTitle>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent>
|
|||
|
|
<div className="flex items-center justify-center h-48 bg-muted rounded-lg border-2 border-dashed">
|
|||
|
|
<div className="text-center text-muted-foreground">
|
|||
|
|
<ImageIcon className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
|||
|
|
<p>템플릿에서 설정한 조인트바 표준 도면이 표시됨</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* 검사 데이터 입력 */}
|
|||
|
|
<Card>
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="text-base">검사 데이터 입력</CardTitle>
|
|||
|
|
<p className="text-sm text-muted-foreground">
|
|||
|
|
* 측정값을 입력하면 판정이 자동 처리됩니다.
|
|||
|
|
</p>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent className="space-y-6">
|
|||
|
|
{inspectionItems.map((item, index) => (
|
|||
|
|
<div key={item.id} className="p-4 border rounded-lg">
|
|||
|
|
<div className="flex items-center justify-between mb-4">
|
|||
|
|
<h4 className="font-medium">
|
|||
|
|
{index + 1}. {item.name}
|
|||
|
|
{item.type === 'measurement' && ` (${(item as MeasurementItem).unit})`}
|
|||
|
|
</h4>
|
|||
|
|
<span className={`text-sm font-medium ${
|
|||
|
|
item.judgment === '적합' ? 'text-green-600' :
|
|||
|
|
item.judgment === '부적합' ? 'text-red-600' :
|
|||
|
|
'text-muted-foreground'
|
|||
|
|
}`}>
|
|||
|
|
판정: {item.judgment || '-'}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label className="text-muted-foreground">기준(Spec)</Label>
|
|||
|
|
<Input
|
|||
|
|
value={item.spec}
|
|||
|
|
disabled
|
|||
|
|
className="bg-muted"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{item.type === 'quality' ? (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label>결과 입력 *</Label>
|
|||
|
|
<RadioGroup
|
|||
|
|
value={(item as QualityCheckItem).result || ''}
|
|||
|
|
onValueChange={(value) => handleQualityResultChange(item.id, value as '양호' | '불량')}
|
|||
|
|
className="flex items-center gap-4 h-10"
|
|||
|
|
>
|
|||
|
|
<div className="flex items-center space-x-2">
|
|||
|
|
<RadioGroupItem value="양호" id={`${item.id}-pass`} />
|
|||
|
|
<Label htmlFor={`${item.id}-pass`} className="cursor-pointer">양호</Label>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center space-x-2">
|
|||
|
|
<RadioGroupItem value="불량" id={`${item.id}-fail`} />
|
|||
|
|
<Label htmlFor={`${item.id}-fail`} className="cursor-pointer">불량</Label>
|
|||
|
|
</div>
|
|||
|
|
</RadioGroup>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<Label>측정값 입력 ({(item as MeasurementItem).unit}) *</Label>
|
|||
|
|
<Input
|
|||
|
|
type="number"
|
|||
|
|
step="0.1"
|
|||
|
|
value={(item as MeasurementItem).measuredValue || ''}
|
|||
|
|
onChange={(e) => handleMeasurementChange(item.id, e.target.value)}
|
|||
|
|
placeholder={`측정값 입력 (${(item as MeasurementItem).unit})`}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
</PageLayout>
|
|||
|
|
);
|
|||
|
|
}
|