join('permissions as p', 'p.id', '=', 'po.permission_id') ->where('po.model_type', Department::class) ->where('po.model_id', $departmentId) ->where('po.effect', 1) // ALLOW만 조회 ->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); } $departmentPermissions = $query->pluck('p.name')->toArray(); $permissions = []; foreach ($departmentPermissions as $permName) { if (preg_match('/^menu:(\d+)\.(\w+)$/', $permName, $matches)) { $menuId = (int) $matches[1]; $type = $matches[2]; if (! isset($permissions[$menuId])) { $permissions[$menuId] = []; } $permissions[$menuId][$type] = true; } } return $permissions; } /** * 특정 메뉴의 특정 권한 토글 (permission_overrides 테이블 사용) * * @param int $departmentId 부서 ID * @param int $menuId 메뉴 ID * @param string $permissionType 권한 유형 * @param int|null $tenantId 테넌트 ID * @param string $guardName Guard 이름 (api 또는 web) * @return bool 토글 후 상태 (true: 허용, false: 거부) */ public function togglePermission(int $departmentId, int $menuId, string $permissionType, ?int $tenantId = null, string $guardName = 'api'): bool { $permissionName = "menu:{$menuId}.{$permissionType}"; // 권한 생성 또는 조회 $permission = Permission::firstOrCreate( ['name' => $permissionName, 'guard_name' => $guardName], ['tenant_id' => null, 'created_by' => auth()->id()] ); $now = now(); // 현재 권한 상태 확인 (유효한 ALLOW 오버라이드가 있는지) $exists = DB::table('permission_overrides') ->where('model_type', Department::class) ->where('model_id', $departmentId) ->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) { // 권한 제거 (soft delete) DB::table('permission_overrides') ->where('model_type', Department::class) ->where('model_id', $departmentId) ->where('permission_id', $permission->id) ->where('tenant_id', $tenantId) ->where('effect', 1) ->update([ 'deleted_at' => now(), 'deleted_by' => auth()->id(), ]); $newValue = false; } else { // 기존에 삭제된 레코드가 있으면 복원, 없으면 생성 $existingRecord = DB::table('permission_overrides') ->where('model_type', Department::class) ->where('model_id', $departmentId) ->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' => Department::class, 'model_id' => $departmentId, '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(), ]); } $newValue = true; } // 하위 부서에 권한 전파 $this->propagateToChildren($departmentId, $menuId, $permissionType, $newValue, $tenantId, $guardName); return $newValue; } /** * 하위 부서에 권한 전파 (permission_overrides 테이블 사용) * * @param int $parentDepartmentId 부모 부서 ID * @param int $menuId 메뉴 ID * @param string $permissionType 권한 유형 * @param bool $value 권한 값 (true: 허용, false: 거부) * @param int|null $tenantId 테넌트 ID * @param string $guardName Guard 이름 (api 또는 web) */ protected function propagateToChildren(int $parentDepartmentId, int $menuId, string $permissionType, bool $value, ?int $tenantId = null, string $guardName = 'api'): void { $children = Department::where('parent_id', $parentDepartmentId)->get(); $now = now(); foreach ($children as $child) { $permissionName = "menu:{$menuId}.{$permissionType}"; $permission = Permission::firstOrCreate( ['name' => $permissionName, 'guard_name' => $guardName], ['tenant_id' => null, 'created_by' => auth()->id()] ); // 현재 권한 상태 확인 (유효한 ALLOW 오버라이드가 있는지) $exists = DB::table('permission_overrides') ->where('model_type', Department::class) ->where('model_id', $child->id) ->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 ($value) { // 권한 추가 if (! $exists) { // 기존에 삭제된 레코드가 있으면 복원, 없으면 생성 $existingRecord = DB::table('permission_overrides') ->where('model_type', Department::class) ->where('model_id', $child->id) ->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' => Department::class, 'model_id' => $child->id, '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(), ]); } } } else { // 권한 제거 (soft delete) if ($exists) { DB::table('permission_overrides') ->where('model_type', Department::class) ->where('model_id', $child->id) ->where('permission_id', $permission->id) ->where('tenant_id', $tenantId) ->where('effect', 1) ->update([ 'deleted_at' => now(), 'deleted_by' => auth()->id(), ]); } } // 재귀적으로 하위 부서 처리 $this->propagateToChildren($child->id, $menuId, $permissionType, $value, $tenantId, $guardName); } } /** * 모든 권한 허용 (permission_overrides 테이블 사용) * * @param int $departmentId 부서 ID * @param int|null $tenantId 테넌트 ID * @param string $guardName Guard 이름 (api 또는 web) */ public function allowAllPermissions(int $departmentId, ?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 오버라이드가 있는지 확인 $exists = DB::table('permission_overrides') ->where('model_type', Department::class) ->where('model_id', $departmentId) ->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', Department::class) ->where('model_id', $departmentId) ->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' => Department::class, 'model_id' => $departmentId, '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(), ]); } } } } } /** * 모든 권한 거부 (permission_overrides 테이블에서 삭제) * * @param int $departmentId 부서 ID * @param int|null $tenantId 테넌트 ID * @param string $guardName Guard 이름 (api 또는 web) */ public function denyAllPermissions(int $departmentId, ?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 ALLOW overrides for this department DB::table('permission_overrides') ->where('model_type', Department::class) ->where('model_id', $departmentId) ->where('permission_id', $permission->id) ->where('tenant_id', $tenantId) ->where('effect', 1) ->whereNull('deleted_at') ->update([ 'deleted_at' => now(), 'deleted_by' => auth()->id(), ]); } } } } /** * 기본 권한으로 초기화 (view만 허용) * * @param int $departmentId 부서 ID * @param int|null $tenantId 테넌트 ID * @param string $guardName Guard 이름 (api 또는 web) */ public function resetToDefaultPermissions(int $departmentId, ?int $tenantId = null, string $guardName = 'api'): void { // 1. 먼저 모든 권한 제거 $this->denyAllPermissions($departmentId, $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', Department::class) ->where('model_id', $departmentId) ->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', Department::class) ->where('model_id', $departmentId) ->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' => Department::class, 'model_id' => $departmentId, '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 { $query = Menu::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 $departmentId 부서 ID * @param int $menuId 메뉴 ID * @param string $permissionType 권한 유형 * @param string $guardName Guard 이름 (api 또는 web) * @return bool 권한 존재 여부 */ public function hasPermission(int $departmentId, 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', Department::class) ->where('po.model_id', $departmentId) ->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(); } }