summaryrefslogtreecommitdiff
path: root/components/common/material/material-selector.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-09-04 03:02:38 +0000
committerjoonhoekim <26rote@gmail.com>2025-09-04 03:02:38 +0000
commit054cb69456825d964c8bd5a70a2b34c5e4b58262 (patch)
treefa5a687652a1667f2c127c2e41805e4cc80dbc59 /components/common/material/material-selector.tsx
parentbe62da14b974419f8e76dfcb086a48412054105c (diff)
(김준회) 네이밍 변경: materialSelector --> materialGroupSelector
Diffstat (limited to 'components/common/material/material-selector.tsx')
-rw-r--r--components/common/material/material-selector.tsx372
1 files changed, 0 insertions, 372 deletions
diff --git a/components/common/material/material-selector.tsx b/components/common/material/material-selector.tsx
deleted file mode 100644
index 67b8c25c..00000000
--- a/components/common/material/material-selector.tsx
+++ /dev/null
@@ -1,372 +0,0 @@
-"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 MaterialSelectorProps {
- selectedMaterials?: MaterialSearchItem[];
- onMaterialsChange?: (materials: MaterialSearchItem[]) => void;
- singleSelect?: boolean;
- placeholder?: string;
- noValuePlaceHolder?: string;
- disabled?: boolean;
- maxSelections?: number;
- className?: string;
- closeOnSelect?: boolean;
- excludeMaterialCodes?: Set<string>; // 제외할 자재그룹코드들
- showInitialData?: boolean; // 초기 클릭시 자재그룹들을 로드할지 여부
-}
-
-export function MaterialSelector({
- selectedMaterials = [],
- onMaterialsChange,
- singleSelect = false,
- placeholder = "자재를 검색하세요...",
- noValuePlaceHolder = "자재를 검색해주세요",
- disabled = false,
- maxSelections,
- className,
- closeOnSelect = true,
- excludeMaterialCodes,
- showInitialData = true
-}: MaterialSelectorProps) {
-
- const [open, setOpen] = useState(false);
- const [searchQuery, setSearchQuery] = useState("");
- const [searchResults, setSearchResults] = useState<MaterialSearchItem[]>([]);
- const [isSearching, setIsSearching] = useState(false);
- const [searchError, setSearchError] = useState<string | null>(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.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 (
- <div className={cn("w-full", className)}>
- <Popover open={open} onOpenChange={setOpen}>
- <PopoverTrigger asChild>
- <Button
- variant="outline"
- role="combobox"
- aria-expanded={open}
- className="w-full justify-between min-h-[2.5rem] h-auto"
- disabled={disabled}
- >
- <div className="flex flex-wrap gap-1 flex-1 text-left">
- {selectedMaterials.length === 0 ? (
- <span className="text-muted-foreground">{noValuePlaceHolder}</span>
- ) : (
- selectedMaterials.map((material) => (
- <Badge
- key={`${material.materialGroupCode}-${material.materialName}`}
- variant="secondary"
- className="gap-1 pr-1"
- >
- <span className="max-w-[200px] truncate">
- {material.displayText}
- </span>
- {!disabled && (
- <button
- type="button"
- className="ml-1 h-3 w-3 rounded-sm hover:bg-red-100 flex items-center justify-center"
- onClick={(e) => {
- e.preventDefault();
- e.stopPropagation();
- handleRemoveMaterial(material);
- }}
- >
- <X className="h-3 w-3 hover:text-red-500" />
- </button>
- )}
- </Badge>
- ))
- )}
- </div>
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
- </Button>
- </PopoverTrigger>
-
- <PopoverContent className="w-full p-0" align="start">
- <Command>
- <div className="flex items-center border-b px-3">
- <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
- <Input
- placeholder={placeholder}
- value={searchQuery}
- onChange={(e) => 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"
- />
- </div>
-
- <CommandList>
- <div className="h-64 overflow-y-auto">
- {!searchQuery.trim() && !showInitialData ? (
- <div className="p-4 text-center text-sm text-muted-foreground">
- 자재를 검색하려면 검색어를 입력해주세요.
- </div>
- ) : !searchQuery.trim() && showInitialData && !initialDataLoaded ? (
- <div className="p-4 text-center text-sm text-muted-foreground">
- 자재 목록을 로드하려면 클릭해주세요.
- </div>
- ) : isSearching ? (
- <div className="p-4 text-center text-sm text-muted-foreground">
- 검색 중...
- </div>
- ) : searchError ? (
- <div className="p-4 text-center text-sm text-red-500">
- {searchError}
- </div>
- ) : searchResults.length === 0 ? (
- <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
- ) : (
- <CommandGroup>
- {searchResults.map((material) => {
- const isExcluded = excludeMaterialCodes?.has(material.materialGroupCode);
- const isSelected = isMaterialSelected(material);
-
- return (
- <CommandItem
- key={`${material.materialGroupCode}-${material.materialName}`}
- onSelect={() => {
- if (!isExcluded) {
- handleMaterialSelect(material);
- }
- }}
- className={cn(
- "cursor-pointer",
- isExcluded && "opacity-50 cursor-not-allowed bg-muted"
- )}
- >
- <div className="mr-2 h-4 w-4 flex items-center justify-center">
- {isExcluded ? (
- <span className="text-xs text-muted-foreground">✓</span>
- ) : (
- <Check
- className={cn(
- "h-4 w-4",
- isSelected ? "opacity-100" : "opacity-0"
- )}
- />
- )}
- </div>
- <div className="flex-1">
- <div className={cn(
- "font-medium",
- isExcluded && "text-muted-foreground"
- )}>
- {material.materialName}
- {isExcluded && (
- <span className="ml-2 text-xs bg-red-100 text-red-600 px-2 py-1 rounded">
- 이미 등록됨
- </span>
- )}
- </div>
- <div className="text-xs text-muted-foreground">
- 자재그룹코드: {material.materialGroupCode}
- </div>
- </div>
- </CommandItem>
- );
- })}
- </CommandGroup>
- )}
- </div>
-
- {/* 페이지네이션 */}
- {searchResults.length > 0 && pagination.pageCount > 1 && (
- <div className="flex items-center justify-between border-t px-3 py-2">
- <div className="text-xs text-muted-foreground">
- 총 {pagination.total}개 중 {((pagination.page - 1) * pagination.perPage) + 1}-
- {Math.min(pagination.page * pagination.perPage, pagination.total)}개 표시
- </div>
- <div className="flex items-center gap-2">
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handlePageChange(currentPage - 1)}
- disabled={!pagination.hasPrevPage}
- className="h-6 w-6 p-0"
- >
- <ChevronLeft className="h-3 w-3" />
- </Button>
- <span className="text-xs">
- {pagination.page} / {pagination.pageCount}
- </span>
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handlePageChange(currentPage + 1)}
- disabled={!pagination.hasNextPage}
- className="h-6 w-6 p-0"
- >
- <ChevronRight className="h-3 w-3" />
- </Button>
- </div>
- </div>
- )}
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- </div>
- );
-}