diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php index ab55171e..75b9a58b 100644 --- a/app/Http/Controllers/RoleController.php +++ b/app/Http/Controllers/RoleController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Services\RolePermissionService; use App\Services\RoleService; use Illuminate\Http\Request; use Illuminate\View\View; @@ -9,7 +10,8 @@ class RoleController extends Controller { public function __construct( - private readonly RoleService $roleService + private readonly RoleService $roleService, + private readonly RolePermissionService $rolePermissionService ) {} /** @@ -27,18 +29,22 @@ public function create(): View { $tenantId = session('selected_tenant_id'); - // 권한 목록 조회 (현재 테넌트 또는 전체) - $query = \Spatie\Permission\Models\Permission::query() - ->where('guard_name', 'web') - ->orderBy('name'); - - if ($tenantId) { - $query->where('tenant_id', $tenantId); + // 테넌트 미선택 시 생성 불가 + if (! $tenantId || $tenantId === 'all') { + return view('roles.create', [ + 'requireTenant' => true, + 'menus' => collect(), + 'permissionTypes' => [], + ]); } - $permissions = $query->get(['id', 'name']); + // 메뉴 트리 조회 (권한 매트릭스용) + $menus = $this->rolePermissionService->getMenuTree($tenantId); - return view('roles.create', compact('permissions')); + // 권한 유형 목록 + $permissionTypes = ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage']; + + return view('roles.create', compact('menus', 'permissionTypes')); } /** @@ -53,21 +59,21 @@ public function edit(int $id): View } $tenantId = session('selected_tenant_id'); + $effectiveTenantId = ($tenantId && $tenantId !== 'all') ? $tenantId : null; - // 권한 목록 조회 (현재 테넌트 또는 전체) - $query = \Spatie\Permission\Models\Permission::query() - ->where('guard_name', 'web') - ->orderBy('name'); + // 메뉴 트리 조회 (권한 매트릭스용) + $menus = $this->rolePermissionService->getMenuTree($effectiveTenantId); - if ($tenantId) { - $query->where('tenant_id', $tenantId); - } + // 권한 유형 목록 + $permissionTypes = ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage']; - $permissions = $query->get(['id', 'name']); + // 현재 역할의 권한 매트릭스 조회 + $permissions = $this->rolePermissionService->getRolePermissionMatrix( + $role->id, + $effectiveTenantId, + $role->guard_name + ); - // 현재 역할이 가진 권한 ID 목록 - $rolePermissionIds = $role->permissions->pluck('id')->toArray(); - - return view('roles.edit', compact('role', 'permissions', 'rolePermissionIds')); + return view('roles.edit', compact('role', 'menus', 'permissionTypes', 'permissions')); } } diff --git a/app/Http/Requests/StoreRoleRequest.php b/app/Http/Requests/StoreRoleRequest.php index 0ff3ccc2..a159b6d2 100644 --- a/app/Http/Requests/StoreRoleRequest.php +++ b/app/Http/Requests/StoreRoleRequest.php @@ -21,6 +21,7 @@ public function authorize(): bool public function rules(): array { $tenantId = session('selected_tenant_id'); + $guardName = $this->input('guard_name', 'api'); return [ 'name' => [ @@ -29,11 +30,13 @@ public function rules(): array 'max:100', Rule::unique('roles', 'name') ->where('tenant_id', $tenantId) - ->where('guard_name', 'web'), + ->where('guard_name', $guardName), ], + 'guard_name' => 'required|in:api,web', 'description' => 'nullable|string|max:500', - 'permissions' => 'nullable|array', - 'permissions.*' => 'exists:permissions,id', + 'menu_permissions' => 'nullable|array', + 'menu_permissions.*' => 'nullable|array', + 'menu_permissions.*.*' => 'in:view,create,update,delete,approve,export,manage', ]; } @@ -44,8 +47,9 @@ public function attributes(): array { return [ 'name' => '역할 이름', + 'guard_name' => 'Guard', 'description' => '설명', - 'permissions' => '권한', + 'menu_permissions' => '메뉴 권한', ]; } @@ -58,8 +62,9 @@ public function messages(): array 'name.required' => '역할 이름은 필수입니다.', 'name.unique' => '이미 존재하는 역할 이름입니다.', 'name.max' => '역할 이름은 최대 100자까지 입력 가능합니다.', + 'guard_name.required' => 'Guard는 필수입니다.', + 'guard_name.in' => 'Guard는 api 또는 web만 선택 가능합니다.', 'description.max' => '설명은 최대 500자까지 입력 가능합니다.', - 'permissions.*.exists' => '유효하지 않은 권한이 포함되어 있습니다.', ]; } } diff --git a/app/Http/Requests/UpdateRoleRequest.php b/app/Http/Requests/UpdateRoleRequest.php index ac512cdc..c3ec5ad2 100644 --- a/app/Http/Requests/UpdateRoleRequest.php +++ b/app/Http/Requests/UpdateRoleRequest.php @@ -22,6 +22,7 @@ public function rules(): array { $tenantId = session('selected_tenant_id'); $roleId = $this->route('id'); // URL 파라미터에서 role ID 가져오기 + $guardName = $this->input('guard_name', 'api'); return [ 'name' => [ @@ -30,12 +31,14 @@ public function rules(): array 'max:100', Rule::unique('roles', 'name') ->where('tenant_id', $tenantId) - ->where('guard_name', 'web') + ->where('guard_name', $guardName) ->ignore($roleId), ], + 'guard_name' => 'required|in:api,web', 'description' => 'nullable|string|max:500', - 'permissions' => 'nullable|array', - 'permissions.*' => 'exists:permissions,id', + 'menu_permissions' => 'nullable|array', + 'menu_permissions.*' => 'nullable|array', + 'menu_permissions.*.*' => 'in:view,create,update,delete,approve,export,manage', ]; } @@ -46,8 +49,9 @@ public function attributes(): array { return [ 'name' => '역할 이름', + 'guard_name' => 'Guard', 'description' => '설명', - 'permissions' => '권한', + 'menu_permissions' => '메뉴 권한', ]; } @@ -60,8 +64,9 @@ public function messages(): array 'name.required' => '역할 이름은 필수입니다.', 'name.unique' => '이미 존재하는 역할 이름입니다.', 'name.max' => '역할 이름은 최대 100자까지 입력 가능합니다.', + 'guard_name.required' => 'Guard는 필수입니다.', + 'guard_name.in' => 'Guard는 api 또는 web만 선택 가능합니다.', 'description.max' => '설명은 최대 500자까지 입력 가능합니다.', - 'permissions.*.exists' => '유효하지 않은 권한이 포함되어 있습니다.', ]; } } diff --git a/app/Models/Role.php b/app/Models/Role.php index 14749a91..9107a285 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\Tenants\Tenant; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; diff --git a/app/Services/RoleService.php b/app/Services/RoleService.php index 9c11d4f7..ca523d8b 100644 --- a/app/Services/RoleService.php +++ b/app/Services/RoleService.php @@ -2,9 +2,12 @@ namespace App\Services; +use App\Models\Permission; +use App\Models\Role; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Collection; -use Spatie\Permission\Models\Role; +use Illuminate\Support\Facades\DB; +use Spatie\Permission\Models\Role as SpatieRole; class RoleService { @@ -15,12 +18,20 @@ public function getRoles(array $filters = [], int $perPage = 15): LengthAwarePag { $tenantId = session('selected_tenant_id'); - $query = Role::query() - ->where('guard_name', 'web') - ->withCount('permissions'); + $query = Role::query()->withCount('permissions'); + + // Guard 필터링 (선택된 경우에만) + if (! empty($filters['guard_name'])) { + $query->where('guard_name', $filters['guard_name']); + } + + // 전체 보기일 때 tenant 정보 로드 + if (! $tenantId || $tenantId === 'all') { + $query->with('tenant'); + } // Tenant 필터링 (선택된 경우에만) - if ($tenantId) { + if ($tenantId && $tenantId !== 'all') { $query->where('tenant_id', $tenantId); } @@ -44,16 +55,14 @@ public function getRoles(array $filters = [], int $perPage = 15): LengthAwarePag /** * 특정 역할 조회 */ - public function getRoleById(int $id): ?Role + public function getRoleById(int $id): ?SpatieRole { $tenantId = session('selected_tenant_id'); - $query = Role::query() - ->where('guard_name', 'web') - ->with('permissions'); + $query = SpatieRole::query()->with('permissions'); // Tenant 필터링 (선택된 경우에만) - if ($tenantId) { + if ($tenantId && $tenantId !== 'all') { $query->where('tenant_id', $tenantId); } @@ -63,20 +72,22 @@ public function getRoleById(int $id): ?Role /** * 역할 생성 */ - public function createRole(array $data): Role + public function createRole(array $data): SpatieRole { $tenantId = session('selected_tenant_id'); + $effectiveTenantId = ($tenantId && $tenantId !== 'all') ? $tenantId : null; + $guardName = $data['guard_name'] ?? 'api'; - $role = Role::create([ - 'tenant_id' => $tenantId, - 'guard_name' => 'web', + $role = SpatieRole::create([ + 'tenant_id' => $effectiveTenantId, + 'guard_name' => $guardName, 'name' => $data['name'], 'description' => $data['description'] ?? null, ]); - // 권한 동기화 (있는 경우) - if (! empty($data['permissions'])) { - $role->syncPermissions($data['permissions']); + // 메뉴 권한 동기화 (있는 경우) + if (! empty($data['menu_permissions'])) { + $this->syncMenuPermissions($role, $data['menu_permissions'], $effectiveTenantId, $guardName); } return $role->fresh(['permissions']); @@ -93,19 +104,71 @@ public function updateRole(int $id, array $data): bool return false; } + $tenantId = session('selected_tenant_id'); + $effectiveTenantId = ($tenantId && $tenantId !== 'all') ? $tenantId : null; + $guardName = $data['guard_name'] ?? $role->guard_name; + $updated = $role->update([ 'name' => $data['name'] ?? $role->name, + 'guard_name' => $guardName, 'description' => $data['description'] ?? $role->description, ]); - // 권한 동기화 (있는 경우) - if (isset($data['permissions'])) { - $role->syncPermissions($data['permissions']); + // 메뉴 권한 동기화 (있는 경우) + if (isset($data['menu_permissions'])) { + $this->syncMenuPermissions($role, $data['menu_permissions'], $effectiveTenantId, $guardName); } return $updated; } + /** + * 메뉴 권한 동기화 + */ + protected function syncMenuPermissions(SpatieRole $role, array $menuPermissions, ?int $tenantId, string $guardName): void + { + // 기존 메뉴 권한 모두 제거 + DB::table('role_has_permissions') + ->where('role_id', $role->id) + ->whereIn('permission_id', function ($query) use ($guardName) { + $query->select('id') + ->from('permissions') + ->where('guard_name', $guardName) + ->where('name', 'like', 'menu:%'); + }) + ->delete(); + + // 새로운 권한 부여 + foreach ($menuPermissions as $menuId => $types) { + if (! is_array($types)) { + continue; + } + + foreach ($types as $type) { + $permissionName = "menu:{$menuId}.{$type}"; + + // 권한 생성 또는 조회 + $permission = Permission::firstOrCreate( + ['name' => $permissionName, 'guard_name' => $guardName, 'tenant_id' => $tenantId], + ['created_by' => auth()->id()] + ); + + // 권한 부여 + $exists = DB::table('role_has_permissions') + ->where('role_id', $role->id) + ->where('permission_id', $permission->id) + ->exists(); + + if (! $exists) { + DB::table('role_has_permissions')->insert([ + 'role_id' => $role->id, + 'permission_id' => $permission->id, + ]); + } + } + } + } + /** * 역할 삭제 */ @@ -133,10 +196,13 @@ public function isNameExists(string $name, ?int $excludeId = null): bool { $tenantId = session('selected_tenant_id'); - $query = Role::where('tenant_id', $tenantId) - ->where('guard_name', 'web') + $query = SpatieRole::where('guard_name', 'web') ->where('name', $name); + if ($tenantId && $tenantId !== 'all') { + $query->where('tenant_id', $tenantId); + } + if ($excludeId) { $query->where('id', '!=', $excludeId); } @@ -151,10 +217,10 @@ public function getActiveRoles(): Collection { $tenantId = session('selected_tenant_id'); - $query = Role::query()->where('guard_name', 'web'); + $query = SpatieRole::query()->where('guard_name', 'web'); // Tenant 필터링 (선택된 경우에만) - if ($tenantId) { + if ($tenantId && $tenantId !== 'all') { $query->where('tenant_id', $tenantId); } @@ -168,10 +234,10 @@ public function getRoleStats(): array { $tenantId = session('selected_tenant_id'); - $baseQuery = Role::query()->where('guard_name', 'web'); + $baseQuery = SpatieRole::query()->where('guard_name', 'web'); // Tenant 필터링 (선택된 경우에만) - if ($tenantId) { + if ($tenantId && $tenantId !== 'all') { $baseQuery->where('tenant_id', $tenantId); } diff --git a/public/js/pagination.js b/public/js/pagination.js index 35a58719..b0dd2ac2 100644 --- a/public/js/pagination.js +++ b/public/js/pagination.js @@ -40,11 +40,8 @@ window.getPerPageFromCookie = function() { // 페이지당 항목 수 변경 핸들러 window.handlePerPageChange = function(perPage) { - console.log('handlePerPageChange called with:', perPage); - // 쿠키에 저장 setCookie('pagination_per_page', perPage); - console.log('Cookie saved. Reading back:', getCookie('pagination_per_page')); // 현재 페이지의 HTMX 타겟 찾기 const target = document.querySelector('[hx-trigger*="filterSubmit"]'); @@ -55,7 +52,6 @@ window.handlePerPageChange = function(perPage) { if (perPageInput && pageInput) { perPageInput.value = perPage; pageInput.value = 1; // 페이지를 1로 초기화 - console.log('Triggering HTMX with per_page:', perPageInput.value); htmx.trigger(target, 'filterSubmit'); } } @@ -119,7 +115,6 @@ document.body.addEventListener('htmx:afterSwap', function(event) { const perPageSelect = document.getElementById('perPageSelect'); if (perPageSelect) { const savedPerPage = getPerPageFromCookie(); - console.log('HTMX afterSwap - Setting selectbox to:', savedPerPage); perPageSelect.value = savedPerPage; } }, 50); diff --git a/resources/views/roles/create.blade.php b/resources/views/roles/create.blade.php index 813affef..456d31cd 100644 --- a/resources/views/roles/create.blade.php +++ b/resources/views/roles/create.blade.php @@ -3,15 +3,26 @@ @section('title', '역할 생성') @section('content') -