"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 { searchMaterialsForSelector, MaterialSearchItem } from "./material-service"; interface MaterialSelectorProps { selectedMaterials?: MaterialSearchItem[]; onMaterialsChange?: (materials: MaterialSearchItem[]) => void; singleSelect?: boolean; placeholder?: string; noValuePlaceHolder?: string; disabled?: boolean; className?: string; closeOnSelect?: boolean; excludeMaterialCodes?: Set; // 제외할 자재코드들 showInitialData?: boolean; // 초기 클릭시 자재들을 로드할지 여부 maxSelections?: number; // 최대 선택 가능한 자재 개수 (1이면 단일 선택, undefined면 제한 없음) } export function MaterialSelector({ selectedMaterials = [], onMaterialsChange, singleSelect = false, placeholder = "자재코드를 검색하세요...", noValuePlaceHolder = "자재를 검색해주세요", disabled = false, className, closeOnSelect = true, excludeMaterialCodes, showInitialData = true, maxSelections }: MaterialSelectorProps) { 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 searchMaterialsForSelector(query, page, 10); 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); } }, []); // 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 handleMaterialSelect = useCallback((material: MaterialSearchItem) => { if (disabled) return; let newSelectedMaterials: MaterialSearchItem[]; // maxSelections가 1이면 단일 선택 모드로 동작 const isSingleSelectMode = singleSelect || maxSelections === 1; if (isSingleSelectMode) { newSelectedMaterials = [material]; } else { const isAlreadySelected = selectedMaterials.some( (selected) => selected.materialCode === material.materialCode && selected.materialName === material.materialName ); if (isAlreadySelected) { newSelectedMaterials = selectedMaterials.filter( (selected) => !(selected.materialCode === material.materialCode && selected.materialName === material.materialName) ); } else { // 최대 선택 개수 확인 if (maxSelections && selectedMaterials.length >= maxSelections) { // 최대 개수에 도달한 경우 선택하지 않음 return; } newSelectedMaterials = [...selectedMaterials, material]; } } onMaterialsChange?.(newSelectedMaterials); if (closeOnSelect && isSingleSelectMode) { setOpen(false); } }, [disabled, singleSelect, maxSelections, selectedMaterials, onMaterialsChange, closeOnSelect]); // 개별 자재 제거 const handleRemoveMaterial = useCallback((materialToRemove: MaterialSearchItem) => { if (disabled) return; const newSelectedMaterials = selectedMaterials.filter( (material) => !(material.materialCode === materialToRemove.materialCode && material.materialName === materialToRemove.materialName) ); onMaterialsChange?.(newSelectedMaterials); }, [disabled, selectedMaterials, onMaterialsChange]); // 선택된 자재가 있는지 확인 const isMaterialSelected = useCallback((material: MaterialSearchItem) => { return selectedMaterials.some( (selected) => selected.materialCode === material.materialCode && selected.materialName === material.materialName ); }, [selectedMaterials]); 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((material) => { const isExcluded = excludeMaterialCodes?.has(material.materialCode); const isSelected = isMaterialSelected(material); const isMaxReached = maxSelections && selectedMaterials.length >= maxSelections && !isSelected; const isDisabled = isExcluded || isMaxReached; return ( { if (!isDisabled) { handleMaterialSelect(material); } }} className={cn( "cursor-pointer", isDisabled && "opacity-50 cursor-not-allowed bg-muted" )} >
{isExcluded ? ( ) : isMaxReached ? ( - ) : ( )}
{material.materialName} {isExcluded && ( 이미 등록됨 )} {isMaxReached && ( 선택 제한 ({maxSelections}개) )}
자재코드: {material.materialCode}
); })}
)}
{/* 고정 페이지네이션 - 항상 밑에 표시 */} {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}
)}
); }