diff options
Diffstat (limited to 'components/permissions/permission-crud-manager.tsx')
| -rw-r--r-- | components/permissions/permission-crud-manager.tsx | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/components/permissions/permission-crud-manager.tsx b/components/permissions/permission-crud-manager.tsx new file mode 100644 index 00000000..01c9959f --- /dev/null +++ b/components/permissions/permission-crud-manager.tsx @@ -0,0 +1,562 @@ +// components/permissions/permission-crud-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 { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Plus, + Edit, + Trash2, + MoreVertical, + Search, + Filter, + Key, + Shield, + Copy, + CheckCircle +} from "lucide-react"; +import { toast } from "sonner"; +import { cn } from "@/lib/utils"; +import { + getAllPermissions, + createPermission, + updatePermission, + deletePermission, + getPermissionCategories, +} from "@/lib/permissions/permission-settings-actions"; + +interface Permission { + id: number; + permissionKey: string; + name: string; + description?: string; + permissionType: string; + resource: string; + action: string; + scope: string; + menuPath?: string; + uiElement?: string; + isSystem: boolean; + isActive: boolean; + createdAt: Date; + updatedAt: Date; +} + +export function PermissionCrudManager() { + const [permissions, setPermissions] = useState<Permission[]>([]); + const [filteredPermissions, setFilteredPermissions] = useState<Permission[]>([]); + const [categories, setCategories] = useState<{ resource: string; count: number }[]>([]); + const [selectedCategory, setSelectedCategory] = useState<string>("all"); + const [searchQuery, setSearchQuery] = useState(""); + const [loading, setLoading] = useState(false); + const [createDialogOpen, setCreateDialogOpen] = useState(false); + const [editingPermission, setEditingPermission] = useState<Permission | null>(null); + + useEffect(() => { + loadPermissions(); + loadCategories(); + }, []); + + useEffect(() => { + filterPermissions(); + }, [permissions, selectedCategory, searchQuery]); + + const loadPermissions = async () => { + setLoading(true); + try { + const data = await getAllPermissions(); + setPermissions(data); + } catch (error) { + toast.error("권한 목록을 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }; + + const loadCategories = async () => { + try { + const data = await getPermissionCategories(); + setCategories(data); + } catch (error) { + console.error("카테고리 로드 실패:", error); + } + }; + + const filterPermissions = () => { + let filtered = permissions; + + if (selectedCategory !== "all") { + filtered = filtered.filter(p => p.resource === selectedCategory); + } + + if (searchQuery) { + filtered = filtered.filter(p => + p.name.toLowerCase().includes(searchQuery.toLowerCase()) || + p.permissionKey.toLowerCase().includes(searchQuery.toLowerCase()) || + p.description?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + } + + setFilteredPermissions(filtered); + }; + + const handleDelete = async (id: number) => { + if (!confirm("이 권한을 삭제하시겠습니까? 관련된 모든 할당이 제거됩니다.")) { + return; + } + + try { + await deletePermission(id); + toast.success("권한이 삭제되었습니다."); + loadPermissions(); + } catch (error) { + toast.error("권한 삭제에 실패했습니다."); + } + }; + + return ( + <div className="space-y-6"> + {/* 헤더 및 필터 */} + <Card> + <CardHeader> + <div className="flex items-center justify-between"> + <div> + <CardTitle>권한 목록</CardTitle> + <CardDescription> + 시스템에 등록된 모든 권한을 관리합니다. + </CardDescription> + </div> + <Button onClick={() => setCreateDialogOpen(true)}> + <Plus className="mr-2 h-4 w-4" /> + 권한 추가 + </Button> + </div> + </CardHeader> + <CardContent> + <div className="flex gap-4 mb-4"> + {/* 검색 */} + <div className="flex-1"> + <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> + </div> + + {/* 카테고리 필터 */} + <Select value={selectedCategory} onValueChange={setSelectedCategory}> + <SelectTrigger className="w-[200px]"> + <SelectValue placeholder="카테고리 선택" /> + </SelectTrigger> + <SelectContent> + <SelectItem value="all">전체 ({permissions.length})</SelectItem> + {categories.map(cat => ( + <SelectItem key={cat.resource} value={cat.resource}> + {cat.resource} ({cat.count}) + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 권한 테이블 */} + <div className="border rounded-lg"> + <Table> + <TableHeader> + <TableRow> + <TableHead>권한명</TableHead> + <TableHead>권한 키</TableHead> + <TableHead>타입</TableHead> + <TableHead>리소스</TableHead> + <TableHead>액션</TableHead> + <TableHead>범위</TableHead> + <TableHead>상태</TableHead> + <TableHead className="text-right">작업</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {filteredPermissions.map(permission => ( + <TableRow key={permission.id}> + <TableCell> + <div> + <div className="font-medium">{permission.name}</div> + {permission.description && ( + <div className="text-xs text-muted-foreground"> + {permission.description} + </div> + )} + </div> + </TableCell> + <TableCell> + <code className="text-xs bg-muted px-1 py-0.5 rounded"> + {permission.permissionKey} + </code> + </TableCell> + <TableCell> + <Badge variant="outline">{permission.permissionType}</Badge> + </TableCell> + <TableCell>{permission.resource}</TableCell> + <TableCell>{permission.action}</TableCell> + <TableCell> + <Badge variant="secondary">{permission.scope}</Badge> + </TableCell> + <TableCell> + <div className="flex items-center gap-2"> + {permission.isActive ? ( + <Badge variant="success">활성</Badge> + ) : ( + <Badge variant="destructive">비활성</Badge> + )} + {permission.isSystem && ( + <Badge variant="outline">시스템</Badge> + )} + </div> + </TableCell> + <TableCell className="text-right"> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" className="h-8 w-8 p-0"> + <MoreVertical className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem + onClick={() => { + navigator.clipboard.writeText(permission.permissionKey); + toast.success("권한 키가 복사되었습니다."); + }} + > + <Copy className="mr-2 h-4 w-4" /> + 키 복사 + </DropdownMenuItem> + <DropdownMenuItem + onClick={() => setEditingPermission(permission)} + > + <Edit className="mr-2 h-4 w-4" /> + 수정 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={() => handleDelete(permission.id)} + className="text-destructive" + disabled={permission.isSystem} + > + <Trash2 className="mr-2 h-4 w-4" /> + 삭제 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + </CardContent> + </Card> + + {/* 권한 생성/수정 다이얼로그 */} + <PermissionFormDialog + open={createDialogOpen || !!editingPermission} + onOpenChange={(open) => { + if (!open) { + setCreateDialogOpen(false); + setEditingPermission(null); + } + }} + permission={editingPermission} + onSuccess={() => { + setCreateDialogOpen(false); + setEditingPermission(null); + loadPermissions(); + }} + /> + </div> + ); +} + +// 권한 생성/수정 폼 다이얼로그 +function PermissionFormDialog({ + open, + onOpenChange, + permission, + onSuccess, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + permission?: Permission | null; + onSuccess: () => void; +}) { + const [formData, setFormData] = useState({ + permissionKey: "", + name: "", + description: "", + permissionType: "action", + resource: "", + action: "", + scope: "own", + menuPath: "", + uiElement: "", + isActive: true, + }); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (permission) { + setFormData({ + permissionKey: permission.permissionKey, + name: permission.name, + description: permission.description || "", + permissionType: permission.permissionType, + resource: permission.resource, + action: permission.action, + scope: permission.scope, + menuPath: permission.menuPath || "", + uiElement: permission.uiElement || "", + isActive: permission.isActive, + }); + } else { + setFormData({ + permissionKey: "", + name: "", + description: "", + permissionType: "action", + resource: "", + action: "", + scope: "own", + menuPath: "", + uiElement: "", + isActive: true, + }); + } + }, [permission]); + + const handleSubmit = async () => { + if (!formData.permissionKey || !formData.name || !formData.resource || !formData.action) { + toast.error("필수 항목을 입력해주세요."); + return; + } + + setSaving(true); + try { + if (permission) { + await updatePermission(permission.id, formData); + toast.success("권한이 수정되었습니다."); + } else { + await createPermission(formData); + toast.success("권한이 생성되었습니다."); + } + onSuccess(); + } catch (error: any) { + toast.error(error.message || "권한 저장에 실패했습니다."); + } finally { + setSaving(false); + } + }; + + // 권한 키 자동 생성 + const generatePermissionKey = () => { + if (formData.resource && formData.action) { + const key = `${formData.resource}.${formData.action}`.toLowerCase().replace(/\s+/g, '_'); + setFormData({ ...formData, permissionKey: key }); + } + }; + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-2xl"> + <DialogHeader> + <DialogTitle>{permission ? "권한 수정" : "권한 생성"}</DialogTitle> + <DialogDescription> + 새로운 권한을 생성하거나 기존 권한을 수정합니다. + </DialogDescription> + </DialogHeader> + + <div className="grid gap-4 py-4"> + <div className="grid grid-cols-2 gap-4"> + <div> + <Label>권한 키*</Label> + <div className="flex gap-2"> + <Input + value={formData.permissionKey} + onChange={(e) => setFormData({ ...formData, permissionKey: e.target.value })} + placeholder="예: rfq.vendor.create" + /> + {!permission && ( + <Button + type="button" + variant="outline" + size="sm" + onClick={generatePermissionKey} + > + 자동 + </Button> + )} + </div> + </div> + <div> + <Label>권한명*</Label> + <Input + value={formData.name} + onChange={(e) => setFormData({ ...formData, name: e.target.value })} + placeholder="예: RFQ 벤더 추가" + /> + </div> + </div> + + <div> + <Label>설명</Label> + <Textarea + value={formData.description} + onChange={(e) => setFormData({ ...formData, description: e.target.value })} + placeholder="권한에 대한 상세 설명" + /> + </div> + + <div className="grid grid-cols-2 gap-4"> + <div> + <Label>권한 타입*</Label> + <Select + value={formData.permissionType} + onValueChange={(v) => setFormData({ ...formData, permissionType: v })} + > + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="menu_access">메뉴 접근</SelectItem> + <SelectItem value="action">액션 실행</SelectItem> + <SelectItem value="data_read">데이터 읽기</SelectItem> + <SelectItem value="data_write">데이터 쓰기</SelectItem> + <SelectItem value="data_delete">데이터 삭제</SelectItem> + <SelectItem value="approve">승인</SelectItem> + <SelectItem value="export">내보내기</SelectItem> + <SelectItem value="import">가져오기</SelectItem> + </SelectContent> + </Select> + </div> + <div> + <Label>범위*</Label> + <Select + value={formData.scope} + onValueChange={(v) => setFormData({ ...formData, scope: v })} + > + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="all">전체</SelectItem> + <SelectItem value="domain">도메인</SelectItem> + <SelectItem value="assigned">담당</SelectItem> + <SelectItem value="own">본인</SelectItem> + <SelectItem value="department">부서</SelectItem> + <SelectItem value="company">회사</SelectItem> + </SelectContent> + </Select> + </div> + </div> + + <div className="grid grid-cols-2 gap-4"> + <div> + <Label>리소스*</Label> + <Input + value={formData.resource} + onChange={(e) => setFormData({ ...formData, resource: e.target.value })} + placeholder="예: rfq_vendor" + /> + </div> + <div> + <Label>액션*</Label> + <Input + value={formData.action} + onChange={(e) => setFormData({ ...formData, action: e.target.value })} + placeholder="예: create" + /> + </div> + </div> + + <div className="grid grid-cols-2 gap-4"> + <div> + <Label>메뉴 경로</Label> + <Input + value={formData.menuPath} + onChange={(e) => setFormData({ ...formData, menuPath: e.target.value })} + placeholder="예: /evcp/rfq-last" + /> + </div> + <div> + <Label>UI 요소</Label> + <Input + value={formData.uiElement} + onChange={(e) => setFormData({ ...formData, uiElement: e.target.value })} + placeholder="예: btn-add-vendor" + /> + </div> + </div> + + <div className="flex items-center gap-2"> + <input + type="checkbox" + id="isActive" + checked={formData.isActive} + onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })} + /> + <Label htmlFor="isActive">활성 상태</Label> + </div> + </div> + + <DialogFooter> + <Button variant="outline" onClick={() => onOpenChange(false)}> + 취소 + </Button> + <Button onClick={handleSubmit} disabled={saving}> + {saving ? "저장 중..." : permission ? "수정" : "생성"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +}
\ No newline at end of file |
