'use client'; /** * 출고 등록 페이지 * 4개 섹션 구조: 기본정보, 수주/배송정보, 배차정보, 제품내용 */ import { useState, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Plus, X as XIcon, ChevronDown, Search } from 'lucide-react'; import { getTodayString } from '@/utils/date'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { shipmentCreateConfig } from './shipmentConfig'; import { createShipment, getLotOptions, getLogisticsOptions, getVehicleTonnageOptions, } from './actions'; import type { ShipmentCreateFormData, DeliveryMethod, FreightCostType, VehicleDispatch, LotOption, LogisticsOption, VehicleTonnageOption, ProductGroup, ProductPart, } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { toast } from 'sonner'; import { useDaumPostcode } from '@/hooks/useDaumPostcode'; import { useDevFill } from '@/components/dev'; import { generateShipmentData } from '@/components/dev/generators/shipmentData'; import { mockProductGroups, mockOtherParts } from './mockData'; // 배송방식 옵션 const deliveryMethodOptions: { value: DeliveryMethod; label: string }[] = [ { value: 'direct_dispatch', label: '직접배차' }, { value: 'loading', label: '상차' }, { value: 'kyungdong_delivery', label: '경동택배' }, { value: 'daesin_delivery', label: '대신택배' }, { value: 'kyungdong_freight', label: '경동화물' }, { value: 'daesin_freight', label: '대신화물' }, { value: 'self_pickup', label: '직접수령' }, ]; // 운임비용 옵션 (선불, 착불, 없음) const freightCostOptions: { value: FreightCostType; label: string }[] = [ { value: 'prepaid', label: '선불' }, { value: 'collect', label: '착불' }, { value: 'none', label: '없음' }, ]; // 빈 배차 행 생성 function createEmptyDispatch(): VehicleDispatch { return { id: `vd-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, logisticsCompany: '', arrivalDateTime: '', tonnage: '', vehicleNo: '', driverContact: '', remarks: '', }; } export function ShipmentCreate() { const router = useRouter(); // 폼 상태 const [formData, setFormData] = useState({ lotNo: '', scheduledDate: getTodayString(), priority: 'normal', deliveryMethod: 'direct_dispatch', shipmentDate: '', freightCost: 'none', receiver: '', receiverContact: '', zipCode: '', address: '', addressDetail: '', vehicleDispatches: [createEmptyDispatch()], logisticsCompany: '', vehicleTonnage: '', loadingTime: '', loadingManager: '', remarks: '', }); // API 옵션 데이터 상태 const [lotOptions, setLotOptions] = useState([]); const [logisticsOptions, setLogisticsOptions] = useState([]); const [vehicleTonnageOptions, setVehicleTonnageOptions] = useState([]); // 제품 데이터 (LOT 선택 시 표시) const [productGroups, setProductGroups] = useState([]); const [otherParts, setOtherParts] = useState([]); // 로딩/에러 상태 const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [validationErrors, setValidationErrors] = useState([]); // 아코디언 상태 const [accordionValue, setAccordionValue] = useState([]); // 우편번호 찾기 const { openPostcode } = useDaumPostcode({ onComplete: (result) => { setFormData(prev => ({ ...prev, zipCode: result.zonecode, address: result.address, })); }, }); // 옵션 데이터 로드 const loadOptions = useCallback(async () => { setIsLoading(true); setError(null); try { const [lotsResult, logisticsResult, tonnageResult] = await Promise.all([ getLotOptions(), getLogisticsOptions(), getVehicleTonnageOptions(), ]); if (lotsResult.success && lotsResult.data) { setLotOptions(lotsResult.data); } if (logisticsResult.success && logisticsResult.data) { setLogisticsOptions(logisticsResult.data); } if (tonnageResult.success && tonnageResult.data) { setVehicleTonnageOptions(tonnageResult.data); } } catch (err) { if (isNextRedirectError(err)) throw err; console.error('[ShipmentCreate] loadOptions error:', err); setError('옵션 데이터를 불러오는 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, []); useEffect(() => { loadOptions(); }, [loadOptions]); // DevToolbar 자동 채우기 useDevFill( 'shipment', useCallback(() => { const lotOptionsForGenerator = lotOptions.map(o => ({ lotNo: o.value, customerName: o.customerName, siteName: o.siteName, })); const logisticsOptionsForGenerator = logisticsOptions.map(o => ({ id: o.value, name: o.label, })); const tonnageOptionsForGenerator = vehicleTonnageOptions.map(o => ({ value: o.value, label: o.label, })); const sampleData = generateShipmentData({ lotOptions: lotOptionsForGenerator as unknown as LotOption[], logisticsOptions: logisticsOptionsForGenerator as unknown as LogisticsOption[], tonnageOptions: tonnageOptionsForGenerator, }); setFormData(prev => ({ ...prev, ...sampleData })); toast.success('[Dev] 출고 폼이 자동으로 채워졌습니다.'); }, [lotOptions, logisticsOptions, vehicleTonnageOptions]) ); // LOT 선택 시 현장명/수주처 자동 매핑 + 목데이터 제품 표시 const handleLotChange = useCallback((lotNo: string) => { setFormData(prev => ({ ...prev, lotNo })); if (lotNo) { // 목데이터로 제품 그룹 표시 setProductGroups(mockProductGroups); setOtherParts(mockOtherParts); } else { setProductGroups([]); setOtherParts([]); } if (validationErrors.length > 0) setValidationErrors([]); }, [validationErrors]); // 배송방식에 따라 운임비용 '없음' 고정 여부 판단 const isFreightCostLocked = (method: DeliveryMethod) => method === 'direct_dispatch' || method === 'self_pickup'; // 폼 입력 핸들러 const handleInputChange = (field: keyof ShipmentCreateFormData, value: string) => { if (field === 'deliveryMethod') { const method = value as DeliveryMethod; if (isFreightCostLocked(method)) { setFormData(prev => ({ ...prev, deliveryMethod: method, freightCost: 'none' as FreightCostType })); } else { setFormData(prev => ({ ...prev, deliveryMethod: method })); } } else { setFormData(prev => ({ ...prev, [field]: value })); } if (validationErrors.length > 0) setValidationErrors([]); }; // 배차 정보 핸들러 const handleDispatchChange = (index: number, field: keyof VehicleDispatch, value: string) => { setFormData(prev => { const newDispatches = [...prev.vehicleDispatches]; newDispatches[index] = { ...newDispatches[index], [field]: value }; return { ...prev, vehicleDispatches: newDispatches }; }); }; const handleAddDispatch = () => { setFormData(prev => ({ ...prev, vehicleDispatches: [...prev.vehicleDispatches, createEmptyDispatch()], })); }; const handleRemoveDispatch = (index: number) => { setFormData(prev => ({ ...prev, vehicleDispatches: prev.vehicleDispatches.filter((_, i) => i !== index), })); }; // 아코디언 제어 const handleExpandAll = useCallback(() => { const allIds = [ ...productGroups.map(g => g.id), ...(otherParts.length > 0 ? ['other-parts'] : []), ]; setAccordionValue(allIds); }, [productGroups, otherParts]); const handleCollapseAll = useCallback(() => { setAccordionValue([]); }, []); const handleCancel = useCallback(() => { router.push('/ko/outbound/shipments'); }, [router]); const validateForm = (): boolean => { const errors: string[] = []; if (!formData.lotNo) errors.push('로트번호는 필수 선택 항목입니다.'); if (!formData.scheduledDate) errors.push('출고예정일은 필수 입력 항목입니다.'); if (!formData.deliveryMethod) errors.push('배송방식은 필수 선택 항목입니다.'); setValidationErrors(errors); return errors.length === 0; }; const handleSubmit = useCallback(async () => { if (!validateForm()) return; setIsSubmitting(true); try { const result = await createShipment(formData); if (result.success) { router.push('/ko/outbound/shipments'); } else { setValidationErrors([result.error || '출고 등록에 실패했습니다.']); } } catch (err) { if (isNextRedirectError(err)) throw err; console.error('[ShipmentCreate] handleSubmit error:', err); setValidationErrors(['저장 중 오류가 발생했습니다.']); } finally { setIsSubmitting(false); } }, [formData, router]); // 제품 부품 테이블 렌더링 const renderPartsTable = (parts: ProductPart[]) => ( 순번 품목명 규격 수량 단위 {parts.map((part) => ( {part.seq} {part.itemName} {part.specification} {part.quantity} {part.unit} ))}
); // LOT에서 선택한 정보 표시 const selectedLot = lotOptions.find(o => o.value === formData.lotNo); // 폼 컨텐츠 렌더링 const renderFormContent = useCallback((_props: { formData: Record; onChange: (key: string, value: unknown) => void; mode: string; errors: Record }) => (
{/* Validation 에러 표시 */} {validationErrors.length > 0 && (
⚠️
입력 내용을 확인해주세요 ({validationErrors.length}개 오류)
    {validationErrors.map((err, index) => (
  • {err}
  • ))}
)} {/* 카드 1: 기본 정보 */} 기본 정보
{/* 출고번호 - 자동생성 */}
출고번호
자동생성
{/* 로트번호 - Select */}
로트번호 *
{/* 현장명 - LOT 선택 시 자동 매핑 */}
현장명
{selectedLot?.siteName || '-'}
{/* 수주처 - LOT 선택 시 자동 매핑 */}
수주처
{selectedLot?.customerName || '-'}
{/* 카드 2: 수주/배송 정보 */} 수주/배송 정보
handleInputChange('scheduledDate', date)} disabled={isSubmitting} />
handleInputChange('shipmentDate', date)} disabled={isSubmitting} />
handleInputChange('receiver', e.target.value)} placeholder="수신자명" disabled={isSubmitting} />
handleInputChange('receiverContact', e.target.value)} placeholder="수신처" disabled={isSubmitting} />
{/* 주소 */}
handleInputChange('addressDetail', e.target.value)} placeholder="상세주소" disabled={isSubmitting} />
{/* 카드 3: 배차 정보 */} 배차 정보 물류업체 입차일시 구분 차량번호 기사연락처 비고 {formData.vehicleDispatches.map((dispatch, index) => ( handleDispatchChange(index, 'arrivalDateTime', e.target.value)} className="h-8" disabled={isSubmitting} /> handleDispatchChange(index, 'vehicleNo', e.target.value)} placeholder="차량번호" className="h-8" disabled={isSubmitting} /> handleDispatchChange(index, 'driverContact', e.target.value)} placeholder="연락처" className="h-8" disabled={isSubmitting} /> handleDispatchChange(index, 'remarks', e.target.value)} placeholder="비고" className="h-8" disabled={isSubmitting} /> {formData.vehicleDispatches.length > 1 && ( )} ))}
{/* 카드 4: 제품내용 (읽기전용 - LOT 선택 시 표시) */} 제품내용 {productGroups.length > 0 && ( 모두 펼치기 모두 접기 )} {productGroups.length > 0 || otherParts.length > 0 ? ( {productGroups.map((group: ProductGroup) => (
{group.productName} ({group.specification}) {group.partCount}개 부품
{renderPartsTable(group.parts)}
))} {otherParts.length > 0 && (
기타부품 {otherParts.length}개 부품
{renderPartsTable(otherParts)}
)}
) : (
{formData.lotNo ? '제품 정보를 불러오는 중...' : '로트를 선택하면 제품 목록이 표시됩니다.'}
)}
), [ formData, validationErrors, isSubmitting, lotOptions, logisticsOptions, vehicleTonnageOptions, selectedLot, productGroups, otherParts, accordionValue, handleLotChange, handleExpandAll, handleCollapseAll, openPostcode, ]); if (error) { return ( ; onChange: (key: string, value: unknown) => void; mode: string; errors: Record }) => ( {error} )} /> ); } return ( ) => { await handleSubmit(); return { success: true }; }} renderForm={renderFormContent} /> ); }