diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-29 13:31:40 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-29 13:31:40 +0000 |
| commit | 4614210aa9878922cfa1e424ce677ef893a1b6b2 (patch) | |
| tree | 5e7edcce05fbee207230af0a43ed08cd351d7c4f /components/permissions/permission-group-assignment-manager.tsx | |
| parent | e41e3af4e72870d44a94b03e0f3246d6ccaaca48 (diff) | |
(대표님) 구매 권한설정, data room 등
Diffstat (limited to 'components/permissions/permission-group-assignment-manager.tsx')
| -rw-r--r-- | components/permissions/permission-group-assignment-manager.tsx | 666 |
1 files changed, 666 insertions, 0 deletions
diff --git a/components/permissions/permission-group-assignment-manager.tsx b/components/permissions/permission-group-assignment-manager.tsx new file mode 100644 index 00000000..cd7531a0 --- /dev/null +++ b/components/permissions/permission-group-assignment-manager.tsx @@ -0,0 +1,666 @@ +// components/permissions/permission-group-assignment-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 { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Users, + User, + Plus, + X, + Search, + Package, + Shield, + Loader2, + UserPlus, + ChevronRight +} from "lucide-react"; +import { toast } from "sonner"; +import { cn } from "@/lib/utils"; +import { + getPermissionGroupAssignments, + assignGroupToRoles, + assignGroupToUsers, + removeGroupFromRole, + removeGroupFromUser, + searchRoles, + searchUsers, +} from "@/lib/permissions/permission-group-assignment-actions"; + +interface PermissionGroup { + id: number; + groupKey: string; + name: string; + description?: string; + domain?: string; + permissionCount: number; + isActive: boolean; +} + +interface AssignedRole { + id: number; + name: string; + domain: string; + userCount: number; + assignedAt: Date; + assignedBy?: string; +} + +interface AssignedUser { + id: number; + name: string; + email: string; + imageUrl?: string; + domain: string; + companyName?: string; + assignedAt: Date; + assignedBy?: string; +} + +export function PermissionGroupAssignmentManager() { + const [groups, setGroups] = useState<PermissionGroup[]>([]); + const [selectedGroup, setSelectedGroup] = useState<PermissionGroup | null>(null); + const [assignedRoles, setAssignedRoles] = useState<AssignedRole[]>([]); + const [assignedUsers, setAssignedUsers] = useState<AssignedUser[]>([]); + const [searchQuery, setSearchQuery] = useState(""); + const [loading, setLoading] = useState(false); + const [addRoleDialogOpen, setAddRoleDialogOpen] = useState(false); + const [addUserDialogOpen, setAddUserDialogOpen] = useState(false); + + useEffect(() => { + loadGroups(); + }, []); + + useEffect(() => { + if (selectedGroup) { + loadAssignments(selectedGroup.id); + } + }, [selectedGroup]); + + const loadGroups = async () => { + setLoading(true); + try { + const data = await getPermissionGroupAssignments(); + setGroups(data.groups); + } catch (error) { + toast.error("권한 그룹을 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }; + + const loadAssignments = async (groupId: number) => { + try { + const data = await getPermissionGroupAssignments(groupId); + setAssignedRoles(data.roles); + setAssignedUsers(data.users); + } catch (error) { + toast.error("할당 정보를 불러오는데 실패했습니다."); + } + }; + + const handleRemoveRole = async (roleId: number) => { + if (!selectedGroup) return; + + try { + await removeGroupFromRole(selectedGroup.id, roleId); + toast.success("역할에서 권한 그룹이 제거되었습니다."); + loadAssignments(selectedGroup.id); + } catch (error) { + toast.error("권한 그룹 제거에 실패했습니다."); + } + }; + + const handleRemoveUser = async (userId: number) => { + if (!selectedGroup) return; + + try { + await removeGroupFromUser(selectedGroup.id, userId); + toast.success("사용자에서 권한 그룹이 제거되었습니다."); + loadAssignments(selectedGroup.id); + } catch (error) { + toast.error("권한 그룹 제거에 실패했습니다."); + } + }; + + // 그룹 필터링 + const filteredGroups = groups.filter(g => + g.name.toLowerCase().includes(searchQuery.toLowerCase()) || + g.groupKey.toLowerCase().includes(searchQuery.toLowerCase()) || + g.description?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + 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-2"> + {filteredGroups.map(group => ( + <button + key={group.id} + onClick={() => setSelectedGroup(group)} + className={cn( + "w-full text-left p-4 rounded-lg border transition-colors", + selectedGroup?.id === group.id + ? "bg-primary/10 border-primary" + : "hover:bg-muted" + )} + > + <div className="space-y-2"> + <div className="flex items-start justify-between"> + <div> + <div className="font-medium">{group.name}</div> + <code className="text-xs bg-muted px-1 py-0.5 rounded"> + {group.groupKey} + </code> + </div> + <div className="flex flex-col items-end gap-1"> + {group.domain && ( + <Badge variant="outline" className="text-xs"> + {group.domain} + </Badge> + )} + <Badge variant="secondary" className="text-xs"> + {group.permissionCount}개 권한 + </Badge> + </div> + </div> + {group.description && ( + <p className="text-sm text-muted-foreground"> + {group.description} + </p> + )} + </div> + </button> + ))} + </div> + </ScrollArea> + )} + </div> + </CardContent> + </Card> + + {/* 할당 관리 */} + {selectedGroup ? ( + <Card> + <CardHeader> + <div> + <CardTitle>{selectedGroup.name}</CardTitle> + <CardDescription className="mt-2"> + <div className="flex gap-2"> + <Badge>{selectedGroup.groupKey}</Badge> + {selectedGroup.domain && ( + <Badge variant="outline">{selectedGroup.domain}</Badge> + )} + <Badge variant="secondary">{selectedGroup.permissionCount}개 권한</Badge> + </div> + </CardDescription> + </div> + </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" + onClick={() => setAddRoleDialogOpen(true)} + > + <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 className="flex-1"> + <div className="font-medium">{role.name}</div> + <div className="text-sm text-muted-foreground"> + {role.domain} • {role.userCount}명 사용자 + </div> + <div className="text-xs text-muted-foreground mt-1"> + {new Date(role.assignedAt).toLocaleDateString()} 할당 + {role.assignedBy && ` • ${role.assignedBy}`} + </div> + </div> + <Button + size="sm" + variant="ghost" + onClick={() => handleRemoveRole(role.id)} + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + + {assignedRoles.length === 0 && ( + <div className="text-center py-8 text-muted-foreground"> + <Users className="h-8 w-8 mx-auto mb-2 opacity-50" /> + <p className="text-sm">할당된 역할이 없습니다.</p> + </div> + )} + </div> + </div> + </TabsContent> + + <TabsContent value="users" className="mt-4"> + <div className="space-y-4"> + <Button + size="sm" + onClick={() => setAddUserDialogOpen(true)} + > + <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 className="flex items-center gap-2 mt-1"> + <Badge variant="outline" className="text-xs"> + {user.domain} + </Badge> + {user.companyName && ( + <span className="text-xs text-muted-foreground"> + {user.companyName} + </span> + )} + </div> + </div> + </div> + <Button + size="sm" + variant="ghost" + onClick={() => handleRemoveUser(user.id)} + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + + {assignedUsers.length === 0 && ( + <div className="text-center py-8 text-muted-foreground"> + <User className="h-8 w-8 mx-auto mb-2 opacity-50" /> + <p className="text-sm">할당된 사용자가 없습니다.</p> + </div> + )} + </div> + </div> + </TabsContent> + </Tabs> + </CardContent> + </Card> + ) : ( + <Card className="flex items-center justify-center"> + <div className="text-center text-muted-foreground"> + <Package className="h-12 w-12 mx-auto mb-4 opacity-50" /> + <p>권한 그룹을 선택하면 할당 정보가 표시됩니다.</p> + </div> + </Card> + )} + + {/* 역할 추가 다이얼로그 */} + {selectedGroup && ( + <AddRoleDialog + open={addRoleDialogOpen} + onOpenChange={setAddRoleDialogOpen} + group={selectedGroup} + onSuccess={() => { + setAddRoleDialogOpen(false); + loadAssignments(selectedGroup.id); + }} + /> + )} + + {/* 사용자 추가 다이얼로그 */} + {selectedGroup && ( + <AddUserDialog + open={addUserDialogOpen} + onOpenChange={setAddUserDialogOpen} + group={selectedGroup} + onSuccess={() => { + setAddUserDialogOpen(false); + loadAssignments(selectedGroup.id); + }} + /> + )} + </div> + ); +} + +// 역할 추가 다이얼로그 +function AddRoleDialog({ + open, + onOpenChange, + group, + onSuccess, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + group: PermissionGroup; + onSuccess: () => void; +}) { + const [availableRoles, setAvailableRoles] = useState<any[]>([]); + const [selectedRoles, setSelectedRoles] = useState<number[]>([]); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (open) { + loadAvailableRoles(); + } + }, [open]); + + const loadAvailableRoles = async () => { + setLoading(true); + try { + const data = await searchRoles(group.id); + setAvailableRoles(data); + } catch (error) { + toast.error("역할 목록을 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async () => { + if (selectedRoles.length === 0) { + toast.error("역할을 선택해주세요."); + return; + } + + setSaving(true); + try { + await assignGroupToRoles(group.id, selectedRoles); + toast.success("역할에 권한 그룹이 추가되었습니다."); + onSuccess(); + } catch (error) { + toast.error("권한 그룹 추가에 실패했습니다."); + } finally { + setSaving(false); + } + }; + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent> + <DialogHeader> + <DialogTitle>역할 추가</DialogTitle> + <DialogDescription> + "{group.name}" 그룹을 할당할 역할을 선택하세요. + </DialogDescription> + </DialogHeader> + + {loading ? ( + <div className="flex justify-center py-8"> + <Loader2 className="h-6 w-6 animate-spin" /> + </div> + ) : ( + <div className="space-y-4"> + <ScrollArea className="h-[300px] border rounded-md p-4"> + <div className="space-y-2"> + {availableRoles.map((role) => ( + <label + key={role.id} + className="flex items-center gap-3 p-2 hover:bg-muted rounded cursor-pointer" + > + <Checkbox + checked={selectedRoles.includes(role.id)} + onCheckedChange={(checked) => { + if (checked) { + setSelectedRoles([...selectedRoles, role.id]); + } else { + setSelectedRoles(selectedRoles.filter(id => id !== role.id)); + } + }} + /> + <div className="flex-1"> + <div className="font-medium">{role.name}</div> + <div className="text-sm text-muted-foreground"> + {role.domain} • {role.userCount}명 사용자 + </div> + </div> + </label> + ))} + </div> + </ScrollArea> + + <div className="text-sm text-muted-foreground"> + {selectedRoles.length}개 역할 선택됨 + </div> + </div> + )} + + <DialogFooter> + <Button variant="outline" onClick={() => onOpenChange(false)}> + 취소 + </Button> + <Button onClick={handleSubmit} disabled={saving}> + {saving ? "추가 중..." : "추가"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} + +// 사용자 추가 다이얼로그 +function AddUserDialog({ + open, + onOpenChange, + group, + onSuccess, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + group: PermissionGroup; + onSuccess: () => void; +}) { + const [searchQuery, setSearchQuery] = useState(""); + const [availableUsers, setAvailableUsers] = useState<any[]>([]); + const [selectedUsers, setSelectedUsers] = useState<number[]>([]); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => { + if (searchQuery && open) { + searchUsersData(searchQuery); + } + }, 300); + return () => clearTimeout(timer); + }, [searchQuery, open]); + + const searchUsersData = async (query: string) => { + setLoading(true); + try { + const data = await searchUsers(query, group.id); + setAvailableUsers(data); + } catch (error) { + toast.error("사용자 검색에 실패했습니다."); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async () => { + if (selectedUsers.length === 0) { + toast.error("사용자를 선택해주세요."); + return; + } + + setSaving(true); + try { + await assignGroupToUsers(group.id, selectedUsers); + toast.success("사용자에게 권한 그룹이 추가되었습니다."); + onSuccess(); + } catch (error) { + toast.error("권한 그룹 추가에 실패했습니다."); + } finally { + setSaving(false); + } + }; + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-2xl"> + <DialogHeader> + <DialogTitle>사용자 추가</DialogTitle> + <DialogDescription> + "{group.name}" 그룹을 할당할 사용자를 검색하고 선택하세요. + </DialogDescription> + </DialogHeader> + + <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-[300px] border rounded-md p-4"> + <div className="space-y-2"> + {availableUsers.map((user) => ( + <label + key={user.id} + className="flex items-center gap-3 p-2 hover:bg-muted rounded cursor-pointer" + > + <Checkbox + checked={selectedUsers.includes(user.id)} + onCheckedChange={(checked) => { + if (checked) { + setSelectedUsers([...selectedUsers, user.id]); + } else { + setSelectedUsers(selectedUsers.filter(id => id !== user.id)); + } + }} + /> + <Avatar className="h-8 w-8"> + <AvatarImage src={user.imageUrl} /> + <AvatarFallback>{user.name[0]}</AvatarFallback> + </Avatar> + <div className="flex-1"> + <div className="font-medium">{user.name}</div> + <div className="text-sm text-muted-foreground">{user.email}</div> + <div className="flex items-center gap-2 mt-1"> + <Badge variant="outline" className="text-xs"> + {user.domain} + </Badge> + {user.companyName && ( + <span className="text-xs text-muted-foreground"> + {user.companyName} + </span> + )} + </div> + </div> + </label> + ))} + </div> + </ScrollArea> + + {availableUsers.length > 0 && ( + <div className="text-sm text-muted-foreground"> + {selectedUsers.length}명 선택됨 + </div> + )} + </> + )} + </div> + + <DialogFooter> + <Button variant="outline" onClick={() => onOpenChange(false)}> + 취소 + </Button> + <Button onClick={handleSubmit} disabled={saving || selectedUsers.length === 0}> + {saving ? "추가 중..." : "추가"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +}
\ No newline at end of file |
