// components/permissions/permission-group-manager.tsx "use client"; import { useState, useEffect } from "react"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Shield, Plus, Edit, Trash2, Copy, Users, Key, MoreVertical, Package, ChevronRight, Loader2, Search } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { getPermissionGroups, createPermissionGroup, updatePermissionGroup, deletePermissionGroup, getGroupPermissions, updateGroupPermissions, clonePermissionGroup, getGroupAssignments, } from "@/lib/permissions/permission-group-actions"; interface PermissionGroup { id: number; groupKey: string; name: string; description?: string; domain?: string; isActive: boolean; permissionCount: number; roleCount: number; userCount: number; createdAt: Date; updatedAt: Date; } interface Permission { id: number; permissionKey: string; name: string; description?: string; resource: string; action: string; permissionType: string; scope: string; } // 폼 스키마 정의 const permissionGroupFormSchema = z.object({ groupKey: z.string() .min(1, "그룹 키는 필수입니다.") .regex(/^[a-z0-9_]+$/, "소문자, 숫자, 언더스코어만 사용 가능합니다."), name: z.string().min(1, "그룹명은 필수입니다."), description: z.string().optional(), domain: z.string().optional(), isActive: z.boolean().default(true), }); type PermissionGroupFormValues = z.infer; export function PermissionGroupManager() { const [groups, setGroups] = useState([]); const [selectedGroup, setSelectedGroup] = useState(null); const [groupPermissions, setGroupPermissions] = useState([]); const [availablePermissions, setAvailablePermissions] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [loading, setLoading] = useState(false); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [editingGroup, setEditingGroup] = useState(null); const [permissionDialogOpen, setPermissionDialogOpen] = useState(false); const [deletingGroupId, setDeletingGroupId] = useState(null); useEffect(() => { loadGroups(); }, []); useEffect(() => { if (selectedGroup) { loadGroupPermissions(selectedGroup.id); } }, [selectedGroup]); const loadGroups = async () => { setLoading(true); try { const data = await getPermissionGroups(); setGroups(data); } catch (error) { toast.error("권한 그룹을 불러오는데 실패했습니다."); } finally { setLoading(false); } }; const loadGroupPermissions = async (groupId: number) => { try { const data = await getGroupPermissions(groupId); setGroupPermissions(data.permissions); setAvailablePermissions(data.availablePermissions); } catch (error) { toast.error("권한 정보를 불러오는데 실패했습니다."); } }; const handleDelete = async (id: number) => { setDeletingGroupId(id); }; const confirmDelete = async () => { if (!deletingGroupId) return; try { await deletePermissionGroup(deletingGroupId); toast.success("권한 그룹이 삭제되었습니다."); if (selectedGroup?.id === deletingGroupId) { setSelectedGroup(null); } loadGroups(); } catch (error) { toast.error("권한 그룹 삭제에 실패했습니다."); } finally { setDeletingGroupId(null); } }; const handleClone = async (group: PermissionGroup) => { try { const cloned = await clonePermissionGroup(group.id); toast.success(`"${cloned.name}" 그룹이 생성되었습니다.`); loadGroups(); } catch (error) { toast.error("권한 그룹 복제에 실패했습니다."); } }; // 검색 필터링 const filteredGroups = groups.filter(group => group.name.toLowerCase().includes(searchQuery.toLowerCase()) || group.groupKey.toLowerCase().includes(searchQuery.toLowerCase()) || group.description?.toLowerCase().includes(searchQuery.toLowerCase()) ); return (
권한 그룹 관련된 권한들을 그룹으로 묶어 효율적으로 관리합니다.
{/* 검색 */}
setSearchQuery(e.target.value)} className="pl-8" />
{/* 그룹 목록 */}
{filteredGroups.map(group => ( setSelectedGroup(group)} onEdit={() => setEditingGroup(group)} onClone={() => handleClone(group)} onDelete={() => handleDelete(group.id)} onManagePermissions={() => { setSelectedGroup(group); setPermissionDialogOpen(true); }} /> ))}
{/* 선택된 그룹 상세 */} {selectedGroup && ( setPermissionDialogOpen(true)} /> )} {/* 그룹 생성/수정 다이얼로그 */} { if (!open) { setCreateDialogOpen(false); setEditingGroup(null); } }} group={editingGroup} onSuccess={() => { setCreateDialogOpen(false); setEditingGroup(null); loadGroups(); }} /> {/* 권한 관리 다이얼로그 */} {selectedGroup && ( { loadGroupPermissions(selectedGroup.id); loadGroups(); }} /> )} {/* 삭제 확인 다이얼로그 */} !open && setDeletingGroupId(null)}> 권한 그룹 삭제 이 권한 그룹을 삭제하시겠습니까? 관련된 모든 할당이 제거되며, 이 작업은 되돌릴 수 없습니다. setDeletingGroupId(null)}>취소 삭제
); } // 그룹 카드 컴포넌트 function GroupCard({ group, isSelected, onSelect, onEdit, onClone, onDelete, onManagePermissions, }: { group: PermissionGroup; isSelected: boolean; onSelect: () => void; onEdit: () => void; onClone: () => void; onDelete: () => void; onManagePermissions: () => void; }) { console.log(group,"group") return (
{group.name}
{group.groupKey} {group.domain && ( {group.domain} )}
e.stopPropagation()}> { e.stopPropagation(); onManagePermissions(); }}> 권한 관리 { e.stopPropagation(); onEdit(); }}> 수정 { e.stopPropagation(); onClone(); }}> 복제 { e.stopPropagation(); onDelete(); }} className="text-destructive" > 삭제
{group.description && (

{group.description}

)}
{group.permissionCount}개 권한
{group.roleCount}개 역할
{group.userCount}명
); } // 그룹 상세 카드 function GroupDetailCard({ group, permissions, onEditPermissions, }: { group: PermissionGroup; permissions: Permission[]; onEditPermissions: () => void; }) { // 리소스별로 권한 그룹화 const groupedPermissions = permissions.reduce((acc, perm) => { const resource = perm.resource; if (!acc[resource]) acc[resource] = []; acc[resource].push(perm); return acc; }, {} as Record); return (
{group.name} 권한 목록 이 그룹에 포함된 모든 권한입니다.
{Object.entries(groupedPermissions).map(([resource, perms]) => (
{resource} {perms.length}개
{perms.map(permission => (
{permission.action}
{permission.name}
{permission.permissionKey}
{permission.description && (
{permission.description}
)}
{permission.scope}
))}
))}
); } // 그룹 생성/수정 폼 다이얼로그 - react-hook-form 적용 function GroupFormDialog({ open, onOpenChange, group, onSuccess, }: { open: boolean; onOpenChange: (open: boolean) => void; group?: PermissionGroup | null; onSuccess: () => void; }) { const [saving, setSaving] = useState(false); const form = useForm({ resolver: zodResolver(permissionGroupFormSchema), defaultValues: { groupKey: "", name: "", description: "", domain: undefined, isActive: true, }, }); console.log(form.getValues()) useEffect(() => { if (group) { form.reset({ groupKey: group.groupKey, name: group.name, description: group.description || "", domain: group.domain || undefined, isActive: group.isActive, }); } else { form.reset({ groupKey: "", name: "", description: "", domain: undefined, isActive: true, }); } }, [group, form]); const onSubmit = async (values: PermissionGroupFormValues) => { setSaving(true); try { // domain이 undefined인 경우 빈 문자열로 변환 const submitData = { ...values, domain: values.domain || "", }; if (group) { await updatePermissionGroup(group.id, submitData); toast.success("권한 그룹이 수정되었습니다."); } else { await createPermissionGroup(submitData); toast.success("권한 그룹이 생성되었습니다."); } onSuccess(); } catch (error: any) { toast.error(error.message || "권한 그룹 저장에 실패했습니다."); } finally { setSaving(false); } }; return ( {group ? "권한 그룹 수정" : "권한 그룹 생성"} 권한 그룹 정보를 입력하세요.
( 그룹 키 * 소문자, 숫자, 언더스코어만 사용 가능합니다. )} /> ( 그룹명 * )} /> ( 설명