summaryrefslogtreecommitdiff
path: root/lib/menu-list/table/menu-list-table.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-30 08:28:13 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-30 08:28:13 +0000
commit5b6313f16f508882a0ea67716b7dbaa1c6967f04 (patch)
tree3d1d8dafea2f31274ace3fbda08333e889e06d1c /lib/menu-list/table/menu-list-table.tsx
parent3f0fad18483a5c800c79c5e33946d9bb384c10e2 (diff)
(대표님) 20250630 16시 - 유저 도메인별 라우터 분리와 보안성검토 대응
Diffstat (limited to 'lib/menu-list/table/menu-list-table.tsx')
-rw-r--r--lib/menu-list/table/menu-list-table.tsx280
1 files changed, 280 insertions, 0 deletions
diff --git a/lib/menu-list/table/menu-list-table.tsx b/lib/menu-list/table/menu-list-table.tsx
new file mode 100644
index 00000000..097be082
--- /dev/null
+++ b/lib/menu-list/table/menu-list-table.tsx
@@ -0,0 +1,280 @@
+// app/evcp/menu-list/components/menu-list-table.tsx
+
+"use client";
+
+import { useState, useMemo } from "react";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import { Switch } from "@/components/ui/switch";
+import { Button } from "@/components/ui/button";
+import { Search, Filter, ExternalLink } from "lucide-react";
+import { toast } from "sonner";
+import { ManagerSelect } from "./manager-select";
+import { toggleMenuActive } from "../servcie";
+
+interface MenuAssignment {
+ id: number;
+ menuPath: string;
+ menuTitle: string;
+ menuDescription?: string | null;
+ menuGroup?: string | null;
+ sectionTitle: string;
+ domain: string;
+ isActive: boolean;
+ createdAt: Date;
+ updatedAt: Date;
+ manager1Id?: number | null;
+ manager2Id?: number | null;
+ manager1Name?: string | null;
+ manager1Email?: string | null;
+ manager2Name?: string | null;
+ manager2Email?: string | null;
+}
+
+interface User {
+ id: number;
+ name: string;
+ email: string;
+ domain: string;
+}
+
+interface MenuListTableProps {
+ initialMenus: MenuAssignment[];
+ initialUsers: User[];
+}
+
+export function MenuListTable({ initialMenus, initialUsers }: MenuListTableProps) {
+ const [searchQuery, setSearchQuery] = useState("");
+ const [domainFilter, setDomainFilter] = useState<string>("all");
+ const [sectionFilter, setSectionFilter] = useState<string>("all");
+ const [statusFilter, setStatusFilter] = useState<string>("all");
+
+ // 필터링된 메뉴 데이터
+ const filteredMenus = useMemo(() => {
+ return initialMenus.filter((menu) => {
+ const matchesSearch =
+ menu.menuTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ menu.menuPath.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ menu.sectionTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ (menu.menuDescription?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false);
+
+ const matchesDomain = domainFilter === "all" || menu.domain === domainFilter;
+ const matchesSection = sectionFilter === "all" || menu.sectionTitle === sectionFilter;
+ const matchesStatus = statusFilter === "all" ||
+ (statusFilter === "active" && menu.isActive) ||
+ (statusFilter === "inactive" && !menu.isActive);
+
+ return matchesSearch && matchesDomain && matchesSection && matchesStatus;
+ });
+ }, [initialMenus, searchQuery, domainFilter, sectionFilter, statusFilter]);
+
+ // 섹션 리스트 추출
+ const sections = useMemo(() => {
+ const sectionSet = new Set(initialMenus.map(menu => menu.sectionTitle));
+ return Array.from(sectionSet).sort();
+ }, [initialMenus]);
+
+ // 도메인별 사용자 필터링
+ const getFilteredUsers = (domain: string) => {
+ return initialUsers.filter(user => user.domain === domain);
+ };
+
+ // 메뉴 활성화/비활성화 토글
+ const handleToggleActive = async (menuPath: string, isActive: boolean) => {
+ try {
+ const result = await toggleMenuActive(menuPath, isActive);
+ if (result.success) {
+ toast.success(result.message);
+ } else {
+ toast.error(result.message);
+ }
+ } catch (error) {
+ toast.error("메뉴 상태 변경 중 오류가 발생했습니다.");
+ }
+ };
+
+ return (
+ <div className="space-y-4">
+ {/* 필터 영역 */}
+ <div className="flex flex-col sm:flex-row gap-4">
+ <div className="flex-1">
+ <div className="relative">
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+ <Input
+ placeholder="메뉴명, 경로, 설명으로 검색..."
+ value={searchQuery}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ className="pl-8"
+ />
+ </div>
+ </div>
+
+ <div className="flex gap-2">
+ <Select value={domainFilter} onValueChange={setDomainFilter}>
+ <SelectTrigger className="w-[120px]">
+ <SelectValue placeholder="도메인" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="all">전체 도메인</SelectItem>
+ <SelectItem value="evcp">EVCP</SelectItem>
+ <SelectItem value="partners">Partners</SelectItem>
+ </SelectContent>
+ </Select>
+
+ <Select value={sectionFilter} onValueChange={setSectionFilter}>
+ <SelectTrigger className="w-[150px]">
+ <SelectValue placeholder="섹션" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="all">전체 섹션</SelectItem>
+ {sections.map((section) => (
+ <SelectItem key={section} value={section}>
+ {section}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
+ <SelectTrigger className="w-[100px]">
+ <SelectValue placeholder="상태" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="all">전체</SelectItem>
+ <SelectItem value="active">활성</SelectItem>
+ <SelectItem value="inactive">비활성</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+
+ {/* 결과 요약 */}
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
+ <span>
+ 총 {filteredMenus.length}개의 메뉴
+ {searchQuery && ` (${initialMenus.length}개 중 검색 결과)`}
+ </span>
+ </div>
+
+ {/* 테이블 */}
+ <div className="rounded-md border">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-[50px]">상태</TableHead>
+ <TableHead>메뉴 정보</TableHead>
+ <TableHead className="w-[100px]">도메인</TableHead>
+ <TableHead className="w-[200px]">담당자 1</TableHead>
+ <TableHead className="w-[200px]">담당자 2</TableHead>
+ {/* <TableHead className="w-[80px]">동작</TableHead> */}
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {filteredMenus.length === 0 ? (
+ <TableRow>
+ <TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
+ 조건에 맞는 메뉴가 없습니다.
+ </TableCell>
+ </TableRow>
+ ) : (
+ filteredMenus.map((menu) => {
+ const domainUsers = getFilteredUsers(menu.domain);
+
+ return (
+ <TableRow key={menu.id}>
+ <TableCell>
+ <Switch
+ checked={menu.isActive}
+ onCheckedChange={(checked) => handleToggleActive(menu.menuPath, checked)}
+ />
+ </TableCell>
+
+ <TableCell>
+ <div className="space-y-1">
+ <div className="flex items-center gap-2">
+ <span className="font-medium">{menu.menuTitle}</span>
+ <Badge variant="outline" className="text-xs">
+ {menu.sectionTitle}
+ </Badge>
+ {menu.menuGroup && (
+ <Badge variant="secondary" className="text-xs">
+ {menu.menuGroup}
+ </Badge>
+ )}
+ </div>
+ <div className="text-xs text-muted-foreground font-mono">
+ {menu.menuPath}
+ </div>
+ {menu.menuDescription && (
+ <div className="text-xs text-muted-foreground">
+ {menu.menuDescription}
+ </div>
+ )}
+ </div>
+ </TableCell>
+
+ <TableCell>
+ <Badge
+ variant={menu.domain === "evcp" ? "default" : "secondary"}
+ className="text-xs"
+ >
+ {menu.domain.toUpperCase()}
+ </Badge>
+ </TableCell>
+
+ <TableCell>
+ <ManagerSelect
+ menuPath={menu.menuPath}
+ currentManagerId={menu.manager1Id}
+ users={domainUsers}
+ placeholder="담당자 1 선택"
+ otherManagerId={menu.manager2Id}
+ type="manager1"
+ />
+ </TableCell>
+
+ <TableCell>
+ <ManagerSelect
+ menuPath={menu.menuPath}
+ currentManagerId={menu.manager2Id}
+ users={domainUsers}
+ placeholder="담당자 2 선택"
+ otherManagerId={menu.manager1Id}
+ type="manager2"
+ />
+ </TableCell>
+
+ {/* <TableCell>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => window.open(menu.menuPath, '_blank')}
+ >
+ <ExternalLink className="h-3 w-3" />
+ </Button>
+ </TableCell> */}
+ </TableRow>
+ );
+ })
+ )}
+ </TableBody>
+ </Table>
+ </div>
+ </div>
+ );
+} \ No newline at end of file