feat: 생산/품질/자재/출고/주문 관리 페이지 구현
- 생산관리: 대시보드, 작업지시, 작업실적, 작업자화면 - 품질관리: 검사관리 (리스트/등록/상세) - 자재관리: 입고관리, 재고현황 - 출고관리: 출하관리 (리스트/등록/상세/수정) - 주문관리: 수주관리, 생산의뢰 - 기존 컴포넌트 개선: CardTransactionInquiry, VendorDetail, QuoteRegistration - IntegratedListTemplateV2 개선 - 공통 컴포넌트 분석 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
323
src/components/material/ReceivingManagement/InspectionCreate.tsx
Normal file
323
src/components/material/ReceivingManagement/InspectionCreate.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 수입검사 등록 (IQC) 페이지
|
||||
* - 검사 대상 선택
|
||||
* - 검사 정보 입력 (검사일, 검사자*, LOT번호)
|
||||
* - 검사 항목 테이블 (겉모양, 두께, 폭, 길이)
|
||||
* - 종합 의견
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ClipboardCheck, Calendar } from 'lucide-react';
|
||||
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 { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { mockInspectionTargets, defaultInspectionItems, generateLotNo } from './mockData';
|
||||
import type { InspectionCheckItem } from './types';
|
||||
import { SuccessDialog } from './SuccessDialog';
|
||||
|
||||
interface Props {
|
||||
id?: string; // 특정 발주건으로 바로 진입하는 경우
|
||||
}
|
||||
|
||||
export function InspectionCreate({ id }: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
// 선택된 검사 대상
|
||||
const [selectedTargetId, setSelectedTargetId] = useState<string>(
|
||||
id || mockInspectionTargets[2]?.id || ''
|
||||
);
|
||||
|
||||
// 검사 정보
|
||||
const [inspectionDate, setInspectionDate] = useState(() => {
|
||||
const today = new Date();
|
||||
return today.toISOString().split('T')[0];
|
||||
});
|
||||
const [inspector, setInspector] = useState('');
|
||||
const [lotNo, setLotNo] = useState(() => generateLotNo());
|
||||
|
||||
// 검사 항목
|
||||
const [inspectionItems, setInspectionItems] = useState<InspectionCheckItem[]>(
|
||||
defaultInspectionItems.map((item) => ({ ...item }))
|
||||
);
|
||||
|
||||
// 종합 의견
|
||||
const [opinion, setOpinion] = useState('');
|
||||
|
||||
// 유효성 검사 에러
|
||||
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
||||
|
||||
// 성공 다이얼로그
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
// 선택된 대상 정보
|
||||
const selectedTarget = useMemo(() => {
|
||||
return mockInspectionTargets.find((t) => t.id === selectedTargetId);
|
||||
}, [selectedTargetId]);
|
||||
|
||||
// 대상 선택 핸들러
|
||||
const handleTargetSelect = useCallback((targetId: string) => {
|
||||
setSelectedTargetId(targetId);
|
||||
setValidationErrors([]);
|
||||
}, []);
|
||||
|
||||
// 판정 변경 핸들러
|
||||
const handleJudgmentChange = useCallback((itemId: string, judgment: '적' | '부적') => {
|
||||
setInspectionItems((prev) =>
|
||||
prev.map((item) => (item.id === itemId ? { ...item, judgment } : item))
|
||||
);
|
||||
setValidationErrors([]);
|
||||
}, []);
|
||||
|
||||
// 비고 변경 핸들러
|
||||
const handleRemarkChange = useCallback((itemId: string, remark: string) => {
|
||||
setInspectionItems((prev) =>
|
||||
prev.map((item) => (item.id === itemId ? { ...item, remark } : item))
|
||||
);
|
||||
}, []);
|
||||
|
||||
// 유효성 검사
|
||||
const validateForm = useCallback((): boolean => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// 필수 필드: 검사자
|
||||
if (!inspector.trim()) {
|
||||
errors.push('검사자는 필수 입력 항목입니다.');
|
||||
}
|
||||
|
||||
// 검사 항목 판정 확인
|
||||
inspectionItems.forEach((item, index) => {
|
||||
if (!item.judgment) {
|
||||
errors.push(`${index + 1}. ${item.name}: 판정을 선택해주세요.`);
|
||||
}
|
||||
});
|
||||
|
||||
setValidationErrors(errors);
|
||||
return errors.length === 0;
|
||||
}, [inspector, inspectionItems]);
|
||||
|
||||
// 검사 저장
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: API 호출
|
||||
console.log('검사 저장:', {
|
||||
targetId: selectedTargetId,
|
||||
inspectionDate,
|
||||
inspector,
|
||||
lotNo,
|
||||
items: inspectionItems,
|
||||
opinion,
|
||||
});
|
||||
|
||||
setShowSuccess(true);
|
||||
}, [validateForm, selectedTargetId, inspectionDate, inspector, lotNo, inspectionItems, opinion]);
|
||||
|
||||
// 취소 - 목록으로
|
||||
const handleCancel = useCallback(() => {
|
||||
router.push('/ko/material/receiving-management');
|
||||
}, [router]);
|
||||
|
||||
// 성공 다이얼로그 닫기
|
||||
const handleSuccessClose = useCallback(() => {
|
||||
setShowSuccess(false);
|
||||
router.push('/ko/material/receiving-management');
|
||||
}, [router]);
|
||||
|
||||
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">수입검사 등록 (IQC)</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
<ClipboardCheck className="w-4 h-4 mr-1.5" />
|
||||
검사 저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* 좌측: 검사 대상 선택 */}
|
||||
<div className="lg:col-span-1 space-y-2">
|
||||
<Label className="text-sm font-medium">검사 대상 선택</Label>
|
||||
<div className="space-y-2 border rounded-lg p-2 bg-white">
|
||||
{mockInspectionTargets.map((target) => (
|
||||
<div
|
||||
key={target.id}
|
||||
onClick={() => handleTargetSelect(target.id)}
|
||||
className={`p-3 rounded-lg cursor-pointer border transition-colors ${
|
||||
selectedTargetId === target.id
|
||||
? 'bg-blue-50 border-blue-300'
|
||||
: 'bg-white border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<p className="font-medium text-sm">{target.orderNo}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{target.supplier} · {target.qty}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측: 검사 정보 및 항목 */}
|
||||
<div className="lg:col-span-3 space-y-6">
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* 검사 정보 */}
|
||||
<div className="space-y-4 bg-white p-4 rounded-lg border">
|
||||
<h3 className="font-medium">검사 정보</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">검사일</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="date"
|
||||
value={inspectionDate}
|
||||
onChange={(e) => setInspectionDate(e.target.value)}
|
||||
className="pr-10"
|
||||
/>
|
||||
<Calendar className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
검사자 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
value={inspector}
|
||||
onChange={(e) => {
|
||||
setInspector(e.target.value);
|
||||
setValidationErrors([]);
|
||||
}}
|
||||
placeholder="검사자명 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">LOT번호 (YYMMDD-##)</Label>
|
||||
<Input value={lotNo} onChange={(e) => setLotNo(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 검사 항목 */}
|
||||
<div className="space-y-4 bg-white p-4 rounded-lg border">
|
||||
<h3 className="font-medium">검사 항목</h3>
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium">검사항목</th>
|
||||
<th className="px-3 py-2 text-left font-medium">규격</th>
|
||||
<th className="px-3 py-2 text-left font-medium">검사방법</th>
|
||||
<th className="px-3 py-2 text-center font-medium w-[100px]">판정</th>
|
||||
<th className="px-3 py-2 text-left font-medium w-[120px]">비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{inspectionItems.map((item) => (
|
||||
<tr key={item.id} className="border-t">
|
||||
<td className="px-3 py-2">{item.name}</td>
|
||||
<td className="px-3 py-2 text-muted-foreground text-xs">
|
||||
{item.specification}
|
||||
</td>
|
||||
<td className="px-3 py-2">{item.method}</td>
|
||||
<td className="px-3 py-2">
|
||||
<Select
|
||||
value={item.judgment || ''}
|
||||
onValueChange={(value) =>
|
||||
handleJudgmentChange(item.id, value as '적' | '부적')
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="적">적</SelectItem>
|
||||
<SelectItem value="부적">부적</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<Input
|
||||
value={item.remark || ''}
|
||||
onChange={(e) => handleRemarkChange(item.id, e.target.value)}
|
||||
placeholder="비고"
|
||||
className="h-8"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 종합 의견 */}
|
||||
<div className="space-y-2 bg-white p-4 rounded-lg border">
|
||||
<Label className="text-sm font-medium">종합 의견</Label>
|
||||
<Textarea
|
||||
value={opinion}
|
||||
onChange={(e) => setOpinion(e.target.value)}
|
||||
placeholder="검사 관련 특이사항 입력"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 성공 다이얼로그 */}
|
||||
<SuccessDialog
|
||||
open={showSuccess}
|
||||
type="inspection"
|
||||
lotNo={lotNo}
|
||||
onClose={handleSuccessClose}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user