From f72142f6cc46c7be5bf90803d365c2ecd144c53d Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 1 Sep 2025 10:22:55 +0000 Subject: (김준회) MDG 자재마스터 정보 조회 기능 및 메뉴 추가, 회원가입시 공급품목 선택 기능 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/common/material/material-selector.tsx | 320 +++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 components/common/material/material-selector.tsx (limited to 'components/common/material') diff --git a/components/common/material/material-selector.tsx b/components/common/material/material-selector.tsx new file mode 100644 index 00000000..aa68d2b5 --- /dev/null +++ b/components/common/material/material-selector.tsx @@ -0,0 +1,320 @@ +"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 { ScrollArea } from "@/components/ui/scroll-area"; +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 MaterialSelectorProps { + selectedMaterials?: MaterialSearchItem[]; + onMaterialsChange?: (materials: MaterialSearchItem[]) => void; + singleSelect?: boolean; + placeholder?: string; + noValuePlaceHolder?: string; + disabled?: boolean; + maxSelections?: number; + className?: string; + closeOnSelect?: boolean; +} + +export function MaterialSelector({ + selectedMaterials = [], + onMaterialsChange, + singleSelect = false, + placeholder = "자재를 검색하세요...", + noValuePlaceHolder = "자재를 검색해주세요", + disabled = false, + maxSelections, + className, + closeOnSelect = true +}: 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 [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); + } + }, []); + + // Debounced 검색어 변경 시 검색 실행 (검색어가 있을 때만) + React.useEffect(() => { + if (debouncedSearchQuery.trim()) { + setCurrentPage(1); + performSearch(debouncedSearchQuery, 1); + } else { + // 검색어가 없으면 결과 초기화 + setSearchResults([]); + setPagination({ + page: 1, + perPage: 10, + total: 0, + pageCount: 0, + hasNextPage: false, + hasPrevPage: false, + }); + } + }, [debouncedSearchQuery, performSearch]); + + // 페이지 변경 처리 - useCallback으로 메모이제이션 + const handlePageChange = useCallback((newPage: number) => { + if (newPage >= 1 && newPage <= pagination.pageCount) { + performSearch(debouncedSearchQuery, newPage); + } + }, [pagination.pageCount, performSearch, debouncedSearchQuery]); + + // 자재 선택 처리 - 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.materialName === material.materialName + ); + + if (isAlreadySelected) { + newSelectedMaterials = selectedMaterials.filter( + (selected) => !(selected.materialGroupCode === material.materialGroupCode && + selected.materialName === material.materialName) + ); + } 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.materialName === materialToRemove.materialName) + ); + onMaterialsChange?.(newSelectedMaterials); + }, [disabled, selectedMaterials, onMaterialsChange]); + + // 선택된 자재가 있는지 확인 + const isMaterialSelected = useCallback((material: MaterialSearchItem) => { + return selectedMaterials.some( + (selected) => selected.materialGroupCode === material.materialGroupCode && + 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" + /> +
+ + + + {!searchQuery.trim() ? ( +
+ 자재를 검색하려면 검색어를 입력해주세요. +
+ ) : isSearching ? ( +
+ 검색 중... +
+ ) : searchError ? ( +
+ {searchError} +
+ ) : searchResults.length === 0 ? ( + 검색 결과가 없습니다. + ) : ( + + {searchResults.map((material) => ( + handleMaterialSelect(material)} + className="cursor-pointer" + > + +
+
{material.materialName}
+
+ 코드: {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} + + +
+
+ )} +
+
+
+
+
+ ); +} -- cgit v1.2.3