diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-12 10:42:36 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-12 10:42:36 +0000 |
| commit | 8642ee064ddf96f1db2b948b4cc8bbbd6cfee820 (patch) | |
| tree | 36bd57d147ba929f1d72918d1fb91ad2c4778624 /components/common/selectors/procurement-item/procurement-item-selector.tsx | |
| parent | 57ea2f740abf1c7933671561cfe0e421fb5ef3fc (diff) | |
(최겸) 구매 일반계약, 입찰 수정
Diffstat (limited to 'components/common/selectors/procurement-item/procurement-item-selector.tsx')
| -rw-r--r-- | components/common/selectors/procurement-item/procurement-item-selector.tsx | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/components/common/selectors/procurement-item/procurement-item-selector.tsx b/components/common/selectors/procurement-item/procurement-item-selector.tsx new file mode 100644 index 00000000..5650959c --- /dev/null +++ b/components/common/selectors/procurement-item/procurement-item-selector.tsx @@ -0,0 +1,176 @@ +"use client"; + +import React, { useState, useCallback, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { + Command, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, +} from "@/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Check, ChevronsUpDown, X, Search } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { useDebounce } from "@/hooks/use-debounce"; +import { searchProcurementItemsForSelector, ProcurementSearchItem, getProcurementItemByCode } from "./procurement-item-service"; + +interface ProcurementItemSelectorProps { + selectedProcurementItem?: ProcurementSearchItem | null; + onProcurementItemSelect?: (item: ProcurementSearchItem | null) => void; + onClear?: () => void; + placeholder?: string; + disabled?: boolean; + className?: string; +} + +export function ProcurementItemSelector({ + selectedProcurementItem, + onProcurementItemSelect, + onClear, + placeholder = "품목을 검색하세요...", + disabled = false, + className, +}: ProcurementItemSelectorProps) { + const [open, setOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [searchResults, setSearchResults] = useState<ProcurementSearchItem[]>([]); + const [isLoading, setIsLoading] = useState(false); + + const debouncedSearchQuery = useDebounce(searchQuery, 300); + + // 검색 쿼리가 변경될 때마다 검색 실행 + useEffect(() => { + const performSearch = async () => { + if (debouncedSearchQuery.length < 1) { + setSearchResults([]); + return; + } + + setIsLoading(true); + try { + const results = await searchProcurementItemsForSelector(debouncedSearchQuery); + setSearchResults(results); + } catch (error) { + console.error("품목 검색 실패:", error); + setSearchResults([]); + } finally { + setIsLoading(false); + } + }; + + performSearch(); + }, [debouncedSearchQuery]); + + // 품목 선택 핸들러 + const handleSelect = useCallback((item: ProcurementSearchItem) => { + onProcurementItemSelect?.(item); + setOpen(false); + setSearchQuery(""); + setSearchResults([]); + }, [onProcurementItemSelect]); + + // 선택 해제 핸들러 + const handleClear = useCallback(() => { + onProcurementItemSelect?.(null); + onClear?.(); + setSearchQuery(""); + setSearchResults([]); + }, [onProcurementItemSelect, onClear]); + + return ( + <div className={cn("flex items-center space-x-2", className)}> + <Popover open={open} onOpenChange={setOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={open} + className="w-full justify-between" + disabled={disabled} + > + {selectedProcurementItem ? ( + <span className="truncate"> + {selectedProcurementItem.displayText} + </span> + ) : ( + <span className="text-muted-foreground">{placeholder}</span> + )} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-full p-0" align="start"> + <div className="p-2"> + <div className="relative"> + <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> + <Input + placeholder="품목코드 또는 품목명으로 검색..." + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + className="pl-8" + autoFocus + /> + </div> + </div> + <Command> + <CommandList> + <CommandEmpty> + {isLoading ? ( + <div className="py-6 text-center text-sm text-muted-foreground"> + 검색 중... + </div> + ) : searchQuery.length < 1 ? ( + <div className="py-6 text-center text-sm text-muted-foreground"> + 품목코드 또는 품목명을 입력하세요 + </div> + ) : ( + <div className="py-6 text-center text-sm text-muted-foreground"> + 검색 결과가 없습니다 + </div> + )} + </CommandEmpty> + <CommandGroup> + {searchResults.map((item) => ( + <CommandItem + key={item.itemCode} + value={item.itemCode} + onSelect={() => handleSelect(item)} + className="cursor-pointer" + > + <Check + className={cn( + "mr-2 h-4 w-4", + selectedProcurementItem?.itemCode === item.itemCode + ? "opacity-100" + : "opacity-0" + )} + /> + <div className="flex flex-col"> + <span className="font-medium">{item.itemCode}</span> + <span className="text-sm text-muted-foreground">{item.itemName}</span> + </div> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + + {/* 선택 해제 버튼 */} + {selectedProcurementItem && ( + <Button + variant="ghost" + size="sm" + onClick={handleClear} + disabled={disabled} + className="h-8 w-8 p-0" + > + <X className="h-4 w-4" /> + </Button> + )} + </div> + ); +} |
