- 공통 템플릿 타입 수정 (IntegratedDetailTemplate, UniversalListPage) - 페이지(app/[locale]) 타입 호환성 수정 (80개) - 재고/자재 모듈 타입 수정 (StockStatus, ReceivingManagement) - 생산 모듈 타입 수정 (WorkOrders, WorkerScreen, WorkResults) - 주문/출고 모듈 타입 수정 (ShipmentManagement, Orders) - 견적/단가 모듈 타입 수정 (Quotes, Pricing) - 건설 모듈 타입 수정 (49개, 17개 하위 모듈) - HR 모듈 타입 수정 (CardManagement, VacationManagement 등) - 설정 모듈 타입 수정 (PermissionManagement, AccountManagement 등) - 게시판 모듈 타입 수정 (BoardManagement, BoardList 등) - 회계 모듈 타입 수정 (VendorManagement, BadDebtCollection 등) - 기타 모듈 타입 수정 (CEODashboard, clients, vehicle 등) - 유틸/훅/API 타입 수정 (hooks, contexts, lib) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
238 lines
8.0 KiB
TypeScript
238 lines
8.0 KiB
TypeScript
'use client';
|
||
|
||
/**
|
||
* 입고처리 다이얼로그
|
||
* - 발주 정보 표시
|
||
* - 입고LOT*, 공급업체LOT, 입고수량* 입력 (필수)
|
||
* - 입고위치, 비고 입력 (선택)
|
||
*/
|
||
|
||
import { useState, useCallback } from 'react';
|
||
import { Loader2 } 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 { QuantityInput } from '@/components/ui/quantity-input';
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from '@/components/ui/dialog';
|
||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||
import type { ReceivingDetail, ReceivingProcessFormData } from './types';
|
||
|
||
// LOT 번호 생성 함수 (YYMMDD-NN 형식)
|
||
function generateLotNo(): string {
|
||
const now = new Date();
|
||
const yy = String(now.getFullYear()).slice(-2);
|
||
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
||
const dd = String(now.getDate()).padStart(2, '0');
|
||
const random = String(Math.floor(Math.random() * 100)).padStart(2, '0');
|
||
return `${yy}${mm}${dd}-${random}`;
|
||
}
|
||
|
||
interface Props {
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
detail: ReceivingDetail;
|
||
onComplete: (formData: ReceivingProcessFormData) => 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 ?? 0).toString());
|
||
const [receivingLocation, setReceivingLocation] = useState('');
|
||
const [remark, setRemark] = useState('');
|
||
|
||
// 유효성 검사 에러
|
||
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
|
||
// 유효성 검사
|
||
const validateForm = useCallback((): boolean => {
|
||
const errors: string[] = [];
|
||
|
||
if (!receivingLot.trim()) {
|
||
errors.push('입고LOT는 필수 입력 항목입니다.');
|
||
}
|
||
|
||
if (!receivingQty.trim() || isNaN(Number(receivingQty)) || Number(receivingQty) <= 0) {
|
||
errors.push('입고수량은 필수 입력 항목입니다. 유효한 숫자를 입력해주세요.');
|
||
}
|
||
|
||
// 입고위치는 선택 항목 (필수 검사 제거)
|
||
|
||
setValidationErrors(errors);
|
||
return errors.length === 0;
|
||
}, [receivingLot, receivingQty]);
|
||
|
||
// 입고 처리
|
||
const handleSubmit = useCallback(async () => {
|
||
if (!validateForm()) {
|
||
return;
|
||
}
|
||
|
||
setIsSubmitting(true);
|
||
|
||
const formData: ReceivingProcessFormData = {
|
||
receivingQty: Number(receivingQty),
|
||
receivingLot,
|
||
supplierLot: supplierLot || undefined,
|
||
receivingLocation: receivingLocation || undefined,
|
||
remark: remark || undefined,
|
||
};
|
||
|
||
await onComplete(formData);
|
||
setIsSubmitting(false);
|
||
}, [validateForm, 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>
|
||
<QuantityInput
|
||
value={parseFloat(receivingQty) || 0}
|
||
onChange={(value) => {
|
||
setReceivingQty(String(value ?? 0));
|
||
setValidationErrors([]);
|
||
}}
|
||
placeholder="수량 입력"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label className="text-sm text-muted-foreground">입고위치</Label>
|
||
<Input
|
||
value={receivingLocation}
|
||
onChange={(e) => setReceivingLocation(e.target.value)}
|
||
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} disabled={isSubmitting}>
|
||
취소
|
||
</Button>
|
||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||
{isSubmitting ? (
|
||
<>
|
||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||
처리 중...
|
||
</>
|
||
) : (
|
||
'입고 처리'
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
} |