diff --git a/app/Http/Controllers/Api/V1/PermissionController.php b/app/Http/Controllers/Api/V1/PermissionController.php new file mode 100644 index 0000000..961c309 --- /dev/null +++ b/app/Http/Controllers/Api/V1/PermissionController.php @@ -0,0 +1,34 @@ +service->getMenuMatrix($id, 'department', $request->all()); + }, '부서 메뉴 권한 매트릭스 조회'); + } + + public function roleMenuMatrix(int $id, Request $request) + { + return ApiResponse::handle(function () use ($id, $request) { + return $this->service->getMenuMatrix($id, 'role', $request->all()); + }, '역할 메뉴 권한 매트릭스 조회'); + } + + public function userMenuMatrix(int $id, Request $request) + { + return ApiResponse::handle(function () use ($id, $request) { + return $this->service->getMenuMatrix($id, 'user', $request->all()); + }, '유저 메뉴 권한 매트릭스 조회'); + } +} diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php new file mode 100644 index 0000000..94371f4 --- /dev/null +++ b/app/Services/PermissionService.php @@ -0,0 +1,141 @@ +tenantId(); + + // 1) 대상 유효성 & model_type 결정 + $modelType = $this->resolveModelType($id, $scope); + if (!$modelType) { + return ['success'=>false,'message'=>'대상 리소스를 찾을 수 없습니다.','data'=>null]; + } + + // 2) 메뉴 목록 + $menus = DB::table('menus') + ->select('id','parent_id','name','url','sort_order') + ->orderBy('sort_order')->orderBy('id') + ->get(); + + // 3) 권한 정의 (permissions.name = "menu:{menuId}.{action}") + $perms = DB::table('permissions') + ->select('id','name','guard_name') + ->where('guard_name', 'api') + ->where('name','like','menu:%') + ->get(); + + $permMap = []; // [menuId][action] => ['id','guard','code'] + foreach ($perms as $p) { + if (preg_match('/^menu:(\d+)\.([a-z_]+)$/', $p->name, $m)) { + $permMap[(int)$m[1]][$m[2]] = [ + 'id' => (int)$p->id, + 'guard'=> $p->guard_name, + 'code' => $p->name, + ]; + } + } + + // 4) 대상의 허용/차단 집합 + $allows = DB::table('model_has_permissions') + ->where('tenant_id', $tenantId) // ★ 강제 + ->where('model_type', $modelType) + ->where('model_id', $id) + ->pluck('permission_id')->all(); + $allowSet = array_fill_keys($allows, true); + + $denies = DB::table('permission_overrides') + ->where('tenant_id', $tenantId) // ★ 강제 + ->where('model_type', $modelType) + ->where('model_id', $id) + ->where('effect', -1) + ->pluck('permission_id')->all(); + $denySet = array_fill_keys($denies, true); + + // 5) 트리 + 액션 상태 구성 + $actions = ['view','create','update','delete','approve']; + $byId = []; + foreach ($menus as $m) { + $node = [ + 'menu_id' => (int)$m->id, + 'parent_id' => $m->parent_id ? (int)$m->parent_id : null, + 'name' => $m->name, + 'url' => $m->url, + 'type' => 'system', + 'children' => [], + 'actions' => [], + ]; + foreach ($actions as $a) { + $perm = $permMap[$m->id][$a] ?? null; + if ($perm) { + $pid = $perm['id']; + $state = isset($denySet[$pid]) ? 'deny' + : (isset($allowSet[$pid]) ? 'allow' : 'none'); + $node['actions'][$a] = [ + 'permission_id' => $pid, + 'permission_code' => $perm['code'], + 'guard_name' => $perm['guard'], + 'state' => $state, + 'is_allowed' => $state === 'allow' ? 1 : 0, + ]; + } else { + $node['actions'][$a] = null; + } + } + $byId[$m->id] = $node; + } + + // 트리 결합 + $roots = []; + foreach ($byId as $key => &$node) { + if ($node['parent_id'] && isset($byId[$node['parent_id']])) { + $byId[$node['parent_id']]['children'][] = &$node; + } else { + $roots[] = &$node; + } + } + unset($node); + + return [ + 'success' => true, + 'message' => $this->titleByScope($scope) . ' 성공', + 'data' => [ + 'actions' => $actions, + 'tree' => $roots, + ], + ]; + } + + /** 스코프별 대상 존재 확인 후 model_type(FQCN)만 반환 */ + private function resolveModelType(int $id, string $scope): ?string + { + return match ($scope) { + 'department' => Department::query()->find($id) ? Department::class : null, + 'role' => SpatieRole::query()->find($id) ? SpatieRole::class : null, + 'user' => User::query()->find($id) ? User::class : null, + default => null, + }; + } + + private function titleByScope(string $scope): string + { + return match ($scope) { + 'department' => '부서 메뉴 권한 매트릭스 조회', + 'role' => '역할 메뉴 권한 매트릭스 조회', + 'user' => '유저 메뉴 권한 매트릭스 조회', + default => '메뉴 권한 매트릭스 조회', + }; + } +} diff --git a/app/Swagger/v1/PermissionApi.php b/app/Swagger/v1/PermissionApi.php new file mode 100644 index 0000000..671126c --- /dev/null +++ b/app/Swagger/v1/PermissionApi.php @@ -0,0 +1,171 @@ + ---> diff --git a/routes/api.php b/routes/api.php index 1210800..8f05593 100644 --- a/routes/api.php +++ b/routes/api.php @@ -17,6 +17,7 @@ use App\Http\Controllers\Api\V1\RolePermissionController; use App\Http\Controllers\Api\V1\UserRoleController; use App\Http\Controllers\Api\V1\DepartmentController; +use App\Http\Controllers\Api\V1\PermissionController; use App\Http\Controllers\Api\V1\TenantFieldSettingController; use App\Http\Controllers\Api\V1\TenantOptionGroupController; use App\Http\Controllers\Api\V1\TenantOptionValueController; @@ -180,6 +181,14 @@ }); + // Permission API + Route::prefix('permissions')->group(function () { + Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('permissions.deptMenuMatrix');; // 부서별 권한 메트릭스 + Route::get('roles/{role_id}/menu-matrix', [PermissionController::class, 'roleMenuMatrix'])->name('permissions.roleMenuMatrix');; // 부서별 권한 메트릭스 + Route::get('users/{user_id}/menu-matrix', [PermissionController::class, 'userMenuMatrix'])->name('permissions.userMenuMatrix');; // 부서별 권한 메트릭스 + }); + + // 테넌트 필드 설정 Route::prefix('fields')->group(function () { Route::get ('', [TenantFieldSettingController::class, 'index'])->name('v1.fields.index'); // 필드 설정 목록(전역+테넌트 병합 효과값)