summaryrefslogtreecommitdiff
path: root/components/permissions/permission-crud-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-crud-manager.tsx
parentd62368d2b68d73da895977e60a18f9b1286b0545 (diff)
(대표님) 권한관리, 문서업로드, rfq첨부, SWP문서룰 등
(최겸) 입찰
Diffstat (limited to 'components/permissions/permission-crud-manager.tsx')
-rw-r--r--components/permissions/permission-crud-manager.tsx562
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