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:
@@ -0,0 +1,227 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 입고처리 다이얼로그
|
||||
* - 발주 정보 표시
|
||||
* - 입고LOT*, 공급업체LOT, 입고수량*, 입고위치* 입력
|
||||
* - 비고 입력
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Download } 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 {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { generateLotNo } from './mockData';
|
||||
import type { ReceivingDetail } from './types';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
detail: ReceivingDetail;
|
||||
onComplete: (lotNo: string) => void;
|
||||
}
|
||||
|
||||
export function ReceivingProcessDialog({ open, onOpenChange, detail, onComplete }: Props) {
|
||||
// 폼 데이터
|
||||
const [receivingLot, setReceivingLot] = useState(() => generateLotNo());
|
||||
const [supplierLot, setSupplierLot] = useState('');
|
||||
const [receivingQty, setReceivingQty] = useState<string>(detail.orderQty.toString());
|
||||
const [receivingLocation, setReceivingLocation] = useState('');
|
||||
const [remark, setRemark] = useState('');
|
||||
|
||||
// 유효성 검사 에러
|
||||
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
||||
|
||||
// 유효성 검사
|
||||
const validateForm = useCallback((): boolean => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!receivingLot.trim()) {
|
||||
errors.push('입고LOT는 필수 입력 항목입니다.');
|
||||
}
|
||||
|
||||
if (!receivingQty.trim() || isNaN(Number(receivingQty)) || Number(receivingQty) <= 0) {
|
||||
errors.push('입고수량은 필수 입력 항목입니다. 유효한 숫자를 입력해주세요.');
|
||||
}
|
||||
|
||||
if (!receivingLocation.trim()) {
|
||||
errors.push('입고위치는 필수 입력 항목입니다.');
|
||||
}
|
||||
|
||||
setValidationErrors(errors);
|
||||
return errors.length === 0;
|
||||
}, [receivingLot, receivingQty, receivingLocation]);
|
||||
|
||||
// 입고 처리
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: API 호출
|
||||
console.log('입고 처리:', {
|
||||
detailId: detail.id,
|
||||
receivingLot,
|
||||
supplierLot,
|
||||
receivingQty: Number(receivingQty),
|
||||
receivingLocation,
|
||||
remark,
|
||||
});
|
||||
|
||||
onComplete(receivingLot);
|
||||
}, [validateForm, detail.id, receivingLot, supplierLot, receivingQty, receivingLocation, remark, onComplete]);
|
||||
|
||||
// 취소
|
||||
const handleCancel = useCallback(() => {
|
||||
onOpenChange(false);
|
||||
}, [onOpenChange]);
|
||||
|
||||
// 다이얼로그 닫힐 때 상태 초기화
|
||||
const handleOpenChange = useCallback(
|
||||
(newOpen: boolean) => {
|
||||
if (!newOpen) {
|
||||
setValidationErrors([]);
|
||||
}
|
||||
onOpenChange(newOpen);
|
||||
},
|
||||
[onOpenChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>입고 처리</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 mt-4">
|
||||
{/* 발주 정보 요약 */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">발주번호:</span>{' '}
|
||||
<span className="font-medium">{detail.orderNo}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">공급업체:</span>{' '}
|
||||
<span className="font-medium">{detail.supplier}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">품목:</span>{' '}
|
||||
<span className="font-medium">{detail.itemName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">발주수량:</span>{' '}
|
||||
<span className="font-medium">{detail.orderQty} {detail.orderUnit}</span>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* 입력 필드 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm">
|
||||
입고LOT <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
value={receivingLot}
|
||||
onChange={(e) => {
|
||||
setReceivingLot(e.target.value);
|
||||
setValidationErrors([]);
|
||||
}}
|
||||
placeholder="예: 251223-41"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">공급업체LOT</Label>
|
||||
<Input
|
||||
value={supplierLot}
|
||||
onChange={(e) => setSupplierLot(e.target.value)}
|
||||
placeholder="예: 2402944"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm">
|
||||
입고수량 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={receivingQty}
|
||||
onChange={(e) => {
|
||||
setReceivingQty(e.target.value);
|
||||
setValidationErrors([]);
|
||||
}}
|
||||
placeholder="수량 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm">
|
||||
입고위치 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
value={receivingLocation}
|
||||
onChange={(e) => {
|
||||
setReceivingLocation(e.target.value);
|
||||
setValidationErrors([]);
|
||||
}}
|
||||
placeholder="예: A-01"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 비고 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">비고</Label>
|
||||
<Textarea
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.target.value)}
|
||||
placeholder="특이사항 입력"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 버튼 영역 */}
|
||||
<div className="flex justify-end gap-2 mt-6 pt-4 border-t">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
입고 처리
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user