feat(user-modal): 사용자 정보 모달 및 컨텍스트 메뉴 확장
사용자 모달 기능: - 사용자 정보 모달 팝업 (조회/삭제/수정) - 권한 요약 정보 (Web/API 권한 카운트) - 2x2 그리드 레이아웃 (테넌트, 역할, 부서, 권한) - 테이블 행 클릭으로 모달 열기 - 권한 관리 링크 클릭 시 해당 사용자 자동 선택 컨텍스트 메뉴 확장: - permission-analyze 페이지 사용자 이름에 컨텍스트 메뉴 - user-permissions 페이지 사용자 버튼에 컨텍스트 메뉴 - 사용자 모달 내 테넌트 칩에 컨텍스트 메뉴 - 헤더 테넌트 배지에 컨텍스트 메뉴 - 테넌트 메뉴에 "이 테넌트로 전환" 기능 추가
This commit is contained in:
@@ -25,6 +25,16 @@ class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex i
|
||||
테넌트 수정
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
data-menu-for="tenant"
|
||||
onclick="handleContextMenuAction('switch-tenant')"
|
||||
class="w-full px-4 py-2 text-left text-sm text-green-700 hover:bg-green-50 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</svg>
|
||||
이 테넌트로 전환
|
||||
</button>
|
||||
|
||||
{{-- 구분선 --}}
|
||||
<div data-menu-for="tenant" class="border-t border-gray-100 my-1"></div>
|
||||
|
||||
@@ -49,4 +59,17 @@ class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex i
|
||||
</svg>
|
||||
사용자 수정
|
||||
</button>
|
||||
|
||||
{{-- 구분선 --}}
|
||||
<div data-menu-for="user" class="border-t border-gray-100 my-1"></div>
|
||||
|
||||
<button type="button"
|
||||
data-menu-for="user"
|
||||
onclick="handleContextMenuAction('delete-user')"
|
||||
class="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
사용자 삭제
|
||||
</button>
|
||||
</div>
|
||||
39
resources/views/components/user-modal.blade.php
Normal file
39
resources/views/components/user-modal.blade.php
Normal file
@@ -0,0 +1,39 @@
|
||||
{{-- 사용자 정보 모달 --}}
|
||||
<div id="user-modal"
|
||||
class="hidden fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-labelledby="user-modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true">
|
||||
|
||||
{{-- 배경 오버레이 --}}
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
|
||||
|
||||
{{-- 모달 컨테이너 --}}
|
||||
<div class="flex min-h-full items-start justify-center p-4 pt-16">
|
||||
<div class="relative bg-white rounded-lg shadow-xl w-full max-w-3xl max-h-[85vh] flex flex-col">
|
||||
|
||||
{{-- 모달 헤더 --}}
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 flex-shrink-0">
|
||||
<h2 id="user-modal-title" class="text-xl font-semibold text-gray-800">
|
||||
사용자 정보
|
||||
</h2>
|
||||
<button type="button"
|
||||
onclick="UserModal.close()"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- 모달 콘텐츠 --}}
|
||||
<div id="user-modal-content" class="flex-1 overflow-y-auto">
|
||||
{{-- 콘텐츠는 JS로 로드됨 --}}
|
||||
<div class="flex items-center justify-center h-64">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,8 +31,7 @@ class="department-button px-4 py-2 text-sm font-medium rounded-lg border transit
|
||||
data-auto-select="{{ $loop->first ? 'true' : 'false' }}"
|
||||
hx-get="/api/admin/department-permissions/matrix"
|
||||
hx-target="#permission-matrix"
|
||||
hx-include="[name='guard_name']"
|
||||
hx-vals='{"department_id": {{ $department->id }}}'
|
||||
hx-vals='{"department_id": {{ $department->id }}, "guard_name": "api"}'
|
||||
onclick="selectDepartment(this)"
|
||||
>
|
||||
{{ $department->name }}
|
||||
@@ -50,21 +49,7 @@ class="department-button px-4 py-2 text-sm font-medium rounded-lg border transit
|
||||
<span class="text-sm font-medium text-gray-700" id="selected-department-name">선택된 부서</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="hidden" name="department_id" id="departmentIdInput" 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>
|
||||
<input type="hidden" name="guard_name" value="api">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -129,14 +114,6 @@ function selectDepartment(button) {
|
||||
document.getElementById('action-buttons').style.display = 'block';
|
||||
}
|
||||
|
||||
// Guard 변경 시 권한 매트릭스 새로고침
|
||||
function reloadPermissions() {
|
||||
const selectedButton = document.querySelector('.department-button.bg-blue-700');
|
||||
if (selectedButton) {
|
||||
htmx.trigger(selectedButton, 'click');
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 로드 시 첫 번째 부서 자동 선택 (특정 테넌트 선택 시에만)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const autoSelectButton = document.querySelector('.department-button[data-auto-select="true"]');
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
<!-- 테넌트 정보 모달 -->
|
||||
@include('components.tenant-modal')
|
||||
|
||||
<!-- 사용자 정보 모달 -->
|
||||
@include('components.user-modal')
|
||||
|
||||
<!-- HTMX CSRF 토큰 설정 -->
|
||||
<script>
|
||||
document.body.addEventListener('htmx:configRequest', (event) => {
|
||||
@@ -42,6 +45,7 @@
|
||||
<script src="{{ asset('js/pagination.js') }}"></script>
|
||||
<script src="{{ asset('js/context-menu.js') }}"></script>
|
||||
<script src="{{ asset('js/tenant-modal.js') }}"></script>
|
||||
<script src="{{ asset('js/user-modal.js') }}"></script>
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -41,7 +41,11 @@ class="border-gray-300 rounded-lg text-sm focus:ring-primary focus:border-primar
|
||||
$currentTenant = $globalTenants->firstWhere('id', session('selected_tenant_id'));
|
||||
@endphp
|
||||
@if($currentTenant)
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-primary/10 text-primary">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-primary/10 text-primary cursor-pointer hover:bg-primary/20"
|
||||
data-context-menu="tenant"
|
||||
data-entity-id="{{ $currentTenant->id }}"
|
||||
data-entity-name="{{ $currentTenant->company_name }}"
|
||||
title="우클릭하여 메뉴 열기">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
||||
@@ -68,7 +68,7 @@ class="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring
|
||||
<!-- 2단 레이아웃 (고정 높이, 좌우 분할) -->
|
||||
<div class="flex gap-6" style="height: calc(100vh - 220px);">
|
||||
<!-- 좌측: 메뉴 트리 (고정 너비) -->
|
||||
<div class="w-80 flex-shrink-0">
|
||||
<div class="flex-shrink-0" style="width: 320px;">
|
||||
<div class="bg-white rounded-lg shadow-sm h-full flex flex-col">
|
||||
<div class="px-4 py-3 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-3">메뉴 트리</h2>
|
||||
|
||||
@@ -48,7 +48,11 @@
|
||||
@foreach($analysis['access_allowed'] as $user)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3">
|
||||
<div class="font-medium text-gray-900">{{ $user['name'] }}</div>
|
||||
<div class="font-medium text-gray-900 cursor-pointer hover:text-blue-600"
|
||||
data-context-menu="user"
|
||||
data-entity-id="{{ $user['id'] }}"
|
||||
data-entity-name="{{ $user['name'] }}"
|
||||
title="우클릭하여 메뉴 열기">{{ $user['name'] }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $user['email'] }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
@@ -136,7 +140,11 @@
|
||||
@foreach($analysis['explicit_deny'] as $user)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3">
|
||||
<div class="font-medium text-gray-900">{{ $user['name'] }}</div>
|
||||
<div class="font-medium text-gray-900 cursor-pointer hover:text-blue-600"
|
||||
data-context-menu="user"
|
||||
data-entity-id="{{ $user['id'] }}"
|
||||
data-entity-name="{{ $user['name'] }}"
|
||||
title="우클릭하여 메뉴 열기">{{ $user['name'] }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $user['email'] }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
@foreach($trace['by_role'] as $item)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-2">
|
||||
<div class="font-medium text-gray-900">{{ $item['user_name'] }}</div>
|
||||
<div class="font-medium text-gray-900 cursor-pointer hover:text-blue-600"
|
||||
data-context-menu="user"
|
||||
data-entity-id="{{ $item['user_id'] }}"
|
||||
data-entity-name="{{ $item['user_name'] }}"
|
||||
title="우클릭하여 메뉴 열기">{{ $item['user_name'] }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $item['email'] }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
@@ -62,7 +66,11 @@
|
||||
@foreach($trace['by_department'] as $item)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-2">
|
||||
<div class="font-medium text-gray-900">{{ $item['user_name'] }}</div>
|
||||
<div class="font-medium text-gray-900 cursor-pointer hover:text-blue-600"
|
||||
data-context-menu="user"
|
||||
data-entity-id="{{ $item['user_id'] }}"
|
||||
data-entity-name="{{ $item['user_name'] }}"
|
||||
title="우클릭하여 메뉴 열기">{{ $item['user_name'] }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $item['email'] }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
@@ -106,7 +114,11 @@
|
||||
@foreach($trace['by_personal'] as $item)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-2">
|
||||
<div class="font-medium text-gray-900">{{ $item['user_name'] }}</div>
|
||||
<div class="font-medium text-gray-900 cursor-pointer hover:text-blue-600"
|
||||
data-context-menu="user"
|
||||
data-entity-id="{{ $item['user_id'] }}"
|
||||
data-entity-name="{{ $item['user_name'] }}"
|
||||
title="우클릭하여 메뉴 열기">{{ $item['user_name'] }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<div class="text-gray-500">{{ $item['email'] }}</div>
|
||||
@@ -144,7 +156,11 @@
|
||||
@foreach($trace['denied_users'] as $item)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-2">
|
||||
<div class="font-medium text-gray-900">{{ $item['user_name'] }}</div>
|
||||
<div class="font-medium text-gray-900 cursor-pointer hover:text-blue-600"
|
||||
data-context-menu="user"
|
||||
data-entity-id="{{ $item['user_id'] }}"
|
||||
data-entity-name="{{ $item['user_name'] }}"
|
||||
title="우클릭하여 메뉴 열기">{{ $item['user_name'] }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<div class="text-gray-500">{{ $item['email'] }}</div>
|
||||
|
||||
@@ -28,14 +28,16 @@
|
||||
class="role-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"
|
||||
data-role-id="{{ $role->id }}"
|
||||
data-role-name="{{ $role->name }}"
|
||||
data-guard-name="{{ $role->guard_name }}"
|
||||
data-auto-select="{{ $loop->first ? 'true' : 'false' }}"
|
||||
hx-get="/api/admin/role-permissions/matrix"
|
||||
hx-target="#permission-matrix"
|
||||
hx-include="[name='guard_name']"
|
||||
hx-vals='{"role_id": {{ $role->id }}}'
|
||||
hx-vals='{"role_id": {{ $role->id }}, "guard_name": "{{ $role->guard_name }}"}'
|
||||
onclick="selectRole(this)"
|
||||
>
|
||||
{{ $role->name }}
|
||||
<span>{{ $role->name }}</span>
|
||||
<span class="ml-1 px-1.5 py-0.5 text-xs rounded {{ $role->guard_name === 'web' ? 'bg-blue-100 text-blue-600' : 'bg-green-100 text-green-600' }}">{{ $role->guard_name }}</span>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -50,21 +52,7 @@ class="role-button px-4 py-2 text-sm font-medium rounded-lg border transition-co
|
||||
<span class="text-sm font-medium text-gray-700" id="selected-role-name">선택된 역할</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="hidden" name="role_id" id="roleIdInput" 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="web" selected>Web</option>
|
||||
<option value="api">API</option>
|
||||
</select>
|
||||
|
||||
<!-- 구분선 -->
|
||||
<div class="h-8 w-px bg-gray-300 mx-1"></div>
|
||||
<input type="hidden" name="guard_name" id="guardNameInput" value="">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -119,24 +107,22 @@ function selectRole(button) {
|
||||
// 역할 정보 저장
|
||||
const roleId = button.getAttribute('data-role-id');
|
||||
const roleName = button.getAttribute('data-role-name');
|
||||
const guardName = button.getAttribute('data-guard-name');
|
||||
const tenantName = button.getAttribute('data-tenant-name');
|
||||
|
||||
document.getElementById('roleIdInput').value = roleId;
|
||||
const displayName = tenantName ? `[${tenantName}] ${roleName} 역할` : `${roleName} 역할`;
|
||||
document.getElementById('selected-role-name').textContent = displayName;
|
||||
document.getElementById('guardNameInput').value = guardName;
|
||||
|
||||
const guardBadge = guardName === 'web'
|
||||
? '<span class="ml-1 px-1.5 py-0.5 text-xs rounded bg-blue-100 text-blue-600">web</span>'
|
||||
: '<span class="ml-1 px-1.5 py-0.5 text-xs rounded bg-green-100 text-green-600">api</span>';
|
||||
const displayName = tenantName ? `[${tenantName}] ${roleName}` : `${roleName}`;
|
||||
document.getElementById('selected-role-name').innerHTML = displayName + ' 역할 ' + guardBadge;
|
||||
|
||||
// 액션 버튼 표시
|
||||
document.getElementById('action-buttons').style.display = 'block';
|
||||
}
|
||||
|
||||
// Guard 변경 시 권한 매트릭스 새로고침
|
||||
function reloadPermissions() {
|
||||
const selectedButton = document.querySelector('.role-button.bg-blue-700');
|
||||
if (selectedButton) {
|
||||
htmx.trigger(selectedButton, 'click');
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 로드 시 첫 번째 역할 자동 선택 (특정 테넌트 선택 시에만)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const autoSelectButton = document.querySelector('.role-button[data-auto-select="true"]');
|
||||
|
||||
@@ -107,6 +107,32 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
<option value="expired" {{ old('tenant_st_code', $tenant->tenant_st_code) === 'expired' ? 'selected' : '' }}>만료</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
테넌트 유형 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="flex items-center gap-4 mt-2">
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="radio" name="tenant_type" value="STD"
|
||||
{{ old('tenant_type', $tenant->tenant_type ?? 'STD') === 'STD' ? 'checked' : '' }}
|
||||
class="w-4 h-4 text-gray-600 bg-gray-100 border-gray-300 focus:ring-gray-500">
|
||||
<span class="ml-2 px-2 py-1 text-sm font-medium bg-gray-100 text-gray-700 rounded">일반</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="radio" name="tenant_type" value="TPL"
|
||||
{{ old('tenant_type', $tenant->tenant_type) === 'TPL' ? 'checked' : '' }}
|
||||
class="w-4 h-4 text-purple-600 bg-gray-100 border-gray-300 focus:ring-purple-500">
|
||||
<span class="ml-2 px-2 py-1 text-sm font-medium bg-purple-100 text-purple-700 rounded">템플릿</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="radio" name="tenant_type" value="HQ"
|
||||
{{ old('tenant_type', $tenant->tenant_type) === 'HQ' ? 'checked' : '' }}
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500">
|
||||
<span class="ml-2 px-2 py-1 text-sm font-medium bg-blue-100 text-blue-700 rounded">본사</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">템플릿: 새 테넌트 생성 시 메뉴 복사 원본 / 본사: 모든 테넌트 관리 가능</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">결제 유형</label>
|
||||
<select name="billing_tp_code"
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
@else
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="text-sm font-medium text-gray-700">사용자 선택:</span>
|
||||
@php
|
||||
$autoSelectId = $selectedUserId ?? ($users->first()->id ?? null);
|
||||
@endphp
|
||||
@foreach($users as $user)
|
||||
<button
|
||||
type="button"
|
||||
@@ -36,7 +39,10 @@ class="user-button px-4 py-2 text-sm font-medium rounded-lg border transition-co
|
||||
data-user-id="{{ $user->id }}"
|
||||
data-user-name="{{ $user->name }}"
|
||||
data-user-login="{{ $user->user_id }}"
|
||||
data-auto-select="{{ $loop->first ? 'true' : 'false' }}"
|
||||
data-auto-select="{{ $user->id == $autoSelectId ? 'true' : 'false' }}"
|
||||
data-context-menu="user"
|
||||
data-entity-id="{{ $user->id }}"
|
||||
data-entity-name="{{ $user->name }}"
|
||||
hx-get="/api/admin/user-permissions/matrix"
|
||||
hx-target="#permission-matrix"
|
||||
hx-include="[name='guard_name']"
|
||||
@@ -44,7 +50,17 @@ class="user-button px-4 py-2 text-sm font-medium rounded-lg border transition-co
|
||||
onclick="selectUser(this)"
|
||||
>
|
||||
{{ $user->name }}
|
||||
<span class="px-1.5 py-0.5 text-xs bg-gray-200 text-gray-600 rounded"> {{ $user->user_id }} </span>
|
||||
<span class="user-id-badge px-1.5 py-0.5 text-xs bg-gray-200 text-gray-600 rounded"> {{ $user->user_id }} </span>
|
||||
@if($user->web_permission_count > 0 || $user->api_permission_count > 0)
|
||||
<span class="permission-counts inline-flex items-center gap-1 text-[10px]">
|
||||
@if($user->web_permission_count > 0)
|
||||
<span class="px-1 py-0.5 bg-blue-100 text-blue-600 rounded">web:{{ $user->web_permission_count }}</span>
|
||||
@endif
|
||||
@if($user->api_permission_count > 0)
|
||||
<span class="px-1 py-0.5 bg-green-100 text-green-600 rounded">api:{{ $user->api_permission_count }}</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -61,17 +77,30 @@ class="user-button px-4 py-2 text-sm font-medium rounded-lg border transition-co
|
||||
<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>
|
||||
<!-- Guard 선택 (라디오 버튼) -->
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="guard_name"
|
||||
value="api"
|
||||
checked
|
||||
class="w-4 h-4 text-green-600 bg-gray-100 border-gray-300 focus:ring-green-500"
|
||||
onchange="reloadPermissions()"
|
||||
>
|
||||
<span class="ml-2 px-2 py-1 text-sm font-medium bg-green-100 text-green-700 rounded">API</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="guard_name"
|
||||
value="web"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500"
|
||||
onchange="reloadPermissions()"
|
||||
>
|
||||
<span class="ml-2 px-2 py-1 text-sm font-medium bg-blue-100 text-blue-700 rounded">Web</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 구분선 -->
|
||||
<div class="h-8 w-px bg-gray-300 mx-1"></div>
|
||||
@@ -120,8 +149,8 @@ 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');
|
||||
// user-id 뱃지 스타일 복원
|
||||
const badge = btn.querySelector('.user-id-badge');
|
||||
if (badge) {
|
||||
badge.classList.remove('bg-blue-500', 'text-white');
|
||||
badge.classList.add('bg-gray-200', 'text-gray-600');
|
||||
@@ -131,8 +160,8 @@ function selectUser(button) {
|
||||
// 클릭된 버튼 활성화
|
||||
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');
|
||||
// user-id 뱃지 스타일 변경
|
||||
const activeBadge = button.querySelector('.user-id-badge');
|
||||
if (activeBadge) {
|
||||
activeBadge.classList.remove('bg-gray-200', 'text-gray-600');
|
||||
activeBadge.classList.add('bg-blue-500', 'text-white');
|
||||
|
||||
@@ -101,6 +101,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
{{ in_array($role->id, old('role_ids', [])) ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">{{ $role->name }}</span>
|
||||
<span class="ml-1 px-1.5 py-0.5 text-xs rounded {{ $role->guard_name === 'web' ? 'bg-blue-100 text-blue-600' : 'bg-green-100 text-green-600' }}">{{ $role->guard_name }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@@ -105,6 +105,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
{{ in_array($role->id, old('role_ids', $userRoleIds)) ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">{{ $role->name }}</span>
|
||||
<span class="ml-1 px-1.5 py-0.5 text-xs rounded {{ $role->guard_name === 'web' ? 'bg-blue-100 text-blue-600' : 'bg-green-100 text-green-600' }}">{{ $role->guard_name }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
199
resources/views/users/partials/modal-info.blade.php
Normal file
199
resources/views/users/partials/modal-info.blade.php
Normal file
@@ -0,0 +1,199 @@
|
||||
{{-- 사용자 모달 - 기본 정보 --}}
|
||||
<div class="p-6">
|
||||
{{-- 삭제된 사용자 경고 배너 --}}
|
||||
@if($user->deleted_at)
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-6 h-6 text-red-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-red-800">이 사용자는 삭제되었습니다.</p>
|
||||
<p class="text-xs text-red-600 mt-0.5">
|
||||
삭제일: {{ $user->deleted_at->format('Y-m-d H:i') }}
|
||||
@if($user->deletedByUser)
|
||||
· 삭제자: {{ $user->deletedByUser->name }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
onclick="event.stopPropagation(); UserModal.restoreUser()"
|
||||
class="px-3 py-1.5 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors">
|
||||
복원하기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 상단: 기본 정보 카드 --}}
|
||||
<div class="bg-gray-50 rounded-lg p-4 mb-6">
|
||||
<div class="flex gap-4">
|
||||
{{-- 좌측: 프로필 이미지/아이콘 --}}
|
||||
<div class="flex-shrink-0">
|
||||
@if($user->profile_photo_path)
|
||||
<img src="{{ asset('storage/' . $user->profile_photo_path) }}"
|
||||
alt="{{ $user->name }}"
|
||||
class="w-20 h-20 rounded-full object-cover">
|
||||
@else
|
||||
<div class="w-20 h-20 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-2xl font-bold text-blue-600">
|
||||
{{ strtoupper(substr($user->name, 0, 1)) }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 우측: 정보 테이블 --}}
|
||||
<div class="flex-1">
|
||||
<div class="mb-2">
|
||||
<h3 id="user-modal-name" class="text-xl font-bold text-gray-900">{{ $user->name }}</h3>
|
||||
@if($user->is_super_admin)
|
||||
<span class="text-xs text-red-600 font-semibold">슈퍼 관리자</span>
|
||||
@endif
|
||||
</div>
|
||||
<table class="w-full text-sm">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="py-1 pr-4 text-gray-500 w-24">사용자 ID</td>
|
||||
<td class="py-1 font-medium text-gray-900">{{ $user->user_id ?? '-' }}</td>
|
||||
<td class="py-1 pr-4 text-gray-500 w-24">이메일</td>
|
||||
<td class="py-1 text-gray-900">{{ $user->email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-1 pr-4 text-gray-500">연락처</td>
|
||||
<td class="py-1 text-gray-900">{{ $user->phone ?? '-' }}</td>
|
||||
<td class="py-1 pr-4 text-gray-500">상태</td>
|
||||
<td class="py-1">
|
||||
@if($user->is_active)
|
||||
<span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800">활성</span>
|
||||
@else
|
||||
<span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-gray-100 text-gray-800">비활성</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-1 pr-4 text-gray-500">가입일</td>
|
||||
<td class="py-1 text-gray-900">{{ $user->created_at?->format('Y-m-d') ?? '-' }}</td>
|
||||
<td class="py-1 pr-4 text-gray-500">최근 로그인</td>
|
||||
<td class="py-1 text-gray-900">{{ $user->last_login_at?->format('Y-m-d H:i') ?? '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 2x2 그리드: 테넌트, 역할, 부서, 권한 --}}
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
{{-- 소속 테넌트 --}}
|
||||
<div class="bg-gray-50 rounded-lg p-3">
|
||||
<h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">소속 테넌트</h4>
|
||||
@if($user->tenants && $user->tenants->count() > 0)
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@foreach($user->tenants as $tenant)
|
||||
<span class="px-2 py-1 text-xs rounded cursor-pointer hover:ring-1 hover:ring-blue-400 {{ $tenant->pivot->is_default ? 'bg-blue-100 text-blue-800 font-medium' : 'bg-white text-gray-700 border border-gray-200' }}"
|
||||
data-context-menu="tenant"
|
||||
data-entity-id="{{ $tenant->id }}"
|
||||
data-entity-name="{{ $tenant->company_name }}"
|
||||
title="우클릭하여 메뉴 열기">
|
||||
{{ $tenant->company_name }}
|
||||
@if($tenant->pivot->is_default)
|
||||
<span class="text-[10px]">(기본)</span>
|
||||
@endif
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-xs text-gray-400">소속된 테넌트 없음</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 역할 정보 --}}
|
||||
<div class="bg-gray-50 rounded-lg p-3">
|
||||
<h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">역할</h4>
|
||||
@if($user->userRoles && $user->userRoles->count() > 0)
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@foreach($user->userRoles as $userRole)
|
||||
@if($userRole->role)
|
||||
<span class="px-2 py-1 text-xs rounded {{ $userRole->role->guard_name === 'web' ? 'bg-blue-100 text-blue-700' : 'bg-purple-100 text-purple-700' }}"
|
||||
title="{{ $userRole->role->description }}">
|
||||
{{ $userRole->role->name }}
|
||||
</span>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-xs text-gray-400">할당된 역할 없음</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 부서 정보 --}}
|
||||
<div class="bg-gray-50 rounded-lg p-3">
|
||||
<h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">부서</h4>
|
||||
@if($user->departmentUsers && $user->departmentUsers->count() > 0)
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@foreach($user->departmentUsers as $departmentUser)
|
||||
@if($departmentUser->department)
|
||||
<span class="px-2 py-1 text-xs rounded {{ $departmentUser->is_primary ? 'bg-green-100 text-green-800 font-medium' : 'bg-white text-gray-700 border border-gray-200' }}">
|
||||
{{ $departmentUser->department->name }}
|
||||
@if($departmentUser->is_primary)
|
||||
<span class="text-[10px]">(주)</span>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-xs text-gray-400">소속된 부서 없음</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 권한 정보 --}}
|
||||
<div class="bg-gray-50 rounded-lg p-3">
|
||||
<h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">권한</h4>
|
||||
@if(isset($user->web_permission_count) || isset($user->api_permission_count))
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="px-1.5 py-0.5 text-[10px] font-medium bg-blue-100 text-blue-700 rounded">Web</span>
|
||||
<span class="text-sm font-semibold text-gray-900">{{ $user->web_permission_count ?? 0 }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="px-1.5 py-0.5 text-[10px] font-medium bg-green-100 text-green-700 rounded">API</span>
|
||||
<span class="text-sm font-semibold text-gray-900">{{ $user->api_permission_count ?? 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('user-permissions.index') }}?user_id={{ $user->id }}"
|
||||
class="text-xs text-blue-600 hover:text-blue-800 hover:underline">
|
||||
관리 →
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-xs text-gray-400">테넌트 선택 필요</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 하단 버튼 --}}
|
||||
<div class="flex justify-end gap-2 mt-6 pt-4 border-t border-gray-200">
|
||||
<button type="button"
|
||||
onclick="UserModal.close()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
|
||||
닫기
|
||||
</button>
|
||||
@if(!$user->deleted_at)
|
||||
<button type="button"
|
||||
onclick="UserModal.deleteUser()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700">
|
||||
삭제
|
||||
</button>
|
||||
@endif
|
||||
<button type="button"
|
||||
onclick="UserModal.goToEdit()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">
|
||||
수정
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,20 +5,29 @@
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">ID</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">이름</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">이메일</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">연락처</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">테넌트</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">부서</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">역할</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">상태</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse($users as $user)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<tr class="{{ $user->deleted_at ? 'bg-gray-100' : '' }} hover:bg-gray-50 cursor-pointer"
|
||||
onclick="UserModal.open({{ $user->id }})"
|
||||
data-user-id="{{ $user->id }}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ $user->user_id ?? '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $user->name }}</div>
|
||||
<div class="text-sm font-medium text-gray-900 cursor-pointer hover:text-blue-600"
|
||||
data-context-menu="user"
|
||||
data-entity-id="{{ $user->id }}"
|
||||
data-entity-name="{{ $user->name }}"
|
||||
title="우클릭하여 메뉴 열기"
|
||||
onclick="event.stopPropagation()">
|
||||
{{ $user->name }}
|
||||
</div>
|
||||
@if($user->is_super_admin)
|
||||
<span class="text-xs text-red-600 font-semibold">슈퍼 관리자</span>
|
||||
@endif
|
||||
@@ -26,11 +35,36 @@
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $user->email }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $user->phone ?? '-' }}
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
@if($user->departmentUsers && $user->departmentUsers->count() > 0)
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@foreach($user->departmentUsers as $du)
|
||||
<span class="px-1.5 py-0.5 text-xs rounded {{ $du->is_primary ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600' }}">
|
||||
{{ $du->department?->name ?? '-' }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $user->currentTenant()?->company_name ?? '-' }}
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
@if($user->userRoles && $user->userRoles->count() > 0)
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@foreach($user->userRoles as $ur)
|
||||
<div class="px-2 py-1 rounded {{ $ur->role?->guard_name === 'web' ? 'bg-blue-50 border border-blue-200' : 'bg-purple-50 border border-purple-200' }}">
|
||||
<div class="text-xs font-medium {{ $ur->role?->guard_name === 'web' ? 'text-blue-700' : 'text-purple-700' }}">
|
||||
{{ $ur->role?->name ?? '-' }}
|
||||
</div>
|
||||
@if($ur->role?->description)
|
||||
<div class="text-[10px] text-gray-500 leading-tight">{{ $ur->role->description }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if($user->is_active)
|
||||
@@ -43,7 +77,7 @@
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium" onclick="event.stopPropagation()">
|
||||
@if($user->deleted_at)
|
||||
<!-- 삭제된 항목 -->
|
||||
<button onclick="confirmRestore({{ $user->id }}, '{{ $user->name }}')"
|
||||
@@ -58,7 +92,9 @@ class="text-red-600 hover:text-red-900">
|
||||
@endif
|
||||
@else
|
||||
<!-- 활성 항목 -->
|
||||
<a href="{{ route('users.edit', $user->id) }}" class="text-blue-600 hover:text-blue-900 mr-3">
|
||||
<a href="{{ route('users.edit', $user->id) }}"
|
||||
onclick="event.stopPropagation()"
|
||||
class="text-blue-600 hover:text-blue-900 mr-3">
|
||||
수정
|
||||
</a>
|
||||
<button onclick="confirmDelete({{ $user->id }}, '{{ $user->name }}')" class="text-red-600 hover:text-red-900">
|
||||
|
||||
Reference in New Issue
Block a user