From 2a9b697baf7d06b36188c57971cc1d65de8daddf Mon Sep 17 00:00:00 2001 From: hskwon Date: Tue, 25 Nov 2025 15:21:48 +0900 Subject: [PATCH] =?UTF-8?q?UI=20=EA=B0=9C=EC=84=A0:=20=ED=85=8C=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A0=ED=83=9D=20=ED=97=A4=EB=8D=94=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EB=B0=8F=20=EC=97=AD=ED=95=A0=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테넌트 선택을 각 페이지에서 헤더로 통합 이동 - 페이지 제목 이모지 제거 및 상단 여백(mt-6) 축소 - 역할 권한 관리 페이지 레이아웃을 다른 페이지와 통일 - 메뉴명 스타일 개선 (depth 들여쓰기, └ 기호 적용) - 상위 메뉴 컬럼 제거로 테이블 간소화 - RolePermissionService에 depth 계산 로직 추가 - pagination.js를 body 끝으로 이동하여 로딩 오류 해결 - 역할 선택 UI를 셀렉트박스에서 버튼 형태로 변경 - 역할 버튼 hover 효과 개선 (선택된 버튼 가독성 향상) 변경된 파일: - resources/views/partials/header.blade.php: 테넌트 선택 UI 추가 - resources/views/dashboard|menus|users|departments|permissions|roles/index.blade.php: tenant-selector 제거, 여백 축소 - resources/views/layouts/app.blade.php: pagination.js 위치 변경 - app/Services/RolePermissionService.php: depth 계산 로직 추가 - resources/views/role-permissions/: 역할 권한 관리 페이지 개선 - routes/web.php, routes/api.php: 역할 권한 관리 라우트 추가 --- CURRENT_WORKS.md | 93 +++++- ROLE_PERMISSION_SESSION_SUMMARY.md | 197 ++++++++++++ .../Api/Admin/RolePermissionController.php | 112 +++++++ .../Controllers/RolePermissionController.php | 35 +++ app/Services/RolePermissionService.php | 287 ++++++++++++++++++ resources/views/dashboard/index.blade.php | 3 - resources/views/departments/index.blade.php | 7 +- resources/views/layouts/app.blade.php | 9 +- resources/views/menus/index.blade.php | 7 +- resources/views/partials/header.blade.php | 52 +++- resources/views/partials/sidebar.blade.php | 4 +- resources/views/permissions/index.blade.php | 7 +- .../views/role-permissions/index.blade.php | 110 +++++++ .../partials/empty-state.blade.php | 17 ++ .../partials/permission-matrix.blade.php | 66 ++++ resources/views/roles/index.blade.php | 7 +- resources/views/users/index.blade.php | 7 +- routes/api.php | 9 + routes/web.php | 4 + 19 files changed, 994 insertions(+), 39 deletions(-) create mode 100644 ROLE_PERMISSION_SESSION_SUMMARY.md create mode 100644 app/Http/Controllers/Api/Admin/RolePermissionController.php create mode 100644 app/Http/Controllers/RolePermissionController.php create mode 100644 app/Services/RolePermissionService.php create mode 100644 resources/views/role-permissions/index.blade.php create mode 100644 resources/views/role-permissions/partials/empty-state.blade.php create mode 100644 resources/views/role-permissions/partials/permission-matrix.blade.php diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 111ee499..7b76c8d4 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -774,6 +774,93 @@ ### Phase 7: 시스템 관리 --- -**최종 업데이트**: 2025-11-24 22:30 -**현재 Phase**: Phase 4 완료, Phase 5 준비 중 -**다음 작업**: 제품 관리 시스템 구현 \ No newline at end of file +## 2025-11-25 (월) - Phase 4-4-2: 역할 권한 관리 기능 구현 + +### 주요 작업 +- 역할-권한 매핑 관리 기능 완전 구현 +- admin 패널의 역할 권한 관리를 MNG로 이식 (Livewire → HTMX) +- 메뉴 기반 권한 체크박스 매트릭스 시스템 + +### 추가된 파일: +- `app/Services/RolePermissionService.php` - 역할 권한 비즈니스 로직 +- `app/Http/Controllers/RolePermissionController.php` - Blade 화면 컨트롤러 +- `app/Http/Controllers/Api/Admin/RolePermissionController.php` - HTMX API 컨트롤러 +- `resources/views/role-permissions/index.blade.php` - 메인 페이지 +- `resources/views/role-permissions/partials/empty-state.blade.php` - 빈 상태 화면 +- `resources/views/role-permissions/partials/permission-matrix.blade.php` - 권한 매트릭스 테이블 + +### 수정된 파일: +- `resources/views/partials/sidebar.blade.php` - 역할 권한 관리 메뉴 활성화 +- `routes/web.php` - 역할 권한 관리 라우트 추가 +- `routes/api.php` - 역할 권한 관리 API 라우트 추가 + +### 기능 상세: + +**RolePermissionService 주요 메서드:** +- `getRolePermissionMatrix(roleId, tenantId)`: 권한 매트릭스 조회 +- `togglePermission(roleId, menuId, type, tenantId)`: 권한 토글 +- `propagateToChildren(roleId, menuId, type, value, tenantId)`: 하위 메뉴 권한 전파 +- `allowAllPermissions(roleId, tenantId)`: 모든 권한 허용 +- `denyAllPermissions(roleId, tenantId)`: 모든 권한 거부 +- `getMenuTree(tenantId)`: 메뉴 트리 조회 +- `hasPermission(roleId, menuId, type)`: 권한 확인 + +**권한 유형 (7가지):** +- view (조회) +- create (생성) +- update (수정) +- delete (삭제) +- approve (승인) +- export (내보내기) +- manage (관리) + +**UI 구조:** +1. **상단 툴바** + - 역할 선택 드롭다운 (HTMX 동적 로딩) + - 전체 허용 (녹색) / 전체 거부 (빨간색) / 초기화 (회색) 버튼 + +2. **권한 매트릭스 테이블** + - 행: 메뉴 목록 (계층 구조, "├─" 인덴트) + - 컬럼: 순번, 메뉴명, 상위 메뉴, URL, 순서 + 권한 체크박스 7개 + - 체크박스: HTMX로 실시간 토글 + +3. **빈 상태** + - 역할 미선택 시 "역할을 선택해주세요" 메시지 표시 + +**HTMX 패턴:** +- 역할 변경: `hx-get="/api/admin/role-permissions/matrix"` +- 권한 토글: `hx-post="/api/admin/role-permissions/toggle"` +- 전체 허용: `hx-post="/api/admin/role-permissions/allow-all"` +- 전체 거부: `hx-post="/api/admin/role-permissions/deny-all"` + +**계층적 권한 전파:** +- 부모 메뉴 권한 변경 시 하위 메뉴에 자동 전파 +- 재귀적 처리로 모든 하위 메뉴에 동일 권한 적용 + +**테넌트 필터링:** +- 세션의 `selected_tenant_id` 기준으로 필터링 +- 테넌트별 메뉴와 권한만 표시 + +### 기술 스택: +- Service-First 패턴 +- HTMX (Livewire 대체) +- Plain Blade (Filament 컴포넌트 없음) +- Spatie Permission (role_has_permissions 테이블) +- Tailwind CSS + +### Admin vs MNG 차이점: +| 항목 | Admin (Filament) | MNG (Plain Laravel) | +|------|------------------|---------------------| +| 프레임워크 | Livewire | HTMX | +| 컴포넌트 | Filament | Tailwind CSS | +| 메서드 | wire:click | hx-post | +| 모델 바인딩 | wire:model.live | JavaScript change | + +### Git 커밋: +- ⏳ 작업 완료, 커밋 대기 중 + +--- + +**최종 업데이트**: 2025-11-25 13:00 +**현재 Phase**: Phase 4-4-2 완료, 브라우저 테스트 대기 중 +**다음 작업**: 브라우저 동작 확인 및 오류 수정 \ No newline at end of file diff --git a/ROLE_PERMISSION_SESSION_SUMMARY.md b/ROLE_PERMISSION_SESSION_SUMMARY.md new file mode 100644 index 00000000..1499b7fc --- /dev/null +++ b/ROLE_PERMISSION_SESSION_SUMMARY.md @@ -0,0 +1,197 @@ +# 역할 권한 관리 기능 구현 - 세션 요약 + +**작업 일시**: 2025-11-25 +**현재 상태**: 코드 작업 완료, 브라우저 테스트 대기 +**다음 모델**: Opus 4.5 + +--- + +## ✅ 완료된 작업 + +### 1. RolePermissionService 생성 +**파일**: `app/Services/RolePermissionService.php` + +**주요 메서드**: +- `getRolePermissionMatrix($roleId, $tenantId)` - 권한 매트릭스 조회 +- `togglePermission($roleId, $menuId, $type, $tenantId)` - 권한 토글 +- `propagateToChildren($roleId, $menuId, $type, $value, $tenantId)` - 하위 메뉴 권한 전파 +- `allowAllPermissions($roleId, $tenantId)` - 모든 권한 허용 +- `denyAllPermissions($roleId, $tenantId)` - 모든 권한 거부 +- `getMenuTree($tenantId)` - 메뉴 트리 조회 +- `hasPermission($roleId, $menuId, $type)` - 권한 확인 + +**권한 유형**: view, create, update, delete, approve, export, manage + +### 2. Controller 생성 +**파일**: +- `app/Http/Controllers/RolePermissionController.php` - Blade 뷰 +- `app/Http/Controllers/Api/Admin/RolePermissionController.php` - HTMX API + +**API 엔드포인트**: +- `GET /api/admin/role-permissions/matrix` - 권한 매트릭스 조회 +- `POST /api/admin/role-permissions/toggle` - 권한 토글 +- `POST /api/admin/role-permissions/allow-all` - 전체 허용 +- `POST /api/admin/role-permissions/deny-all` - 전체 거부 + +### 3. Blade View 생성 +**파일**: +- `resources/views/role-permissions/index.blade.php` - 메인 페이지 +- `resources/views/role-permissions/partials/empty-state.blade.php` - 빈 상태 +- `resources/views/role-permissions/partials/permission-matrix.blade.php` - 권한 매트릭스 테이블 + +**UI 구조**: +1. 역할 선택 드롭다운 (왼쪽) +2. 액션 버튼 (전체 허용/거부/초기화) (오른쪽) +3. 권한 매트릭스 테이블 (메뉴 × 권한 유형) + +### 4. 라우트 등록 +**web.php**: +```php +Route::get('/role-permissions', [RolePermissionController::class, 'index']) + ->name('role-permissions.index'); +``` + +**api.php**: +```php +Route::prefix('role-permissions')->name('role-permissions.')->group(function () { + Route::get('/matrix', [RolePermissionController::class, 'getMatrix']); + Route::post('/toggle', [RolePermissionController::class, 'toggle']); + Route::post('/allow-all', [RolePermissionController::class, 'allowAll']); + Route::post('/deny-all', [RolePermissionController::class, 'denyAll']); +}); +``` + +### 5. 사이드바 메뉴 활성화 +**파일**: `resources/views/partials/sidebar.blade.php` +- "역할 권한 관리" 메뉴 `href="#"` → `href="{{ route('role-permissions.index') }}"`로 변경 +- 활성 상태 표시 추가 + +### 6. 문서 업데이트 +**파일**: `CURRENT_WORKS.md` +- Phase 4-4-2 작업 내역 추가 + +--- + +## ⏳ 남은 작업 + +### 1. 브라우저 테스트 (최우선) +**테스트 URL**: https://mng.sam.kr/role-permissions + +**확인 사항**: +- [ ] 페이지 로딩 (500 에러 없이) +- [ ] 역할 선택 드롭다운 동작 +- [ ] 권한 매트릭스 테이블 표시 +- [ ] 체크박스 토글 동작 (HTMX) +- [ ] 전체 허용/거부 버튼 동작 +- [ ] 하위 메뉴 권한 전파 확인 + +**예상 오류 및 해결**: +1. **Role 모델 경로 오류** + - 현재: `\App\Models\Role` + - 확인 필요: Role 모델이 실제로 존재하는지 + - 해결: 경로 수정 또는 모델 생성 + +2. **Menu 모델 관계 오류** + - `Menu::with('parent')` 관계 확인 + - parent 관계가 정의되어 있는지 확인 + +3. **HTMX 로딩 안됨** + - layouts/app.blade.php에 HTMX 스크립트 포함 확인 + - CSRF 토큰 헤더 설정 확인 + +### 2. 오류 수정 +발견된 오류를 즉시 수정 + +### 3. Git 커밋 +**커밋 메시지 (한글)**: +``` +feat: 역할 권한 관리 기능 구현 + +- RolePermissionService 생성 (권한 매트릭스 조회, 토글, 전체 허용/거부) +- HTMX 기반 실시간 권한 토글 +- 계층적 권한 전파 (부모 → 자식 메뉴) +- admin 패널 역할 권한 관리 기능 이식 (Livewire → HTMX) + +변경 파일: +- app/Services/RolePermissionService.php (신규) +- app/Http/Controllers/RolePermissionController.php (신규) +- app/Http/Controllers/Api/Admin/RolePermissionController.php (신규) +- resources/views/role-permissions/ (신규) +- routes/web.php, routes/api.php +- resources/views/partials/sidebar.blade.php +- CURRENT_WORKS.md +``` + +--- + +## 🔧 기술 스택 + +- **패턴**: Service-First +- **프론트**: HTMX + Tailwind CSS +- **백엔드**: Plain Laravel (Livewire 없음) +- **권한**: Spatie Permission (role_has_permissions 테이블) + +--- + +## 📊 Admin vs MNG 비교 + +| 항목 | Admin (Filament) | MNG (구현됨) | +|------|------------------|--------------| +| 프레임워크 | Livewire | HTMX | +| 컴포넌트 | Filament | Tailwind CSS | +| 상태 관리 | wire:model.live | JavaScript | +| 이벤트 | wire:click | hx-post | +| 리렌더링 | Livewire 자동 | HTMX 타겟 | + +--- + +## 🎯 다음 세션 시작 방법 + +```bash +# 1. Opus 4.5로 새 세션 시작 +claude-code chat --model opus-4-5 + +# 2. 이 파일 읽기 +"ROLE_PERMISSION_SESSION_SUMMARY.md 파일 읽어줘" + +# 3. 바로 테스트 진행 +"브라우저에서 https://mng.sam.kr/role-permissions 테스트해줘" +``` + +--- + +## 💡 참고 정보 + +**DB 스키마**: +- `role_has_permissions` 테이블 (Spatie) + - role_id, permission_id (복합 키) +- `permissions` 테이블 + - name 패턴: `menu:{menu_id}.{type}` (예: menu:1.view) + +**테넌트 필터링**: +- 세션: `selected_tenant_id` +- 필터링: 메뉴, 권한, 역할 모두 테넌트별 표시 + +**계층 전파 로직**: +- 부모 메뉴 권한 변경 → 모든 하위 메뉴에 재귀적 적용 +- `propagateToChildren()` 메서드 구현됨 + +--- + +## 📝 현재 Todo 상태 + +- [x] RolePermissionService 생성 +- [x] RolePermissionController 생성 (Blade) +- [x] Api/Admin/RolePermissionController 생성 (HTMX API) +- [x] Blade View 생성 +- [x] 라우트 등록 +- [x] 사이드바 메뉴 활성화 +- [x] CURRENT_WORKS.md 업데이트 +- [ ] 브라우저 테스트 및 오류 수정 ← **다음 작업** +- [ ] Git 커밋 + +--- + +**작성일**: 2025-11-25 13:10 +**작성자**: Sonnet 4.5 +**다음 담당**: Opus 4.5 diff --git a/app/Http/Controllers/Api/Admin/RolePermissionController.php b/app/Http/Controllers/Api/Admin/RolePermissionController.php new file mode 100644 index 00000000..82d20e96 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/RolePermissionController.php @@ -0,0 +1,112 @@ +rolePermissionService = $rolePermissionService; + } + + /** + * 권한 매트릭스 조회 (역할 변경 시 호출) + */ + public function getMatrix(Request $request) + { + $roleId = $request->input('role_id'); + $tenantId = session('selected_tenant_id'); + + if (! $roleId) { + return view('role-permissions.partials.empty-state'); + } + + // 메뉴 트리 조회 + $menus = $this->rolePermissionService->getMenuTree($tenantId); + + // 권한 매트릭스 조회 + $permissions = $this->rolePermissionService->getRolePermissionMatrix($roleId, $tenantId); + + return view('role-permissions.partials.permission-matrix', [ + 'menus' => $menus, + 'permissions' => $permissions, + 'roleId' => $roleId, + ]); + } + + /** + * 권한 토글 + */ + public function toggle(Request $request) + { + $roleId = $request->input('role_id'); + $menuId = $request->input('menu_id'); + $permissionType = $request->input('permission_type'); + $tenantId = session('selected_tenant_id'); + + $newValue = $this->rolePermissionService->togglePermission( + $roleId, + $menuId, + $permissionType, + $tenantId + ); + + // 전체 매트릭스 다시 로드 + $menus = $this->rolePermissionService->getMenuTree($tenantId); + $permissions = $this->rolePermissionService->getRolePermissionMatrix($roleId, $tenantId); + + return view('role-permissions.partials.permission-matrix', [ + 'menus' => $menus, + 'permissions' => $permissions, + 'roleId' => $roleId, + ]); + } + + /** + * 전체 허용 + */ + public function allowAll(Request $request) + { + $roleId = $request->input('role_id'); + $tenantId = session('selected_tenant_id'); + + $this->rolePermissionService->allowAllPermissions($roleId, $tenantId); + + // 전체 매트릭스 다시 로드 + $menus = $this->rolePermissionService->getMenuTree($tenantId); + $permissions = $this->rolePermissionService->getRolePermissionMatrix($roleId, $tenantId); + + return view('role-permissions.partials.permission-matrix', [ + 'menus' => $menus, + 'permissions' => $permissions, + 'roleId' => $roleId, + ]); + } + + /** + * 전체 거부 + */ + public function denyAll(Request $request) + { + $roleId = $request->input('role_id'); + $tenantId = session('selected_tenant_id'); + + $this->rolePermissionService->denyAllPermissions($roleId, $tenantId); + + // 전체 매트릭스 다시 로드 + $menus = $this->rolePermissionService->getMenuTree($tenantId); + $permissions = $this->rolePermissionService->getRolePermissionMatrix($roleId, $tenantId); + + return view('role-permissions.partials.permission-matrix', [ + 'menus' => $menus, + 'permissions' => $permissions, + 'roleId' => $roleId, + ]); + } +} diff --git a/app/Http/Controllers/RolePermissionController.php b/app/Http/Controllers/RolePermissionController.php new file mode 100644 index 00000000..a558d3fe --- /dev/null +++ b/app/Http/Controllers/RolePermissionController.php @@ -0,0 +1,35 @@ +rolePermissionService = $rolePermissionService; + } + + /** + * 역할 권한 관리 메인 페이지 + */ + public function index(Request $request) + { + $tenantId = session('selected_tenant_id'); + + // 역할 목록 조회 + $roles = \App\Models\Role::query(); + if ($tenantId && $tenantId !== 'all') { + $roles->where('tenant_id', $tenantId); + } + $roles = $roles->orderBy('name')->get(); + + return view('role-permissions.index', [ + 'roles' => $roles, + ]); + } +} diff --git a/app/Services/RolePermissionService.php b/app/Services/RolePermissionService.php new file mode 100644 index 00000000..db296391 --- /dev/null +++ b/app/Services/RolePermissionService.php @@ -0,0 +1,287 @@ +join('permissions', 'role_has_permissions.permission_id', '=', 'permissions.id') + ->where('role_has_permissions.role_id', $roleId) + ->where('permissions.name', 'like', 'menu:%'); + + if ($tenantId) { + $query->where('permissions.tenant_id', $tenantId); + } + + $rolePermissions = $query->pluck('permissions.name')->toArray(); + + $permissions = []; + foreach ($rolePermissions 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; + } + + /** + * 특정 메뉴의 특정 권한 토글 + * + * @param int $roleId 역할 ID + * @param int $menuId 메뉴 ID + * @param string $permissionType 권한 유형 + * @param int|null $tenantId 테넌트 ID + * @return bool 토글 후 상태 (true: 허용, false: 거부) + */ + public function togglePermission(int $roleId, int $menuId, string $permissionType, ?int $tenantId = null): bool + { + $permissionName = "menu:{$menuId}.{$permissionType}"; + + // 권한 생성 또는 조회 + $permission = Permission::firstOrCreate( + ['name' => $permissionName, 'guard_name' => 'web', 'tenant_id' => $tenantId], + ['created_by' => auth()->id()] + ); + + // 현재 권한 상태 확인 + $exists = DB::table('role_has_permissions') + ->where('role_id', $roleId) + ->where('permission_id', $permission->id) + ->exists(); + + if ($exists) { + // 권한 제거 + DB::table('role_has_permissions') + ->where('role_id', $roleId) + ->where('permission_id', $permission->id) + ->delete(); + + $newValue = false; + } else { + // 권한 부여 + DB::table('role_has_permissions')->insert([ + 'role_id' => $roleId, + 'permission_id' => $permission->id, + ]); + + $newValue = true; + } + + // 하위 메뉴에 권한 전파 + $this->propagateToChildren($roleId, $menuId, $permissionType, $newValue, $tenantId); + + return $newValue; + } + + /** + * 하위 메뉴에 권한 전파 + * + * @param int $roleId 역할 ID + * @param int $parentMenuId 부모 메뉴 ID + * @param string $permissionType 권한 유형 + * @param bool $value 권한 값 (true: 허용, false: 거부) + * @param int|null $tenantId 테넌트 ID + */ + protected function propagateToChildren(int $roleId, int $parentMenuId, string $permissionType, bool $value, ?int $tenantId = null): void + { + $children = Menu::where('parent_id', $parentMenuId)->get(); + + foreach ($children as $child) { + $permissionName = "menu:{$child->id}.{$permissionType}"; + $permission = Permission::firstOrCreate( + ['name' => $permissionName, 'guard_name' => 'web', 'tenant_id' => $tenantId], + ['created_by' => auth()->id()] + ); + + if ($value) { + // 권한 부여 + $exists = DB::table('role_has_permissions') + ->where('role_id', $roleId) + ->where('permission_id', $permission->id) + ->exists(); + + if (! $exists) { + DB::table('role_has_permissions')->insert([ + 'role_id' => $roleId, + 'permission_id' => $permission->id, + ]); + } + } else { + // 권한 제거 + DB::table('role_has_permissions') + ->where('role_id', $roleId) + ->where('permission_id', $permission->id) + ->delete(); + } + + // 재귀적으로 하위 메뉴 처리 + $this->propagateToChildren($roleId, $child->id, $permissionType, $value, $tenantId); + } + } + + /** + * 모든 권한 허용 + * + * @param int $roleId 역할 ID + * @param int|null $tenantId 테넌트 ID + */ + public function allowAllPermissions(int $roleId, ?int $tenantId = null): 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::firstOrCreate( + ['name' => $permissionName, 'guard_name' => 'web', 'tenant_id' => $tenantId], + ['created_by' => auth()->id()] + ); + + // 권한 부여 + $exists = DB::table('role_has_permissions') + ->where('role_id', $roleId) + ->where('permission_id', $permission->id) + ->exists(); + + if (! $exists) { + DB::table('role_has_permissions')->insert([ + 'role_id' => $roleId, + 'permission_id' => $permission->id, + ]); + } + } + } + } + + /** + * 모든 권한 거부 + * + * @param int $roleId 역할 ID + * @param int|null $tenantId 테넌트 ID + */ + public function denyAllPermissions(int $roleId, ?int $tenantId = null): 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)->first(); + + if ($permission) { + DB::table('role_has_permissions') + ->where('role_id', $roleId) + ->where('permission_id', $permission->id) + ->delete(); + } + } + } + } + + /** + * 메뉴 트리 조회 (권한 매트릭스 표시용) + * + * @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 현재 깊이 + * @return \Illuminate\Support\Collection + */ + 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; + } + + /** + * 특정 역할의 활성 메뉴 권한 확인 + * + * @param int $roleId 역할 ID + * @param int $menuId 메뉴 ID + * @param string $permissionType 권한 유형 + * @return bool 권한 존재 여부 + */ + public function hasPermission(int $roleId, int $menuId, string $permissionType): bool + { + $permissionName = "menu:{$menuId}.{$permissionType}"; + + return DB::table('role_has_permissions') + ->join('permissions', 'role_has_permissions.permission_id', '=', 'permissions.id') + ->where('role_has_permissions.role_id', $roleId) + ->where('permissions.name', $permissionName) + ->exists(); + } +} diff --git a/resources/views/dashboard/index.blade.php b/resources/views/dashboard/index.blade.php index d695433a..a90b04df 100644 --- a/resources/views/dashboard/index.blade.php +++ b/resources/views/dashboard/index.blade.php @@ -4,9 +4,6 @@ @section('page-title', '대시보드') @section('content') - - @include('partials.tenant-selector') -
diff --git a/resources/views/departments/index.blade.php b/resources/views/departments/index.blade.php index 78ce4474..082af5e6 100644 --- a/resources/views/departments/index.blade.php +++ b/resources/views/departments/index.blade.php @@ -3,12 +3,9 @@ @section('title', '부서 관리') @section('content') - - @include('partials.tenant-selector') - -
-

🏢 부서 관리

+
+

부서 관리

+ 새 부서 diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index b4b2eeb3..6675daba 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -7,7 +7,6 @@ @yield('title', 'Dashboard') - {{ config('app.name') }} @vite(['resources/css/app.css', 'resources/js/app.js']) - @stack('styles') @@ -27,6 +26,14 @@
+ + + + @stack('scripts') diff --git a/resources/views/menus/index.blade.php b/resources/views/menus/index.blade.php index e4fceb67..dcde7456 100644 --- a/resources/views/menus/index.blade.php +++ b/resources/views/menus/index.blade.php @@ -3,12 +3,9 @@ @section('title', '메뉴 관리') @section('content') - - @include('partials.tenant-selector') - -
-

📋 메뉴 관리

+
+

메뉴 관리

+ 새 메뉴 diff --git a/resources/views/partials/header.blade.php b/resources/views/partials/header.blade.php index 47eba669..5d03b0c2 100644 --- a/resources/views/partials/header.blade.php +++ b/resources/views/partials/header.blade.php @@ -1,10 +1,52 @@
- -
-

- @yield('page-title', '대시보드') -

+ +
+
+ + + + +
+ +
+ @csrf + +
+ + + @if(session('selected_tenant_id')) + @php + $currentTenant = $globalTenants->firstWhere('id', session('selected_tenant_id')); + @endphp + @if($currentTenant) + + + + + {{ $currentTenant->company_name }} + + @endif + @else + 전체 + @endif
diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index 906b0aaf..284e1b24 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -102,8 +102,8 @@ class="flex items-center gap-2 pr-3 py-2 rounded-lg text-sm text-gray-700 hover:
  • - diff --git a/resources/views/permissions/index.blade.php b/resources/views/permissions/index.blade.php index b2a3dac1..0b5540f4 100644 --- a/resources/views/permissions/index.blade.php +++ b/resources/views/permissions/index.blade.php @@ -3,12 +3,9 @@ @section('title', '권한 관리') @section('content') - - @include('partials.tenant-selector') - -
    -

    🛡️ 권한 관리

    +
    +

    권한 관리

    + 새 권한 diff --git a/resources/views/role-permissions/index.blade.php b/resources/views/role-permissions/index.blade.php new file mode 100644 index 00000000..80c80c35 --- /dev/null +++ b/resources/views/role-permissions/index.blade.php @@ -0,0 +1,110 @@ +@extends('layouts.app') + +@section('title', '역할 권한 관리') + +@section('content') + +
    +

    역할 권한 관리

    +
    + + +
    +
    +
    + 역할 선택: + @foreach($roles as $role) + + @endforeach +
    +
    +
    + + + + + +
    + @include('role-permissions.partials.empty-state') +
    + + +@endsection diff --git a/resources/views/role-permissions/partials/empty-state.blade.php b/resources/views/role-permissions/partials/empty-state.blade.php new file mode 100644 index 00000000..60a4d63c --- /dev/null +++ b/resources/views/role-permissions/partials/empty-state.blade.php @@ -0,0 +1,17 @@ +
    +
    +
    +
    + + + +
    +
    +

    + 역할을 선택해주세요 +

    +

    + 상단에서 역할을 선택하면 해당 역할의 권한을 설정할 수 있습니다. +

    +
    +
    diff --git a/resources/views/role-permissions/partials/permission-matrix.blade.php b/resources/views/role-permissions/partials/permission-matrix.blade.php new file mode 100644 index 00000000..687a714d --- /dev/null +++ b/resources/views/role-permissions/partials/permission-matrix.blade.php @@ -0,0 +1,66 @@ +
    + + + + + + + + + + + + + + + + + + @php + $permissionTypes = ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage']; + @endphp + @forelse($menus as $index => $menu) + + + + + + @foreach($permissionTypes as $type) + + @endforeach + + @empty + + + + @endforelse + +
    순번메뉴명URL순서조회생성수정삭제승인내보내기관리
    + {{ $index + 1 }} + +
    + @if(($menu->depth ?? 0) > 0) + + @endif + {{ $menu->name }} +
    +
    + + {{ $menu->url }} + + + {{ $menu->sort_order }} + + id][$type]) && $permissions[$menu->id][$type] ? 'checked' : '' }} + class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer" + hx-post="/api/admin/role-permissions/toggle" + hx-target="#permission-matrix" + hx-include="[name='role_id']" + hx-vals='{"menu_id": {{ $menu->id }}, "permission_type": "{{ $type }}"}' + > +
    + 활성화된 메뉴가 없습니다. +
    +
    diff --git a/resources/views/roles/index.blade.php b/resources/views/roles/index.blade.php index 48eff886..b004e581 100644 --- a/resources/views/roles/index.blade.php +++ b/resources/views/roles/index.blade.php @@ -3,12 +3,9 @@ @section('title', '역할 관리') @section('content') - - @include('partials.tenant-selector') - -
    -

    🔑 역할 관리

    +
    +

    역할 관리

    + 새 역할 diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php index 68167fc8..1ec68258 100644 --- a/resources/views/users/index.blade.php +++ b/resources/views/users/index.blade.php @@ -3,12 +3,9 @@ @section('title', '사용자 관리') @section('content') - - @include('partials.tenant-selector') - -
    -

    👥 사용자 관리

    +
    +

    사용자 관리

    + 새 사용자 diff --git a/routes/api.php b/routes/api.php index f5e90b13..51dd594d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Api\Admin\MenuController; use App\Http\Controllers\Api\Admin\PermissionController; use App\Http\Controllers\Api\Admin\RoleController; +use App\Http\Controllers\Api\Admin\RolePermissionController; use App\Http\Controllers\Api\Admin\TenantController; use App\Http\Controllers\Api\Admin\UserController; use Illuminate\Support\Facades\Route; @@ -94,4 +95,12 @@ Route::put('/{id}', [PermissionController::class, 'update'])->name('update'); Route::delete('/{id}', [PermissionController::class, 'destroy'])->name('destroy'); }); + + // 역할 권한 관리 API + Route::prefix('role-permissions')->name('role-permissions.')->group(function () { + Route::get('/matrix', [RolePermissionController::class, 'getMatrix'])->name('matrix'); + Route::post('/toggle', [RolePermissionController::class, 'toggle'])->name('toggle'); + Route::post('/allow-all', [RolePermissionController::class, 'allowAll'])->name('allowAll'); + Route::post('/deny-all', [RolePermissionController::class, 'denyAll'])->name('denyAll'); + }); }); diff --git a/routes/web.php b/routes/web.php index 7fb09baa..3b1fa43e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,7 @@ use App\Http\Controllers\MenuController; use App\Http\Controllers\PermissionController; use App\Http\Controllers\RoleController; +use App\Http\Controllers\RolePermissionController; use App\Http\Controllers\TenantController; use App\Http\Controllers\UserController; use Illuminate\Support\Facades\Route; @@ -74,6 +75,9 @@ Route::get('/{id}/edit', [PermissionController::class, 'edit'])->name('edit'); }); + // 역할 권한 관리 (Blade 화면만) + Route::get('/role-permissions', [RolePermissionController::class, 'index'])->name('role-permissions.index'); + // 대시보드 Route::get('/dashboard', function () { return view('dashboard.index');