fix : 부서,역할,유저 권한 메트릭스 조회 API

This commit is contained in:
2025-08-21 20:48:39 +09:00
parent 3ee7719cbc
commit d861465276
5 changed files with 357 additions and 2 deletions

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Services\PermissionService;
use Illuminate\Http\Request;
class PermissionController extends Controller
{
public function __construct(private PermissionService $service) {}
public function deptMenuMatrix(int $id, Request $request)
{
return ApiResponse::handle(function () use ($id, $request) {
return $this->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());
}, '유저 메뉴 권한 매트릭스 조회');
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Spatie\Permission\Models\Role as SpatieRole;
use App\Models\Members\User;
use App\Models\Tenants\Department;
class PermissionService extends Service
{
/**
* 메뉴 권한 매트릭스 조회
* $scope: 'department' | 'role' | 'user'
*/
public function getMenuMatrix(int $id, string $scope, array $params = []): array
{
// 0) 강제 테넌트 (없으면 400)
$tenantId = $this->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 => '메뉴 권한 매트릭스 조회',
};
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace App\Swagger\v1;
/**
* @OA\Tag(
* name="Permission",
* description="부서/역할/사용자 단위의 메뉴 권한 매트릭스 조회 API"
* )
*/
/**
* =========================
* 공통 응답 스키마
* =========================
*/
/**
* @OA\Schema(
* schema="ApiResponse",
* type="object",
* required={"success","message"},
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="부서 메뉴 권한 매트릭스 조회 성공"),
* @OA\Property(property="data", nullable=true)
* )
*
* @OA\Schema(
* schema="ErrorResponse",
* type="object",
* required={"success","message"},
* @OA\Property(property="success", type="boolean", example=false),
* @OA\Property(property="message", type="string", example="부서 메뉴 권한 매트릭스 조회 실패"),
* @OA\Property(property="data", type="null", example=null)
* )
*/
/**
* =========================
* 메뉴 매트릭스 스키마
* =========================
*/
/**
* @OA\Schema(
* schema="MenuMatrixAction",
* type="object",
* @OA\Property(property="permission_id", type="integer", example=1),
* @OA\Property(property="permission_code", type="string", example="menu:16.view"),
* @OA\Property(property="guard_name", type="string", example="api"),
* @OA\Property(property="state", type="string", enum={"allow","deny","none"}, example="allow"),
* @OA\Property(property="is_allowed", type="integer", enum={0,1}, example=1)
* )
*
* @OA\Schema(
* schema="MenuMatrixNode",
* type="object",
* @OA\Property(property="menu_id", type="integer", example=16),
* @OA\Property(property="parent_id", type="integer", nullable=true, example=10),
* @OA\Property(property="name", type="string", example="스크린 작업"),
* @OA\Property(property="path", type="string", example="/tenant/production/screen_work.php"),
* @OA\Property(property="type", type="string", example="workflow"),
* @OA\Property(
* property="actions",
* type="object",
* @OA\Property(property="view", ref="#/components/schemas/MenuMatrixAction"),
* @OA\Property(property="create", ref="#/components/schemas/MenuMatrixAction"),
* @OA\Property(property="update", ref="#/components/schemas/MenuMatrixAction"),
* @OA\Property(property="delete", ref="#/components/schemas/MenuMatrixAction"),
* @OA\Property(property="approve", ref="#/components/schemas/MenuMatrixAction")
* ),
* @OA\Property(
* property="children",
* type="array",
* @OA\Items(ref="#/components/schemas/MenuMatrixNode")
* )
* )
*
* @OA\Schema(
* schema="MenuMatrixPayload",
* type="object",
* @OA\Property(
* property="actions",
* type="array",
* @OA\Items(type="string", example="view")
* ),
* @OA\Property(
* property="tree",
* type="array",
* @OA\Items(ref="#/components/schemas/MenuMatrixNode")
* )
* )
*/
class PermissionApi
{
/**
* @OA\Get(
* path="/api/v1/permissions/departments/{id}/menu-matrix",
* summary="부서 메뉴 권한 매트릭스",
* description="부서 기준으로 메뉴 트리 및 액션별 권한 상태(allow/deny/none)를 반환합니다.",
* tags={"Permission"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="부서 ID", @OA\Schema(type="integer", example=1)),
* @OA\Response(
* response=200,
* description="부서 메뉴 권한 매트릭스 조회 성공",
* @OA\JsonContent(
* allOf={
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
* @OA\Property(property="data", ref="#/components/schemas/MenuMatrixPayload")
* )
* }
* )
* ),
* @OA\Response(response=404, description="부서 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function deptMenuMatrix() {}
/**
* @OA\Get(
* path="/api/v1/permissions/roles/{id}/menu-matrix",
* summary="역할 메뉴 권한 매트릭스",
* description="스파티 기본 Role 기준으로 메뉴 트리 및 액션별 권한 상태(allow/deny/none)를 반환합니다.",
* tags={"Permission"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="역할 ID", @OA\Schema(type="integer", example=3)),
* @OA\Response(
* response=200,
* description="역할 메뉴 권한 매트릭스 조회 성공",
* @OA\JsonContent(
* allOf={
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
* @OA\Property(property="data", ref="#/components/schemas/MenuMatrixPayload")
* )
* }
* )
* ),
* @OA\Response(response=404, description="역할 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function roleMenuMatrix() {}
/**
* @OA\Get(
* path="/api/v1/permissions/users/{id}/menu-matrix",
* summary="사용자 메뉴 권한 매트릭스",
* description="사용자 기준으로 메뉴 트리 및 액션별 권한 상태(allow/deny/none)를 반환합니다.",
* tags={"Permission"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="사용자 ID", @OA\Schema(type="integer", example=12)),
* @OA\Response(
* response=200,
* description="유저 메뉴 권한 매트릭스 조회 성공",
* @OA\JsonContent(
* allOf={
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
* @OA\Property(property="data", ref="#/components/schemas/MenuMatrixPayload")
* )
* }
* )
* ),
* @OA\Response(response=404, description="사용자 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function userMenuMatrix() {}
}

View File

@@ -33,7 +33,7 @@
style="width:100vw; height:100vh;"
></rapi-doc>
<script type="module">
<!--<script type="module">
const doc = document.getElementById('doc');
// 스펙이 로드되어 컴포넌트 내부가 그려진 뒤 실행
@@ -71,6 +71,6 @@
// 느린 렌더 케이스 대비, 살짝 늦게 한 번 더
setTimeout(collapseAll, 200);
});
</script>
</script>-->
</body>
</html>

View File

@@ -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'); // 필드 설정 목록(전역+테넌트 병합 효과값)