diff options
Diffstat (limited to 'components/PackageSelector.tsx')
| -rw-r--r-- | components/PackageSelector.tsx | 129 |
1 files changed, 129 insertions, 0 deletions
diff --git a/components/PackageSelector.tsx b/components/PackageSelector.tsx new file mode 100644 index 00000000..9ad2727c --- /dev/null +++ b/components/PackageSelector.tsx @@ -0,0 +1,129 @@ +"use client" + +import * as React from "react" +import { Check, ChevronsUpDown } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" +import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command" +import { cn } from "@/lib/utils" +import { getPackagesByProject, type PackageItem } from "@/lib/items/service" + +interface PackageSelectorProps { + projectNo?: string | null; + selectedPackage?: PackageItem | null; + onPackageSelect: (packageItem: PackageItem) => void; + placeholder?: string; + disabled?: boolean; +} + +export function PackageSelector({ + projectNo, + selectedPackage, + onPackageSelect, + placeholder = "패키지 선택...", + disabled = false +}: PackageSelectorProps) { + const [open, setOpen] = React.useState(false) + const [searchTerm, setSearchTerm] = React.useState("") + const [packages, setPackages] = React.useState<PackageItem[]>([]) + const [isLoading, setIsLoading] = React.useState(false) + + // 프로젝트가 변경되면 패키지 목록 로드 + React.useEffect(() => { + if (projectNo) { + loadPackages(projectNo); + } else { + setPackages([]); + } + }, [projectNo]); + + const loadPackages = async (projNo: string) => { + setIsLoading(true); + try { + // 서버 액션 호출 + const packageList = await getPackagesByProject(projNo); + setPackages(packageList); + } catch (error) { + console.error("패키지 목록 로드 오류:", error); + setPackages([]); + } finally { + setIsLoading(false); + } + }; + + // 클라이언트 측에서 검색어로 필터링 + const filteredPackages = React.useMemo(() => { + if (!searchTerm.trim()) return packages; + + const lowerSearch = searchTerm.toLowerCase(); + return packages.filter( + pkg => + pkg.packageCode.toLowerCase().includes(lowerSearch) || + (pkg.description && pkg.description.toLowerCase().includes(lowerSearch)) + ); + }, [packages, searchTerm]); + + // 패키지 선택 처리 + const handleSelectPackage = (pkg: PackageItem) => { + onPackageSelect(pkg); + setOpen(false); + }; + + return ( + <Popover open={open} onOpenChange={setOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={open} + className="w-full justify-between" + disabled={disabled || !projectNo} + > + {selectedPackage + ? `${selectedPackage.packageCode} - ${selectedPackage.description}` + : (projectNo ? placeholder : "프로젝트를 먼저 선택하세요")} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[400px] p-0"> + <Command> + <CommandInput + placeholder="패키지 코드/설명 검색..." + onValueChange={setSearchTerm} + /> + <CommandList className="max-h-[300px]" onWheel={(e) => { + e.stopPropagation(); + const target = e.currentTarget; + target.scrollTop += e.deltaY; + }}> + <CommandEmpty>검색 결과가 없습니다</CommandEmpty> + {isLoading ? ( + <div className="py-6 text-center text-sm">로딩 중...</div> + ) : ( + <CommandGroup> + {filteredPackages.map((pkg) => ( + <CommandItem + key={pkg.packageCode} + value={`${pkg.packageCode} ${pkg.description}`} + onSelect={() => handleSelectPackage(pkg)} + > + <Check + className={cn( + "mr-2 h-4 w-4", + selectedPackage?.packageCode === pkg.packageCode + ? "opacity-100" + : "opacity-0" + )} + /> + <span className="font-medium">{pkg.packageCode}</span> + <span className="ml-2 text-gray-500 truncate">- {pkg.description}</span> + </CommandItem> + ))} + </CommandGroup> + )} + </CommandList> + </Command> + </PopoverContent> + </Popover> + ); +}
\ No newline at end of file |
