diff --git a/app/Http/Controllers/Api/Admin/TenantController.php b/app/Http/Controllers/Api/Admin/TenantController.php index 30b058d3..9d94f1ab 100644 --- a/app/Http/Controllers/Api/Admin/TenantController.php +++ b/app/Http/Controllers/Api/Admin/TenantController.php @@ -205,4 +205,111 @@ public function stats(Request $request): JsonResponse 'data' => $stats, ]); } + + /** + * 테넌트 모달 정보 조회 + */ + public function modal(Request $request, int $id): JsonResponse + { + $tenant = $this->tenantService->getTenantForModal($id); + + if (! $tenant) { + return response()->json([ + 'success' => false, + 'message' => '테넌트를 찾을 수 없습니다.', + ], 404); + } + + $html = view('tenants.partials.modal-info', compact('tenant'))->render(); + + return response()->json([ + 'success' => true, + 'html' => $html, + ]); + } + + /** + * 테넌트 사용자 탭 데이터 + */ + public function users(Request $request, int $id): JsonResponse + { + $users = $this->tenantService->getTenantUsers($id); + + $html = view('tenants.partials.modal-users', compact('users'))->render(); + + return response()->json([ + 'success' => true, + 'html' => $html, + ]); + } + + /** + * 테넌트 부서 탭 데이터 + */ + public function departments(Request $request, int $id): JsonResponse + { + $departments = $this->tenantService->getTenantDepartments($id); + + $html = view('tenants.partials.modal-departments', compact('departments'))->render(); + + return response()->json([ + 'success' => true, + 'html' => $html, + ]); + } + + /** + * 테넌트 역할 탭 데이터 + */ + public function roles(Request $request, int $id): JsonResponse + { + $roles = $this->tenantService->getTenantRoles($id); + + $html = view('tenants.partials.modal-roles', compact('roles'))->render(); + + return response()->json([ + 'success' => true, + 'html' => $html, + ]); + } + + /** + * 테넌트 메뉴 탭 데이터 + */ + public function menus(Request $request, int $id): JsonResponse + { + $menus = $this->tenantService->getTenantMenus($id); + + $html = view('tenants.partials.modal-menus', compact('menus'))->render(); + + return response()->json([ + 'success' => true, + 'html' => $html, + ]); + } + + /** + * 테넌트 구독 정보 탭 데이터 + */ + public function subscription(Request $request, int $id): JsonResponse + { + $tenant = $this->tenantService->getTenantForModal($id); + + if (! $tenant) { + return response()->json([ + 'success' => false, + 'message' => '테넌트를 찾을 수 없습니다.', + ], 404); + } + + $subscription = $this->tenantService->getTenantSubscription($id); + $usage = $this->tenantService->getTenantUsage($id); + + $html = view('tenants.partials.modal-subscription', compact('tenant', 'subscription', 'usage'))->render(); + + return response()->json([ + 'success' => true, + 'html' => $html, + ]); + } } diff --git a/app/Models/Tenants/Tenant.php b/app/Models/Tenants/Tenant.php index adf823c5..db04a72a 100644 --- a/app/Models/Tenants/Tenant.php +++ b/app/Models/Tenants/Tenant.php @@ -29,6 +29,8 @@ class Tenant extends Model 'fax', // 구독 정보 'tenant_st_code', + 'tenant_type', + 'template_id', 'billing_tp_code', 'max_users', 'trial_ends_at', @@ -36,6 +38,8 @@ class Tenant extends Model 'last_paid_at', // 관리 메모 'admin_memo', + // 삭제 정보 + 'deleted_by', ]; protected $casts = [ @@ -88,6 +92,14 @@ public function roles(): HasMany return $this->hasMany(\App\Models\Permissions\Role::class, 'tenant_id'); } + /** + * 관계: 삭제한 사용자 + */ + public function deletedByUser(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(User::class, 'deleted_by'); + } + /** * 상태 배지 색상 (Blade 뷰에서 사용) */ diff --git a/app/Services/TenantService.php b/app/Services/TenantService.php index 6650b56e..d67f8505 100644 --- a/app/Services/TenantService.php +++ b/app/Services/TenantService.php @@ -5,6 +5,7 @@ use App\Models\Tenants\Tenant; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Facades\DB; class TenantService { @@ -88,6 +89,10 @@ public function deleteTenant(int $id): bool { $tenant = Tenant::findOrFail($id); + // 삭제자 기록 + $tenant->deleted_by = auth()->id(); + $tenant->save(); + return $tenant->delete(); } @@ -98,6 +103,9 @@ public function restoreTenant(int $id): bool { $tenant = Tenant::onlyTrashed()->findOrFail($id); + // 삭제 정보 초기화 + $tenant->deleted_by = null; + return $tenant->restore(); } @@ -156,4 +164,195 @@ public function getTenantStats(): array 'trashed' => Tenant::onlyTrashed()->count(), ]; } + + /** + * 모달용 테넌트 상세 정보 조회 + */ + public function getTenantForModal(int $id): ?Tenant + { + return Tenant::query() + ->with('deletedByUser') + ->withCount(['users', 'departments', 'menus', 'roles']) + ->withTrashed() + ->find($id); + } + + /** + * 테넌트 소속 사용자 목록 조회 + */ + public function getTenantUsers(int $tenantId): Collection + { + $tenant = Tenant::find($tenantId); + if (! $tenant) { + return collect(); + } + + // 사용자와 함께 역할/부서 정보를 가져옴 + return $tenant->users() + ->with(['userRoles' => function ($query) use ($tenantId) { + $query->where('tenant_id', $tenantId)->with('role'); + }, 'departmentUsers' => function ($query) use ($tenantId) { + $query->where('tenant_id', $tenantId)->with('department'); + }]) + ->orderBy('name') + ->get() + ->map(function ($user) { + // 역할과 부서를 플랫하게 매핑 + $user->roles = $user->userRoles->pluck('role')->filter(); + $user->department = $user->departmentUsers->first()?->department; + + return $user; + }); + } + + /** + * 테넌트 부서 목록 조회 (계층 구조) + */ + public function getTenantDepartments(int $tenantId): Collection + { + // Department 모델의 users() 관계가 잘못된 네임스페이스를 참조하므로 직접 카운트 + $departments = \App\Models\Department::query() + ->where('tenant_id', $tenantId) + ->orderBy('sort_order') + ->orderBy('name') + ->get(); + + // 부서별 사용자 수 직접 카운트 + $userCounts = DB::table('department_user') + ->whereIn('department_id', $departments->pluck('id')) + ->selectRaw('department_id, count(*) as cnt') + ->groupBy('department_id') + ->pluck('cnt', 'department_id'); + + return $departments->map(function ($dept) use ($userCounts, $departments) { + // 사용자 수 설정 + $dept->users_count = $userCounts[$dept->id] ?? 0; + + // 계층 깊이 계산 + $depth = 0; + $parentId = $dept->parent_id; + while ($parentId) { + $depth++; + $parent = $departments->firstWhere('id', $parentId); + $parentId = $parent?->parent_id; + } + $dept->depth = $depth; + + // 상위 부서 이름 + $dept->parent = $departments->firstWhere('id', $dept->parent_id); + + return $dept; + }); + } + + /** + * 테넌트 역할 목록 조회 + */ + public function getTenantRoles(int $tenantId): Collection + { + // Role 모델의 userRoles() 관계가 잘못된 네임스페이스를 참조하므로 직접 카운트 + $roles = \App\Models\Role::query() + ->where('tenant_id', $tenantId) + ->withCount('permissions') + ->with('permissions') + ->orderBy('name') + ->get(); + + // 역할별 사용자 수 직접 카운트 + $userCounts = DB::table('user_roles') + ->whereIn('role_id', $roles->pluck('id')) + ->selectRaw('role_id, count(*) as cnt') + ->groupBy('role_id') + ->pluck('cnt', 'role_id'); + + return $roles->map(function ($role) use ($userCounts) { + $role->users_count = $userCounts[$role->id] ?? 0; + + return $role; + }); + } + + /** + * 테넌트 메뉴 목록 조회 (계층 구조 - 트리 정렬) + */ + public function getTenantMenus(int $tenantId): \Illuminate\Support\Collection + { + $menus = \App\Models\Commons\Menu::query() + ->where('tenant_id', $tenantId) + ->orderBy('sort_order') + ->orderBy('id') + ->get(); + + // 트리 구조로 정렬 후 플랫한 배열로 변환 + return $this->flattenMenuTree($menus); + } + + /** + * 트리 구조를 플랫한 배열로 변환 (depth 정보 포함) + */ + private function flattenMenuTree(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; + } + + /** + * 테넌트 구독 정보 조회 + */ + public function getTenantSubscription(int $tenantId): object + { + $tenant = Tenant::find($tenantId); + + // 기본 구독 정보 (실제 구현 시 별도 테이블에서 조회) + return (object) [ + 'plan_name' => $tenant?->subscription_plan ?? '기본 플랜', + 'status' => $tenant?->subscription_status ?? 'active', + 'started_at' => $tenant?->created_at?->format('Y-m-d'), + 'expires_at' => $tenant?->subscription_expires_at ?? null, + 'next_billing_date' => null, + 'max_users' => $tenant?->max_users ?? null, + 'max_storage' => $tenant?->max_storage ?? null, + 'max_storage_mb' => null, + 'has_api_access' => true, + 'has_advanced_reports' => false, + 'features' => [ + ['name' => '기본 기능', 'enabled' => true], + ['name' => '사용자 관리', 'enabled' => true], + ['name' => '부서 관리', 'enabled' => true], + ['name' => '역할 관리', 'enabled' => true], + ['name' => 'API 접근', 'enabled' => true], + ['name' => '고급 보고서', 'enabled' => false], + ], + ]; + } + + /** + * 테넌트 사용량 정보 조회 + */ + public function getTenantUsage(int $tenantId): object + { + $tenant = Tenant::withCount('users')->find($tenantId); + + return (object) [ + 'users_count' => $tenant?->users_count ?? 0, + 'storage_used' => '0 MB', + 'storage_used_mb' => 0, + ]; + } } diff --git a/public/js/context-menu.js b/public/js/context-menu.js new file mode 100644 index 00000000..5cf82676 --- /dev/null +++ b/public/js/context-menu.js @@ -0,0 +1,139 @@ +/** + * 컨텍스트 메뉴 (우클릭 메뉴) + * 테넌트명, 사용자명 등에서 우클릭 시 해당 위치에 메뉴 표시 + */ + +class ContextMenu { + constructor() { + this.menuElement = null; + this.currentTarget = null; + this.init(); + } + + init() { + // 컨텍스트 메뉴 요소 찾기 + this.menuElement = document.getElementById('context-menu'); + if (!this.menuElement) return; + + // 우클릭 이벤트 등록 (이벤트 위임) + document.addEventListener('contextmenu', (e) => this.handleContextMenu(e)); + + // 클릭 시 메뉴 닫기 + document.addEventListener('click', () => this.hide()); + + // ESC 키로 메뉴 닫기 + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') this.hide(); + }); + + // 스크롤 시 메뉴 닫기 + document.addEventListener('scroll', () => this.hide(), true); + } + + handleContextMenu(e) { + const trigger = e.target.closest('[data-context-menu]'); + if (!trigger) return; + + e.preventDefault(); + + const menuType = trigger.dataset.contextMenu; + const entityId = trigger.dataset.entityId; + const entityName = trigger.dataset.entityName; + + this.currentTarget = { + type: menuType, + id: entityId, + name: entityName, + element: trigger + }; + + this.show(e.clientX, e.clientY, menuType); + } + + show(x, y, menuType) { + if (!this.menuElement) return; + + // 메뉴 타입에 따라 항목 표시/숨김 + this.updateMenuItems(menuType); + + // 메뉴 표시 + this.menuElement.classList.remove('hidden'); + + // 위치 조정 (화면 밖으로 나가지 않도록) + const menuRect = this.menuElement.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + let posX = x; + let posY = y; + + if (x + menuRect.width > viewportWidth) { + posX = viewportWidth - menuRect.width - 10; + } + + if (y + menuRect.height > viewportHeight) { + posY = viewportHeight - menuRect.height - 10; + } + + this.menuElement.style.left = `${posX}px`; + this.menuElement.style.top = `${posY}px`; + } + + hide() { + if (this.menuElement) { + this.menuElement.classList.add('hidden'); + } + this.currentTarget = null; + } + + updateMenuItems(menuType) { + // 모든 메뉴 항목 숨기기 + const allItems = this.menuElement.querySelectorAll('[data-menu-for]'); + allItems.forEach(item => { + const forTypes = item.dataset.menuFor.split(','); + if (forTypes.includes(menuType) || forTypes.includes('all')) { + item.classList.remove('hidden'); + } else { + item.classList.add('hidden'); + } + }); + } + + // 메뉴 항목 클릭 핸들러 + handleMenuAction(action) { + if (!this.currentTarget) return; + + const { type, id, name } = this.currentTarget; + + switch (action) { + case 'view-tenant': + if (typeof TenantModal !== 'undefined') { + TenantModal.open(id); + } + break; + case 'edit-tenant': + window.location.href = `/tenants/${id}/edit`; + break; + case 'view-user': + if (typeof UserModal !== 'undefined') { + UserModal.open(id); + } + break; + case 'edit-user': + window.location.href = `/users/${id}/edit`; + break; + default: + console.log('Unknown action:', action, { type, id, name }); + } + + this.hide(); + } +} + +// 전역 인스턴스 생성 +const contextMenu = new ContextMenu(); + +// 전역 함수로 노출 (onclick에서 사용) +function handleContextMenuAction(action) { + contextMenu.handleMenuAction(action); +} \ No newline at end of file diff --git a/public/js/tenant-modal.js b/public/js/tenant-modal.js new file mode 100644 index 00000000..8cabbaee --- /dev/null +++ b/public/js/tenant-modal.js @@ -0,0 +1,242 @@ +/** + * 테넌트 정보 모달 + * 테넌트 상세 정보를 팝업으로 표시하고 탭으로 관련 정보 관리 + */ + +const TenantModal = { + modalElement: null, + currentTenantId: null, + currentTab: 'info', + + init() { + this.modalElement = document.getElementById('tenant-modal'); + if (!this.modalElement) return; + + // ESC 키로 모달 닫기 + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && this.isOpen()) { + this.close(); + } + }); + + // 배경 클릭 시 모달 닫기 + this.modalElement.addEventListener('click', (e) => { + if (e.target === this.modalElement) { + this.close(); + } + }); + }, + + isOpen() { + return this.modalElement && !this.modalElement.classList.contains('hidden'); + }, + + async open(tenantId) { + if (!this.modalElement) { + console.error('Tenant modal element not found'); + return; + } + + this.currentTenantId = tenantId; + this.currentTab = 'subscription'; + + // 모달 표시 + this.modalElement.classList.remove('hidden'); + document.body.style.overflow = 'hidden'; + + // 로딩 표시 + this.showLoading(); + + // 테넌트 정보 로드 + await this.loadTenantInfo(); + }, + + close() { + if (this.modalElement) { + this.modalElement.classList.add('hidden'); + document.body.style.overflow = ''; + } + this.currentTenantId = null; + }, + + showLoading() { + const content = document.getElementById('tenant-modal-content'); + if (content) { + content.innerHTML = ` +
+
+
+ `; + } + }, + + async loadTenantInfo() { + try { + const response = await fetch(`/api/admin/tenants/${this.currentTenantId}/modal`, { + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'), + 'Accept': 'application/json' + } + }); + + const data = await response.json(); + + if (data.html) { + const content = document.getElementById('tenant-modal-content'); + if (content) { + content.innerHTML = data.html; + } + // 구독정보 탭 자동 로드 + this.currentTab = ''; // switchTab에서 early return 방지 + await this.switchTab('subscription'); + } + } catch (error) { + console.error('Failed to load tenant info:', error); + this.showError('테넌트 정보를 불러오는데 실패했습니다.'); + } + }, + + async switchTab(tab) { + if (this.currentTab === tab) return; + + this.currentTab = tab; + + // 탭 버튼 스타일 업데이트 + document.querySelectorAll('#tenant-modal [data-tab-btn]').forEach(btn => { + if (btn.dataset.tabBtn === tab) { + btn.classList.add('border-blue-500', 'text-blue-600'); + btn.classList.remove('border-transparent', 'text-gray-500'); + } else { + btn.classList.remove('border-blue-500', 'text-blue-600'); + btn.classList.add('border-transparent', 'text-gray-500'); + } + }); + + // 탭 콘텐츠 로드 + const tabContent = document.getElementById('tenant-modal-tab-content'); + if (!tabContent) return; + + // 로딩 표시 + tabContent.innerHTML = ` +
+
+
+ `; + + try { + const response = await fetch(`/api/admin/tenants/${this.currentTenantId}/${tab}`, { + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'), + 'Accept': 'application/json' + } + }); + + const data = await response.json(); + + if (data.html) { + tabContent.innerHTML = data.html; + } + } catch (error) { + console.error(`Failed to load ${tab} tab:`, error); + tabContent.innerHTML = ` +
+ 데이터를 불러오는데 실패했습니다. +
+ `; + } + }, + + showError(message) { + const content = document.getElementById('tenant-modal-content'); + if (content) { + content.innerHTML = ` +
+ + + +

${message}

+ +
+ `; + } + }, + + // 수정 페이지로 이동 + goToEdit() { + if (this.currentTenantId) { + window.location.href = `/tenants/${this.currentTenantId}/edit`; + } + } +}; + +/** + * 모달 내 메뉴 자식 토글 + * @param {number} menuId 부모 메뉴 ID + */ +window.toggleModalMenuChildren = function(menuId) { + const rows = document.querySelectorAll('.modal-menu-row'); + const toggleBtn = document.querySelector(`.modal-menu-toggle[data-menu-id="${menuId}"]`); + const chevron = toggleBtn?.querySelector('.chevron-icon'); + + // 직접 자식들 찾기 + const directChildren = []; + rows.forEach(row => { + if (row.dataset.parentId == menuId) { + directChildren.push(row); + } + }); + + if (directChildren.length === 0) return; + + // 첫 번째 자식의 현재 상태 확인 + const isHidden = directChildren[0].classList.contains('hidden'); + + if (isHidden) { + // 펼치기: 직접 자식만 표시 + directChildren.forEach(child => { + child.classList.remove('hidden'); + }); + // 쉐브론 회전 초기화 + if (chevron) { + chevron.classList.remove('-rotate-90'); + } + } else { + // 접기: 모든 자손 숨기기 (재귀적으로) + window.hideModalMenuDescendants(menuId, rows); + // 쉐브론 회전 + if (chevron) { + chevron.classList.add('-rotate-90'); + } + } +}; + +/** + * 특정 메뉴의 모든 자손 숨기기 (재귀) + * @param {number} parentId 부모 메뉴 ID + * @param {NodeList} rows 모든 메뉴 행 + */ +window.hideModalMenuDescendants = function(parentId, rows) { + rows.forEach(row => { + if (row.dataset.parentId == parentId) { + row.classList.add('hidden'); + // 자식의 쉐브론도 접힌 상태로 + const childToggle = row.querySelector('.modal-menu-toggle'); + const childChevron = childToggle?.querySelector('.chevron-icon'); + if (childChevron) { + childChevron.classList.add('-rotate-90'); + } + // 손자들도 재귀적으로 숨기기 + const childMenuId = row.dataset.menuId; + if (childMenuId) { + window.hideModalMenuDescendants(childMenuId, rows); + } + } + }); +}; + +// DOM 로드 시 초기화 +document.addEventListener('DOMContentLoaded', () => { + TenantModal.init(); +}); \ No newline at end of file diff --git a/resources/views/components/context-menu.blade.php b/resources/views/components/context-menu.blade.php new file mode 100644 index 00000000..e5c757e4 --- /dev/null +++ b/resources/views/components/context-menu.blade.php @@ -0,0 +1,52 @@ +{{-- 컨텍스트 메뉴 (우클릭 메뉴) --}} + \ No newline at end of file diff --git a/resources/views/components/tenant-modal.blade.php b/resources/views/components/tenant-modal.blade.php new file mode 100644 index 00000000..864c11fe --- /dev/null +++ b/resources/views/components/tenant-modal.blade.php @@ -0,0 +1,39 @@ +{{-- 테넌트 정보 모달 --}} + \ No newline at end of file diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 6675daba..f17dd927 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -26,6 +26,12 @@ + + @include('components.context-menu') + + + @include('components.tenant-modal') + + + @stack('scripts') diff --git a/resources/views/tenants/index.blade.php b/resources/views/tenants/index.blade.php index b2cbb552..0e3eaf4c 100644 --- a/resources/views/tenants/index.blade.php +++ b/resources/views/tenants/index.blade.php @@ -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(); + } }); } }; diff --git a/resources/views/tenants/partials/modal-departments.blade.php b/resources/views/tenants/partials/modal-departments.blade.php new file mode 100644 index 00000000..4ad13b8a --- /dev/null +++ b/resources/views/tenants/partials/modal-departments.blade.php @@ -0,0 +1,74 @@ +{{-- 테넌트 모달 - 부서 탭 --}} +
+ {{-- 헤더 --}} +
+

부서 목록

+ 총 {{ $departments->count() }}개 +
+ + @if($departments->count() > 0) + {{-- 부서 트리 구조 --}} +
+ + + + + + + + + + + + @foreach($departments as $department) + + + + + + + + @endforeach + +
부서명코드상위 부서소속 인원상태
+
+ @if($department->depth > 0) + + @for($i = 0; $i < $department->depth; $i++) +    + @endfor + └ + + @endif + + {{ $department->name }} + +
+
+ {{ $department->code ?? '-' }} + + {{ $department->parent?->name ?? '-' }} + + {{ $department->users_count ?? 0 }}명 + + @if($department->is_active ?? true) + + 활성 + + @else + + 비활성 + + @endif +
+
+ @else + {{-- 빈 상태 --}} +
+ + + +

등록된 부서가 없습니다.

+
+ @endif +
\ No newline at end of file diff --git a/resources/views/tenants/partials/modal-info.blade.php b/resources/views/tenants/partials/modal-info.blade.php new file mode 100644 index 00000000..2ba09421 --- /dev/null +++ b/resources/views/tenants/partials/modal-info.blade.php @@ -0,0 +1,159 @@ +{{-- 테넌트 모달 - 기본 정보 + 탭 구조 --}} +
+ {{-- 삭제된 테넌트 경고 배너 --}} + @if($tenant->deleted_at) +
+
+
+ + + +
+

이 테넌트는 삭제되었습니다.

+

+ 삭제일: {{ $tenant->deleted_at->format('Y-m-d H:i') }} + @if($tenant->deletedByUser) + · 삭제자: {{ $tenant->deletedByUser->name }} + @endif +

+
+
+ +
+
+ @endif + + {{-- 상단: 기본 정보 카드 --}} +
+
+ {{-- 좌측: 아이콘 --}} +
+
+ + + +
+
+ + {{-- 우측: 정보 테이블 --}} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
회사명{{ $tenant->company_name }}코드{{ $tenant->code }}유형 + @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 + + {{ $typeLabels[$type] ?? $type }} + +
대표자{{ $tenant->ceo_name ?? '-' }}사업자번호{{ $tenant->business_num ?? '-' }}상태 + + {{ $tenant->status_label }} + +
이메일{{ $tenant->email ?? '-' }}전화번호{{ $tenant->phone ?? '-' }}등록일{{ $tenant->created_at?->format('Y-m-d') ?? '-' }}
주소{{ $tenant->address ?? '-' }}
+
+
+
+ + {{-- 탭 메뉴 --}} +
+ +
+ + {{-- 탭 콘텐츠 --}} +
+
+ + + +

위 탭을 선택하여 상세 정보를 확인하세요.

+
+
+ + {{-- 하단 버튼 --}} +
+ + +
+
\ No newline at end of file diff --git a/resources/views/tenants/partials/modal-menus.blade.php b/resources/views/tenants/partials/modal-menus.blade.php new file mode 100644 index 00000000..f335ede5 --- /dev/null +++ b/resources/views/tenants/partials/modal-menus.blade.php @@ -0,0 +1,108 @@ +{{-- 테넌트 모달 - 메뉴 탭 --}} +
+ {{-- 헤더 --}} +
+

메뉴 목록

+ 총 {{ $menus->count() }}개 +
+ + @if($menus->count() > 0) +
+ + + + + + + + + + + + + @foreach($menus as $menu) + + + + + + + + + @endforeach + +
ID메뉴명URL정렬활성숨김
+
+ @else + {{-- 빈 상태 --}} +
+ + + +

등록된 메뉴가 없습니다.

+
+ @endif +
diff --git a/resources/views/tenants/partials/modal-roles.blade.php b/resources/views/tenants/partials/modal-roles.blade.php new file mode 100644 index 00000000..3bc3ead7 --- /dev/null +++ b/resources/views/tenants/partials/modal-roles.blade.php @@ -0,0 +1,77 @@ +{{-- 테넌트 모달 - 역할 탭 --}} +
+ {{-- 헤더 --}} +
+

역할 목록

+ 총 {{ $roles->count() }}개 +
+ + @if($roles->count() > 0) + {{-- 역할 카드 그리드 --}} +
+ @foreach($roles as $role) +
+
+
+

{{ $role->name }}

+ @if($role->description) +

{{ $role->description }}

+ @endif +
+
+ @if($role->is_system ?? false) + + 시스템 + + @else + + 사용자 정의 + + @endif +
+
+ + {{-- 역할 통계 --}} +
+ + + + + {{ $role->users_count ?? 0 }}명 + + + + + + {{ $role->permissions_count ?? 0 }}개 권한 + +
+ + {{-- 주요 권한 미리보기 --}} + @if(isset($role->permissions) && $role->permissions->count() > 0) +
+ @foreach($role->permissions->take(3) as $permission) + + {{ $permission->name }} + + @endforeach + @if($role->permissions->count() > 3) + + +{{ $role->permissions->count() - 3 }} + + @endif +
+ @endif +
+ @endforeach +
+ @else + {{-- 빈 상태 --}} +
+ + + +

등록된 역할이 없습니다.

+
+ @endif +
\ No newline at end of file diff --git a/resources/views/tenants/partials/modal-subscription.blade.php b/resources/views/tenants/partials/modal-subscription.blade.php new file mode 100644 index 00000000..9ffa64a6 --- /dev/null +++ b/resources/views/tenants/partials/modal-subscription.blade.php @@ -0,0 +1,135 @@ +{{-- 테넌트 모달 - 구독 정보 탭 --}} +
+ {{-- 헤더 --}} +
+

구독 정보

+
+ + {{-- 구독 상태 카드 --}} +
+
+
+ 현재 플랜 +

+ {{ $subscription->plan_name ?? '기본 플랜' }} +

+
+
+ @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 + + {{ $statusLabels[$status] ?? $status }} + +
+
+
+ + {{-- 구독 상세 정보 --}} +
+ {{-- 기간 정보 --}} +
+
기간 정보
+
+
+ 시작일 + {{ $subscription->started_at ?? $tenant->created_at?->format('Y-m-d') ?? '-' }} +
+
+ 만료일 + {{ $subscription->expires_at ?? '-' }} +
+
+ 갱신 예정일 + {{ $subscription->next_billing_date ?? '-' }} +
+
+
+ + {{-- 사용량 정보 --}} +
+
사용량
+
+ {{-- 사용자 수 --}} +
+
+ 사용자 + + {{ $usage->users_count ?? $tenant->users_count ?? 0 }} / {{ $subscription->max_users ?? '무제한' }} + +
+ @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 +
+
+
+ @endif +
+ + {{-- 저장 용량 --}} +
+
+ 저장 용량 + + {{ $usage->storage_used ?? '0 MB' }} / {{ $subscription->max_storage ?? '무제한' }} + +
+ @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 +
+
+
+ @endif +
+
+
+
+ + {{-- 포함된 기능 --}} +
+
포함된 기능
+
+ @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) +
+ @if($feature['enabled'] ?? false) + + + + {{ $feature['name'] }} + @else + + + + {{ $feature['name'] }} + @endif +
+ @endforeach +
+
+
diff --git a/resources/views/tenants/partials/modal-users.blade.php b/resources/views/tenants/partials/modal-users.blade.php new file mode 100644 index 00000000..976636cb --- /dev/null +++ b/resources/views/tenants/partials/modal-users.blade.php @@ -0,0 +1,86 @@ +{{-- 테넌트 모달 - 사용자 탭 --}} +
+ {{-- 헤더 --}} +
+

소속 사용자 목록

+ 총 {{ $users->count() }}명 +
+ + @if($users->count() > 0) + {{-- 사용자 테이블 --}} +
+ + + + + + + + + + + + + @foreach($users as $user) + + + + + + + + + @endforeach + +
이름이메일부서역할상태가입일
+ + {{ $user->name }} + + + {{ $user->email }} + + {{ $user->department?->name ?? '-' }} + + @if($user->roles && $user->roles->count() > 0) +
+ @foreach($user->roles->take(2) as $role) + + {{ $role->name }} + + @endforeach + @if($user->roles->count() > 2) + + +{{ $user->roles->count() - 2 }} + + @endif +
+ @else + - + @endif +
+ @if($user->is_active) + + 활성 + + @else + + 비활성 + + @endif + + {{ $user->created_at?->format('Y-m-d') ?? '-' }} +
+
+ @else + {{-- 빈 상태 --}} +
+ + + +

등록된 사용자가 없습니다.

+
+ @endif +
\ No newline at end of file diff --git a/resources/views/tenants/partials/table.blade.php b/resources/views/tenants/partials/table.blade.php index 2ae7d674..de1cfb8d 100644 --- a/resources/views/tenants/partials/table.blade.php +++ b/resources/views/tenants/partials/table.blade.php @@ -6,6 +6,7 @@ 회사명 코드 상태 + 유형 이메일 전화번호 사용자 @@ -18,12 +19,19 @@ @forelse($tenants as $tenant) - + {{ $tenant->id }} -
{{ $tenant->company_name }}
+
+ {{ $tenant->company_name }} +
@if($tenant->ceo_name)
대표: {{ $tenant->ceo_name }}
@endif @@ -39,6 +47,20 @@ {{ $tenant->status_label }} + + @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 + + {{ $typeLabels[$type] ?? $type }} + + {{ $tenant->email ?? '-' }} @@ -60,7 +82,7 @@ {{ $tenant->created_at?->format('Y-m-d') ?? '-' }} - + @if($tenant->deleted_at)