"use client" import React, { useState, useCallback } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Command, CommandList, CommandEmpty, CommandGroup, CommandItem, } from "@/components/ui/command" import { Check, ChevronsUpDown, X, Search, ChevronLeft, ChevronRight } from "lucide-react" import { cn } from "@/lib/utils" import { useDebounce } from "@/hooks/use-debounce" import { searchVendorsForSelector, VendorSearchItem } from "./vendor-service" interface VendorSelectorProps { selectedVendors?: VendorSearchItem[] onVendorsChange?: (vendors: VendorSearchItem[]) => void singleSelect?: boolean placeholder?: string noValuePlaceHolder?: string disabled?: boolean className?: string closeOnSelect?: boolean excludeVendorIds?: Set // 제외할 벤더 ID들 showInitialData?: boolean // 초기 클릭시 벤더들을 로드할지 여부 maxSelections?: number // 최대 선택 가능한 벤더 개수 statusFilter?: string // 특정 상태의 벤더만 표시 includeCountry?: boolean // 국가 정보 포함 여부 } export function VendorSelector({ selectedVendors = [], onVendorsChange, singleSelect = false, placeholder = "벤더를 검색하세요...", noValuePlaceHolder = "벤더를 검색해주세요", disabled = false, className, closeOnSelect = true, excludeVendorIds, showInitialData = true, maxSelections, statusFilter, includeCountry = false }: VendorSelectorProps) { const [open, setOpen] = useState(false) const [searchQuery, setSearchQuery] = useState("") const [searchResults, setSearchResults] = useState([]) const [isSearching, setIsSearching] = useState(false) const [searchError, setSearchError] = useState(null) const [currentPage, setCurrentPage] = useState(1) const [initialDataLoaded, setInitialDataLoaded] = useState(false) const [pagination, setPagination] = useState({ page: 1, perPage: 10, total: 0, pageCount: 0, hasNextPage: false, hasPrevPage: false, }) // Debounce 적용된 검색어 const debouncedSearchQuery = useDebounce(searchQuery, 300) // 검색 실행 - useCallback으로 메모이제이션 const performSearch = useCallback(async (query: string, page: number = 1) => { setIsSearching(true) setSearchError(null) try { const result = await searchVendorsForSelector(query, page, 10, { statusFilter, sortBy: 'vendorName', sortOrder: 'asc', includeCountry }) if (result.success) { setSearchResults(result.data) setPagination(result.pagination) setCurrentPage(page) } else { setSearchResults([]) setSearchError("검색 중 오류가 발생했습니다.") setPagination({ page: 1, perPage: 10, total: 0, pageCount: 0, hasNextPage: false, hasPrevPage: false, }) } } catch (err) { console.error("벤더 검색 실패:", err) setSearchResults([]) setSearchError("검색 중 오류가 발생했습니다.") setPagination({ page: 1, perPage: 10, total: 0, pageCount: 0, hasNextPage: false, hasPrevPage: false, }) } finally { setIsSearching(false) } }, [statusFilter, includeCountry]) // Popover 열림시 초기 데이터 로드 React.useEffect(() => { if (open && showInitialData && !initialDataLoaded && !searchQuery.trim()) { setInitialDataLoaded(true) performSearch("", 1) // 빈 쿼리로 초기 데이터 로드 } }, [open, showInitialData, initialDataLoaded, searchQuery, performSearch]) // Debounced 검색어 변경 시 검색 실행 (검색어가 있을 때만) React.useEffect(() => { if (debouncedSearchQuery.trim()) { setCurrentPage(1) performSearch(debouncedSearchQuery, 1) } else if (showInitialData && initialDataLoaded) { // 검색어가 없고 초기 데이터를 보여주는 경우 초기 데이터 유지 // 아무것도 하지 않음 (기존 데이터 유지) } else { // 검색어가 없으면 결과 초기화 setSearchResults([]) setPagination({ page: 1, perPage: 10, total: 0, pageCount: 0, hasNextPage: false, hasPrevPage: false, }) } }, [debouncedSearchQuery, performSearch, showInitialData, initialDataLoaded]) // 페이지 변경 처리 - useCallback으로 메모이제이션 const handlePageChange = useCallback((newPage: number) => { if (newPage >= 1 && newPage <= pagination.pageCount) { const query = debouncedSearchQuery.trim() || (showInitialData && initialDataLoaded ? "" : debouncedSearchQuery) performSearch(query, newPage) } }, [pagination.pageCount, performSearch, debouncedSearchQuery, showInitialData, initialDataLoaded]) // 벤더 선택 처리 - useCallback으로 메모이제이션 const handleVendorSelect = useCallback((vendor: VendorSearchItem) => { if (disabled) return let newSelectedVendors: VendorSearchItem[] // maxSelections가 1이면 단일 선택 모드로 동작 const isSingleSelectMode = singleSelect || maxSelections === 1 if (isSingleSelectMode) { newSelectedVendors = [vendor] } else { const isAlreadySelected = selectedVendors.some( (selected) => selected.id === vendor.id ) if (isAlreadySelected) { newSelectedVendors = selectedVendors.filter( (selected) => selected.id !== vendor.id ) } else { // 최대 선택 개수 확인 if (maxSelections && selectedVendors.length >= maxSelections) { // 최대 개수에 도달한 경우 선택하지 않음 return } newSelectedVendors = [...selectedVendors, vendor] } } onVendorsChange?.(newSelectedVendors) if (closeOnSelect && isSingleSelectMode) { setOpen(false) } }, [disabled, singleSelect, maxSelections, selectedVendors, onVendorsChange, closeOnSelect]) // 개별 벤더 제거 const handleRemoveVendor = useCallback((vendorToRemove: VendorSearchItem) => { if (disabled) return const newSelectedVendors = selectedVendors.filter( (vendor) => vendor.id !== vendorToRemove.id ) onVendorsChange?.(newSelectedVendors) }, [disabled, selectedVendors, onVendorsChange]) // 선택된 벤더가 있는지 확인 const isVendorSelected = useCallback((vendor: VendorSearchItem) => { return selectedVendors.some((selected) => selected.id === vendor.id) }, [selectedVendors]) // 벤더 상태별 색상 const getStatusColor = (status: string) => { switch (status) { case 'ACTIVE': return 'bg-green-100 text-green-800' case 'APPROVED': return 'bg-blue-100 text-blue-800' case 'PENDING_REVIEW': return 'bg-yellow-100 text-yellow-800' case 'INACTIVE': return 'bg-gray-100 text-gray-800' default: return 'bg-gray-100 text-gray-800' } } return (
)} )) )}
setSearchQuery(e.target.value)} className="flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none border-0 focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50" />
{/* 스크롤 컨테이너 + 고정 페이지네이션 */}
{ e.stopPropagation() // 이벤트 전파 차단 const target = e.currentTarget target.scrollTop += e.deltaY // 직접 스크롤 처리 }} > {!searchQuery.trim() && !showInitialData ? (
벤더를 검색하려면 검색어를 입력해주세요.
) : !searchQuery.trim() && showInitialData && !initialDataLoaded ? (
벤더 목록을 로드하려면 클릭해주세요.
) : isSearching ? (
검색 중...
) : searchError ? (
{searchError}
) : searchResults.length === 0 ? ( 검색 결과가 없습니다. ) : ( {searchResults.map((vendor) => { const isExcluded = excludeVendorIds?.has(vendor.id) const isSelected = isVendorSelected(vendor) const isMaxReached = maxSelections && selectedVendors.length >= maxSelections && !isSelected const isDisabled = isExcluded || isMaxReached return ( { if (!isDisabled) { handleVendorSelect(vendor) } }} className={cn( "cursor-pointer", isDisabled && "opacity-50 cursor-not-allowed bg-muted" )} >
{isExcluded ? ( ) : isMaxReached ? ( - ) : ( )}
{vendor.vendorName} {isExcluded && ( 제외됨 )} {isMaxReached && ( 선택 제한 ({maxSelections}개) )}
{vendor.status} {includeCountry && vendor.country && ( {vendor.country} )}
{vendor.vendorCode ? ( 벤더코드: {vendor.vendorCode} ) : ( 벤더코드: - )}
) })}
)}
{/* 고정 페이지네이션 - 항상 밑에 표시 */} {searchResults.length > 0 && pagination.pageCount > 1 && (
총 {pagination.total}개 중 {((pagination.page - 1) * pagination.perPage) + 1}- {Math.min(pagination.page * pagination.perPage, pagination.total)}개 표시
{pagination.page} / {pagination.pageCount}
)}
) }