summaryrefslogtreecommitdiff
path: root/lib/swp/table/swp-table-toolbar.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-23 18:44:19 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-23 18:44:19 +0900
commit04bd1965c3699a4b29ed9c9627574bfeedd3d6c6 (patch)
tree691b9a6e844a788937a240d47e77e8cfa848a88a /lib/swp/table/swp-table-toolbar.tsx
parent535e234dbd674bf2e5ecf344e03ed8ae5b2cbd6c (diff)
(김준회) SWP 문서 업로드 (Submisssion) 초기 개발건
Diffstat (limited to 'lib/swp/table/swp-table-toolbar.tsx')
-rw-r--r--lib/swp/table/swp-table-toolbar.tsx340
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>
+ );
+}
+