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') -
+
-

🔑 역할 생성

+

역할 생성

← 목록으로
+ @if(!empty($requireTenant)) + +
+
⚠️
+

테넌트 선택이 필요합니다

+

역할을 생성하려면 먼저 상단 헤더에서 테넌트를 선택해주세요.

+ + 목록으로 돌아가기 + +
+ @else

기본 정보

-
+
+ + +

API: REST API용, Web: 웹 인증용

+
+
-

최대 500자까지 입력 가능합니다.

@@ -42,34 +64,75 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
- +

권한 선택

- @if($permissions->isEmpty()) + @if($menus->isEmpty())
-

선택 가능한 권한이 없습니다.

-

테넌트를 선택하거나 권한을 먼저 생성해주세요.

+

선택 가능한 메뉴가 없습니다.

+

테넌트를 선택하거나 메뉴를 먼저 생성해주세요.

@else -
- @foreach($permissions as $permission) - - @endforeach -
-
+ +
- | + +
+ + +
+ + + + + + + + + + + + + + + + @foreach($menus as $index => $menu) + + + + @foreach($permissionTypes as $type) + + @endforeach + + @endforeach + +
#메뉴명조회생성수정삭제승인내보내기관리
+ {{ $index + 1 }} + +
+ @if(($menu->depth ?? 0) > 0) + + @endif + {{ $menu->name }} +
+
+ +
@endif
@@ -87,6 +150,7 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition"
+ @endif
@endsection @@ -95,18 +159,25 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition" -@endpush \ No newline at end of file +@endpush diff --git a/resources/views/roles/edit.blade.php b/resources/views/roles/edit.blade.php index 38d9dc03..49ec22ca 100644 --- a/resources/views/roles/edit.blade.php +++ b/resources/views/roles/edit.blade.php @@ -3,10 +3,10 @@ @section('title', '역할 수정') @section('content') -
+
-

🔑 역할 수정

+

역할 수정

← 목록으로 @@ -23,7 +23,7 @@

기본 정보

-
+
+ + +

API: REST API용, Web: 웹 인증용

+
+
-

최대 500자까지 입력 가능합니다.

@@ -44,35 +55,76 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
- +

권한 선택

- @if($permissions->isEmpty()) + @if($menus->isEmpty())
-

선택 가능한 권한이 없습니다.

-

테넌트를 선택하거나 권한을 먼저 생성해주세요.

+

선택 가능한 메뉴가 없습니다.

+

테넌트를 선택하거나 메뉴를 먼저 생성해주세요.

@else -
- @foreach($permissions as $permission) - - @endforeach -
-
+ +
- | + +
+ + +
+ + + + + + + + + + + + + + + + @foreach($menus as $index => $menu) + + + + @foreach($permissionTypes as $type) + + @endforeach + + @endforeach + +
#메뉴명조회생성수정삭제승인내보내기관리
+ {{ $index + 1 }} + +
+ @if(($menu->depth ?? 0) > 0) + + @endif + {{ $menu->name }} +
+
+ id][$type]) && $permissions[$menu->id][$type] ? 'checked' : '' }} + class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer permission-checkbox"> +
@endif
@@ -80,22 +132,24 @@ class="text-sm text-blue-600 hover:text-blue-700 underline">

역할 정보

-
+
+
+ 역할 ID: + #{{ $role->id }} +
+
+ Guard: + + {{ strtoupper($role->guard_name) }} + +
생성일: - {{ $role->created_at->format('Y-m-d H:i') }} + {{ $role->created_at->format('Y-m-d H:i') }}
수정일: - {{ $role->updated_at->format('Y-m-d H:i') }} -
-
- 현재 권한 수: - {{ $role->permissions->count() }}개 -
-
- 역할 ID: - #{{ $role->id }} + {{ $role->updated_at->format('Y-m-d H:i') }}
@@ -121,18 +175,25 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition" -@endpush \ No newline at end of file +@endpush diff --git a/resources/views/roles/index.blade.php b/resources/views/roles/index.blade.php index b004e581..d63cddb2 100644 --- a/resources/views/roles/index.blade.php +++ b/resources/views/roles/index.blade.php @@ -14,6 +14,16 @@
+ +
+ +
+
+ @if($isAllTenants) + + @endif + @@ -16,9 +25,21 @@ + @if($isAllTenants) + + @endif + @@ -41,7 +62,7 @@ class="text-red-600 hover:text-red-900"> @empty -
ID테넌트역할 이름Guard 설명 권한 수 생성일 {{ $role->id }} + + {{ $role->tenant?->company_name ?? '미지정' }} + +
{{ $role->name }}
+ + {{ strtoupper($role->guard_name ?? '-') }} + +
{{ $role->description ?? '-' }}
+ 등록된 역할이 없습니다.