- 생산관리: 대시보드, 작업지시, 작업실적, 작업자화면 - 품질관리: 검사관리 (리스트/등록/상세) - 자재관리: 입고관리, 재고현황 - 출고관리: 출하관리 (리스트/등록/상세/수정) - 주문관리: 수주관리, 생산의뢰 - 기존 컴포넌트 개선: CardTransactionInquiry, VendorDetail, QuoteRegistration - IntegratedListTemplateV2 개선 - 공통 컴포넌트 분석 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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>
|
||
);
|
||
} |