diff --git a/app/Http/Controllers/Api/Admin/PermissionAnalyzeController.php b/app/Http/Controllers/Api/Admin/PermissionAnalyzeController.php new file mode 100644 index 00000000..f7d3daaa --- /dev/null +++ b/app/Http/Controllers/Api/Admin/PermissionAnalyzeController.php @@ -0,0 +1,142 @@ +input('search'); + + $menuTree = $this->service->getMenuTree($tenantId, $search); + + if ($request->header('HX-Request')) { + $html = view('permission-analyze.partials.menu-tree', [ + 'menuTree' => $menuTree, + ])->render(); + + return response()->json(['html' => $html]); + } + + return response()->json([ + 'success' => true, + 'data' => $menuTree, + ]); + } + + /** + * 특정 메뉴의 권한 분석 + */ + public function analyzeMenu(Request $request): JsonResponse|string + { + $menuId = $request->input('menu_id'); + $permissionType = $request->input('permission_type', 'view'); + $tenantId = session('selected_tenant_id'); + + if (! $menuId) { + return response()->json([ + 'success' => false, + 'message' => '메뉴를 선택해주세요.', + ], 400); + } + + $analysis = $this->service->analyzeMenuPermission($menuId, $permissionType, $tenantId); + + if ($request->header('HX-Request')) { + $html = view('permission-analyze.partials.analysis-result', [ + 'analysis' => $analysis, + 'permissionType' => $permissionType, + ])->render(); + + return response()->json(['html' => $html]); + } + + return response()->json([ + 'success' => true, + 'data' => $analysis, + ]); + } + + /** + * 사용자 역추적 + */ + public function traceUsers(Request $request): JsonResponse|string + { + $menuId = $request->input('menu_id'); + $permissionType = $request->input('permission_type', 'view'); + $tenantId = session('selected_tenant_id'); + + if (! $menuId) { + return response()->json([ + 'success' => false, + 'message' => '메뉴를 선택해주세요.', + ], 400); + } + + $trace = $this->service->traceUsersWithPermission($menuId, $permissionType, $tenantId); + + if ($request->header('HX-Request')) { + $html = view('permission-analyze.partials.trace-result', [ + 'trace' => $trace, + 'permissionType' => $permissionType, + ])->render(); + + return response()->json(['html' => $html]); + } + + return response()->json([ + 'success' => true, + 'data' => $trace, + ]); + } + + /** + * CSV 내보내기 + */ + public function exportCsv(Request $request): Response + { + $menuId = $request->input('menu_id'); + $permissionType = $request->input('permission_type', 'view'); + $tenantId = session('selected_tenant_id'); + + if (! $menuId) { + return response('메뉴를 선택해주세요.', 400); + } + + $csv = $this->service->exportToCsv($menuId, $permissionType, $tenantId); + + $filename = "permission_analysis_{$menuId}_{$permissionType}_".date('Ymd_His').'.csv'; + + return response($csv) + ->header('Content-Type', 'text/csv; charset=UTF-8') + ->header('Content-Disposition', "attachment; filename=\"{$filename}\"") + ->header('Content-Transfer-Encoding', 'binary'); + } + + /** + * 권한 재계산 (캐시 무효화) + */ + public function recalculate(Request $request): JsonResponse + { + app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); + + return response()->json([ + 'success' => true, + 'message' => '권한이 재계산되었습니다.', + ]); + } +} diff --git a/app/Http/Controllers/PermissionAnalyzeController.php b/app/Http/Controllers/PermissionAnalyzeController.php new file mode 100644 index 00000000..b6db5086 --- /dev/null +++ b/app/Http/Controllers/PermissionAnalyzeController.php @@ -0,0 +1,27 @@ +service->getMenuTree($tenantId); + $permissionTypes = $this->service->getPermissionTypes(); + + return view('permission-analyze.index', [ + 'menuTree' => $menuTree, + 'permissionTypes' => $permissionTypes, + ]); + } +} diff --git a/app/Services/PermissionAnalyzeService.php b/app/Services/PermissionAnalyzeService.php new file mode 100644 index 00000000..50f933db --- /dev/null +++ b/app/Services/PermissionAnalyzeService.php @@ -0,0 +1,525 @@ +where('is_active', 1); + + if ($tenantId) { + // 특정 테넌트: 해당 테넌트 메뉴만 (각 테넌트별로 메뉴가 복제되어 있음) + $query->where('tenant_id', $tenantId); + } else { + // 전체 보기: 공통 메뉴만 + $query->whereNull('tenant_id'); + } + + if ($search) { + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('url', 'like', "%{$search}%"); + }); + } + + $allMenus = $query->orderBy('sort_order', 'asc') + ->orderBy('id', 'asc') + ->get(); + + return $this->flattenMenuTree($allMenus); + } + + /** + * 트리 구조를 플랫한 배열로 변환 (depth 정보 포함) + */ + private function flattenMenuTree(Collection $menus, ?int $parentId = null, int $depth = 0): Collection + { + $result = collect(); + $filteredMenus = $menus->where('parent_id', $parentId)->sortBy('sort_order'); + + foreach ($filteredMenus as $menu) { + $menu->depth = $depth; + $menu->has_children = $menus->where('parent_id', $menu->id)->count() > 0; + $result->push($menu); + + $children = $this->flattenMenuTree($menus, $menu->id, $depth + 1); + $result = $result->merge($children); + } + + return $result; + } + + /** + * 특정 메뉴의 권한 분석 (접근 가능/불가능 사용자 목록) + * + * @param int $menuId 메뉴 ID + * @param string $permissionType 권한 유형 (view, create, update, delete, approve) + * @param int|null $tenantId 테넌트 ID + * @param string $guardName Guard 이름 + * @return array 분석 결과 + */ + public function analyzeMenuPermission(int $menuId, string $permissionType = 'view', ?int $tenantId = null, string $guardName = 'api'): array + { + $menu = Menu::find($menuId); + if (! $menu) { + return ['error' => '메뉴를 찾을 수 없습니다.']; + } + + $permissionName = "menu:{$menuId}.{$permissionType}"; + + // 테넌트별 사용자 목록 조회 + $users = $this->getUsersByTenant($tenantId); + + $accessAllowed = []; + $accessDenied = []; + + foreach ($users as $user) { + $analysis = $this->analyzeUserPermission($user->id, $menuId, $permissionType, $tenantId, $guardName); + + $userInfo = [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'departments' => $this->getUserDepartments($user->id, $tenantId), + 'roles' => $this->getUserRoles($user->id, $tenantId), + 'personal_override' => $analysis['personal'], + 'source' => $analysis['source'], + 'effective' => $analysis['effective'], + ]; + + if ($analysis['effective'] === 'allow') { + $accessAllowed[] = $userInfo; + } else { + $accessDenied[] = $userInfo; + } + } + + return [ + 'menu' => [ + 'id' => $menu->id, + 'name' => $menu->name, + 'url' => $menu->url, + ], + 'permission_type' => $permissionType, + 'permission_rule' => 'ALLOW = 부서 OR 역할 OR (개인 ALLOW) → 개인 DENY 최우선', + 'access_allowed' => $accessAllowed, + 'access_denied' => $accessDenied, + 'summary' => [ + 'total_users' => count($users), + 'allowed_count' => count($accessAllowed), + 'denied_count' => count($accessDenied), + ], + ]; + } + + /** + * 특정 사용자의 특정 메뉴 권한 분석 + * + * @param int $userId 사용자 ID + * @param int $menuId 메뉴 ID + * @param string $permissionType 권한 유형 + * @param int|null $tenantId 테넌트 ID + * @param string $guardName Guard 이름 + * @return array 분석 결과 + */ + public function analyzeUserPermission(int $userId, int $menuId, string $permissionType = 'view', ?int $tenantId = null, string $guardName = 'api'): array + { + $permissionName = "menu:{$menuId}.{$permissionType}"; + $now = now(); + + // 1. 역할 권한 확인 + $hasRolePermission = $this->checkRolePermission($userId, $permissionName, $guardName); + + // 2. 부서 권한 확인 + $hasDeptPermission = $this->checkDepartmentPermission($userId, $permissionName, $tenantId, $guardName); + + // 3. 개인 오버라이드 확인 + $personalOverride = $this->checkPersonalOverride($userId, $permissionName, $tenantId, $guardName); + + // 최종 권한 계산 (AccessService와 동일한 우선순위) + // 1) 개인 DENY → 거부 + // 2) 역할 권한 → 허용 + // 3) 부서 ALLOW → 허용 + // 4) 개인 ALLOW → 허용 + // 5) 기본 → 없음 (거부) + + $effective = null; + $source = null; + + if ($personalOverride === 'deny') { + $effective = 'deny'; + $source = 'personal'; + } elseif ($hasRolePermission) { + $effective = 'allow'; + $source = 'role'; + } elseif ($hasDeptPermission) { + $effective = 'allow'; + $source = 'department'; + } elseif ($personalOverride === 'allow') { + $effective = 'allow'; + $source = 'personal'; + } else { + $effective = 'deny'; + $source = null; + } + + return [ + 'effective' => $effective, + 'source' => $source, + 'role' => $hasRolePermission, + 'department' => $hasDeptPermission, + 'personal' => $personalOverride, + ]; + } + + /** + * 역할 권한 확인 + */ + private function checkRolePermission(int $userId, string $permissionName, string $guardName): bool + { + return DB::table('model_has_roles as mhr') + ->join('role_has_permissions as rhp', 'rhp.role_id', '=', 'mhr.role_id') + ->join('permissions as p', 'p.id', '=', 'rhp.permission_id') + ->where('mhr.model_type', User::class) + ->where('mhr.model_id', $userId) + ->where('p.guard_name', $guardName) + ->where('p.name', $permissionName) + ->exists(); + } + + /** + * 부서 권한 확인 + */ + private function checkDepartmentPermission(int $userId, string $permissionName, ?int $tenantId, string $guardName): bool + { + $now = now(); + + $query = DB::table('department_user as du') + ->join('permission_overrides as po', function ($j) use ($now) { + $j->on('po.model_id', '=', 'du.department_id') + ->where('po.model_type', Department::class) + ->whereNull('po.deleted_at') + ->where('po.effect', 1) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now); + }) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now); + }); + }) + ->join('permissions as p', 'p.id', '=', 'po.permission_id') + ->whereNull('du.deleted_at') + ->where('du.user_id', $userId) + ->where('p.guard_name', $guardName) + ->where('p.name', $permissionName); + + if ($tenantId) { + $query->where('du.tenant_id', $tenantId) + ->where('po.tenant_id', $tenantId); + } + + return $query->exists(); + } + + /** + * 개인 오버라이드 확인 + */ + private function checkPersonalOverride(int $userId, string $permissionName, ?int $tenantId, string $guardName): ?string + { + $now = now(); + + $query = DB::table('permission_overrides as po') + ->join('permissions as p', 'p.id', '=', 'po.permission_id') + ->select('po.effect') + ->where('po.model_type', User::class) + ->where('po.model_id', $userId) + ->where('p.guard_name', $guardName) + ->where('p.name', $permissionName) + ->whereNull('po.deleted_at') + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now); + }) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now); + }); + + if ($tenantId) { + $query->where('po.tenant_id', $tenantId); + } + + $override = $query->first(); + + if (! $override) { + return null; + } + + return $override->effect == 1 ? 'allow' : 'deny'; + } + + /** + * 테넌트별 사용자 목록 조회 + */ + private function getUsersByTenant(?int $tenantId): Collection + { + $query = User::where('is_active', true); + + if ($tenantId) { + $query->whereHas('tenants', function ($q) use ($tenantId) { + $q->where('tenants.id', $tenantId) + ->where('user_tenants.is_active', true); + }); + } + + return $query->orderBy('name')->get(); + } + + /** + * 사용자의 부서 목록 조회 + */ + private function getUserDepartments(int $userId, ?int $tenantId): array + { + $query = DB::table('department_user as du') + ->join('departments as d', 'd.id', '=', 'du.department_id') + ->select('d.id', 'd.name', 'd.code', 'du.is_primary') + ->where('du.user_id', $userId) + ->whereNull('du.deleted_at') + ->whereNull('d.deleted_at'); + + if ($tenantId) { + $query->where('du.tenant_id', $tenantId); + } + + return $query->get()->map(function ($dept) { + return [ + 'id' => $dept->id, + 'name' => $dept->name, + 'code' => $dept->code, + 'is_primary' => $dept->is_primary, + ]; + })->toArray(); + } + + /** + * 사용자의 역할 목록 조회 + */ + private function getUserRoles(int $userId, ?int $tenantId): array + { + $query = DB::table('user_roles as ur') + ->join('roles as r', 'r.id', '=', 'ur.role_id') + ->select('r.id', 'r.name', 'r.display_name') + ->where('ur.user_id', $userId) + ->whereNull('ur.deleted_at'); + + if ($tenantId) { + $query->where('ur.tenant_id', $tenantId); + } + + return $query->get()->map(function ($role) { + return [ + 'id' => $role->id, + 'name' => $role->name, + 'display_name' => $role->display_name, + ]; + })->toArray(); + } + + /** + * 사용자 역추적 - 특정 권한을 가진 모든 사용자 검색 + * + * @param int $menuId 메뉴 ID + * @param string $permissionType 권한 유형 + * @param int|null $tenantId 테넌트 ID + * @param string $guardName Guard 이름 + * @return array 역추적 결과 + */ + public function traceUsersWithPermission(int $menuId, string $permissionType = 'view', ?int $tenantId = null, string $guardName = 'api'): array + { + $permissionName = "menu:{$menuId}.{$permissionType}"; + + // 역할로 권한이 있는 사용자 + $usersFromRole = DB::table('model_has_roles as mhr') + ->join('role_has_permissions as rhp', 'rhp.role_id', '=', 'mhr.role_id') + ->join('permissions as p', 'p.id', '=', 'rhp.permission_id') + ->join('users as u', 'u.id', '=', 'mhr.model_id') + ->join('roles as r', 'r.id', '=', 'mhr.role_id') + ->select('u.id as user_id', 'u.name as user_name', 'u.email', 'r.id as role_id', 'r.name as role_name', 'r.display_name as role_display_name') + ->where('mhr.model_type', User::class) + ->where('p.guard_name', $guardName) + ->where('p.name', $permissionName) + ->where('u.is_active', true) + ->get(); + + // 부서로 권한이 있는 사용자 + $now = now(); + $usersFromDepartment = DB::table('department_user as du') + ->join('permission_overrides as po', function ($j) use ($now) { + $j->on('po.model_id', '=', 'du.department_id') + ->where('po.model_type', Department::class) + ->whereNull('po.deleted_at') + ->where('po.effect', 1) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now); + }) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now); + }); + }) + ->join('permissions as p', 'p.id', '=', 'po.permission_id') + ->join('users as u', 'u.id', '=', 'du.user_id') + ->join('departments as d', 'd.id', '=', 'du.department_id') + ->select('u.id as user_id', 'u.name as user_name', 'u.email', 'd.id as department_id', 'd.name as department_name', 'd.code as department_code') + ->whereNull('du.deleted_at') + ->where('p.guard_name', $guardName) + ->where('p.name', $permissionName) + ->where('u.is_active', true); + + if ($tenantId) { + $usersFromDepartment->where('du.tenant_id', $tenantId) + ->where('po.tenant_id', $tenantId); + } + + $usersFromDepartment = $usersFromDepartment->get(); + + // 개인 ALLOW 오버라이드가 있는 사용자 + $usersFromPersonal = DB::table('permission_overrides as po') + ->join('permissions as p', 'p.id', '=', 'po.permission_id') + ->join('users as u', 'u.id', '=', 'po.model_id') + ->select('u.id as user_id', 'u.name as user_name', 'u.email', 'po.effect') + ->where('po.model_type', User::class) + ->where('p.guard_name', $guardName) + ->where('p.name', $permissionName) + ->where('po.effect', 1) + ->whereNull('po.deleted_at') + ->where('u.is_active', true) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now); + }) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now); + }); + + if ($tenantId) { + $usersFromPersonal->where('po.tenant_id', $tenantId); + } + + $usersFromPersonal = $usersFromPersonal->get(); + + // 개인 DENY 오버라이드가 있는 사용자 + $usersWithDeny = DB::table('permission_overrides as po') + ->join('permissions as p', 'p.id', '=', 'po.permission_id') + ->join('users as u', 'u.id', '=', 'po.model_id') + ->select('u.id as user_id', 'u.name as user_name', 'u.email') + ->where('po.model_type', User::class) + ->where('p.guard_name', $guardName) + ->where('p.name', $permissionName) + ->where('po.effect', 0) + ->whereNull('po.deleted_at') + ->where('u.is_active', true) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now); + }) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now); + }); + + if ($tenantId) { + $usersWithDeny->where('po.tenant_id', $tenantId); + } + + $usersWithDeny = $usersWithDeny->get(); + + return [ + 'by_role' => $usersFromRole->map(function ($item) { + return [ + 'user_id' => $item->user_id, + 'user_name' => $item->user_name, + 'email' => $item->email, + 'role' => [ + 'id' => $item->role_id, + 'name' => $item->role_name, + 'display_name' => $item->role_display_name, + ], + ]; + })->toArray(), + 'by_department' => $usersFromDepartment->map(function ($item) { + return [ + 'user_id' => $item->user_id, + 'user_name' => $item->user_name, + 'email' => $item->email, + 'department' => [ + 'id' => $item->department_id, + 'name' => $item->department_name, + 'code' => $item->department_code, + ], + ]; + })->toArray(), + 'by_personal' => $usersFromPersonal->map(function ($item) { + return [ + 'user_id' => $item->user_id, + 'user_name' => $item->user_name, + 'email' => $item->email, + ]; + })->toArray(), + 'denied_users' => $usersWithDeny->map(function ($item) { + return [ + 'user_id' => $item->user_id, + 'user_name' => $item->user_name, + 'email' => $item->email, + ]; + })->toArray(), + ]; + } + + /** + * 권한 유형 목록 반환 + */ + public function getPermissionTypes(): array + { + return $this->permissionTypes; + } + + /** + * CSV 내보내기용 데이터 생성 + */ + public function exportToCsv(int $menuId, string $permissionType = 'view', ?int $tenantId = null): string + { + $analysis = $this->analyzeMenuPermission($menuId, $permissionType, $tenantId); + + $csv = "사용자,이메일,부서,역할,개인모드,최종,근거\n"; + + foreach ($analysis['access_allowed'] as $user) { + $departments = collect($user['departments'])->pluck('name')->join(', '); + $roles = collect($user['roles'])->pluck('display_name')->join(', '); + $csv .= "\"{$user['name']}\",\"{$user['email']}\",\"{$departments}\",\"{$roles}\",\"{$user['personal_override']}\",\"허용\",\"{$user['source']}\"\n"; + } + + foreach ($analysis['access_denied'] as $user) { + $departments = collect($user['departments'])->pluck('name')->join(', '); + $roles = collect($user['roles'])->pluck('display_name')->join(', '); + $csv .= "\"{$user['name']}\",\"{$user['email']}\",\"{$departments}\",\"{$roles}\",\"{$user['personal_override']}\",\"거부\",\"{$user['source']}\"\n"; + } + + return $csv; + } +} diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index d2b2147b..5abf515b 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -132,8 +132,8 @@ class="flex items-center gap-2 pr-3 py-2 rounded-lg text-sm text-gray-700 hover:
  • - diff --git a/resources/views/permission-analyze/index.blade.php b/resources/views/permission-analyze/index.blade.php new file mode 100644 index 00000000..833ecc68 --- /dev/null +++ b/resources/views/permission-analyze/index.blade.php @@ -0,0 +1,285 @@ +@extends('layouts.app') + +@section('title', '권한 분석') + +@section('content') + +
    +

    권한 분석

    +
    + + +
    +
    + + +
    +
    + +
    + + +
    + + +
    + + 권한 규칙: ALLOW = 부서 OR 역할 OR (개인 ALLOW) → 개인 DENY 최우선 + +
    +
    +
    + + +
    + +
    +
    +
    +

    메뉴 트리

    + + +
    + +
    +
    + + +
    +
    +
    +

    선택된 메뉴

    +
    + + +
    + +
    + + +
    +
    + + + +

    좌측에서 분석할 메뉴를 선택해주세요.

    +
    +
    +
    +
    +
    + + +@endsection + +@push('scripts') + + +@endpush \ No newline at end of file diff --git a/resources/views/permission-analyze/partials/analysis-result.blade.php b/resources/views/permission-analyze/partials/analysis-result.blade.php new file mode 100644 index 00000000..68527911 --- /dev/null +++ b/resources/views/permission-analyze/partials/analysis-result.blade.php @@ -0,0 +1,207 @@ +@php + $activeTab = request('tab', 'access-allowed'); +@endphp + +@if(isset($analysis['error'])) +
    + + + +

    {{ $analysis['error'] }}

    +
    +@else + +
    +
    +
    + + 전체 사용자: {{ $analysis['summary']['total_users'] ?? 0 }}명 + + + 접근 가능: {{ $analysis['summary']['allowed_count'] ?? 0 }}명 + + + 접근 불가: {{ $analysis['summary']['denied_count'] ?? 0 }}명 + +
    + + {{ strtoupper($permissionType) }} 권한 + +
    +
    + + +
    + @if(count($analysis['access_allowed'] ?? []) > 0) +
    + + + + + + + + + + + + @foreach($analysis['access_allowed'] as $user) + + + + + + + + @endforeach + +
    사용자부서역할개인 설정근거
    +
    {{ $user['name'] }}
    +
    {{ $user['email'] }}
    +
    + @if(count($user['departments']) > 0) + @foreach($user['departments'] as $dept) + + {{ $dept['name'] }} + @if($dept['is_primary']) + * + @endif + + @endforeach + @else + - + @endif + + @if(count($user['roles']) > 0) + @foreach($user['roles'] as $role) + + {{ $role['display_name'] ?? $role['name'] }} + + @endforeach + @else + - + @endif + + @if($user['personal_override'] === 'allow') + + ALLOW + + @elseif($user['personal_override'] === 'deny') + + DENY + + @else + - + @endif + + @if($user['source'] === 'role') + + 역할 + + @elseif($user['source'] === 'department') + + 부서 + + @elseif($user['source'] === 'personal') + + 개인 + + @else + - + @endif +
    +
    + @else +
    +

    접근 가능한 사용자가 없습니다.

    +
    + @endif +
    + + +
    + @if(count($analysis['access_denied'] ?? []) > 0) +
    + + + + + + + + + + + + @foreach($analysis['access_denied'] as $user) + + + + + + + + @endforeach + +
    사용자부서역할개인 설정거부 사유
    +
    {{ $user['name'] }}
    +
    {{ $user['email'] }}
    +
    + @if(count($user['departments']) > 0) + @foreach($user['departments'] as $dept) + + {{ $dept['name'] }} + + @endforeach + @else + - + @endif + + @if(count($user['roles']) > 0) + @foreach($user['roles'] as $role) + + {{ $role['display_name'] ?? $role['name'] }} + + @endforeach + @else + - + @endif + + @if($user['personal_override'] === 'deny') + + DENY + + @else + - + @endif + + @if($user['source'] === 'personal') + + 개인 DENY + + @else + + 권한 없음 + + @endif +
    +
    + @else +
    +

    접근 불가 사용자가 없습니다.

    +
    + @endif +
    +@endif + + \ No newline at end of file diff --git a/resources/views/permission-analyze/partials/menu-tree.blade.php b/resources/views/permission-analyze/partials/menu-tree.blade.php new file mode 100644 index 00000000..65337825 --- /dev/null +++ b/resources/views/permission-analyze/partials/menu-tree.blade.php @@ -0,0 +1,44 @@ +@forelse($menuTree as $menu) + +@empty +
    +

    메뉴가 없습니다.

    +
    +@endforelse \ No newline at end of file diff --git a/resources/views/permission-analyze/partials/trace-result.blade.php b/resources/views/permission-analyze/partials/trace-result.blade.php new file mode 100644 index 00000000..87b315a6 --- /dev/null +++ b/resources/views/permission-analyze/partials/trace-result.blade.php @@ -0,0 +1,163 @@ + +
    + +
    +

    + + {{ count($trace['by_role'] ?? []) }} + + 역할로 권한 부여된 사용자 +

    + @if(count($trace['by_role'] ?? []) > 0) +
    + + + + + + + + + @foreach($trace['by_role'] as $item) + + + + + @endforeach + +
    사용자역할
    +
    {{ $item['user_name'] }}
    +
    {{ $item['email'] }}
    +
    + + {{ $item['role']['display_name'] ?? $item['role']['name'] }} + +
    +
    + @else +
    +

    역할로 권한 부여된 사용자가 없습니다.

    +
    + @endif +
    + + +
    +

    + + {{ count($trace['by_department'] ?? []) }} + + 부서로 권한 부여된 사용자 +

    + @if(count($trace['by_department'] ?? []) > 0) +
    + + + + + + + + + @foreach($trace['by_department'] as $item) + + + + + @endforeach + +
    사용자부서
    +
    {{ $item['user_name'] }}
    +
    {{ $item['email'] }}
    +
    + + {{ $item['department']['name'] }} + @if($item['department']['code']) + ({{ $item['department']['code'] }}) + @endif + +
    +
    + @else +
    +

    부서로 권한 부여된 사용자가 없습니다.

    +
    + @endif +
    + + +
    +

    + + {{ count($trace['by_personal'] ?? []) }} + + 개인 ALLOW로 권한 부여된 사용자 +

    + @if(count($trace['by_personal'] ?? []) > 0) +
    + + + + + + + + + @foreach($trace['by_personal'] as $item) + + + + + @endforeach + +
    사용자이메일
    +
    {{ $item['user_name'] }}
    +
    +
    {{ $item['email'] }}
    +
    +
    + @else +
    +

    개인 ALLOW로 권한 부여된 사용자가 없습니다.

    +
    + @endif +
    + + +
    +

    + + {{ count($trace['denied_users'] ?? []) }} + + 개인 DENY로 거부된 사용자 +

    + @if(count($trace['denied_users'] ?? []) > 0) +
    + + + + + + + + + @foreach($trace['denied_users'] as $item) + + + + + @endforeach + +
    사용자이메일
    +
    {{ $item['user_name'] }}
    +
    +
    {{ $item['email'] }}
    +
    +
    + @else +
    +

    개인 DENY로 거부된 사용자가 없습니다.

    +
    + @endif +
    +
    \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index defdd463..779784d6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -124,4 +124,13 @@ Route::post('/deny-all', [\App\Http\Controllers\Api\Admin\UserPermissionController::class, 'denyAll'])->name('denyAll'); Route::post('/reset', [\App\Http\Controllers\Api\Admin\UserPermissionController::class, 'reset'])->name('reset'); }); + + // 권한 분석 API + Route::prefix('permission-analyze')->name('permission-analyze.')->group(function () { + Route::get('/menu-tree', [\App\Http\Controllers\Api\Admin\PermissionAnalyzeController::class, 'menuTree'])->name('menuTree'); + Route::get('/analyze', [\App\Http\Controllers\Api\Admin\PermissionAnalyzeController::class, 'analyzeMenu'])->name('analyze'); + Route::get('/trace', [\App\Http\Controllers\Api\Admin\PermissionAnalyzeController::class, 'traceUsers'])->name('trace'); + Route::get('/export-csv', [\App\Http\Controllers\Api\Admin\PermissionAnalyzeController::class, 'exportCsv'])->name('exportCsv'); + Route::post('/recalculate', [\App\Http\Controllers\Api\Admin\PermissionAnalyzeController::class, 'recalculate'])->name('recalculate'); + }); }); diff --git a/routes/web.php b/routes/web.php index 8802bdeb..603b96db 100644 --- a/routes/web.php +++ b/routes/web.php @@ -84,6 +84,9 @@ // 개인 권한 관리 (Blade 화면만) Route::get('/user-permissions', [\App\Http\Controllers\UserPermissionController::class, 'index'])->name('user-permissions.index'); + // 권한 분석 (Blade 화면만) + Route::get('/permission-analyze', [\App\Http\Controllers\PermissionAnalyzeController::class, 'index'])->name('permission-analyze.index'); + // 대시보드 Route::get('/dashboard', function () { return view('dashboard.index');