"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 "@/lib/material/material-group-service"; interface MaterialGroupSelectorProps { selectedMaterials?: MaterialSearchItem[]; onMaterialsChange?: (materials: MaterialSearchItem[]) => void; singleSelect?: boolean; placeholder?: string; noValuePlaceHolder?: string; disabled?: boolean; maxSelections?: number; className?: string; closeOnSelect?: boolean; excludeMaterialCodes?: Set; // 제외할 자재그룹코드들 showInitialData?: boolean; // 초기 클릭시 자재그룹들을 로드할지 여부 } export function MaterialGroupSelector({ selectedMaterials = [], onMaterialsChange, singleSelect = false, placeholder = "자재를 검색하세요...", noValuePlaceHolder = "자재를 검색해주세요", disabled = false, maxSelections, className, closeOnSelect = true, excludeMaterialCodes, showInitialData = true }: MaterialGroupSelectorProps) { 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[]; if (singleSelect) { newSelectedMaterials = [material]; } else { const isAlreadySelected = selectedMaterials.some( (selected) => selected.materialGroupCode === material.materialGroupCode && selected.materialGroupDesc === material.materialGroupDesc ); if (isAlreadySelected) { newSelectedMaterials = selectedMaterials.filter( (selected) => !(selected.materialGroupCode === material.materialGroupCode && selected.materialGroupDesc === material.materialGroupDesc) ); } else { if (maxSelections && selectedMaterials.length >= maxSelections) { return; // 최대 선택 수 초과 시 추가하지 않음 } newSelectedMaterials = [...selectedMaterials, material]; } } onMaterialsChange?.(newSelectedMaterials); if (closeOnSelect && singleSelect) { setOpen(false); } }, [disabled, singleSelect, selectedMaterials, maxSelections, onMaterialsChange, closeOnSelect]); // 개별 자재 제거 const handleRemoveMaterial = useCallback((materialToRemove: MaterialSearchItem) => { if (disabled) return; const newSelectedMaterials = selectedMaterials.filter( (material) => !(material.materialGroupCode === materialToRemove.materialGroupCode && material.materialGroupDesc === materialToRemove.materialGroupDesc) ); onMaterialsChange?.(newSelectedMaterials); }, [disabled, selectedMaterials, onMaterialsChange]); // 선택된 자재가 있는지 확인 const isMaterialSelected = useCallback((material: MaterialSearchItem) => { return selectedMaterials.some( (selected) => selected.materialGroupCode === material.materialGroupCode && selected.materialGroupDesc === material.materialGroupDesc ); }, [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" />
{!searchQuery.trim() && !showInitialData ? (
자재를 검색하려면 검색어를 입력해주세요.
) : !searchQuery.trim() && showInitialData && !initialDataLoaded ? (
자재 목록을 로드하려면 클릭해주세요.
) : isSearching ? (
검색 중...
) : searchError ? (
{searchError}
) : searchResults.length === 0 ? ( 검색 결과가 없습니다. ) : ( {searchResults.map((material) => { const isExcluded = excludeMaterialCodes?.has(material.materialGroupCode); const isSelected = isMaterialSelected(material); return ( { if (!isExcluded) { handleMaterialSelect(material); } }} className={cn( "cursor-pointer", isExcluded && "opacity-50 cursor-not-allowed bg-muted" )} >
{isExcluded ? ( ) : ( )}
{material.materialGroupDesc} {isExcluded && ( 이미 등록됨 )}
자재그룹코드: {material.materialGroupCode}
); })}
)}
{/* 페이지네이션 */} {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}
)}
); }