diff options
Diffstat (limited to 'components/permissions/permission-assignment-manager.tsx')
| -rw-r--r-- | components/permissions/permission-assignment-manager.tsx | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/components/permissions/permission-assignment-manager.tsx b/components/permissions/permission-assignment-manager.tsx new file mode 100644 index 00000000..3649631f --- /dev/null +++ b/components/permissions/permission-assignment-manager.tsx @@ -0,0 +1,319 @@ +// components/permissions/permission-assignment-manager.tsx (업데이트) + +"use client"; + +import { useState, useEffect } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Users, + User, + Plus, + X, + Search, + Shield, + Loader2 +} from "lucide-react"; +import { toast } from "sonner"; +import { + getPermissionAssignments, + assignPermissionToRoles, + assignPermissionToUsers, + removePermissionFromRole, + removePermissionFromUser, +} from "@/lib/permissions/permission-assignment-actions"; +import { cn } from "@/lib/utils"; + +interface Permission { + id: number; + permissionKey: string; + name: string; + description?: string; + permissionType: string; + resource: string; + action: string; + scope: string; + menuPath?: string; +} + +interface AssignedRole { + id: number; + name: string; + domain: string; + userCount: number; +} + +interface AssignedUser { + id: number; + name: string; + email: string; + imageUrl?: string; + domain: string; + isGrant: boolean; + reason?: string; +} + +export function PermissionAssignmentManager() { + const [permissions, setPermissions] = useState<Permission[]>([]); + const [selectedPermission, setSelectedPermission] = useState<Permission | null>(null); + const [assignedRoles, setAssignedRoles] = useState<AssignedRole[]>([]); + const [assignedUsers, setAssignedUsers] = useState<AssignedUser[]>([]); + const [searchQuery, setSearchQuery] = useState(""); + const [loading, setLoading] = useState(false); + + useEffect(() => { + loadPermissions(); + }, []); + + useEffect(() => { + if (selectedPermission) { + loadAssignments(selectedPermission.id); + } + }, [selectedPermission]); + + const loadPermissions = async () => { + setLoading(true); + try { + const data = await getPermissionAssignments(); + setPermissions(data.permissions); + } catch (error) { + toast.error("권한 목록을 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }; + + const loadAssignments = async (permissionId: number) => { + try { + const data = await getPermissionAssignments(permissionId); + setAssignedRoles(data.roles); + setAssignedUsers(data.users); + } catch (error) { + toast.error("할당 정보를 불러오는데 실패했습니다."); + } + }; + + const handleRemoveRole = async (roleId: number) => { + if (!selectedPermission) return; + + try { + await removePermissionFromRole(selectedPermission.id, roleId); + toast.success("역할에서 권한이 제거되었습니다."); + loadAssignments(selectedPermission.id); + } catch (error) { + toast.error("권한 제거에 실패했습니다."); + } + }; + + const handleRemoveUser = async (userId: number) => { + if (!selectedPermission) return; + + try { + await removePermissionFromUser(selectedPermission.id, userId); + toast.success("사용자에서 권한이 제거되었습니다."); + loadAssignments(selectedPermission.id); + } catch (error) { + toast.error("권한 제거에 실패했습니다."); + } + }; + + // 권한 필터링 + const filteredPermissions = permissions.filter(p => + p.name.toLowerCase().includes(searchQuery.toLowerCase()) || + p.permissionKey.toLowerCase().includes(searchQuery.toLowerCase()) || + p.resource.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // 리소스별 권한 그룹화 + const groupedPermissions = filteredPermissions.reduce((acc, perm) => { + const group = perm.resource; + if (!acc[group]) acc[group] = []; + acc[group].push(perm); + return acc; + }, {} as Record<string, Permission[]>); + + return ( + <div className="grid grid-cols-2 gap-6"> + {/* 권한 목록 */} + <Card> + <CardHeader> + <CardTitle>권한 목록</CardTitle> + <CardDescription>권한을 선택하여 할당을 관리하세요.</CardDescription> + </CardHeader> + <CardContent> + <div className="space-y-4"> + {/* 검색 */} + <div className="relative"> + <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> + <Input + placeholder="권한 검색..." + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + className="pl-8" + /> + </div> + + {/* 권한 목록 */} + {loading ? ( + <div className="flex justify-center py-8"> + <Loader2 className="h-6 w-6 animate-spin" /> + </div> + ) : ( + <ScrollArea className="h-[500px]"> + <div className="space-y-4"> + {Object.entries(groupedPermissions).map(([resource, perms]) => ( + <div key={resource}> + <h4 className="font-medium mb-2 text-sm text-muted-foreground"> + {resource} + </h4> + <div className="space-y-1"> + {perms.map(permission => ( + <button + key={permission.id} + onClick={() => setSelectedPermission(permission)} + className={cn( + "w-full text-left p-3 rounded-lg border transition-colors", + selectedPermission?.id === permission.id + ? "bg-primary/10 border-primary" + : "hover:bg-muted" + )} + > + <div className="flex items-start justify-between"> + <div> + <div className="font-medium text-sm">{permission.name}</div> + <div className="text-xs text-muted-foreground mt-1"> + <code>{permission.permissionKey}</code> + </div> + </div> + <div className="flex gap-1"> + <Badge variant="outline" className="text-xs"> + {permission.permissionType} + </Badge> + <Badge variant="secondary" className="text-xs"> + {permission.scope} + </Badge> + </div> + </div> + </button> + ))} + </div> + </div> + ))} + </div> + </ScrollArea> + )} + </div> + </CardContent> + </Card> + + {/* 할당 관리 */} + {selectedPermission ? ( + <Card> + <CardHeader> + <CardTitle>{selectedPermission.name}</CardTitle> + <CardDescription> + <div className="flex gap-2 mt-2"> + <Badge>{selectedPermission.permissionKey}</Badge> + <Badge variant="outline">{selectedPermission.permissionType}</Badge> + <Badge variant="secondary">{selectedPermission.scope}</Badge> + </div> + </CardDescription> + </CardHeader> + <CardContent> + <Tabs defaultValue="roles"> + <TabsList className="grid w-full grid-cols-2"> + <TabsTrigger value="roles"> + <Users className="mr-2 h-4 w-4" /> + 역할 ({assignedRoles.length}) + </TabsTrigger> + <TabsTrigger value="users"> + <User className="mr-2 h-4 w-4" /> + 사용자 ({assignedUsers.length}) + </TabsTrigger> + </TabsList> + + <TabsContent value="roles" className="mt-4"> + <div className="space-y-4"> + <Button size="sm" variant="outline"> + <Plus className="mr-2 h-4 w-4" /> + 역할 추가 + </Button> + <div className="space-y-2"> + {assignedRoles.map((role) => ( + <div key={role.id} className="flex items-center justify-between p-3 border rounded-lg"> + <div> + <div className="font-medium">{role.name}</div> + <div className="text-sm text-muted-foreground"> + {role.domain} • {role.userCount}명 사용자 + </div> + </div> + <Button + size="sm" + variant="ghost" + onClick={() => handleRemoveRole(role.id)} + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + </div> + </div> + </TabsContent> + + <TabsContent value="users" className="mt-4"> + <div className="space-y-4"> + <Button size="sm" variant="outline"> + <Plus className="mr-2 h-4 w-4" /> + 사용자 추가 + </Button> + <div className="space-y-2"> + {assignedUsers.map((user) => ( + <div key={user.id} className="flex items-center justify-between p-3 border rounded-lg"> + <div className="flex items-center gap-3"> + <Avatar className="h-8 w-8"> + <AvatarImage src={user.imageUrl} /> + <AvatarFallback>{user.name[0]}</AvatarFallback> + </Avatar> + <div> + <div className="font-medium">{user.name}</div> + <div className="text-sm text-muted-foreground">{user.email}</div> + </div> + </div> + <div className="flex items-center gap-2"> + {user.isGrant ? ( + <Badge variant="success">부여</Badge> + ) : ( + <Badge variant="destructive">제한</Badge> + )} + <Button + size="sm" + variant="ghost" + onClick={() => handleRemoveUser(user.id)} + > + <X className="h-4 w-4" /> + </Button> + </div> + </div> + ))} + </div> + </div> + </TabsContent> + </Tabs> + </CardContent> + </Card> + ) : ( + <Card className="flex items-center justify-center"> + <div className="text-center text-muted-foreground"> + <Shield className="h-12 w-12 mx-auto mb-4 opacity-50" /> + <p>권한을 선택하면 할당 정보가 표시됩니다.</p> + </div> + </Card> + )} + </div> + ); +}
\ No newline at end of file |
