summaryrefslogtreecommitdiff
path: root/components/PackageSelector.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/PackageSelector.tsx')
-rw-r--r--components/PackageSelector.tsx129
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