"use client"; /** * 수주 등록 컴포넌트 * * IntegratedDetailTemplate 마이그레이션 (2026-01-20) * - 견적 불러오기 섹션 * - 기본 정보 섹션 * - 수주/배송 정보 섹션 (주소 포함) * - 비고 섹션 * - 품목 내역 섹션 */ import { useState, useEffect, useCallback } from "react"; import { useDaumPostcode } from "@/hooks/useDaumPostcode"; import { useClientList } from "@/hooks/useClientList"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { QuantityInput } from "@/components/ui/quantity-input"; import { NumberInput } from "@/components/ui/number-input"; import { PhoneInput } from "@/components/ui/phone-input"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { FileText, Search, X, Plus, Trash2, Truck, Package, MessageSquare, } from "lucide-react"; import { toast } from "sonner"; import { IntegratedDetailTemplate } from "@/components/templates/IntegratedDetailTemplate"; import { orderCreateConfig, orderEditConfig } from "./orderConfig"; import { FormSection } from "@/components/organisms/FormSection"; import { QuotationSelectDialog } from "./QuotationSelectDialog"; import { type QuotationForSelect, type QuotationItem } from "./actions"; import { ItemAddDialog, OrderItem } from "./ItemAddDialog"; import { formatAmount } from "@/utils/formatAmount"; import { cn } from "@/lib/utils"; import { useDevFill } from "@/components/dev"; import { generateOrderData } from "@/components/dev/generators/orderData"; // 수주 폼 데이터 타입 export interface OrderFormData { // 견적 정보 selectedQuotation?: QuotationForSelect; // 기본 정보 clientId: string; clientName: string; siteName: string; manager: string; contact: string; // 수주/배송 정보 expectedShipDate: string; expectedShipDateUndecided: boolean; deliveryRequestDate: string; deliveryRequestDateUndecided: boolean; deliveryMethod: string; shippingCost: string; receiver: string; receiverContact: string; // 수신처 주소 zipCode: string; address: string; addressDetail: string; // 비고 remarks: string; // 품목 내역 items: OrderItem[]; // 금액 정보 subtotal: number; discountRate: number; totalAmount: number; } // 초기 폼 데이터 const INITIAL_FORM: OrderFormData = { clientId: "", clientName: "", siteName: "", manager: "", contact: "", expectedShipDate: "", expectedShipDateUndecided: false, deliveryRequestDate: "", deliveryRequestDateUndecided: false, deliveryMethod: "", shippingCost: "", receiver: "", receiverContact: "", zipCode: "", address: "", addressDetail: "", remarks: "", items: [], subtotal: 0, discountRate: 0, totalAmount: 0, }; // 배송방식 옵션 const DELIVERY_METHODS = [ { value: "direct", label: "직접배차" }, { value: "pickup", label: "상차" }, { value: "courier", label: "택배" }, { value: "self", label: "직접수령" }, { value: "freight", label: "화물" }, ]; // 운임비용 옵션 const SHIPPING_COSTS = [ { value: "free", label: "무료" }, { value: "prepaid", label: "선불" }, { value: "collect", label: "착불" }, { value: "negotiable", label: "협의" }, ]; interface OrderRegistrationProps { onBack: () => void; onSave: (formData: OrderFormData) => Promise; initialData?: Partial; isEditMode?: boolean; } // 필드별 에러 타입 interface FieldErrors { clientName?: string; siteName?: string; deliveryRequestDate?: string; receiver?: string; receiverContact?: string; items?: string; } // 필드명 한글 매핑 const FIELD_NAME_MAP: Record = { clientName: "수주처", siteName: "현장명", deliveryRequestDate: "납품요청일", receiver: "수신자", receiverContact: "수신처", items: "품목 내역", }; export function OrderRegistration({ onBack, onSave, initialData, isEditMode = false, }: OrderRegistrationProps) { const [form, setForm] = useState({ ...INITIAL_FORM, ...initialData, }); const [isQuotationDialogOpen, setIsQuotationDialogOpen] = useState(false); const [isItemDialogOpen, setIsItemDialogOpen] = useState(false); const [isSaving, setIsSaving] = useState(false); const [fieldErrors, setFieldErrors] = useState({}); // Config 선택 const config = isEditMode ? orderEditConfig : orderCreateConfig; // 거래처 목록 조회 const { clients, fetchClients, isLoading: isClientsLoading } = useClientList(); // 컴포넌트 마운트 시 거래처 목록 불러오기 useEffect(() => { fetchClients({ onlyActive: true, size: 100 }); }, [fetchClients]); // Daum 우편번호 서비스 const { openPostcode } = useDaumPostcode({ onComplete: (result) => { setForm((prev) => ({ ...prev, zipCode: result.zonecode, address: result.address, })); }, }); // 금액 계산 useEffect(() => { const subtotal = form.items.reduce((sum, item) => sum + item.amount, 0); const discountAmount = subtotal * (form.discountRate / 100); const totalAmount = subtotal - discountAmount; setForm((prev) => ({ ...prev, subtotal, totalAmount, })); }, [form.items, form.discountRate]); // DevToolbar 자동 채우기 (배송 정보만 - 기본정보는 견적에서 불러옴) useDevFill( 'order', useCallback(() => { const sampleData = generateOrderData(); setForm(prev => ({ ...prev, ...sampleData })); toast.success('[Dev] 수주 배송정보가 자동으로 채워졌습니다.'); }, []) ); // 견적 선택 핸들러 const handleQuotationSelect = (quotation: QuotationForSelect) => { // 견적 정보로 폼 자동 채우기 (견적에서 가져온 품목은 삭제 불가) const items: OrderItem[] = (quotation.items || []).map((qi: QuotationItem) => ({ id: qi.id, itemCode: qi.itemCode, itemName: qi.itemName, type: qi.type, symbol: qi.symbol, spec: qi.spec, width: 0, height: 0, quantity: qi.quantity, unit: qi.unit, unitPrice: qi.unitPrice, amount: qi.amount, isFromQuotation: true, // 견적에서 가져온 품목 표시 })); setForm((prev) => ({ ...prev, selectedQuotation: quotation, clientId: quotation.clientId || "", // 견적의 발주처 ID 설정 clientName: quotation.client, siteName: quotation.siteName, manager: quotation.manager || "", contact: quotation.contact || "", items, })); // 발주처 에러 초기화 clearFieldError("clientName"); toast.success("견적 정보가 불러와졌습니다."); }; // 견적 해제 핸들러 const handleClearQuotation = () => { setForm((prev) => ({ ...prev, selectedQuotation: undefined, // 기본 정보는 유지하고 품목만 초기화할지, 전체 초기화할지 선택 가능 items: [], })); }; // 품목 추가 핸들러 const handleAddItem = (item: OrderItem) => { setForm((prev) => ({ ...prev, items: [...prev.items, item], })); // 품목 에러 초기화 setFieldErrors((prev) => { if (prev.items) { const { items: _, ...rest } = prev; return rest; } return prev; }); toast.success("품목이 추가되었습니다."); }; // 품목 삭제 핸들러 const handleRemoveItem = (itemId: string) => { setForm((prev) => ({ ...prev, items: prev.items.filter((item) => item.id !== itemId), })); }; // 품목 수량 변경 핸들러 const handleQuantityChange = (itemId: string, quantity: number) => { setForm((prev) => ({ ...prev, items: prev.items.map((item) => item.id === itemId ? { ...item, quantity, amount: item.unitPrice * quantity } : item ), })); }; // 유효성 검사 함수 const validateForm = useCallback((): FieldErrors => { const errors: FieldErrors = {}; if (!form.clientName.trim()) { errors.clientName = "발주처를 선택해주세요."; } if (!form.siteName.trim()) { errors.siteName = "현장명을 입력해주세요."; } if (!form.deliveryRequestDate && !form.deliveryRequestDateUndecided) { errors.deliveryRequestDate = "납품요청일을 입력하거나 '미정'을 선택해주세요."; } if (!form.receiver.trim()) { errors.receiver = "수신자명을 입력해주세요."; } if (!form.receiverContact.trim()) { errors.receiverContact = "연락처를 입력해주세요."; } if (form.items.length === 0) { errors.items = "최소 1개 이상의 품목을 추가해주세요."; } return errors; }, [form]); // 필드 에러 초기화 (필드 값 변경 시) const clearFieldError = useCallback((field: keyof FieldErrors) => { setFieldErrors((prev) => { if (prev[field]) { const { [field]: _, ...rest } = prev; return rest; } return prev; }); }, []); // 저장 핸들러 const handleSave = useCallback(async () => { // 유효성 검사 const errors = validateForm(); setFieldErrors(errors); const errorCount = Object.keys(errors).length; if (errorCount > 0) { toast.error(`입력 내용을 확인해주세요. (${errorCount}개 오류)`); return; } setIsSaving(true); try { await onSave(form); } finally { setIsSaving(false); } }, [form, validateForm, onSave]); // 폼 콘텐츠 렌더링 const renderFormContent = useCallback( () => (
{/* Validation 에러 Alert */} {Object.keys(fieldErrors).length > 0 && (
⚠️
입력 내용을 확인해주세요 ({Object.keys(fieldErrors).length}개 오류)
    {Object.entries(fieldErrors).map(([field, error]) => { const fieldName = FIELD_NAME_MAP[field] || field; return (
  • {fieldName}: {error}
  • ); })}
)} {/* 견적 불러오기 섹션 */}
{form.selectedQuotation ? (
{form.selectedQuotation.quoteNumber} {form.selectedQuotation.grade} (우량)
{form.selectedQuotation.client} / {form.selectedQuotation.siteName} / {formatAmount(form.selectedQuotation.amount)}
) : ( )}
{/* 기본 정보 섹션 */}
{/* 첫 번째 줄: 로트번호, 접수일, 수주처, 현장명 */}
{fieldErrors.clientName && (

{fieldErrors.clientName}

)}
{ setForm((prev) => ({ ...prev, siteName: e.target.value })); clearFieldError("siteName"); }} disabled={!!form.selectedQuotation} className={cn(fieldErrors.siteName && "border-red-500")} /> {fieldErrors.siteName && (

{fieldErrors.siteName}

)}
{/* 두 번째 줄: 담당자, 연락처, 상태 */}
setForm((prev) => ({ ...prev, manager: e.target.value })) } />
setForm((prev) => ({ ...prev, contact: value })) } />
{/* 수주/배송 정보 섹션 */}
{/* 첫 번째 줄: 수주일, 납품요청일, 출고예정일, 배송방식 */}
{ setForm((prev) => ({ ...prev, deliveryRequestDate: e.target.value, })); clearFieldError("deliveryRequestDate"); }} disabled={form.deliveryRequestDateUndecided} className={cn(fieldErrors.deliveryRequestDate && "border-red-500")} /> {fieldErrors.deliveryRequestDate && (

{fieldErrors.deliveryRequestDate}

)}
setForm((prev) => ({ ...prev, expectedShipDate: e.target.value, })) } disabled={form.expectedShipDateUndecided} />
{/* 두 번째 줄: 운임비용, 수신자, 수신처 */}
{ setForm((prev) => ({ ...prev, receiver: e.target.value })); clearFieldError("receiver"); }} className={cn(fieldErrors.receiver && "border-red-500")} /> {fieldErrors.receiver && (

{fieldErrors.receiver}

)}
{ setForm((prev) => ({ ...prev, receiverContact: e.target.value, })); clearFieldError("receiverContact"); }} className={cn(fieldErrors.receiverContact && "border-red-500")} /> {fieldErrors.receiverContact && (

{fieldErrors.receiverContact}

)}
{/* 주소 - 전체 너비 사용 */}
setForm((prev) => ({ ...prev, zipCode: e.target.value })) } className="w-32" /> setForm((prev) => ({ ...prev, address: e.target.value })) } className="flex-1" />
{/* 비고 섹션 */}