summaryrefslogtreecommitdiff
path: root/components/common/selectors/procurement-item/procurement-item-selector.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/common/selectors/procurement-item/procurement-item-selector.tsx')
-rw-r--r--components/common/selectors/procurement-item/procurement-item-selector.tsx176
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>
+ );
+}