"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, Star, ChevronRight, ChevronLeft } 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, FormLabel } from "@/components/ui/form" import { Input } from "@/components/ui/input" import { ScrollArea } from "@/components/ui/scroll-area" import { Badge } from "@/components/ui/badge" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Checkbox } from "@/components/ui/checkbox" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { addTechVendorsToTechSalesRfq, getTechSalesRfqCandidateVendors, searchTechVendors } from "@/lib/techsales-rfq/service" // 벤더 구분자 타입 정의 type VendorFlags = { isCustomerPreferred?: boolean; // 고객(선주) 선호 벤더 isNewDiscovery?: boolean; // 신규 발굴 벤더 isProjectApproved?: boolean; // Project Approved Vendor isShiProposal?: boolean; // SHI Proposal Vendor } // 폼 유효성 검증 스키마 - 벤더 선택 + 구분자 정보 const vendorFormSchema = z.object({ vendorIds: z.array(z.number()).min(1, "최소 하나의 벤더를 선택해주세요"), vendorFlags: z.record(z.string(), z.object({ isCustomerPreferred: z.boolean().optional(), isNewDiscovery: z.boolean().optional(), isProjectApproved: z.boolean().optional(), isShiProposal: z.boolean().optional(), })), }) type VendorFormValues = z.infer // 기술영업 RFQ 타입 정의 type TechSalesRfq = { id: number rfqCode: string | null rfqType: "SHIP" | "TOP" | "HULL" | null ptypeNm: string | null // 프로젝트 타입명 추가 status: string [key: string]: any // eslint-disable-line @typescript-eslint/no-explicit-any } // 벤더 검색 결과 타입 (techVendor 기반) type VendorSearchResult = { id: number vendorName: string vendorCode: string | null status: string country: string | null techVendorType?: string | null matchedItemCount?: number // 후보 벤더 정보 } 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 [candidateVendors, setCandidateVendors] = useState([]) const [isSearching, setIsSearching] = useState(false) const [isLoadingCandidates, setIsLoadingCandidates] = useState(false) const [hasSearched, setHasSearched] = useState(false) const [hasCandidatesLoaded, setHasCandidatesLoaded] = useState(false) // 선택된 벤더들을 별도로 관리하여 검색과 독립적으로 유지 const [selectedVendorData, setSelectedVendorData] = useState([]) const [activeTab, setActiveTab] = useState("step1") const [currentStep, setCurrentStep] = useState(1) const form = useForm({ resolver: zodResolver(vendorFormSchema), defaultValues: { vendorIds: [], vendorFlags: {}, }, }) const selectedVendorIds = form.watch("vendorIds") const vendorFlags = form.watch("vendorFlags") // 후보 벤더 로드 함수 const loadCandidateVendors = useCallback(async () => { if (!selectedRfq?.id) return setIsLoadingCandidates(true) try { const result = await getTechSalesRfqCandidateVendors(selectedRfq.id) if (result.error) { toast.error(result.error) setCandidateVendors([]) } else { // 이미 추가된 벤더 제외 const filteredCandidates = result.data?.filter(vendor => !existingVendorIds.includes(vendor.id)) || [] setCandidateVendors(filteredCandidates) } setHasCandidatesLoaded(true) } catch (error) { console.error("후보 벤더 로드 오류:", error) toast.error("후보 벤더를 불러오는 중 오류가 발생했습니다") setCandidateVendors([]) } finally { setIsLoadingCandidates(false) } }, [selectedRfq?.id, existingVendorIds]) // 벤더 검색 함수 (techVendor 기반) const searchVendorsDebounced = useCallback( async (term: string) => { if (!term.trim()) { setSearchResults([]) setHasSearched(false) return } setIsSearching(true) try { // 선택된 RFQ의 타입을 기반으로 벤더 검색 const rfqType = selectedRfq?.rfqType || undefined; console.log("rfqType", rfqType) // 디버깅용 const results = await searchTechVendors(term, 100, rfqType) // 이미 추가된 벤더 제외 const filteredResults = results.filter((vendor: VendorSearchResult) => !existingVendorIds.includes(vendor.id)) setSearchResults(filteredResults) setHasSearched(true) } catch (error) { console.error("벤더 검색 오류:", error) toast.error("벤더 검색 중 오류가 발생했습니다") setSearchResults([]) } finally { setIsSearching(false) } }, [existingVendorIds, selectedRfq?.rfqType] ) // 검색어 변경 시 디바운스 적용 useEffect(() => { const timer = setTimeout(() => { searchVendorsDebounced(searchTerm) }, 300) return () => clearTimeout(timer) }, [searchTerm, searchVendorsDebounced]) // 다이얼로그 열릴 때 후보 벤더 로드 useEffect(() => { if (open && selectedRfq?.id && !hasCandidatesLoaded) { loadCandidateVendors() } }, [open, selectedRfq?.id, hasCandidatesLoaded, loadCandidateVendors]) // 벤더 선택/해제 핸들러 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) // 구분자 정보도 제거 const currentFlags = form.getValues("vendorFlags") const { [vendor.id.toString()]: _, ...newFlags } = currentFlags form.setValue("vendorFlags", newFlags) } 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) // 구분자 정보도 제거 const currentFlags = form.getValues("vendorFlags") const { [vendorId.toString()]: _, ...newFlags } = currentFlags form.setValue("vendorFlags", newFlags) } // 벤더 구분자 변경 핸들러 const handleVendorFlagChange = (vendorId: number, flagKey: keyof VendorFlags, checked: boolean) => { const currentFlags = form.getValues("vendorFlags") const vendorFlags = currentFlags[vendorId.toString()] || {} form.setValue("vendorFlags", { ...currentFlags, [vendorId.toString()]: { ...vendorFlags, [flagKey]: checked, } }) } // 다음 단계로 이동 const handleNextStep = () => { if (selectedVendorIds.length === 0) { toast.error("최소 하나의 벤더를 선택해주세요") return } setCurrentStep(2) setActiveTab("step2") } // 이전 단계로 이동 const handlePrevStep = () => { setCurrentStep(1) setActiveTab("step1") } // 폼 제출 핸들러 async function onSubmit(values: VendorFormValues) { if (!selectedRfq) { toast.error("선택된 RFQ가 없습니다") return } if (!session?.user?.id) { toast.error("로그인이 필요합니다") return } try { setIsSubmitting(true) // 새로운 서비스 함수 호출 (구분자 정보 포함) const result = await addTechVendorsToTechSalesRfq({ rfqId: selectedRfq.id, vendorIds: values.vendorIds, vendorFlags: values.vendorFlags, // 구분자 정보 추가 createdBy: Number(session.user.id), }) if (result.error) { toast.error(result.error) } else { const successCount = result.data?.length || 0 toast.success(`${successCount}개의 벤더가 성공적으로 추가되었습니다`) onOpenChange(false) form.reset() setSearchTerm("") setSearchResults([]) setCandidateVendors([]) setHasSearched(false) setHasCandidatesLoaded(false) setSelectedVendorData([]) setCurrentStep(1) setActiveTab("step1") onSuccess?.() } } catch (error) { console.error("벤더 추가 오류:", error) toast.error("벤더 추가 중 오류가 발생했습니다") } finally { setIsSubmitting(false) } } // 다이얼로그 닫기 시 폼 리셋 React.useEffect(() => { if (!open) { form.reset() setSearchTerm("") setSearchResults([]) setCandidateVendors([]) setHasSearched(false) setHasCandidatesLoaded(false) setSelectedVendorData([]) setCurrentStep(1) setActiveTab("step1") } }, [open, form]) // 벤더 목록 렌더링 함수 const renderVendorList = (vendors: VendorSearchResult[], showMatchCount = false) => (
{vendors.length > 0 ? ( vendors.map((vendor, index) => (
handleVendorToggle(vendor)} >
{vendor.vendorName} {showMatchCount && vendor.matchedItemCount && vendor.matchedItemCount > 0 && ( {vendor.matchedItemCount}개 매칭 )} {vendor.techVendorType && ( {vendor.techVendorType} )}
{vendor.vendorCode || 'N/A'} {vendor.country && `• ${vendor.country}`}
)) ) : (
{showMatchCount ? "매칭되는 후보 벤더가 없습니다" : "검색 결과가 없습니다"}
)}
) // 벤더 구분자 설정 UI const renderVendorFlagsStep = () => { const isShipRfq = selectedRfq?.rfqType === 'SHIP' return (
선택된 벤더들의 구분자를 설정해주세요. 각 벤더별로 여러 구분자를 선택할 수 있습니다.
{selectedVendorData.length > 0 ? (
{selectedVendorData.map((vendor) => ( {vendor.vendorName} {vendor.vendorCode || 'N/A'} {vendor.country && `• ${vendor.country}`}
{/* 조선 RFQ인 경우: 고객 선호벤더, 신규발굴벤더, SHI Proposal Vendor 표시 */} {isShipRfq && ( <>
handleVendorFlagChange(vendor.id, 'isCustomerPreferred', checked as boolean) } />
handleVendorFlagChange(vendor.id, 'isNewDiscovery', checked as boolean) } />
handleVendorFlagChange(vendor.id, 'isShiProposal', checked as boolean) } />
)} {/* 조선 RFQ가 아닌 경우: Project Approved Vendor, SHI Proposal Vendor 표시 */} {!isShipRfq && ( <>
handleVendorFlagChange(vendor.id, 'isProjectApproved', checked as boolean) } />
handleVendorFlagChange(vendor.id, 'isShiProposal', checked as boolean) } />
)}
))}
) : (
선택된 벤더가 없습니다
)}
) } return ( {/* 헤더 */} 벤더 추가 {selectedRfq ? ( <> {selectedRfq.rfqCode} RFQ에 벤더를 추가합니다. ) : ( "RFQ에 벤더를 추가합니다." )} {/* 단계 표시 */}
= 1 ? 'text-primary' : 'text-muted-foreground'}`}>
= 1 ? 'bg-primary text-primary-foreground' : 'bg-muted' }`}> 1
벤더 선택
= 2 ? 'text-primary' : 'text-muted-foreground'}`}>
= 2 ? 'bg-primary text-primary-foreground' : 'bg-muted' }`}> 2
구분자 설정
{/* 콘텐츠 */}
{/* 숨겨진 FormField - 폼 검증을 위해 필요 */} <>} /> <>} /> {/* 탭 메뉴 */} 벤더 선택 ({selectedVendorData.length}개) 구분자 설정 {/* 1단계: 벤더 선택 탭 */}
{/* 후보 벤더 */}
{isLoadingCandidates ? (
후보 벤더를 불러오는 중...
) : ( renderVendorList(candidateVendors, true) )}
💡 RFQ 아이템과 매칭되는 벤더들이 매칭 아이템 수가 많은 순으로 표시됩니다.
{/* 벤더 검색 */}
setSearchTerm(e.target.value)} className="pl-10" /> {isSearching && ( )}
{/* 검색 결과 */} {hasSearched && (
검색 결과 ({searchResults.length}개)
{renderVendorList(searchResults)}
)} {/* 선택된 벤더 목록 */}
선택된 벤더 ({selectedVendorData.length}개)
{selectedVendorData.length > 0 ? (
{selectedVendorData.map((vendor) => ( {vendor.vendorName} ({vendor.vendorCode || 'N/A'}) handleRemoveVendor(vendor.id)} /> ))}
) : (
선택된 벤더가 없습니다
)}
{/* 2단계: 구분자 설정 탭 */} {renderVendorFlagsStep()}
{/* 푸터 */} {currentStep === 1 ? ( ) : (
)}
) }