Files
sam-manage/resources/views/user-permissions/index.blade.php
hskwon 7546771ee5 feat(mng): 개인 권한 관리 통합 매트릭스 구현
- 역할/부서/개인 권한을 통합하여 최종 유효 권한 표시
- 권한 소스별 색상 구분 UI (보라=역할, 파랑=부서, 녹색=개인허용, 빨강=개인거부)
- 스마트 토글 로직 (상속된 권한 오버라이드 지원)
- UserPermissionService: getRolePermissions(), getDepartmentPermissions(), getPersonalOverrides()
- 사용자 ID 뱃지 스타일 개선
2025-11-26 20:40:54 +09:00

173 lines
8.4 KiB
PHP

@extends('layouts.app')
@section('title', '개인 권한 관리')
@section('content')
<!-- 페이지 헤더 -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">개인 권한 관리</h1>
</div>
<!-- 사용자 선택 -->
<div class="bg-white rounded-lg shadow-sm mb-6">
<div class="px-6 py-4">
@if($requireTenant)
{{-- 전체 테넌트 선택 : 테넌트 선택 안내 --}}
<div class="text-center py-8">
<div class="text-yellow-500 text-4xl mb-3">⚠️</div>
<p class="text-gray-700 font-medium mb-2">테넌트를 선택해주세요</p>
<p class="text-gray-500 text-sm">개인 권한을 관리하려면 상단 헤더에서 특정 테넌트를 선택해야 합니다.</p>
</div>
@else
{{-- 특정 테넌트 선택 : 사용자 목록 표시 --}}
@if($users->isEmpty())
<div class="text-center py-8">
<div class="text-gray-400 text-4xl mb-3">👤</div>
<p class="text-gray-700 font-medium mb-2">사용자가 없습니다</p>
<p class="text-gray-500 text-sm">선택한 테넌트에 등록된 사용자가 없습니다.</p>
</div>
@else
<div class="flex flex-wrap items-center gap-3">
<span class="text-sm font-medium text-gray-700">사용자 선택:</span>
@foreach($users as $user)
<button
type="button"
class="user-button px-4 py-2 text-sm font-medium rounded-lg border transition-colors bg-white text-gray-700 border-gray-300 hover:bg-gray-50 inline-flex items-center gap-2"
data-user-id="{{ $user->id }}"
data-user-name="{{ $user->name }}"
data-user-login="{{ $user->user_id }}"
data-auto-select="{{ $loop->first ? 'true' : 'false' }}"
hx-get="/api/admin/user-permissions/matrix"
hx-target="#permission-matrix"
hx-include="[name='guard_name']"
hx-vals='{"user_id": {{ $user->id }}}'
onclick="selectUser(this)"
>
{{ $user->name }}
<span class="px-1.5 py-0.5 text-xs bg-gray-200 text-gray-600 rounded">&nbsp;{{ $user->user_id }}&nbsp;</span>
</button>
@endforeach
</div>
@endif
@endif
</div>
</div>
<!-- 액션 버튼 -->
<div class="bg-white rounded-lg shadow-sm mb-6" id="action-buttons" style="display: none;">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-700" id="selected-user-name">선택된 사용자</span>
<div class="flex items-center gap-2">
<input type="hidden" name="user_id" id="userIdInput" value="">
<!-- Guard 선택 -->
<span class="text-sm font-medium text-gray-700">Guard:</span>
<select
id="guardNameSelect"
name="guard_name"
class="px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-primary focus:border-primary"
onchange="reloadPermissions()"
>
<option value="api" selected>API</option>
<option value="web">Web</option>
</select>
<!-- 구분선 -->
<div class="h-8 w-px bg-gray-300 mx-1"></div>
<button
type="button"
class="px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500"
hx-post="/api/admin/user-permissions/allow-all"
hx-target="#permission-matrix"
hx-include="[name='user_id'],[name='guard_name']"
>
전체 허용
</button>
<button
type="button"
class="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500"
hx-post="/api/admin/user-permissions/deny-all"
hx-target="#permission-matrix"
hx-include="[name='user_id'],[name='guard_name']"
>
전체 거부
</button>
<button
type="button"
class="px-4 py-2 bg-gray-500 text-white text-sm font-medium rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400"
hx-post="/api/admin/user-permissions/reset"
hx-target="#permission-matrix"
hx-include="[name='user_id'],[name='guard_name']"
title="모든 메뉴의 조회(view) 권한만 허용"
>
초기화
</button>
</div>
</div>
</div>
</div>
<!-- 권한 매트릭스 테이블 -->
<div id="permission-matrix" class="bg-white rounded-lg shadow-sm">
@include('user-permissions.partials.empty-state')
</div>
<script>
function selectUser(button) {
// 모든 버튼의 활성 상태 제거
document.querySelectorAll('.user-button').forEach(btn => {
btn.classList.remove('bg-blue-700', 'text-white', 'border-blue-700', 'hover:bg-blue-800');
btn.classList.add('bg-white', 'text-gray-700', 'border-gray-300', 'hover:bg-gray-50');
// 내부 뱃지 스타일 복원
const badge = btn.querySelector('span');
if (badge) {
badge.classList.remove('bg-blue-500', 'text-white');
badge.classList.add('bg-gray-200', 'text-gray-600');
}
});
// 클릭된 버튼 활성화
button.classList.remove('bg-white', 'text-gray-700', 'border-gray-300', 'hover:bg-gray-50');
button.classList.add('bg-blue-700', 'text-white', 'border-blue-700', 'hover:bg-blue-800');
// 내부 뱃지 스타일 변경
const activeBadge = button.querySelector('span');
if (activeBadge) {
activeBadge.classList.remove('bg-gray-200', 'text-gray-600');
activeBadge.classList.add('bg-blue-500', 'text-white');
}
// 사용자 정보 저장
const userId = button.getAttribute('data-user-id');
const userName = button.getAttribute('data-user-name');
const userLogin = button.getAttribute('data-user-login');
document.getElementById('userIdInput').value = userId;
document.getElementById('selected-user-name').innerHTML = `${userName} <span class="px-3 py-1 text-sm text-blue-600 border border-blue-400 rounded ml-2">&nbsp;${userLogin}&nbsp;</span>`;
// 액션 버튼 표시
document.getElementById('action-buttons').style.display = 'block';
}
// Guard 변경 시 권한 매트릭스 새로고침
function reloadPermissions() {
const selectedButton = document.querySelector('.user-button.bg-blue-700');
if (selectedButton) {
htmx.trigger(selectedButton, 'click');
}
}
// 페이지 로드 시 첫 번째 사용자 자동 선택 (특정 테넌트 선택 시에만)
document.addEventListener('DOMContentLoaded', function() {
const autoSelectButton = document.querySelector('.user-button[data-auto-select="true"]');
if (autoSelectButton) {
// onclick 핸들러 실행
selectUser(autoSelectButton);
// HTMX 이벤트 트리거
htmx.trigger(autoSelectButton, 'click');
}
});
</script>
@endsection