From 8b23b471638a155fd1bfa3a8c853b26d9315b272 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 26 Sep 2025 09:57:24 +0000 Subject: (대표님) 권한관리, 문서업로드, rfq첨부, SWP문서룰 등 (최겸) 입찰 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/permissions/menu-permission-manager.tsx | 552 +++++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 components/permissions/menu-permission-manager.tsx (limited to 'components/permissions/menu-permission-manager.tsx') diff --git a/components/permissions/menu-permission-manager.tsx b/components/permissions/menu-permission-manager.tsx new file mode 100644 index 00000000..1b771520 --- /dev/null +++ b/components/permissions/menu-permission-manager.tsx @@ -0,0 +1,552 @@ +// components/permissions/menu-permission-manager.tsx + +"use client"; + +import { useState, useEffect } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Separator } from "@/components/ui/separator"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Menu, + Search, + Shield, + Lock, + Unlock, + Users, + User, + Settings, + FileText, + Eye, + Edit, + Trash, + Plus, + ChevronRight, + AlertCircle, + CheckCircle, + ExternalLink +} from "lucide-react"; +import { toast } from "sonner"; +import { cn } from "@/lib/utils"; +import { + getMenuPermissions, + updateMenuPermissions, + getMenuManagers, + updateMenuManagers, +} from "@/lib/permissions/service"; + +// 메뉴 구조 타입 +interface MenuItem { + menuPath: string; + menuTitle: string; + menuDescription?: string; + sectionTitle?: string; + menuGroup?: string; + domain: string; + isActive: boolean; + manager1?: { id: number; name: string; email: string; imageUrl?: string }; + manager2?: { id: number; name: string; email: string; imageUrl?: string }; + requiredPermissions: Array<{ + id: number; + permissionKey: string; + name: string; + description?: string; + isRequired: boolean; + }>; + accessCount?: number; // 접근 통계 + lastAccessed?: Date; +} + +// 메뉴 섹션별 그룹화 +interface MenuSection { + title: string; + items: MenuItem[]; +} + +export function MenuPermissionManager() { + const [searchQuery, setSearchQuery] = useState(""); + const [menus, setMenus] = useState([]); + const [selectedMenu, setSelectedMenu] = useState(null); + const [availablePermissions, setAvailablePermissions] = useState([]); + const [loading, setLoading] = useState(false); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [selectedDomain, setSelectedDomain] = useState("all"); + + useEffect(() => { + loadMenus(); + }, [selectedDomain]); + + const loadMenus = async () => { + setLoading(true); + try { + const data = await getMenuPermissions(selectedDomain); + setMenus(data.menus); + setAvailablePermissions(data.availablePermissions); + } catch (error) { + toast.error("메뉴 정보를 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }; + + // 메뉴 검색 필터링 + const filteredMenus = menus.filter(menu => + menu.menuTitle.toLowerCase().includes(searchQuery.toLowerCase()) || + menu.menuPath.toLowerCase().includes(searchQuery.toLowerCase()) || + menu.menuDescription?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // 섹션별로 메뉴 그룹화 + const groupedMenus = filteredMenus.reduce((acc, menu) => { + const section = menu.sectionTitle || "기타"; + if (!acc[section]) { + acc[section] = []; + } + acc[section].push(menu); + return acc; + }, {} as Record); + + return ( +
+ {/* 메뉴 목록 */} + + + 메뉴 목록 + 권한을 설정할 메뉴를 선택하세요. + + +
+ {/* 도메인 필터 */} +
+ setSelectedDomain("all")} + > + 전체 + + setSelectedDomain("evcp")} + > + EVCP + + setSelectedDomain("partners")} + > + Partners + +
+ + {/* 검색 */} +
+ + setSearchQuery(e.target.value)} + className="pl-8" + /> +
+ + {/* 메뉴 트리 */} + + + {Object.entries(groupedMenus).map(([section, items]) => ( + + +
+ {section} + + {items.length} + +
+
+ +
+ {items.map((menu) => ( + + ))} +
+
+
+ ))} +
+
+
+
+
+ + {/* 메뉴 상세 및 권한 설정 */} + {selectedMenu ? ( + + +
+
+ + {selectedMenu.menuTitle} + {!selectedMenu.isActive && ( + 비활성 + )} + + {selectedMenu.menuPath} + {selectedMenu.menuDescription && ( +

+ {selectedMenu.menuDescription} +

+ )} +
+ +
+
+ +
+ {/* 담당자 정보 */} +
+

담당자

+
+ {/* 담당자 변경 다이얼로그 */}} + /> + {/* 담당자 변경 다이얼로그 */}} + /> +
+
+ + + + {/* 필수 권한 */} +
+

필수 권한

+ {selectedMenu.requiredPermissions.length > 0 ? ( +
+ {selectedMenu.requiredPermissions.map((perm) => ( +
+
+
{perm.name}
+
+ {perm.permissionKey} +
+ {perm.description && ( +
+ {perm.description} +
+ )} +
+
+ {perm.isRequired ? ( + 필수 + ) : ( + 선택 + )} +
+
+ ))} +
+ ) : ( +
+ +

설정된 권한이 없습니다.

+

모든 사용자가 접근 가능합니다.

+
+ )} +
+ + + + {/* 접근 통계 */} +
+

접근 통계

+
+
+
+ {selectedMenu.accessCount || 0} +
+
+ 총 접근 횟수 +
+
+
+
+ {selectedMenu.lastAccessed + ? new Date(selectedMenu.lastAccessed).toLocaleDateString() + : "-"} +
+
+ 최근 접근일 +
+
+
+
+
+
+
+ ) : ( + +
+ +

메뉴를 선택하면 권한 설정이 표시됩니다.

+
+
+ )} + + {/* 메뉴 권한 편집 다이얼로그 */} + {selectedMenu && ( + { + loadMenus(); + setEditDialogOpen(false); + }} + /> + )} +
+ ); +} + +// 담당자 정보 컴포넌트 +function ManagerInfo({ + label, + manager, + onEdit +}: { + label: string; + manager?: { id: number; name: string; email: string; imageUrl?: string }; + onEdit: () => void; +}) { + if (!manager) { + return ( +
+
+ + {label}: 미지정 +
+ +
+ ); + } + + return ( +
+
+ + + {manager.name[0]} + +
+
{manager.name}
+
{manager.email}
+
+
+ +
+ ); +} + +// 메뉴 권한 편집 다이얼로그 +function MenuPermissionEditDialog({ + open, + onOpenChange, + menu, + availablePermissions, + onSuccess, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + menu: MenuItem; + availablePermissions: any[]; + onSuccess: () => void; +}) { + const [selectedPermissions, setSelectedPermissions] = useState< + Array<{ id: number; isRequired: boolean }> + >(() => menu.requiredPermissions.map(p => ({ id: p.id, isRequired: p.isRequired }))) + + + const [saving, setSaving] = useState(false); + + const handleSave = async () => { + setSaving(true); + try { + await updateMenuPermissions(menu.menuPath, selectedPermissions); + toast.success("메뉴 권한이 업데이트되었습니다."); + onSuccess(); + } catch (error) { + toast.error("권한 업데이트에 실패했습니다."); + } finally { + setSaving(false); + } + }; + + const togglePermission = (permissionId: number) => { + const existing = selectedPermissions.find(p => p.id === permissionId); + if (existing) { + setSelectedPermissions(selectedPermissions.filter(p => p.id !== permissionId)); + } else { + setSelectedPermissions([...selectedPermissions, { id: permissionId, isRequired: true }]); + } + }; + + const toggleRequired = (permissionId: number) => { + setSelectedPermissions( + selectedPermissions.map(p => + p.id === permissionId ? { ...p, isRequired: !p.isRequired } : p + ) + ); + }; + + // 권한을 카테고리별로 그룹화 + const groupedPermissions = availablePermissions.reduce((acc, perm) => { + const category = perm.resource || "기타"; + if (!acc[category]) acc[category] = []; + acc[category].push(perm); + return acc; + }, {} as Record); + + return ( + + + + {menu.menuTitle} 권한 설정 + + 이 메뉴에 접근하기 위한 필수/선택 권한을 설정합니다. + + + + +
+ {Object.entries(groupedPermissions).map(([category, perms]) => ( +
+

{category}

+
+ {perms.map((permission) => { + const selected = selectedPermissions.find(p => p.id === permission.id); + return ( +
+
+ togglePermission(permission.id)} + /> +
+
{permission.name}
+
+ {permission.permissionKey} +
+ {permission.description && ( +
+ {permission.description} +
+ )} +
+
+ + {selected && ( +
+ +
+ )} +
+ ); + })} +
+
+ ))} +
+
+ + + + + +
+
+ ); +} \ No newline at end of file -- cgit v1.2.3