feat: 테넌트 정보 모달 팝업 기능 추가

- 테넌트 row 클릭 시 모달 팝업 표시
- 컨텍스트 메뉴 (우클릭) 지원
- 탭 구조: 구독정보, 사용자, 부서, 역할, 메뉴
- 메뉴 탭 트리 구조 접기/펼치기 기능
- 삭제된 테넌트 경고 배너 (삭제일, 삭제자 표시)
- 복원 버튼으로 즉시 복원 및 모달 새로고침
- 액션 버튼 (수정/삭제) 클릭 시 모달 미표시
This commit is contained in:
2025-11-27 19:11:32 +09:00
parent ff943ab728
commit b32f6cfcf0
17 changed files with 1476 additions and 4 deletions

View File

@@ -0,0 +1,52 @@
{{-- 컨텍스트 메뉴 (우클릭 메뉴) --}}
<div id="context-menu"
class="hidden fixed z-50 bg-white rounded-lg shadow-lg border border-gray-200 py-1 min-w-[160px]"
style="left: 0; top: 0;">
{{-- 테넌트 관련 메뉴 --}}
<button type="button"
data-menu-for="tenant"
onclick="handleContextMenuAction('view-tenant')"
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 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="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
테넌트 조회
</button>
<button type="button"
data-menu-for="tenant"
onclick="handleContextMenuAction('edit-tenant')"
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
테넌트 수정
</button>
{{-- 구분선 --}}
<div data-menu-for="tenant" class="border-t border-gray-100 my-1"></div>
{{-- 사용자 관련 메뉴 --}}
<button type="button"
data-menu-for="user"
onclick="handleContextMenuAction('view-user')"
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 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="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
사용자 조회
</button>
<button type="button"
data-menu-for="user"
onclick="handleContextMenuAction('edit-user')"
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
사용자 수정
</button>
</div>

View File

@@ -0,0 +1,39 @@
{{-- 테넌트 정보 모달 --}}
<div id="tenant-modal"
class="hidden fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="tenant-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-5xl 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="tenant-modal-title" class="text-xl font-semibold text-gray-800">
테넌트 정보
</h2>
<button type="button"
onclick="TenantModal.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="tenant-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>

View File

@@ -26,6 +26,12 @@
</div>
</div>
<!-- 전역 컨텍스트 메뉴 -->
@include('components.context-menu')
<!-- 테넌트 정보 모달 -->
@include('components.tenant-modal')
<!-- HTMX CSRF 토큰 설정 -->
<script>
document.body.addEventListener('htmx:configRequest', (event) => {
@@ -34,6 +40,8 @@
</script>
<script src="{{ asset('js/pagination.js') }}"></script>
<script src="{{ asset('js/context-menu.js') }}"></script>
<script src="{{ asset('js/tenant-modal.js') }}"></script>
@stack('scripts')
</body>
</html>

View File

@@ -109,6 +109,10 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}
}).then(() => {
htmx.trigger('#tenant-table', 'filterSubmit');
// 모달이 열려있으면 내용 새로고침
if (TenantModal.isOpen() && TenantModal.currentTenantId === id) {
TenantModal.loadTenantInfo();
}
});
}
};

View File

@@ -0,0 +1,74 @@
{{-- 테넌트 모달 - 부서 --}}
<div class="space-y-4">
{{-- 헤더 --}}
<div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-700">부서 목록</h3>
<span class="text-xs text-gray-500"> {{ $departments->count() }}</span>
</div>
@if($departments->count() > 0)
{{-- 부서 트리 구조 --}}
<div class="overflow-hidden border border-gray-200 rounded-lg">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">부서명</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">코드</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">상위 부서</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">소속 인원</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">상태</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach($departments as $department)
<tr class="hover:bg-gray-50">
<td class="px-4 py-2 whitespace-nowrap">
<div class="flex items-center">
@if($department->depth > 0)
<span class="text-gray-300 mr-1">
@for($i = 0; $i < $department->depth; $i++)
&nbsp;&nbsp;
@endfor
</span>
@endif
<span class="text-sm font-medium text-gray-900">
{{ $department->name }}
</span>
</div>
</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $department->code ?? '-' }}
</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $department->parent?->name ?? '-' }}
</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $department->users_count ?? 0 }}
</td>
<td class="px-4 py-2 whitespace-nowrap">
@if($department->is_active ?? true)
<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>
@endforeach
</tbody>
</table>
</div>
@else
{{-- 상태 --}}
<div class="text-center py-8 text-gray-500">
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<p>등록된 부서가 없습니다.</p>
</div>
@endif
</div>

View File

@@ -0,0 +1,159 @@
{{-- 테넌트 모달 - 기본 정보 + 구조 --}}
<div class="p-6">
{{-- 삭제된 테넌트 경고 배너 --}}
@if($tenant->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">
삭제일: {{ $tenant->deleted_at->format('Y-m-d H:i') }}
@if($tenant->deletedByUser)
· 삭제자: {{ $tenant->deletedByUser->name }}
@endif
</p>
</div>
</div>
<button type="button"
onclick="event.stopPropagation(); confirmRestore({{ $tenant->id }}, '{{ $tenant->company_name }}')"
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">
<div class="w-20 h-20 bg-blue-100 rounded-lg flex items-center justify-center">
<svg class="w-10 h-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
</div>
{{-- 우측: 정보 테이블 --}}
<div class="flex-1">
<table class="w-full text-sm">
<tbody>
<tr>
<td class="py-1 pr-4 text-gray-500 w-24">회사명</td>
<td class="py-1 font-medium text-gray-900">{{ $tenant->company_name }}</td>
<td class="py-1 pr-4 text-gray-500 w-24">코드</td>
<td class="py-1 font-medium text-gray-900">{{ $tenant->code }}</td>
<td class="py-1 pr-4 text-gray-500 w-24">유형</td>
<td class="py-1">
@php
$typeLabels = ['STD' => '일반', 'TPL' => '템플릿', 'HQ' => '본사'];
$typeColors = [
'STD' => 'bg-gray-100 text-gray-800',
'TPL' => 'bg-purple-100 text-purple-800',
'HQ' => 'bg-blue-100 text-blue-800'
];
$type = $tenant->tenant_type ?? 'STD';
@endphp
<span class="px-2 py-0.5 text-xs font-semibold rounded-full {{ $typeColors[$type] ?? 'bg-gray-100 text-gray-800' }}">
{{ $typeLabels[$type] ?? $type }}
</span>
</td>
</tr>
<tr>
<td class="py-1 pr-4 text-gray-500">대표자</td>
<td class="py-1 text-gray-900">{{ $tenant->ceo_name ?? '-' }}</td>
<td class="py-1 pr-4 text-gray-500">사업자번호</td>
<td class="py-1 text-gray-900">{{ $tenant->business_num ?? '-' }}</td>
<td class="py-1 pr-4 text-gray-500">상태</td>
<td class="py-1">
<span class="px-2 py-0.5 text-xs font-semibold rounded-full
{{ $tenant->status_badge_color === 'success' ? 'bg-green-100 text-green-800' : '' }}
{{ $tenant->status_badge_color === 'warning' ? 'bg-yellow-100 text-yellow-800' : '' }}
{{ $tenant->status_badge_color === 'error' ? 'bg-red-100 text-red-800' : '' }}">
{{ $tenant->status_label }}
</span>
</td>
</tr>
<tr>
<td class="py-1 pr-4 text-gray-500">이메일</td>
<td class="py-1 text-gray-900">{{ $tenant->email ?? '-' }}</td>
<td class="py-1 pr-4 text-gray-500">전화번호</td>
<td class="py-1 text-gray-900">{{ $tenant->phone ?? '-' }}</td>
<td class="py-1 pr-4 text-gray-500">등록일</td>
<td class="py-1 text-gray-900">{{ $tenant->created_at?->format('Y-m-d') ?? '-' }}</td>
</tr>
<tr>
<td class="py-1 pr-4 text-gray-500">주소</td>
<td colspan="5" class="py-1 text-gray-900">{{ $tenant->address ?? '-' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{{-- 메뉴 --}}
<div class="border-b border-gray-200 mb-4">
<nav class="flex -mb-px gap-1">
<button type="button"
data-tab-btn="users"
onclick="TenantModal.switchTab('users')"
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
사용자 <span class="ml-1 px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">{{ $tenant->users_count ?? 0 }}</span>
</button>
<button type="button"
data-tab-btn="departments"
onclick="TenantModal.switchTab('departments')"
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
부서 <span class="ml-1 px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">{{ $tenant->departments_count ?? 0 }}</span>
</button>
<button type="button"
data-tab-btn="roles"
onclick="TenantModal.switchTab('roles')"
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
역할 <span class="ml-1 px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">{{ $tenant->roles_count ?? 0 }}</span>
</button>
<button type="button"
data-tab-btn="menus"
onclick="TenantModal.switchTab('menus')"
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
메뉴 <span class="ml-1 px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">{{ $tenant->menus_count ?? 0 }}</span>
</button>
<button type="button"
data-tab-btn="subscription"
onclick="TenantModal.switchTab('subscription')"
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
구독 정보
</button>
</nav>
</div>
{{-- 콘텐츠 --}}
<div id="tenant-modal-tab-content" class="min-h-[200px]">
<div class="text-center py-12 text-gray-500">
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p> 탭을 선택하여 상세 정보를 확인하세요.</p>
</div>
</div>
{{-- 하단 버튼 --}}
<div class="flex justify-end gap-2 mt-6 pt-4 border-t border-gray-200">
<button type="button"
onclick="TenantModal.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>
<button type="button"
onclick="TenantModal.goToEdit()"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">
수정
</button>
</div>
</div>

View File

@@ -0,0 +1,108 @@
{{-- 테넌트 모달 - 메뉴 --}}
<div class="space-y-4">
{{-- 헤더 --}}
<div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-700">메뉴 목록</h3>
<span class="text-xs text-gray-500"> {{ $menus->count() }}</span>
</div>
@if($menus->count() > 0)
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<table class="w-full">
<thead class="bg-gray-50 border-b">
<tr>
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase">ID</th>
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase">메뉴명</th>
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase">URL</th>
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-700 uppercase">정렬</th>
<th class="px-4 py-2 text-center text-xs font-semibold text-gray-700 uppercase">활성</th>
<th class="px-4 py-2 text-center text-xs font-semibold text-gray-700 uppercase">숨김</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach($menus as $menu)
<tr class="hover:bg-gray-50 modal-menu-row"
data-menu-id="{{ $menu->id }}"
data-parent-id="{{ $menu->parent_id ?? '' }}"
data-depth="{{ $menu->depth ?? 0 }}">
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
{{ $menu->id }}
</td>
<td class="px-4 py-2 whitespace-nowrap">
<div class="flex items-center gap-2" style="padding-left: {{ (($menu->depth ?? 0) * 1.5) }}rem;">
{{-- 트리 구조 표시 --}}
@if(($menu->depth ?? 0) > 0)
<span class="text-gray-300 text-xs font-mono flex-shrink-0">└─</span>
@endif
{{-- 폴더/아이템 아이콘 --}}
@if($menu->has_children ?? false)
<button type="button"
onclick="toggleModalMenuChildren({{ $menu->id }})"
class="modal-menu-toggle flex items-center text-blue-500 hover:text-blue-700 focus:outline-none"
data-menu-id="{{ $menu->id }}">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<svg class="w-3 h-3 ml-0.5 transform transition-transform chevron-icon" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
@else
<svg class="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
@endif
{{-- 메뉴 정보 --}}
<div class="flex items-center gap-2">
<span class="text-sm {{ ($menu->depth ?? 0) === 0 ? 'font-semibold text-gray-900' : 'font-medium text-gray-700' }}">
{{ $menu->name }}
</span>
@if($menu->is_external ?? false)
<span class="text-xs text-blue-600">(외부)</span>
@endif
</div>
</div>
</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
@if(($menu->is_external ?? false) && $menu->external_url)
<a href="{{ $menu->external_url }}" target="_blank" class="text-blue-600 hover:underline">
{{ Str::limit($menu->external_url, 25) }}
</a>
@elseif($menu->url)
{{ Str::limit($menu->url, 25) }}
@else
-
@endif
</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $menu->sort_order ?? 0 }}
</td>
<td class="px-4 py-2 whitespace-nowrap text-center">
{{-- 활성 상태 표시 (액션 없음) --}}
<span class="relative inline-flex h-4 w-8 items-center rounded-full {{ $menu->is_active ? 'bg-blue-500' : 'bg-gray-300' }}">
<span class="inline-block h-3 w-3 transform rounded-full bg-white shadow-sm {{ $menu->is_active ? 'translate-x-4' : 'translate-x-0.5' }}"></span>
</span>
</td>
<td class="px-4 py-2 whitespace-nowrap text-center">
{{-- 숨김 상태 표시 (액션 없음) --}}
<span class="relative inline-flex h-4 w-8 items-center rounded-full {{ $menu->hidden ? 'bg-amber-500' : 'bg-gray-300' }}">
<span class="inline-block h-3 w-3 transform rounded-full bg-white shadow-sm {{ $menu->hidden ? 'translate-x-4' : 'translate-x-0.5' }}"></span>
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
{{-- 상태 --}}
<div class="text-center py-8 text-gray-500">
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<p>등록된 메뉴가 없습니다.</p>
</div>
@endif
</div>

View File

@@ -0,0 +1,77 @@
{{-- 테넌트 모달 - 역할 --}}
<div class="space-y-4">
{{-- 헤더 --}}
<div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-700">역할 목록</h3>
<span class="text-xs text-gray-500"> {{ $roles->count() }}</span>
</div>
@if($roles->count() > 0)
{{-- 역할 카드 그리드 --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@foreach($roles as $role)
<div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50">
<div class="flex items-start justify-between">
<div class="flex-1">
<h4 class="text-sm font-medium text-gray-900">{{ $role->name }}</h4>
@if($role->description)
<p class="mt-1 text-xs text-gray-500">{{ $role->description }}</p>
@endif
</div>
<div class="ml-4">
@if($role->is_system ?? false)
<span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">
시스템
</span>
@else
<span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
사용자 정의
</span>
@endif
</div>
</div>
{{-- 역할 통계 --}}
<div class="mt-3 flex items-center gap-4 text-xs text-gray-500">
<span class="flex items-center gap-1">
<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="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
{{ $role->users_count ?? 0 }}
</span>
<span class="flex items-center gap-1">
<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="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
{{ $role->permissions_count ?? 0 }} 권한
</span>
</div>
{{-- 주요 권한 미리보기 --}}
@if(isset($role->permissions) && $role->permissions->count() > 0)
<div class="mt-3 flex flex-wrap gap-1">
@foreach($role->permissions->take(3) as $permission)
<span class="px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">
{{ $permission->name }}
</span>
@endforeach
@if($role->permissions->count() > 3)
<span class="px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">
+{{ $role->permissions->count() - 3 }}
</span>
@endif
</div>
@endif
</div>
@endforeach
</div>
@else
{{-- 상태 --}}
<div class="text-center py-8 text-gray-500">
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<p>등록된 역할이 없습니다.</p>
</div>
@endif
</div>

View File

@@ -0,0 +1,135 @@
{{-- 테넌트 모달 - 구독 정보 --}}
<div class="space-y-4">
{{-- 헤더 --}}
<div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-700">구독 정보</h3>
</div>
{{-- 구독 상태 카드 --}}
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-4 border border-blue-100">
<div class="flex items-center justify-between">
<div>
<span class="text-sm text-gray-500">현재 플랜</span>
<h4 class="text-xl font-bold text-gray-900 mt-1">
{{ $subscription->plan_name ?? '기본 플랜' }}
</h4>
</div>
<div class="text-right">
@php
$statusColors = [
'active' => 'bg-green-100 text-green-800',
'trial' => 'bg-blue-100 text-blue-800',
'expired' => 'bg-red-100 text-red-800',
'cancelled' => 'bg-gray-100 text-gray-800',
];
$statusLabels = [
'active' => '활성',
'trial' => '체험판',
'expired' => '만료',
'cancelled' => '취소됨',
];
$status = $subscription->status ?? 'active';
@endphp
<span class="px-3 py-1 text-sm font-semibold rounded-full {{ $statusColors[$status] ?? 'bg-gray-100 text-gray-800' }}">
{{ $statusLabels[$status] ?? $status }}
</span>
</div>
</div>
</div>
{{-- 구독 상세 정보 --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{{-- 기간 정보 --}}
<div class="border border-gray-200 rounded-lg p-4">
<h5 class="text-xs font-medium text-gray-500 uppercase mb-3">기간 정보</h5>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-500">시작일</span>
<span class="text-gray-900">{{ $subscription->started_at ?? $tenant->created_at?->format('Y-m-d') ?? '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-500">만료일</span>
<span class="text-gray-900">{{ $subscription->expires_at ?? '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-500">갱신 예정일</span>
<span class="text-gray-900">{{ $subscription->next_billing_date ?? '-' }}</span>
</div>
</div>
</div>
{{-- 사용량 정보 --}}
<div class="border border-gray-200 rounded-lg p-4">
<h5 class="text-xs font-medium text-gray-500 uppercase mb-3">사용량</h5>
<div class="space-y-3">
{{-- 사용자 --}}
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-500">사용자</span>
<span class="text-gray-900">
{{ $usage->users_count ?? $tenant->users_count ?? 0 }} / {{ $subscription->max_users ?? '무제한' }}
</span>
</div>
@if(isset($subscription->max_users) && $subscription->max_users > 0)
@php
$usersPercent = min(100, (($usage->users_count ?? $tenant->users_count ?? 0) / $subscription->max_users) * 100);
@endphp
<div class="w-full bg-gray-200 rounded-full h-1.5">
<div class="bg-blue-600 h-1.5 rounded-full" style="width: {{ $usersPercent }}%"></div>
</div>
@endif
</div>
{{-- 저장 용량 --}}
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-500">저장 용량</span>
<span class="text-gray-900">
{{ $usage->storage_used ?? '0 MB' }} / {{ $subscription->max_storage ?? '무제한' }}
</span>
</div>
@if(isset($subscription->max_storage_mb) && $subscription->max_storage_mb > 0)
@php
$storagePercent = min(100, (($usage->storage_used_mb ?? 0) / $subscription->max_storage_mb) * 100);
@endphp
<div class="w-full bg-gray-200 rounded-full h-1.5">
<div class="bg-green-600 h-1.5 rounded-full" style="width: {{ $storagePercent }}%"></div>
</div>
@endif
</div>
</div>
</div>
</div>
{{-- 포함된 기능 --}}
<div class="border border-gray-200 rounded-lg p-4">
<h5 class="text-xs font-medium text-gray-500 uppercase mb-3">포함된 기능</h5>
<div class="grid grid-cols-2 md:grid-cols-3 gap-2">
@php
$features = $subscription->features ?? [
['name' => '기본 기능', 'enabled' => true],
['name' => '사용자 관리', 'enabled' => true],
['name' => '부서 관리', 'enabled' => true],
['name' => '역할 관리', 'enabled' => true],
['name' => 'API 접근', 'enabled' => $subscription->has_api_access ?? false],
['name' => '고급 보고서', 'enabled' => $subscription->has_advanced_reports ?? false],
];
@endphp
@foreach($features as $feature)
<div class="flex items-center gap-2 text-sm">
@if($feature['enabled'] ?? false)
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<span class="text-gray-900">{{ $feature['name'] }}</span>
@else
<svg class="w-4 h-4 text-gray-300" 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>
<span class="text-gray-400">{{ $feature['name'] }}</span>
@endif
</div>
@endforeach
</div>
</div>
</div>

View File

@@ -0,0 +1,86 @@
{{-- 테넌트 모달 - 사용자 --}}
<div class="space-y-4">
{{-- 헤더 --}}
<div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-700">소속 사용자 목록</h3>
<span class="text-xs text-gray-500"> {{ $users->count() }}</span>
</div>
@if($users->count() > 0)
{{-- 사용자 테이블 --}}
<div class="overflow-hidden border border-gray-200 rounded-lg">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">이름</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">이메일</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">부서</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">역할</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">상태</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">가입일</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach($users as $user)
<tr class="hover:bg-gray-50">
<td class="px-4 py-2 whitespace-nowrap">
<span 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 }}">
{{ $user->name }}
</span>
</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $user->email }}
</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $user->department?->name ?? '-' }}
</td>
<td class="px-4 py-2 whitespace-nowrap">
@if($user->roles && $user->roles->count() > 0)
<div class="flex flex-wrap gap-1">
@foreach($user->roles->take(2) as $role)
<span class="px-1.5 py-0.5 text-xs bg-blue-100 text-blue-800 rounded">
{{ $role->name }}
</span>
@endforeach
@if($user->roles->count() > 2)
<span class="px-1.5 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">
+{{ $user->roles->count() - 2 }}
</span>
@endif
</div>
@else
<span class="text-xs text-gray-400">-</span>
@endif
</td>
<td class="px-4 py-2 whitespace-nowrap">
@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>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $user->created_at?->format('Y-m-d') ?? '-' }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
{{-- 상태 --}}
<div class="text-center py-8 text-gray-500">
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<p>등록된 사용자가 없습니다.</p>
</div>
@endif
</div>

View File

@@ -6,6 +6,7 @@
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">회사명</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">코드</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">상태</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">유형</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">이메일</th>
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">전화번호</th>
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider">사용자</th>
@@ -18,12 +19,19 @@
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($tenants as $tenant)
<tr class="{{ $tenant->deleted_at ? 'bg-gray-100' : '' }}">
<tr class="{{ $tenant->deleted_at ? 'bg-gray-100' : '' }} hover:bg-gray-50 cursor-pointer"
onclick="TenantModal.open({{ $tenant->id }})"
data-tenant-id="{{ $tenant->id }}">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ $tenant->id }}
</td>
<td class="px-3 py-2 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">{{ $tenant->company_name }}</div>
<div class="text-sm font-medium text-gray-900 cursor-pointer hover:text-blue-600"
data-context-menu="tenant"
data-entity-id="{{ $tenant->id }}"
data-entity-name="{{ $tenant->company_name }}">
{{ $tenant->company_name }}
</div>
@if($tenant->ceo_name)
<div class="text-sm text-gray-500">대표: {{ $tenant->ceo_name }}</div>
@endif
@@ -39,6 +47,20 @@
{{ $tenant->status_label }}
</span>
</td>
<td class="px-3 py-2 whitespace-nowrap text-center">
@php
$typeLabels = ['STD' => '일반', 'TPL' => '템플릿', 'HQ' => '본사'];
$typeColors = [
'STD' => 'bg-gray-100 text-gray-800',
'TPL' => 'bg-purple-100 text-purple-800',
'HQ' => 'bg-blue-100 text-blue-800'
];
$type = $tenant->tenant_type ?? 'STD';
@endphp
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $typeColors[$type] ?? 'bg-gray-100 text-gray-800' }}">
{{ $typeLabels[$type] ?? $type }}
</span>
</td>
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $tenant->email ?? '-' }}
</td>
@@ -60,7 +82,7 @@
<td class="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
{{ $tenant->created_at?->format('Y-m-d') ?? '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" onclick="event.stopPropagation()">
@if($tenant->deleted_at)
<!-- 삭제된 항목 -->
<button onclick="confirmRestore({{ $tenant->id }}, '{{ $tenant->company_name }}')"
@@ -76,6 +98,7 @@ class="text-red-600 hover:text-red-900">
@else
<!-- 활성 항목 -->
<a href="{{ route('tenants.edit', $tenant->id) }}"
onclick="event.stopPropagation()"
class="text-blue-600 hover:text-blue-900 mr-3">
수정
</a>
@@ -88,7 +111,7 @@ class="text-red-600 hover:text-red-900">
</tr>
@empty
<tr>
<td colspan="12" class="px-6 py-12 text-center text-gray-500">
<td colspan="13" class="px-6 py-12 text-center text-gray-500">
등록된 테넌트가 없습니다.
</td>
</tr>