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 = []; $explicitDeny = []; 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; } elseif ($analysis['personal'] === 'deny') { // 명시적 DENY만 별도 목록에 추가 $explicitDeny[] = $userInfo; } // 권한이 없는 사용자(no permission)는 목록에 포함하지 않음 } return [ 'menu' => [ 'id' => $menu->id, 'name' => $menu->name, 'url' => $menu->url, ], 'permission_type' => $permissionType, 'permission_rule' => 'ALLOW = 부서 OR 역할 OR (개인 ALLOW) → 개인 DENY 최우선', 'access_allowed' => $accessAllowed, 'explicit_deny' => $explicitDeny, 'summary' => [ 'total_users' => count($users), 'allowed_count' => count($accessAllowed), 'explicit_deny_count' => count($explicitDeny), ], ]; } /** * 특정 사용자의 특정 메뉴 권한 분석 * * @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, $tenantId); // 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, ]; } /** * 역할 권한 확인 (model_has_roles + user_roles) */ private function checkRolePermission(int $userId, string $permissionName, string $guardName, ?int $tenantId = null): bool { // 1. Spatie model_has_roles 테이블에서 확인 $hasSpatiePermission = 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(); if ($hasSpatiePermission) { return true; } // 2. user_roles 테이블에서 확인 $userRolesQuery = DB::table('user_roles as ur') ->join('role_has_permissions as rhp', 'rhp.role_id', '=', 'ur.role_id') ->join('permissions as p', 'p.id', '=', 'rhp.permission_id') ->where('ur.user_id', $userId) ->whereNull('ur.deleted_at') ->where('p.guard_name', $guardName) ->where('p.name', $permissionName); if ($tenantId) { $userRolesQuery->where('ur.tenant_id', $tenantId); } return $userRolesQuery->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 (! auth()->user()?->is_super_admin) { $query->where('is_super_admin', false); } 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(); } /** * 사용자의 역할 목록 조회 (model_has_roles + user_roles) */ private function getUserRoles(int $userId, ?int $tenantId): array { // 1. Spatie model_has_roles 테이블에서 조회 $spatieQuery = DB::table('model_has_roles as mhr') ->join('roles as r', 'r.id', '=', 'mhr.role_id') ->select('r.id', 'r.name', 'r.description') ->where('mhr.model_type', User::class) ->where('mhr.model_id', $userId); if ($tenantId) { $spatieQuery->where('mhr.tenant_id', $tenantId); } $spatieRoles = $spatieQuery->get(); // 2. user_roles 테이블에서 조회 $userRolesQuery = DB::table('user_roles as ur') ->join('roles as r', 'r.id', '=', 'ur.role_id') ->select('r.id', 'r.name', 'r.description') ->where('ur.user_id', $userId) ->whereNull('ur.deleted_at'); if ($tenantId) { $userRolesQuery->where('ur.tenant_id', $tenantId); } $userRoles = $userRolesQuery->get(); // 3. 두 결과 합치기 (중복 제거) return $spatieRoles->merge($userRoles)->unique('id')->map(function ($role) { return [ 'id' => $role->id, 'name' => $role->name, 'display_name' => $role->description, ]; })->values()->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}"; $excludeSuperAdmin = ! auth()->user()?->is_super_admin; // 역할로 권한이 있는 사용자 (model_has_roles) $usersFromSpatieRoleQuery = 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.description as role_display_name') ->where('mhr.model_type', User::class) ->where('p.guard_name', $guardName) ->where('p.name', $permissionName) ->where('u.is_active', true); if ($excludeSuperAdmin) { $usersFromSpatieRoleQuery->where('u.is_super_admin', false); } // 역할로 권한이 있는 사용자 (user_roles) $usersFromUserRolesQuery = DB::table('user_roles as ur') ->join('role_has_permissions as rhp', 'rhp.role_id', '=', 'ur.role_id') ->join('permissions as p', 'p.id', '=', 'rhp.permission_id') ->join('users as u', 'u.id', '=', 'ur.user_id') ->join('roles as r', 'r.id', '=', 'ur.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.description as role_display_name') ->whereNull('ur.deleted_at') ->where('p.guard_name', $guardName) ->where('p.name', $permissionName) ->where('u.is_active', true); if ($excludeSuperAdmin) { $usersFromUserRolesQuery->where('u.is_super_admin', false); } if ($tenantId) { $usersFromUserRolesQuery->where('ur.tenant_id', $tenantId); } // 두 쿼리 결과 합치기 $usersFromRole = $usersFromSpatieRoleQuery->get()->merge($usersFromUserRolesQuery->get())->unique('user_id'); // 부서로 권한이 있는 사용자 $now = now(); $usersFromDepartmentQuery = 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 ($excludeSuperAdmin) { $usersFromDepartmentQuery->where('u.is_super_admin', false); } if ($tenantId) { $usersFromDepartmentQuery->where('du.tenant_id', $tenantId) ->where('po.tenant_id', $tenantId); } $usersFromDepartment = $usersFromDepartmentQuery->get(); // 개인 ALLOW 오버라이드가 있는 사용자 $usersFromPersonalQuery = 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 ($excludeSuperAdmin) { $usersFromPersonalQuery->where('u.is_super_admin', false); } if ($tenantId) { $usersFromPersonalQuery->where('po.tenant_id', $tenantId); } $usersFromPersonal = $usersFromPersonalQuery->get(); // 개인 DENY 오버라이드가 있는 사용자 $usersWithDenyQuery = 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 ($excludeSuperAdmin) { $usersWithDenyQuery->where('u.is_super_admin', false); } if ($tenantId) { $usersWithDenyQuery->where('po.tenant_id', $tenantId); } $usersWithDeny = $usersWithDenyQuery->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['explicit_deny'] 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']}\",\"명시적 DENY\",\"{$user['source']}\"\n"; } return $csv; } }