summaryrefslogtreecommitdiff
path: root/components/permissions/permission-assignment-manager.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-26 09:57:24 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-26 09:57:24 +0000
commit8b23b471638a155fd1bfa3a8c853b26d9315b272 (patch)
tree47353e9dd342011cb2f1dcd24b09661707a8421b /components/permissions/permission-assignment-manager.tsx
parentd62368d2b68d73da895977e60a18f9b1286b0545 (diff)
(대표님) 권한관리, 문서업로드, rfq첨부, SWP문서룰 등
(최겸) 입찰
Diffstat (limited to 'components/permissions/permission-assignment-manager.tsx')
-rw-r--r--components/permissions/permission-assignment-manager.tsx319
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