From c5002d77087b256599b174ada611621657fcc523 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Sun, 15 Jun 2025 04:40:22 +0000 Subject: (최겸) 기술영업 조선,해양RFQ 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/detail-table/add-vendor-dialog.tsx | 271 +++++++++++++++------ 1 file changed, 194 insertions(+), 77 deletions(-) (limited to 'lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx') diff --git a/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx b/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx index b66f4d77..3574111f 100644 --- a/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx +++ b/lib/techsales-rfq/table/detail-table/add-vendor-dialog.tsx @@ -6,7 +6,7 @@ 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 { Check, X, Search, Loader2, Star } from "lucide-react" import { useSession } from "next-auth/react" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" @@ -15,8 +15,8 @@ import { Form, FormField, FormItem, FormLabel, FormMessage } from "@/components/ 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" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { addTechVendorsToTechSalesRfq, getTechSalesRfqCandidateVendors, searchTechVendors } from "@/lib/techsales-rfq/service" // 폼 유효성 검증 스키마 - 간단화 const vendorFormSchema = z.object({ @@ -33,13 +33,15 @@ type TechSalesRfq = { [key: string]: any // eslint-disable-line @typescript-eslint/no-explicit-any } -// 벤더 검색 결과 타입 (searchVendors 함수 반환 타입과 일치) +// 벤더 검색 결과 타입 (techVendor 기반) type VendorSearchResult = { id: number vendorName: string vendorCode: string | null status: string country: string | null + techVendorType?: string | null + matchedItemCount?: number // 후보 벤더 정보 } interface AddVendorDialogProps { @@ -61,10 +63,14 @@ export function AddVendorDialog({ 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("candidates") const form = useForm({ resolver: zodResolver(vendorFormSchema), @@ -75,7 +81,32 @@ export function AddVendorDialog({ const selectedVendorIds = form.watch("vendorIds") - // 검색 함수 (디바운스 적용) + // 후보 벤더 로드 함수 + 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()) { @@ -86,9 +117,15 @@ export function AddVendorDialog({ setIsSearching(true) try { - const results = await searchVendors(term, 100) + // 선택된 RFQ의 타입을 기반으로 벤더 검색 + const rfqType = selectedRfq?.rfqCode?.includes("SHIP") ? "SHIP" : + selectedRfq?.rfqCode?.includes("TOP") ? "TOP" : + selectedRfq?.rfqCode?.includes("HULL") ? "HULL" : undefined; + + const results = await searchTechVendors(term, 100, rfqType) + // 이미 추가된 벤더 제외 - const filteredResults = results.filter(vendor => !existingVendorIds.includes(vendor.id)) + const filteredResults = results.filter((vendor: VendorSearchResult) => !existingVendorIds.includes(vendor.id)) setSearchResults(filteredResults) setHasSearched(true) } catch (error) { @@ -111,6 +148,13 @@ export function AddVendorDialog({ 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") @@ -155,8 +199,8 @@ export function AddVendorDialog({ try { setIsSubmitting(true) - // 서비스 함수 호출 - const result = await addVendorsToTechSalesRfq({ + // 새로운 서비스 함수 호출 + const result = await addTechVendorsToTechSalesRfq({ rfqId: selectedRfq.id, vendorIds: values.vendorIds, createdBy: Number(session.user.id), @@ -165,15 +209,16 @@ export function AddVendorDialog({ 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) + const successCount = result.data?.length || 0 + toast.success(`${successCount}개의 벤더가 성공적으로 추가되었습니다`) onOpenChange(false) form.reset() setSearchTerm("") setSearchResults([]) + setCandidateVendors([]) setHasSearched(false) + setHasCandidatesLoaded(false) setSelectedVendorData([]) onSuccess?.() } @@ -191,14 +236,69 @@ export function AddVendorDialog({ form.reset() setSearchTerm("") setSearchResults([]) + setCandidateVendors([]) setHasSearched(false) + setHasCandidatesLoaded(false) setSelectedVendorData([]) + setActiveTab("candidates") } }, [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 ? "매칭되는 후보 벤더가 없습니다" : "검색 결과가 없습니다"} +
+ )} +
+
+ ) + return ( - + {/* 헤더 */} 벤더 추가 @@ -217,73 +317,91 @@ export function AddVendorDialog({
- {/* 벤더 검색 필드 */} -
- -
- - setSearchTerm(e.target.value)} - className="pl-10" - /> - {isSearching && ( - - )} -
-
+ {/* 탭 메뉴 */} + + + + 후보 벤더 ({candidateVendors.length}) + + + 벤더 검색 + + - {/* 검색 결과 */} - {hasSearched && ( -
-
- 검색 결과 ({searchResults.length}개) -
- -
- {searchResults.length > 0 ? ( - searchResults.map((vendor) => ( -
handleVendorToggle(vendor)} - > -
- -
-
{vendor.vendorName}
-
- {vendor.vendorCode || 'N/A'} {vendor.country && `• ${vendor.country}`} -
-
-
-
- )) - ) : ( -
- 검색 결과가 없습니다 + {/* 후보 벤더 탭 */} + +
+
+ + +
+ + {isLoadingCandidates ? ( +
+
+ + 후보 벤더를 불러오는 중...
+
+ ) : ( + renderVendorList(candidateVendors, true) + )} + +
+ 💡 RFQ 아이템과 매칭되는 벤더들이 매칭 아이템 수가 많은 순으로 표시됩니다. +
+
+
+ + {/* 벤더 검색 탭 */} + + {/* 벤더 검색 필드 */} +
+ +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> + {isSearching && ( + )}
- -
- )} +
- {/* 검색 안내 메시지 */} - {!hasSearched && !searchTerm && ( -
- 벤더명 또는 벤더코드를 입력하여 검색해주세요 -
- )} + {/* 검색 결과 */} + {hasSearched ? ( +
+
+ 검색 결과 ({searchResults.length}개) +
+ {renderVendorList(searchResults)} +
+ ) : ( +
+ 벤더명 또는 벤더코드를 입력하여 검색해주세요 +
+ )} + + {/* 선택된 벤더 목록 - 하단에 항상 표시 */} - {/*

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

*/} +

• 후보 벤더는 RFQ 아이템 코드와 매칭되는 기술영업 벤더들입니다.

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

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

-

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

-- cgit v1.2.3