summaryrefslogtreecommitdiff
path: root/components/permissions/menu-permission-generator.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/permissions/menu-permission-generator.tsx')
-rw-r--r--components/permissions/menu-permission-generator.tsx404
1 files changed, 404 insertions, 0 deletions
diff --git a/components/permissions/menu-permission-generator.tsx b/components/permissions/menu-permission-generator.tsx
new file mode 100644
index 00000000..4c8d60d0
--- /dev/null
+++ b/components/permissions/menu-permission-generator.tsx
@@ -0,0 +1,404 @@
+// components/permissions/menu-permission-generator-optimized.tsx
+
+"use client";
+
+import { useState, useEffect, useMemo, useCallback } 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 { Checkbox } from "@/components/ui/checkbox";
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ RefreshCw,
+ AlertCircle,
+ CheckCircle,
+ Plus,
+ Search,
+ ChevronDown,
+ ChevronUp
+} from "lucide-react";
+import { toast } from "sonner";
+import { cn } from "@/lib/utils";
+import { analyzeMenuPermissions, generateMenuPermissions } from "@/lib/permissions/permission-settings-actions";
+
+export function MenuBasedPermissionGenerator() {
+ const [analysis, setAnalysis] = useState<MenuPermissionAnalysis[]>([]);
+ const [selectedMenus, setSelectedMenus] = useState<Set<string>>(new Set());
+ const [selectedPermissions, setSelectedPermissions] = useState<Map<string, Set<string>>>(new Map());
+ const [loading, setLoading] = useState(false);
+ const [generating, setGenerating] = useState(false);
+
+ // 필터링과 페이지네이션
+ const [searchQuery, setSearchQuery] = useState("");
+ const [filterType, setFilterType] = useState<"all" | "configured" | "unconfigured">("unconfigured");
+ const [currentPage, setCurrentPage] = useState(1);
+ const [expandedRow, setExpandedRow] = useState<string | null>(null); // 한 번에 하나만 확장
+ const itemsPerPage = 20;
+
+ // 필터링된 데이터
+ const filteredAnalysis = useMemo(() => {
+ let filtered = analysis;
+
+ if (searchQuery) {
+ filtered = filtered.filter(m =>
+ m.menuTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ m.menuPath.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+ }
+
+ if (filterType === "configured") {
+ filtered = filtered.filter(m => m.existingPermissions.length > 0);
+ } else if (filterType === "unconfigured") {
+ filtered = filtered.filter(m => m.existingPermissions.length === 0);
+ }
+
+ return filtered;
+ }, [analysis, searchQuery, filterType]);
+
+ // 페이지네이션 적용
+ const paginatedData = useMemo(() => {
+ const start = (currentPage - 1) * itemsPerPage;
+ return filteredAnalysis.slice(start, start + itemsPerPage);
+ }, [filteredAnalysis, currentPage, itemsPerPage]);
+
+ const totalPages = Math.ceil(filteredAnalysis.length / itemsPerPage);
+
+ // 필터 변경 시 첫 페이지로
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [searchQuery, filterType]);
+
+ const loadAnalysis = async () => {
+ setLoading(true);
+ try {
+ const data = await analyzeMenuPermissions();
+ setAnalysis(data);
+
+ // 초기에는 선택하지 않음 (사용자가 필요한 것만 선택)
+ setSelectedMenus(new Set());
+ setSelectedPermissions(new Map());
+ } catch (error) {
+ toast.error("메뉴 분석에 실패했습니다.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadAnalysis();
+ }, []);
+
+ const toggleMenu = useCallback((menuPath: string) => {
+ setSelectedMenus(prev => {
+ const newSelected = new Set(prev);
+ if (newSelected.has(menuPath)) {
+ newSelected.delete(menuPath);
+ setSelectedPermissions(perms => {
+ const newPerms = new Map(perms);
+ newPerms.delete(menuPath);
+ return newPerms;
+ });
+ } else {
+ newSelected.add(menuPath);
+ const menu = analysis.find(m => m.menuPath === menuPath);
+ if (menu) {
+ setSelectedPermissions(perms => {
+ const newPerms = new Map(perms);
+ newPerms.set(
+ menuPath,
+ new Set(menu.suggestedPermissions.map(p => p.permissionKey))
+ );
+ return newPerms;
+ });
+ }
+ }
+ return newSelected;
+ });
+ }, [analysis]);
+
+ const togglePermission = useCallback((menuPath: string, permissionKey: string) => {
+ setSelectedPermissions(prev => {
+ const newPerms = new Map(prev);
+ const menuPerms = newPerms.get(menuPath) || new Set();
+
+ if (menuPerms.has(permissionKey)) {
+ menuPerms.delete(permissionKey);
+ } else {
+ menuPerms.add(permissionKey);
+ }
+
+ newPerms.set(menuPath, menuPerms);
+ return newPerms;
+ });
+ }, []);
+
+ const handleGenerate = async () => {
+ const permissionsToGenerate = [];
+
+ for (const [menuPath, permKeys] of selectedPermissions.entries()) {
+ const menu = analysis.find(m => m.menuPath === menuPath);
+ if (menu) {
+ for (const permKey of permKeys) {
+ const perm = menu.suggestedPermissions.find(p => p.permissionKey === permKey);
+ if (perm) {
+ permissionsToGenerate.push({
+ ...perm,
+ menuPath,
+ });
+ }
+ }
+ }
+ }
+
+ if (permissionsToGenerate.length === 0) {
+ toast.error("생성할 권한을 선택해주세요.");
+ return;
+ }
+
+ setGenerating(true);
+ try {
+ const result = await generateMenuPermissions(permissionsToGenerate);
+ toast.success(`${result.created}개의 권한이 생성되었습니다.`);
+ loadAnalysis();
+ } catch (error) {
+ toast.error("권한 생성에 실패했습니다.");
+ } finally {
+ setGenerating(false);
+ }
+ };
+
+ const selectAllInPage = () => {
+ paginatedData.forEach(menu => {
+ if (!selectedMenus.has(menu.menuPath)) {
+ toggleMenu(menu.menuPath);
+ }
+ });
+ };
+
+ const deselectAllInPage = () => {
+ paginatedData.forEach(menu => {
+ if (selectedMenus.has(menu.menuPath)) {
+ toggleMenu(menu.menuPath);
+ }
+ });
+ };
+
+ const totalSelected = Array.from(selectedPermissions.values())
+ .reduce((sum, set) => sum + set.size, 0);
+
+ return (
+ <div className="space-y-6">
+ <Card>
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div>
+ <CardTitle>메뉴 기반 권한 자동 생성</CardTitle>
+ <CardDescription>
+ 등록된 메뉴를 분석하여 필요한 권한을 자동으로 생성합니다.
+ </CardDescription>
+ </div>
+ <div className="flex gap-2">
+ <Button variant="outline" onClick={loadAnalysis} disabled={loading}>
+ <RefreshCw className={cn("mr-2 h-4 w-4", loading && "animate-spin")} />
+ 분석
+ </Button>
+ <Button
+ onClick={handleGenerate}
+ disabled={totalSelected === 0 || generating}
+ >
+ <Plus className="mr-2 h-4 w-4" />
+ {totalSelected}개 권한 생성
+ </Button>
+ </div>
+ </div>
+ </CardHeader>
+ <CardContent>
+ {/* 요약 정보 */}
+ <div className="grid grid-cols-3 gap-4 mb-6">
+ <Card className="cursor-pointer" onClick={() => setFilterType("all")}>
+ <CardContent className="pt-6">
+ <div className="text-2xl font-bold">{analysis.length}</div>
+ <p className="text-xs text-muted-foreground">전체 메뉴</p>
+ </CardContent>
+ </Card>
+ <Card className="cursor-pointer" onClick={() => setFilterType("configured")}>
+ <CardContent className="pt-6">
+ <div className="text-2xl font-bold">
+ {analysis.filter(m => m.existingPermissions.length > 0).length}
+ </div>
+ <p className="text-xs text-muted-foreground">권한 설정됨</p>
+ </CardContent>
+ </Card>
+ <Card className="cursor-pointer" onClick={() => setFilterType("unconfigured")}>
+ <CardContent className="pt-6">
+ <div className="text-2xl font-bold text-orange-600">
+ {analysis.filter(m => m.existingPermissions.length === 0).length}
+ </div>
+ <p className="text-xs text-muted-foreground">권한 미설정</p>
+ </CardContent>
+ </Card>
+ </div>
+
+ {/* 필터 섹션 */}
+ <div className="flex gap-4 mb-4">
+ <div className="flex-1 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>
+ <Select value={filterType} onValueChange={(v: any) => setFilterType(v)}>
+ <SelectTrigger className="w-[150px]">
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="all">전체</SelectItem>
+ <SelectItem value="unconfigured">미설정</SelectItem>
+ <SelectItem value="configured">설정됨</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+
+ {/* 일괄 선택 버튼 */}
+ <div className="flex justify-between items-center mb-4">
+ <span className="text-sm text-muted-foreground">
+ {filteredAnalysis.length}개 중 {paginatedData.length}개 표시
+ </span>
+ <div className="flex gap-2">
+ <Button size="sm" variant="outline" onClick={selectAllInPage}>
+ 현재 페이지 전체 선택
+ </Button>
+ <Button size="sm" variant="outline" onClick={deselectAllInPage}>
+ 현재 페이지 전체 해제
+ </Button>
+ </div>
+ </div>
+
+ {/* 메뉴 리스트 */}
+ <div className="border rounded-lg divide-y">
+ {paginatedData.map(menu => {
+ const isSelected = selectedMenus.has(menu.menuPath);
+ const isExpanded = expandedRow === menu.menuPath;
+ const menuPermissions = selectedPermissions.get(menu.menuPath);
+
+ return (
+ <div key={menu.menuPath} className="p-4">
+ <div className="flex items-center gap-4">
+ <Checkbox
+ checked={isSelected}
+ onCheckedChange={() => toggleMenu(menu.menuPath)}
+ />
+
+ <div className="flex-1">
+ <div className="font-medium">{menu.menuTitle}</div>
+ <div className="text-xs text-muted-foreground">
+ {menu.menuPath}
+ </div>
+ </div>
+
+ <Badge variant="outline">{menu.domain}</Badge>
+
+ {menu.existingPermissions.length > 0 ? (
+ <div className="flex items-center gap-1 text-green-600">
+ <CheckCircle className="h-4 w-4" />
+ <span className="text-sm">{menu.existingPermissions.length}개</span>
+ </div>
+ ) : (
+ <div className="flex items-center gap-1 text-orange-600">
+ <AlertCircle className="h-4 w-4" />
+ <span className="text-sm">미설정</span>
+ </div>
+ )}
+
+ {isSelected && menu.suggestedPermissions.length > 0 && (
+ <Button
+ size="sm"
+ variant="ghost"
+ onClick={() => setExpandedRow(isExpanded ? null : menu.menuPath)}
+ >
+ {isExpanded ? <ChevronUp /> : <ChevronDown />}
+ <span className="ml-1">{menu.suggestedPermissions.length}개 권한</span>
+ </Button>
+ )}
+ </div>
+
+ {/* 권한 상세 (확장 시에만 표시) */}
+ {isExpanded && isSelected && (
+ <div className="mt-4 pl-10 space-y-2">
+ {menu.suggestedPermissions.map(perm => (
+ <label
+ key={perm.permissionKey}
+ className="flex items-center gap-2 cursor-pointer"
+ >
+ <Checkbox
+ checked={menuPermissions?.has(perm.permissionKey) || false}
+ onCheckedChange={() => togglePermission(menu.menuPath, perm.permissionKey)}
+ />
+ <span className="text-sm">{perm.name}</span>
+ <Badge variant="outline" className="text-xs">
+ {perm.action}
+ </Badge>
+ </label>
+ ))}
+ </div>
+ )}
+ </div>
+ );
+ })}
+ </div>
+
+ {/* 페이지네이션 */}
+ {totalPages > 1 && (
+ <div className="flex items-center justify-center gap-2 mt-4">
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => setCurrentPage(1)}
+ disabled={currentPage === 1}
+ >
+ 처음
+ </Button>
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
+ disabled={currentPage === 1}
+ >
+ 이전
+ </Button>
+ <span className="px-3 text-sm">
+ {currentPage} / {totalPages}
+ </span>
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
+ disabled={currentPage === totalPages}
+ >
+ 다음
+ </Button>
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => setCurrentPage(totalPages)}
+ disabled={currentPage === totalPages}
+ >
+ 마지막
+ </Button>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ </div>
+ );
+} \ No newline at end of file