diff options
Diffstat (limited to 'lib/swp/table/swp-table-toolbar.tsx')
| -rw-r--r-- | lib/swp/table/swp-table-toolbar.tsx | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/lib/swp/table/swp-table-toolbar.tsx b/lib/swp/table/swp-table-toolbar.tsx new file mode 100644 index 00000000..656dfd4a --- /dev/null +++ b/lib/swp/table/swp-table-toolbar.tsx @@ -0,0 +1,340 @@ +"use client"; + +import { useState, useTransition, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Label } from "@/components/ui/label"; +import { RefreshCw, Download, Search, X, Check, ChevronsUpDown } from "lucide-react"; +import { syncSwpProjectAction, type SwpTableFilters } from "../actions"; +import { useToast } from "@/hooks/use-toast"; +import { useRouter } from "next/navigation"; +import { cn } from "@/lib/utils"; + +interface SwpTableToolbarProps { + filters: SwpTableFilters; + onFiltersChange: (filters: SwpTableFilters) => void; + projects?: Array<{ PROJ_NO: string; PROJ_NM: string }>; + mode?: "admin" | "vendor"; // admin: SWP 동기화 가능, vendor: 읽기 전용 +} + +export function SwpTableToolbar({ + filters, + onFiltersChange, + projects = [], + mode = "admin", +}: SwpTableToolbarProps) { + const [isSyncing, startSync] = useTransition(); + const [localFilters, setLocalFilters] = useState<SwpTableFilters>(filters); + const { toast } = useToast(); + const router = useRouter(); + const [projectSearchOpen, setProjectSearchOpen] = useState(false); + const [projectSearch, setProjectSearch] = useState(""); + + // 동기화 핸들러 + const handleSync = () => { + const projectNo = localFilters.projNo; + + if (!projectNo) { + toast({ + variant: "destructive", + title: "프로젝트 선택 필요", + description: "동기화할 프로젝트를 먼저 선택해주세요.", + }); + return; + } + + startSync(async () => { + try { + toast({ + title: "동기화 시작", + description: `프로젝트 ${projectNo} 동기화를 시작합니다...`, + }); + + const result = await syncSwpProjectAction(projectNo, "V"); + + if (result.success) { + toast({ + title: "동기화 완료", + description: `문서 ${result.stats.documents.total}개, 파일 ${result.stats.files.total}개 동기화 완료`, + }); + + // 페이지 새로고침 + router.refresh(); + } else { + throw new Error(result.errors.join(", ")); + } + } catch (error) { + console.error("동기화 실패:", error); + toast({ + variant: "destructive", + title: "동기화 실패", + description: error instanceof Error ? error.message : "알 수 없는 오류", + }); + } + }); + }; + + // 검색 적용 + const handleSearch = () => { + onFiltersChange(localFilters); + }; + + // 검색 초기화 + const handleReset = () => { + const resetFilters: SwpTableFilters = {}; + setLocalFilters(resetFilters); + onFiltersChange(resetFilters); + }; + + // 프로젝트 필터링 + const filteredProjects = useMemo(() => { + if (!projectSearch) return projects; + + const search = projectSearch.toLowerCase(); + return projects.filter( + (proj) => + proj.PROJ_NO.toLowerCase().includes(search) || + proj.PROJ_NM.toLowerCase().includes(search) + ); + }, [projects, projectSearch]); + + return ( + <div className="space-y-4"> + {/* 상단 액션 바 */} + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + {mode === "admin" && ( + <Button + onClick={handleSync} + disabled={isSyncing || !localFilters.projNo} + size="sm" + > + <RefreshCw className={`h-4 w-4 mr-2 ${isSyncing ? "animate-spin" : ""}`} /> + {isSyncing ? "동기화 중..." : "SWP 동기화"} + </Button> + )} + + <Button variant="outline" size="sm" disabled> + <Download className="h-4 w-4 mr-2" /> + Excel 내보내기 + </Button> + </div> + + <div className="text-sm text-muted-foreground"> + {mode === "vendor" ? "문서 조회 및 업로드" : "SWP 문서 관리 시스템"} + </div> + </div> + + {/* 검색 필터 */} + <div className="rounded-lg border p-4 space-y-4"> + <div className="flex items-center justify-between"> + <h3 className="text-sm font-semibold">검색 필터</h3> + <Button + variant="ghost" + size="sm" + onClick={handleReset} + className="h-8" + > + <X className="h-4 w-4 mr-1" /> + 초기화 + </Button> + </div> + + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> + {/* 프로젝트 번호 */} + <div className="space-y-2"> + <Label htmlFor="projNo">프로젝트 번호</Label> + {projects.length > 0 ? ( + <Popover open={projectSearchOpen} onOpenChange={setProjectSearchOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={projectSearchOpen} + className="w-full justify-between" + > + {localFilters.projNo ? ( + <span className="truncate"> + {projects.find((p) => p.PROJ_NO === localFilters.projNo)?.PROJ_NO || localFilters.projNo} + {" - "} + {projects.find((p) => p.PROJ_NO === localFilters.projNo)?.PROJ_NM} + </span> + ) : ( + <span className="text-muted-foreground">프로젝트 선택</span> + )} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[400px] p-0" align="start"> + <div className="p-2"> + <div className="flex items-center border rounded-md px-3"> + <Search className="h-4 w-4 mr-2 opacity-50" /> + <Input + placeholder="프로젝트 번호 또는 이름으로 검색..." + value={projectSearch} + onChange={(e) => setProjectSearch(e.target.value)} + className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0" + /> + </div> + </div> + <div className="max-h-[300px] overflow-y-auto"> + <div className="p-1"> + <Button + variant="ghost" + className="w-full justify-start font-normal" + onClick={() => { + setLocalFilters({ ...localFilters, projNo: undefined }); + setProjectSearchOpen(false); + setProjectSearch(""); + }} + > + <Check + className={cn( + "mr-2 h-4 w-4", + !localFilters.projNo ? "opacity-100" : "opacity-0" + )} + /> + 전체 + </Button> + {filteredProjects.map((proj) => ( + <Button + key={proj.PROJ_NO} + variant="ghost" + className="w-full justify-start font-normal" + onClick={() => { + setLocalFilters({ ...localFilters, projNo: proj.PROJ_NO }); + setProjectSearchOpen(false); + setProjectSearch(""); + }} + > + <Check + className={cn( + "mr-2 h-4 w-4", + localFilters.projNo === proj.PROJ_NO ? "opacity-100" : "opacity-0" + )} + /> + <div className="flex flex-col items-start"> + <span className="font-mono text-sm">{proj.PROJ_NO}</span> + <span className="text-xs text-muted-foreground">{proj.PROJ_NM}</span> + </div> + </Button> + ))} + {filteredProjects.length === 0 && ( + <div className="py-6 text-center text-sm text-muted-foreground"> + 검색 결과가 없습니다. + </div> + )} + </div> + </div> + </PopoverContent> + </Popover> + ) : ( + <Input + id="projNo" + placeholder="예: SN2190" + value={localFilters.projNo || ""} + onChange={(e) => + setLocalFilters({ ...localFilters, projNo: e.target.value }) + } + /> + )} + </div> + + {/* 문서 번호 */} + <div className="space-y-2"> + <Label htmlFor="docNo">문서 번호</Label> + <Input + id="docNo" + placeholder="문서 번호 검색" + value={localFilters.docNo || ""} + onChange={(e) => + setLocalFilters({ ...localFilters, docNo: e.target.value }) + } + /> + </div> + + {/* 문서 제목 */} + <div className="space-y-2"> + <Label htmlFor="docTitle">문서 제목</Label> + <Input + id="docTitle" + placeholder="제목 검색" + value={localFilters.docTitle || ""} + onChange={(e) => + setLocalFilters({ ...localFilters, docTitle: e.target.value }) + } + /> + </div> + + {/* 패키지 번호 */} + <div className="space-y-2"> + <Label htmlFor="pkgNo">패키지</Label> + <Input + id="pkgNo" + placeholder="패키지 번호" + value={localFilters.pkgNo || ""} + onChange={(e) => + setLocalFilters({ ...localFilters, pkgNo: e.target.value }) + } + /> + </div> + + {/* 업체 코드 */} + <div className="space-y-2"> + <Label htmlFor="vndrCd">업체 코드</Label> + <Input + id="vndrCd" + placeholder="업체 코드" + value={localFilters.vndrCd || ""} + onChange={(e) => + setLocalFilters({ ...localFilters, vndrCd: e.target.value }) + } + /> + </div> + + {/* 스테이지 */} + <div className="space-y-2"> + <Label htmlFor="stage">스테이지</Label> + <Select + value={localFilters.stage || "__all__"} + onValueChange={(value) => + setLocalFilters({ ...localFilters, stage: value === "__all__" ? undefined : value }) + } + > + <SelectTrigger id="stage"> + <SelectValue placeholder="전체" /> + </SelectTrigger> + <SelectContent> + <SelectItem value="__all__">전체</SelectItem> + <SelectItem value="IFA">IFA</SelectItem> + <SelectItem value="IFC">IFC</SelectItem> + <SelectItem value="AFC">AFC</SelectItem> + <SelectItem value="BFC">BFC</SelectItem> + </SelectContent> + </Select> + </div> + </div> + + <div className="flex justify-end"> + <Button onClick={handleSearch} size="sm"> + <Search className="h-4 w-4 mr-2" /> + 검색 + </Button> + </div> + </div> + </div> + ); +} + |
