"use client" import * as React from "react" import { useState, useEffect, useCallback } from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { z } from "zod" import { toast } from "sonner" import { Check, X, Search, Loader2 } from "lucide-react" import { useSession } from "next-auth/react" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Form, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" import { Input } from "@/components/ui/input" import { ScrollArea } from "@/components/ui/scroll-area" import { Badge } from "@/components/ui/badge" import { addVendorsToTechSalesRfq } from "@/lib/techsales-rfq/service" import { searchVendors } from "@/lib/vendors/service" // 폼 유효성 검증 스키마 - 간단화 const vendorFormSchema = z.object({ vendorIds: z.array(z.number()).min(1, "최소 하나의 벤더를 선택해주세요"), }) type VendorFormValues = z.infer // 기술영업 RFQ 타입 정의 type TechSalesRfq = { id: number rfqCode: string | null status: string [key: string]: any // eslint-disable-line @typescript-eslint/no-explicit-any } // 벤더 검색 결과 타입 (searchVendors 함수 반환 타입과 일치) type VendorSearchResult = { id: number vendorName: string vendorCode: string | null status: string country: string | null } interface AddVendorDialogProps { open: boolean onOpenChange: (open: boolean) => void selectedRfq: TechSalesRfq | null onSuccess?: () => void existingVendorIds?: number[] } export function AddVendorDialog({ open, onOpenChange, selectedRfq, onSuccess, existingVendorIds = [], }: AddVendorDialogProps) { const { data: session } = useSession() const [isSubmitting, setIsSubmitting] = useState(false) const [searchTerm, setSearchTerm] = useState("") const [searchResults, setSearchResults] = useState([]) const [isSearching, setIsSearching] = useState(false) const [hasSearched, setHasSearched] = useState(false) // 선택된 벤더들을 별도로 관리하여 검색과 독립적으로 유지 const [selectedVendorData, setSelectedVendorData] = useState([]) const form = useForm({ resolver: zodResolver(vendorFormSchema), defaultValues: { vendorIds: [], }, }) const selectedVendorIds = form.watch("vendorIds") // 검색 함수 (디바운스 적용) const searchVendorsDebounced = useCallback( async (term: string) => { if (!term.trim()) { setSearchResults([]) setHasSearched(false) return } setIsSearching(true) try { const results = await searchVendors(term, 100) // 이미 추가된 벤더 제외 const filteredResults = results.filter(vendor => !existingVendorIds.includes(vendor.id)) setSearchResults(filteredResults) setHasSearched(true) } catch (error) { console.error("벤더 검색 오류:", error) toast.error("벤더 검색 중 오류가 발생했습니다") setSearchResults([]) } finally { setIsSearching(false) } }, [existingVendorIds] ) // 검색어 변경 시 디바운스 적용 useEffect(() => { const timer = setTimeout(() => { searchVendorsDebounced(searchTerm) }, 300) return () => clearTimeout(timer) }, [searchTerm, searchVendorsDebounced]) // 벤더 선택/해제 핸들러 const handleVendorToggle = (vendor: VendorSearchResult) => { const currentIds = form.getValues("vendorIds") const isSelected = currentIds.includes(vendor.id) if (isSelected) { // 선택 해제 const newIds = currentIds.filter(id => id !== vendor.id) const newSelectedData = selectedVendorData.filter(v => v.id !== vendor.id) form.setValue("vendorIds", newIds, { shouldValidate: true }) setSelectedVendorData(newSelectedData) } else { // 선택 추가 const newIds = [...currentIds, vendor.id] const newSelectedData = [...selectedVendorData, vendor] form.setValue("vendorIds", newIds, { shouldValidate: true }) setSelectedVendorData(newSelectedData) } } // 선택된 벤더 제거 핸들러 const handleRemoveVendor = (vendorId: number) => { const currentIds = form.getValues("vendorIds") const newIds = currentIds.filter(id => id !== vendorId) const newSelectedData = selectedVendorData.filter(v => v.id !== vendorId) form.setValue("vendorIds", newIds, { shouldValidate: true }) setSelectedVendorData(newSelectedData) } // 폼 제출 핸들러 async function onSubmit(values: VendorFormValues) { if (!selectedRfq) { toast.error("선택된 RFQ가 없습니다") return } if (!session?.user?.id) { toast.error("로그인이 필요합니다") return } try { setIsSubmitting(true) // 서비스 함수 호출 const result = await addVendorsToTechSalesRfq({ rfqId: selectedRfq.id, vendorIds: values.vendorIds, createdBy: Number(session.user.id), }) if (result.error) { toast.error(result.error) } else { const successMessage = `${result.successCount}개의 벤더가 성공적으로 추가되었습니다` const errorMessage = result.errorCount && result.errorCount > 0 ? ` (${result.errorCount}개 실패)` : "" toast.success(successMessage + errorMessage) onOpenChange(false) form.reset() setSearchTerm("") setSearchResults([]) setHasSearched(false) setSelectedVendorData([]) onSuccess?.() } } catch (error) { console.error("벤더 추가 오류:", error) toast.error("벤더 추가 중 오류가 발생했습니다") } finally { setIsSubmitting(false) } } // 다이얼로그 닫기 시 폼 리셋 React.useEffect(() => { if (!open) { form.reset() setSearchTerm("") setSearchResults([]) setHasSearched(false) setSelectedVendorData([]) } }, [open, form]) return ( {/* 헤더 */} 벤더 추가 {selectedRfq ? ( <> {selectedRfq.rfqCode} RFQ에 벤더를 추가합니다. ) : ( "RFQ에 벤더를 추가합니다." )} {/* 콘텐츠 */}
{/* 벤더 검색 필드 */}
setSearchTerm(e.target.value)} className="pl-10" /> {isSearching && ( )}
{/* 검색 결과 */} {hasSearched && (
검색 결과 ({searchResults.length}개)
{searchResults.length > 0 ? ( searchResults.map((vendor) => (
handleVendorToggle(vendor)} >
{vendor.vendorName}
{vendor.vendorCode || 'N/A'} {vendor.country && `• ${vendor.country}`}
)) ) : (
검색 결과가 없습니다
)}
)} {/* 검색 안내 메시지 */} {!hasSearched && !searchTerm && (
벤더명 또는 벤더코드를 입력하여 검색해주세요
)} {/* 선택된 벤더 목록 - 하단에 항상 표시 */} (
선택된 벤더 ({selectedVendorData.length}개)
{selectedVendorData.length > 0 ? (
{selectedVendorData.map((vendor) => ( {vendor.vendorName} ({vendor.vendorCode || 'N/A'}) handleRemoveVendor(vendor.id)} /> ))}
) : (
선택된 벤더가 없습니다
)}
)} /> {/* 안내 메시지 */}
{/*

• 검색은 ACTIVE 상태의 벤더만 대상으로 합니다.

*/}

• 선택된 벤더들은 Draft 상태로 추가됩니다.

• 벤더별 견적 정보는 추가 후 개별적으로 입력할 수 있습니다.

• 이미 추가된 벤더는 검색 결과에서 체크됩니다.

{/* 푸터 */}
) }