'allow'|'deny'|null, 'source' => 'role'|'department'|'personal'|null, 'personal' => 'allow'|'deny'|null] */ public function getUserPermissionMatrix(int $userId, ?int $tenantId = null, string $guardName = 'api'): array { $now = now(); // 1. 역할 권한 조회 (Spatie + user_roles) $rolePermissions = $this->getRolePermissions($userId, $guardName, $tenantId); // 2. 부서 권한 조회 (permission_overrides with Department) $departmentPermissions = $this->getDepartmentPermissions($userId, $tenantId, $guardName); // 3. 개인 오버라이드 조회 (permission_overrides with User) $personalOverrides = $this->getPersonalOverrides($userId, $tenantId, $guardName); // 4. 통합 매트릭스 생성 $permissions = []; // 모든 메뉴 ID 수집 $allMenuIds = array_unique(array_merge( array_keys($rolePermissions), array_keys($departmentPermissions), array_keys($personalOverrides) )); foreach ($allMenuIds as $menuId) { if (! isset($permissions[$menuId])) { $permissions[$menuId] = []; } foreach ($this->permissionTypes as $type) { $hasRole = isset($rolePermissions[$menuId][$type]) && $rolePermissions[$menuId][$type]; $hasDept = isset($departmentPermissions[$menuId][$type]) && $departmentPermissions[$menuId][$type]; $personal = $personalOverrides[$menuId][$type] ?? null; // 최종 권한 계산 (API AccessService 우선순위와 동일) // 1) 개인 DENY → 거부 // 2) 역할 권한 → 허용 // 3) 부서 ALLOW → 허용 // 4) 개인 ALLOW → 허용 // 5) 기본 → 없음 $effective = null; $source = null; if ($personal === 'deny') { $effective = 'deny'; $source = 'personal'; } elseif ($hasRole) { $effective = 'allow'; $source = 'role'; } elseif ($hasDept) { $effective = 'allow'; $source = 'department'; } elseif ($personal === 'allow') { $effective = 'allow'; $source = 'personal'; } $permissions[$menuId][$type] = [ 'effective' => $effective, 'source' => $source, 'personal' => $personal, ]; } } return $permissions; } /** * 역할 권한 조회 (Spatie model_has_roles + user_roles + role_has_permissions) */ private function getRolePermissions(int $userId, string $guardName, ?int $tenantId = null): array { // 1. Spatie model_has_roles 테이블에서 권한 조회 $spatiePermissions = 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', 'like', 'menu:%') ->pluck('p.name') ->toArray(); // 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', 'like', 'menu:%'); if ($tenantId) { $userRolesQuery->where('ur.tenant_id', $tenantId); } $userRolesPermissions = $userRolesQuery->pluck('p.name')->toArray(); // 3. 권한 통합 (중복 제거) $rolePermissions = array_unique(array_merge($spatiePermissions, $userRolesPermissions)); $result = []; foreach ($rolePermissions as $permName) { if (preg_match('/^menu:(\d+)\.(\w+)$/', $permName, $matches)) { $menuId = (int) $matches[1]; $type = $matches[2]; if (! isset($result[$menuId])) { $result[$menuId] = []; } $result[$menuId][$type] = true; } } return $result; } /** * 부서 권한 조회 (permission_overrides with Department) */ private function getDepartmentPermissions(int $userId, ?int $tenantId, string $guardName): array { $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', 'App\\Models\\Tenants\\Department') ->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', 'like', 'menu:%'); if ($tenantId) { $query->where('du.tenant_id', $tenantId) ->where('po.tenant_id', $tenantId); } $deptPermissions = $query->pluck('p.name')->toArray(); $result = []; foreach ($deptPermissions as $permName) { if (preg_match('/^menu:(\d+)\.(\w+)$/', $permName, $matches)) { $menuId = (int) $matches[1]; $type = $matches[2]; if (! isset($result[$menuId])) { $result[$menuId] = []; } $result[$menuId][$type] = true; } } return $result; } /** * 개인 오버라이드 조회 (permission_overrides with User) */ private function getPersonalOverrides(int $userId, ?int $tenantId, string $guardName): array { $now = now(); $query = DB::table('permission_overrides as po') ->join('permissions as p', 'p.id', '=', 'po.permission_id') ->select('p.name', 'po.effect') ->where('po.model_type', User::class) ->where('po.model_id', $userId) ->where('p.guard_name', $guardName) ->where('p.name', 'like', 'menu:%') ->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); } $userPermissions = $query->get(); $result = []; foreach ($userPermissions as $perm) { if (preg_match('/^menu:(\d+)\.(\w+)$/', $perm->name, $matches)) { $menuId = (int) $matches[1]; $type = $matches[2]; if (! isset($result[$menuId])) { $result[$menuId] = []; } $result[$menuId][$type] = $perm->effect == 1 ? 'allow' : 'deny'; } } return $result; } /** * 특정 메뉴의 특정 권한 토글 (스마트 토글) * - 역할/부서 권한 있음 (개인 오버라이드 없음) → 개인 DENY 추가 * - 개인 DENY → 제거 (역할/부서 권한으로 복원 또는 미설정) * - 미설정 (권한 없음) → 개인 ALLOW 추가 * - 개인 ALLOW → 개인 DENY로 변경 * * @param int $userId 사용자 ID * @param int $menuId 메뉴 ID * @param string $permissionType 권한 유형 * @param int|null $tenantId 테넌트 ID * @param string $guardName Guard 이름 (api 또는 web) * @return string|null 토글 후 개인 오버라이드 상태 (null: 미설정, 'allow': 허용, 'deny': 거부) */ public function togglePermission(int $userId, int $menuId, string $permissionType, ?int $tenantId = null, string $guardName = 'api'): ?string { $permissionName = "menu:{$menuId}.{$permissionType}"; // 권한 생성 또는 조회 $permission = Permission::firstOrCreate( ['name' => $permissionName, 'guard_name' => $guardName], ['tenant_id' => null, 'created_by' => auth()->id()] ); $now = now(); // 현재 개인 오버라이드 조회 $currentOverride = DB::table('permission_overrides') ->where('model_type', User::class) ->where('model_id', $userId) ->where('permission_id', $permission->id) ->where('tenant_id', $tenantId) ->whereNull('deleted_at') ->where(function ($w) use ($now) { $w->whereNull('effective_from')->orWhere('effective_from', '<=', $now); }) ->where(function ($w) use ($now) { $w->whereNull('effective_to')->orWhere('effective_to', '>=', $now); }) ->first(); // 역할/부서 권한 확인 $hasRolePermission = $this->hasRolePermission($userId, $permissionName, $tenantId, $guardName); $hasDeptPermission = $this->hasDeptPermission($userId, $permissionName, $tenantId, $guardName); $hasInheritedPermission = $hasRolePermission || $hasDeptPermission; // 스마트 토글 로직 if ($currentOverride) { if ($currentOverride->effect == 0) { // 개인 DENY → 제거 (역할/부서로 복원 또는 미설정) DB::table('permission_overrides') ->where('id', $currentOverride->id) ->update([ 'deleted_at' => now(), 'deleted_by' => auth()->id(), ]); return null; } else { // 개인 ALLOW → 개인 DENY DB::table('permission_overrides') ->where('id', $currentOverride->id) ->update([ 'effect' => 0, // DENY 'updated_at' => now(), 'updated_by' => auth()->id(), ]); return 'deny'; } } else { // 개인 오버라이드 없음 if ($hasInheritedPermission) { // 역할/부서 권한 있음 → 개인 DENY 추가 (오버라이드) $this->createPersonalOverride($userId, $permission->id, $tenantId, 0); // DENY return 'deny'; } else { // 권한 없음 → 개인 ALLOW 추가 $this->createPersonalOverride($userId, $permission->id, $tenantId, 1); // ALLOW return 'allow'; } } } /** * 역할 권한 존재 여부 확인 (Spatie + user_roles) */ private function hasRolePermission(int $userId, string $permissionName, ?int $tenantId, string $guardName): 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 hasDeptPermission(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', 'App\\Models\\Tenants\\Department') ->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 createPersonalOverride(int $userId, int $permissionId, ?int $tenantId, int $effect): void { $deletedRecord = DB::table('permission_overrides') ->where('model_type', User::class) ->where('model_id', $userId) ->where('permission_id', $permissionId) ->where('tenant_id', $tenantId) ->whereNotNull('deleted_at') ->first(); if ($deletedRecord) { DB::table('permission_overrides') ->where('id', $deletedRecord->id) ->update([ 'effect' => $effect, 'deleted_at' => null, 'deleted_by' => null, 'updated_at' => now(), 'updated_by' => auth()->id(), ]); } else { DB::table('permission_overrides')->insert([ 'tenant_id' => $tenantId, 'model_type' => User::class, 'model_id' => $userId, 'permission_id' => $permissionId, 'effect' => $effect, 'reason' => null, 'effective_from' => null, 'effective_to' => null, 'created_at' => now(), 'created_by' => auth()->id(), 'updated_at' => now(), 'updated_by' => auth()->id(), ]); } } /** * 모든 권한 허용 (permission_overrides 테이블 사용) * 모든 메뉴에 대해 ALLOW 상태로 설정 (기존 DENY 포함하여 모두 ALLOW로 변경) * * @param int $userId 사용자 ID * @param int|null $tenantId 테넌트 ID * @param string $guardName Guard 이름 (api 또는 web) */ public function allowAllPermissions(int $userId, ?int $tenantId = null, string $guardName = 'api'): void { $query = Menu::where('is_active', 1); if ($tenantId) { $query->where('tenant_id', $tenantId); } $menus = $query->get(); $now = now(); foreach ($menus as $menu) { foreach ($this->permissionTypes as $type) { $permissionName = "menu:{$menu->id}.{$type}"; $permission = Permission::firstOrCreate( ['name' => $permissionName, 'guard_name' => $guardName], ['tenant_id' => null, 'created_by' => auth()->id()] ); // 현재 유효한 오버라이드 확인 (ALLOW 또는 DENY) $existingOverride = DB::table('permission_overrides') ->where('model_type', User::class) ->where('model_id', $userId) ->where('permission_id', $permission->id) ->where('tenant_id', $tenantId) ->whereNull('deleted_at') ->where(function ($w) use ($now) { $w->whereNull('effective_from')->orWhere('effective_from', '<=', $now); }) ->where(function ($w) use ($now) { $w->whereNull('effective_to')->orWhere('effective_to', '>=', $now); }) ->first(); if ($existingOverride) { // 기존 오버라이드가 있으면 ALLOW로 변경 if ($existingOverride->effect != 1) { DB::table('permission_overrides') ->where('id', $existingOverride->id) ->update([ 'effect' => 1, // ALLOW 'updated_at' => now(), 'updated_by' => auth()->id(), ]); } } else { // 삭제된 레코드가 있으면 복원, 없으면 생성 $deletedRecord = DB::table('permission_overrides') ->where('model_type', User::class) ->where('model_id', $userId) ->where('permission_id', $permission->id) ->where('tenant_id', $tenantId) ->whereNotNull('deleted_at') ->first(); if ($deletedRecord) { DB::table('permission_overrides') ->where('id', $deletedRecord->id) ->update([ 'effect' => 1, // ALLOW 'deleted_at' => null, 'deleted_by' => null, 'updated_at' => now(), 'updated_by' => auth()->id(), ]); } else { DB::table('permission_overrides')->insert([ 'tenant_id' => $tenantId, 'model_type' => User::class, 'model_id' => $userId, 'permission_id' => $permission->id, 'effect' => 1, // ALLOW 'reason' => null, 'effective_from' => null, 'effective_to' => null, 'created_at' => now(), 'created_by' => auth()->id(), 'updated_at' => now(), 'updated_by' => auth()->id(), ]); } } } } } /** * 모든 권한 초기화 (모두 미설정으로) * 모든 오버라이드 레코드를 soft delete하여 미설정 상태로 초기화 * * @param int $userId 사용자 ID * @param int|null $tenantId 테넌트 ID * @param string $guardName Guard 이름 (api 또는 web) */ public function denyAllPermissions(int $userId, ?int $tenantId = null, string $guardName = 'api'): void { $query = Menu::where('is_active', 1); if ($tenantId) { $query->where('tenant_id', $tenantId); } $menus = $query->get(); foreach ($menus as $menu) { foreach ($this->permissionTypes as $type) { $permissionName = "menu:{$menu->id}.{$type}"; $permission = Permission::where('name', $permissionName) ->where('guard_name', $guardName) ->first(); if ($permission) { // Soft delete all overrides (ALLOW or DENY) for this user DB::table('permission_overrides') ->where('model_type', User::class) ->where('model_id', $userId) ->where('permission_id', $permission->id) ->where('tenant_id', $tenantId) ->whereNull('deleted_at') ->update([ 'deleted_at' => now(), 'deleted_by' => auth()->id(), ]); } } } } /** * 기본 권한으로 초기화 (view만 허용) * * @param int $userId 사용자 ID * @param int|null $tenantId 테넌트 ID * @param string $guardName Guard 이름 (api 또는 web) */ public function resetToDefaultPermissions(int $userId, ?int $tenantId = null, string $guardName = 'api'): void { // 1. 먼저 모든 권한 제거 $this->denyAllPermissions($userId, $tenantId, $guardName); // 2. view 권한만 허용 $query = Menu::where('is_active', 1); if ($tenantId) { $query->where('tenant_id', $tenantId); } $menus = $query->get(); $now = now(); foreach ($menus as $menu) { $permissionName = "menu:{$menu->id}.view"; $permission = Permission::firstOrCreate( ['name' => $permissionName, 'guard_name' => $guardName], ['tenant_id' => null, 'created_by' => auth()->id()] ); // 이미 유효한 ALLOW 오버라이드가 있는지 확인 $exists = DB::table('permission_overrides') ->where('model_type', User::class) ->where('model_id', $userId) ->where('permission_id', $permission->id) ->where('tenant_id', $tenantId) ->where('effect', 1) ->whereNull('deleted_at') ->where(function ($w) use ($now) { $w->whereNull('effective_from')->orWhere('effective_from', '<=', $now); }) ->where(function ($w) use ($now) { $w->whereNull('effective_to')->orWhere('effective_to', '>=', $now); }) ->exists(); if (! $exists) { // 기존에 삭제된 레코드가 있으면 복원, 없으면 생성 $existingRecord = DB::table('permission_overrides') ->where('model_type', User::class) ->where('model_id', $userId) ->where('permission_id', $permission->id) ->where('tenant_id', $tenantId) ->where('effect', 1) ->first(); if ($existingRecord) { DB::table('permission_overrides') ->where('id', $existingRecord->id) ->update([ 'deleted_at' => null, 'deleted_by' => null, 'updated_at' => now(), 'updated_by' => auth()->id(), ]); } else { DB::table('permission_overrides')->insert([ 'tenant_id' => $tenantId, 'model_type' => User::class, 'model_id' => $userId, 'permission_id' => $permission->id, 'effect' => 1, // ALLOW 'reason' => null, 'effective_from' => null, 'effective_to' => null, 'created_at' => now(), 'created_by' => auth()->id(), 'updated_at' => now(), 'updated_by' => auth()->id(), ]); } } } } /** * 메뉴 트리 조회 (권한 매트릭스 표시용) * * @param int|null $tenantId 테넌트 ID * @return \Illuminate\Support\Collection 메뉴 트리 */ public function getMenuTree(?int $tenantId = null): \Illuminate\Support\Collection { // TenantScope를 비활성화하고 명시적으로 tenant_id 필터 적용 // (HQ 관리자가 다른 테넌트의 메뉴를 조회할 수 있도록) $query = Menu::withoutGlobalScope(TenantScope::class) ->with('parent') ->where('is_active', 1); if ($tenantId) { $query->where('tenant_id', $tenantId); } $allMenus = $query->orderBy('sort_order', 'asc') ->orderBy('id', 'asc') ->get(); // depth 계산하여 플랫한 구조로 변환 return $this->flattenMenuTree($allMenus); } /** * 트리 구조를 플랫한 배열로 변환 (depth 정보 포함) * * @param \Illuminate\Support\Collection $menus 메뉴 컬렉션 * @param int|null $parentId 부모 메뉴 ID * @param int $depth 현재 깊이 */ private function flattenMenuTree(\Illuminate\Support\Collection $menus, ?int $parentId = null, int $depth = 0): \Illuminate\Support\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; } /** * 특정 사용자의 활성 메뉴 권한 확인 (permission_overrides 테이블 사용) * * @param int $userId 사용자 ID * @param int $menuId 메뉴 ID * @param string $permissionType 권한 유형 * @param string $guardName Guard 이름 (api 또는 web) * @return bool 권한 존재 여부 */ public function hasPermission(int $userId, int $menuId, string $permissionType, string $guardName = 'api'): bool { $permissionName = "menu:{$menuId}.{$permissionType}"; $now = now(); return DB::table('permission_overrides as po') ->join('permissions as p', 'p.id', '=', 'po.permission_id') ->where('po.model_type', User::class) ->where('po.model_id', $userId) ->where('po.effect', 1) ->where('p.name', $permissionName) ->where('p.guard_name', $guardName) ->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); }) ->exists(); } /** * 대상 사용자가 슈퍼관리자인지 검증 (일반관리자는 슈퍼관리자 수정 불가) * * @param int $targetUserId 대상 사용자 ID * @return bool true면 수정 가능, false면 수정 불가 */ public function canModifyUser(int $targetUserId): bool { // withTrashed()를 사용하여 일관성 유지 $targetUser = User::withTrashed()->find($targetUserId); $currentUser = auth()->user(); // 대상 사용자가 슈퍼관리자이고 현재 사용자가 슈퍼관리자가 아니면 수정 불가 if ($targetUser?->is_super_admin && ! $currentUser?->is_super_admin) { return false; } return true; } /** * 테넌트별 사용자 목록 조회 (권한 개수 포함) * 일반관리자는 슈퍼관리자가 목록에서 제외됨 * * @param int $tenantId 테넌트 ID * @return \Illuminate\Support\Collection 사용자 목록 */ public function getUsersByTenant(int $tenantId): \Illuminate\Support\Collection { $currentUser = auth()->user(); $query = User::whereHas('tenants', function ($query) use ($tenantId) { $query->where('tenants.id', $tenantId) ->where('user_tenants.is_active', true); }) ->where('is_active', true); // 일반관리자는 슈퍼관리자를 볼 수 없음 if (! $currentUser?->is_super_admin) { $query->where('is_super_admin', false); } $users = $query->orderBy('name')->get(); // 각 사용자별 권한 개수 계산 $now = now(); foreach ($users as $user) { $permissionCounts = $this->getUserPermissionCounts($user->id, $tenantId, $now); $user->web_permission_count = $permissionCounts['web']; $user->api_permission_count = $permissionCounts['api']; } return $users; } /** * 사용자별 guard별 권한 개수 조회 (역할 + 부서 + 개인 오버라이드 통합) * * @param int $userId 사용자 ID * @param int $tenantId 테넌트 ID * @param \Carbon\Carbon $now 현재 시간 * @return array ['web' => int, 'api' => int] */ private function getUserPermissionCounts(int $userId, int $tenantId, $now): array { $result = ['web' => 0, 'api' => 0]; foreach (['web', 'api'] as $guardName) { // 1-1. Spatie 역할 권한 (model_has_roles) $spatieRolePermissions = 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', 'like', 'menu:%') ->pluck('p.name') ->toArray(); // 1-2. 테넌트별 역할 권한 (user_roles) $userRolesPermissions = 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) ->where('ur.tenant_id', $tenantId) ->whereNull('ur.deleted_at') ->where('p.guard_name', $guardName) ->where('p.name', 'like', 'menu:%') ->pluck('p.name') ->toArray(); // 역할 권한 통합 $rolePermissions = array_unique(array_merge($spatieRolePermissions, $userRolesPermissions)); // 2. 부서 권한 $deptPermissions = DB::table('department_user as du') ->join('permission_overrides as po', function ($j) use ($now, $tenantId) { $j->on('po.model_id', '=', 'du.department_id') ->where('po.model_type', 'App\\Models\\Tenants\\Department') ->where('po.tenant_id', $tenantId) ->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('du.tenant_id', $tenantId) ->where('p.guard_name', $guardName) ->where('p.name', 'like', 'menu:%') ->pluck('p.name') ->toArray(); // 3. 개인 오버라이드 (ALLOW) $personalAllows = DB::table('permission_overrides as po') ->join('permissions as p', 'p.id', '=', 'po.permission_id') ->where('po.model_type', User::class) ->where('po.model_id', $userId) ->where('po.tenant_id', $tenantId) ->where('po.effect', 1) ->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); }) ->where('p.guard_name', $guardName) ->where('p.name', 'like', 'menu:%') ->pluck('p.name') ->toArray(); // 4. 개인 오버라이드 (DENY) - 제외할 권한 $personalDenies = DB::table('permission_overrides as po') ->join('permissions as p', 'p.id', '=', 'po.permission_id') ->where('po.model_type', User::class) ->where('po.model_id', $userId) ->where('po.tenant_id', $tenantId) ->where('po.effect', 0) ->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); }) ->where('p.guard_name', $guardName) ->where('p.name', 'like', 'menu:%') ->pluck('p.name') ->toArray(); // 통합: (역할 OR 부서 OR 개인ALLOW) - 개인DENY $allAllowed = array_unique(array_merge($rolePermissions, $deptPermissions, $personalAllows)); $effectivePermissions = array_diff($allAllowed, $personalDenies); $result[$guardName] = count($effectivePermissions); } return $result; } }