diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 874a6bee..ff8c3f12 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1360,3 +1360,83 @@ ### 코드 품질: - ✅ Pint 포맷팅 통과 --- + +## 2025-11-27 (수) - 테넌트 정보 모달 팝업 기능 구현 + +### 주요 작업 +- 테넌트 행 클릭 시 모달 팝업 오픈 기능 구현 +- 모달 내 JS 함수 window 객체 등록 (동적 HTML 접근 문제 해결) +- 삭제된 테넌트 경고 배너 추가 (삭제일, 삭제자 표시) +- 복원 후 모달 내용 자동 새로고침 + +### 수정된 파일: + +#### JavaScript +- `public/js/tenant-modal.js` + - `toggleModalMenuChildren`, `hideModalMenuDescendants` → `window` 객체에 등록 + - 모달 오픈 시 구독정보 탭 자동 로드 (`switchTab('subscription')`) + - `isOpen()` 메서드 추가 (모달 상태 확인용) + +#### Blade Views +- `resources/views/tenants/partials/table.blade.php` + - 행 클릭 시 모달 오픈: `onclick="TenantModal.open({{ $tenant->id }})"` + - 액션 버튼 우선권: `onclick="event.stopPropagation()"` + +- `resources/views/tenants/partials/modal-info.blade.php` + - 삭제된 테넌트 경고 배너 추가 (빨간색 bg-red-50) + - 삭제일/삭제자 정보 표시 + - 배너 내 복원 버튼 추가 + +- `resources/views/tenants/index.blade.php` + - `confirmRestore()`: 복원 후 모달 내용 새로고침 로직 추가 + +#### Models & Services +- `app/Models/Tenants/Tenant.php` + - `deleted_by` 컬럼 fillable 추가 + - `deletedByUser()` 관계 추가 (삭제자 정보 조회) + +- `app/Services/TenantService.php` + - `deleteTenant()`: deleted_by 기록 + - `restoreTenant()`: deleted_by null 초기화 + - `getTenantForModal()`: deletedByUser 관계 eager loading 추가 + +### 기술 상세: + +**JS 함수 window 등록 (동적 HTML 문제 해결):** +```javascript +// Before: function toggleModalMenuChildren() {} +// After: window.toggleModalMenuChildren = function() {} +``` +- 동적으로 로드된 HTML에서 함수 접근 가능 + +**행 클릭 + 액션 버튼 우선권:** +```html + + + + + +``` +- `event.stopPropagation()`으로 버블링 방지 + +**삭제된 테넌트 경고 배너:** +- 빨간색 배경 (bg-red-50) +- 삭제일 + 삭제자 정보 표시 +- 복원 버튼 포함 + +**복원 후 모달 새로고침:** +```javascript +if (TenantModal.isOpen() && TenantModal.currentTenantId === id) { + TenantModal.loadTenantInfo(); +} +``` + +### 이슈 해결: +- **toggleModalMenuChildren not defined**: window 객체에 함수 등록으로 해결 +- **복원 후 배너 미갱신**: `loadTenantInfo()` 호출 추가 + +### Git 커밋: +- ✅ `b32f6cf` "feat: 테넌트 정보 모달 팝업 기능 추가" + - 17 files changed, 1476 insertions(+), 4 deletions(-) + +--- diff --git a/app/Http/Controllers/Api/Admin/UserController.php b/app/Http/Controllers/Api/Admin/UserController.php index 87350a96..424db1a5 100644 --- a/app/Http/Controllers/Api/Admin/UserController.php +++ b/app/Http/Controllers/Api/Admin/UserController.php @@ -164,6 +164,28 @@ public function restore(Request $request, int $id): JsonResponse ]); } + /** + * 사용자 모달 정보 조회 + */ + public function modal(Request $request, int $id): JsonResponse + { + $user = $this->userService->getUserForModal($id); + + if (! $user) { + return response()->json([ + 'success' => false, + 'message' => '사용자를 찾을 수 없습니다.', + ], 404); + } + + $html = view('users.partials.modal-info', compact('user'))->render(); + + return response()->json([ + 'success' => true, + 'html' => $html, + ]); + } + /** * 사용자 영구 삭제 (슈퍼관리자 전용) */ diff --git a/app/Http/Controllers/UserPermissionController.php b/app/Http/Controllers/UserPermissionController.php index e2b78d4a..b58af1ff 100644 --- a/app/Http/Controllers/UserPermissionController.php +++ b/app/Http/Controllers/UserPermissionController.php @@ -20,6 +20,7 @@ public function __construct(UserPermissionService $userPermissionService) public function index(Request $request) { $tenantId = session('selected_tenant_id'); + $selectedUserId = $request->query('user_id'); // 테넌트 미선택 또는 전체 선택 시 if (! $tenantId || $tenantId === 'all') { @@ -27,6 +28,7 @@ public function index(Request $request) 'users' => collect(), 'requireTenant' => true, 'selectedTenantId' => $tenantId, + 'selectedUserId' => $selectedUserId, ]); } @@ -37,6 +39,7 @@ public function index(Request $request) 'users' => $users, 'requireTenant' => false, 'selectedTenantId' => $tenantId, + 'selectedUserId' => $selectedUserId, ]); } } diff --git a/app/Http/Requests/UpdateTenantRequest.php b/app/Http/Requests/UpdateTenantRequest.php index 06d93f86..053bdff1 100644 --- a/app/Http/Requests/UpdateTenantRequest.php +++ b/app/Http/Requests/UpdateTenantRequest.php @@ -44,6 +44,7 @@ public function rules(): array // 구독 정보 'tenant_st_code' => ['required', 'string', 'in:trial,active,suspended,expired'], + 'tenant_type' => ['required', 'string', 'in:STD,TPL,HQ'], 'billing_tp_code' => ['nullable', 'string', 'in:monthly,yearly,free'], 'max_users' => ['nullable', 'integer', 'min:1'], 'trial_ends_at' => ['nullable', 'date'], @@ -69,6 +70,8 @@ public function messages(): array 'homepage.url' => '올바른 URL 형식이 아닙니다.', 'tenant_st_code.required' => '상태는 필수입니다.', 'tenant_st_code.in' => '올바른 상태를 선택해주세요.', + 'tenant_type.required' => '테넌트 유형은 필수입니다.', + 'tenant_type.in' => '올바른 테넌트 유형을 선택해주세요.', 'billing_tp_code.in' => '올바른 결제 유형을 선택해주세요.', 'max_users.integer' => '최대 사용자 수는 숫자여야 합니다.', 'max_users.min' => '최대 사용자 수는 최소 1명 이상이어야 합니다.', @@ -92,6 +95,7 @@ public function attributes(): array 'homepage' => '홈페이지', 'fax' => '팩스', 'tenant_st_code' => '상태', + 'tenant_type' => '테넌트 유형', 'billing_tp_code' => '결제 유형', 'max_users' => '최대 사용자 수', 'trial_ends_at' => '트라이얼 종료일', diff --git a/app/Models/User.php b/app/Models/User.php index 247dbe30..42b8aee0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -127,4 +127,12 @@ public function getDepartmentsForTenant(int $tenantId) ->get() ->pluck('department'); } + + /** + * 관계: 삭제한 사용자 + */ + public function deletedByUser(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(User::class, 'deleted_by'); + } } diff --git a/app/Services/UserPermissionService.php b/app/Services/UserPermissionService.php index 57e450cb..ccc86331 100644 --- a/app/Services/UserPermissionService.php +++ b/app/Services/UserPermissionService.php @@ -692,19 +692,125 @@ public function hasPermission(int $userId, int $menuId, string $permissionType, } /** - * 테넌트별 사용자 목록 조회 + * 테넌트별 사용자 목록 조회 (권한 개수 포함) * * @param int $tenantId 테넌트 ID * @return \Illuminate\Support\Collection 사용자 목록 */ public function getUsersByTenant(int $tenantId): \Illuminate\Support\Collection { - return User::whereHas('tenants', function ($query) use ($tenantId) { + $users = User::whereHas('tenants', function ($query) use ($tenantId) { $query->where('tenants.id', $tenantId) ->where('user_tenants.is_active', true); }) ->where('is_active', true) ->orderBy('name') ->get(); + + // 각 사용자별 권한 개수 계산 + $now = now(); + foreach ($users as $user) { + $permissionCounts = $this->getUserPermissionCounts($user->id, $tenantId, $now); + $user->web_permission_count = $permissionCounts['web']; + $user->api_permission_count = $permissionCounts['api']; + } + + return $users; + } + + /** + * 사용자별 guard별 권한 개수 조회 (역할 + 부서 + 개인 오버라이드 통합) + * + * @param int $userId 사용자 ID + * @param int $tenantId 테넌트 ID + * @param \Carbon\Carbon $now 현재 시간 + * @return array ['web' => int, 'api' => int] + */ + private function getUserPermissionCounts(int $userId, int $tenantId, $now): array + { + $result = ['web' => 0, 'api' => 0]; + + foreach (['web', 'api'] as $guardName) { + // 1. 역할 권한 + $rolePermissions = DB::table('model_has_roles as mhr') + ->join('role_has_permissions as rhp', 'rhp.role_id', '=', 'mhr.role_id') + ->join('permissions as p', 'p.id', '=', 'rhp.permission_id') + ->where('mhr.model_type', User::class) + ->where('mhr.model_id', $userId) + ->where('p.guard_name', $guardName) + ->where('p.name', 'like', 'menu:%') + ->pluck('p.name') + ->toArray(); + + // 2. 부서 권한 + $deptPermissions = DB::table('department_user as du') + ->join('permission_overrides as po', function ($j) use ($now, $tenantId) { + $j->on('po.model_id', '=', 'du.department_id') + ->where('po.model_type', 'App\\Models\\Tenants\\Department') + ->where('po.tenant_id', $tenantId) + ->whereNull('po.deleted_at') + ->where('po.effect', 1) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now); + }) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now); + }); + }) + ->join('permissions as p', 'p.id', '=', 'po.permission_id') + ->whereNull('du.deleted_at') + ->where('du.user_id', $userId) + ->where('du.tenant_id', $tenantId) + ->where('p.guard_name', $guardName) + ->where('p.name', 'like', 'menu:%') + ->pluck('p.name') + ->toArray(); + + // 3. 개인 오버라이드 (ALLOW) + $personalAllows = DB::table('permission_overrides as po') + ->join('permissions as p', 'p.id', '=', 'po.permission_id') + ->where('po.model_type', User::class) + ->where('po.model_id', $userId) + ->where('po.tenant_id', $tenantId) + ->where('po.effect', 1) + ->whereNull('po.deleted_at') + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now); + }) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now); + }) + ->where('p.guard_name', $guardName) + ->where('p.name', 'like', 'menu:%') + ->pluck('p.name') + ->toArray(); + + // 4. 개인 오버라이드 (DENY) - 제외할 권한 + $personalDenies = DB::table('permission_overrides as po') + ->join('permissions as p', 'p.id', '=', 'po.permission_id') + ->where('po.model_type', User::class) + ->where('po.model_id', $userId) + ->where('po.tenant_id', $tenantId) + ->where('po.effect', 0) + ->whereNull('po.deleted_at') + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now); + }) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now); + }) + ->where('p.guard_name', $guardName) + ->where('p.name', 'like', 'menu:%') + ->pluck('p.name') + ->toArray(); + + // 통합: (역할 OR 부서 OR 개인ALLOW) - 개인DENY + $allAllowed = array_unique(array_merge($rolePermissions, $deptPermissions, $personalAllows)); + $effectivePermissions = array_diff($allAllowed, $personalDenies); + + $result[$guardName] = count($effectivePermissions); + } + + return $result; } } diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 96e7004f..0f37f955 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -18,6 +18,14 @@ public function getUsers(array $filters = [], int $perPage = 15): LengthAwarePag $tenantId = session('selected_tenant_id'); $query = User::query()->withTrashed(); + // 역할/부서 관계 eager loading (테넌트별) + if ($tenantId) { + $query->with([ + 'userRoles' => fn ($q) => $q->where('tenant_id', $tenantId)->with('role'), + 'departmentUsers' => fn ($q) => $q->where('tenant_id', $tenantId)->with('department'), + ]); + } + // 테넌트 필터링 (user_tenants pivot을 통한 필터링) if ($tenantId) { $query->whereHas('tenants', function ($q) use ($tenantId) { @@ -226,6 +234,132 @@ public function forceDeleteUser(int $id): bool return $user->forceDelete(); } + /** + * 모달용 사용자 상세 정보 조회 + */ + public function getUserForModal(int $id): ?User + { + $tenantId = session('selected_tenant_id'); + + $query = User::query() + ->with('deletedByUser') + ->withTrashed(); + + // 역할/부서 관계 eager loading (테넌트별) + if ($tenantId) { + $query->with([ + 'userRoles' => fn ($q) => $q->where('tenant_id', $tenantId)->with('role'), + 'departmentUsers' => fn ($q) => $q->where('tenant_id', $tenantId)->with('department'), + 'tenants', + ]); + } else { + $query->with(['userRoles.role', 'departmentUsers.department', 'tenants']); + } + + $user = $query->find($id); + + // 권한 카운트 추가 + if ($user && $tenantId) { + $permissionCounts = $this->getUserPermissionCounts($user->id, $tenantId); + $user->web_permission_count = $permissionCounts['web']; + $user->api_permission_count = $permissionCounts['api']; + } + + return $user; + } + + /** + * 사용자별 guard별 권한 개수 조회 (역할 + 부서 + 개인 오버라이드 통합) + */ + private function getUserPermissionCounts(int $userId, int $tenantId): array + { + $result = ['web' => 0, 'api' => 0]; + $now = now(); + + foreach (['web', 'api'] as $guardName) { + // 1. 역할 권한 + $rolePermissions = \DB::table('model_has_roles as mhr') + ->join('role_has_permissions as rhp', 'rhp.role_id', '=', 'mhr.role_id') + ->join('permissions as p', 'p.id', '=', 'rhp.permission_id') + ->where('mhr.model_type', User::class) + ->where('mhr.model_id', $userId) + ->where('p.guard_name', $guardName) + ->where('p.name', 'like', 'menu:%') + ->pluck('p.name') + ->toArray(); + + // 2. 부서 권한 + $deptPermissions = \DB::table('department_user as du') + ->join('permission_overrides as po', function ($j) use ($now, $tenantId) { + $j->on('po.model_id', '=', 'du.department_id') + ->where('po.model_type', 'App\\Models\\Tenants\\Department') + ->where('po.tenant_id', $tenantId) + ->whereNull('po.deleted_at') + ->where('po.effect', 1) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now); + }) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now); + }); + }) + ->join('permissions as p', 'p.id', '=', 'po.permission_id') + ->whereNull('du.deleted_at') + ->where('du.user_id', $userId) + ->where('du.tenant_id', $tenantId) + ->where('p.guard_name', $guardName) + ->where('p.name', 'like', 'menu:%') + ->pluck('p.name') + ->toArray(); + + // 3. 개인 오버라이드 (ALLOW) + $personalAllows = \DB::table('permission_overrides as po') + ->join('permissions as p', 'p.id', '=', 'po.permission_id') + ->where('po.model_type', User::class) + ->where('po.model_id', $userId) + ->where('po.tenant_id', $tenantId) + ->where('po.effect', 1) + ->whereNull('po.deleted_at') + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now); + }) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now); + }) + ->where('p.guard_name', $guardName) + ->where('p.name', 'like', 'menu:%') + ->pluck('p.name') + ->toArray(); + + // 4. 개인 오버라이드 (DENY) - 제외할 권한 + $personalDenies = \DB::table('permission_overrides as po') + ->join('permissions as p', 'p.id', '=', 'po.permission_id') + ->where('po.model_type', User::class) + ->where('po.model_id', $userId) + ->where('po.tenant_id', $tenantId) + ->where('po.effect', 0) + ->whereNull('po.deleted_at') + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_from')->orWhere('po.effective_from', '<=', $now); + }) + ->where(function ($w) use ($now) { + $w->whereNull('po.effective_to')->orWhere('po.effective_to', '>=', $now); + }) + ->where('p.guard_name', $guardName) + ->where('p.name', 'like', 'menu:%') + ->pluck('p.name') + ->toArray(); + + // 통합: (역할 OR 부서 OR 개인ALLOW) - 개인DENY + $allAllowed = array_unique(array_merge($rolePermissions, $deptPermissions, $personalAllows)); + $effectivePermissions = array_diff($allAllowed, $personalDenies); + + $result[$guardName] = count($effectivePermissions); + } + + return $result; + } + /** * 활성 사용자 목록 조회 (드롭다운용) */ diff --git a/public/js/context-menu.js b/public/js/context-menu.js index 5cf82676..b0a27d38 100644 --- a/public/js/context-menu.js +++ b/public/js/context-menu.js @@ -114,6 +114,9 @@ class ContextMenu { case 'edit-tenant': window.location.href = `/tenants/${id}/edit`; break; + case 'switch-tenant': + this.switchTenant(id, name); + break; case 'view-user': if (typeof UserModal !== 'undefined') { UserModal.open(id); @@ -122,12 +125,83 @@ class ContextMenu { case 'edit-user': window.location.href = `/users/${id}/edit`; break; + case 'delete-user': + if (typeof UserModal !== 'undefined') { + // 모달 열고 삭제 실행 + UserModal.currentUserId = id; + UserModal.deleteUser(); + } else { + // UserModal이 없으면 직접 삭제 확인 + if (confirm(`"${name}"을(를) 삭제하시겠습니까?`)) { + this.deleteUser(id); + } + } + break; default: console.log('Unknown action:', action, { type, id, name }); } this.hide(); } + + // 테넌트 전환 + async switchTenant(tenantId, tenantName) { + try { + const form = document.createElement('form'); + form.method = 'POST'; + form.action = '/tenant/switch'; + + // CSRF 토큰 + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + const csrfInput = document.createElement('input'); + csrfInput.type = 'hidden'; + csrfInput.name = '_token'; + csrfInput.value = csrfToken; + form.appendChild(csrfInput); + + // 테넌트 ID + const tenantInput = document.createElement('input'); + tenantInput.type = 'hidden'; + tenantInput.name = 'tenant_id'; + tenantInput.value = tenantId; + form.appendChild(tenantInput); + + document.body.appendChild(form); + form.submit(); + } catch (error) { + console.error('Failed to switch tenant:', error); + alert('테넌트 전환에 실패했습니다.'); + } + } + + // 사용자 삭제 (fallback) + async deleteUser(userId) { + try { + const response = await fetch(`/api/admin/users/${userId}`, { + method: 'DELETE', + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'), + 'Accept': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + // 테이블 새로고침 + if (typeof htmx !== 'undefined') { + htmx.trigger('#user-table', 'filterSubmit'); + } else { + window.location.reload(); + } + } else { + alert(data.message || '삭제에 실패했습니다.'); + } + } catch (error) { + console.error('Failed to delete user:', error); + alert('삭제에 실패했습니다.'); + } + } } // 전역 인스턴스 생성 diff --git a/public/js/user-modal.js b/public/js/user-modal.js new file mode 100644 index 00000000..f261a8d1 --- /dev/null +++ b/public/js/user-modal.js @@ -0,0 +1,191 @@ +/** + * 사용자 정보 모달 + * 사용자 상세 정보를 팝업으로 표시 + */ + +const UserModal = { + modalElement: null, + currentUserId: null, + + init() { + this.modalElement = document.getElementById('user-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(userId) { + if (!this.modalElement) { + console.error('User modal element not found'); + return; + } + + this.currentUserId = userId; + + // 모달 표시 + this.modalElement.classList.remove('hidden'); + document.body.style.overflow = 'hidden'; + + // 로딩 표시 + this.showLoading(); + + // 사용자 정보 로드 + await this.loadUserInfo(); + }, + + close() { + if (this.modalElement) { + this.modalElement.classList.add('hidden'); + document.body.style.overflow = ''; + } + this.currentUserId = null; + }, + + showLoading() { + const content = document.getElementById('user-modal-content'); + if (content) { + content.innerHTML = ` +
+
+
+ `; + } + }, + + async loadUserInfo() { + try { + const response = await fetch(`/api/admin/users/${this.currentUserId}/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('user-modal-content'); + if (content) { + content.innerHTML = data.html; + } + } + } catch (error) { + console.error('Failed to load user info:', error); + this.showError('사용자 정보를 불러오는데 실패했습니다.'); + } + }, + + showError(message) { + const content = document.getElementById('user-modal-content'); + if (content) { + content.innerHTML = ` +
+ + + +

${message}

+ +
+ `; + } + }, + + // 수정 페이지로 이동 + goToEdit() { + if (this.currentUserId) { + window.location.href = `/users/${this.currentUserId}/edit`; + } + }, + + // 삭제 실행 + async deleteUser() { + if (!this.currentUserId) return; + + const userName = document.getElementById('user-modal-name')?.textContent || '이 사용자'; + + if (confirm(`"${userName}"을(를) 삭제하시겠습니까?`)) { + try { + const response = await fetch(`/api/admin/users/${this.currentUserId}`, { + method: 'DELETE', + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'), + 'Accept': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + this.close(); + // 테이블 새로고침 + if (typeof htmx !== 'undefined') { + htmx.trigger('#user-table', 'filterSubmit'); + } + } else { + alert(data.message || '삭제에 실패했습니다.'); + } + } catch (error) { + console.error('Failed to delete user:', error); + alert('삭제에 실패했습니다.'); + } + } + }, + + // 복원 실행 + async restoreUser() { + if (!this.currentUserId) return; + + const userName = document.getElementById('user-modal-name')?.textContent || '이 사용자'; + + if (confirm(`"${userName}"을(를) 복원하시겠습니까?`)) { + try { + const response = await fetch(`/api/admin/users/${this.currentUserId}/restore`, { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'), + 'Accept': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + // 모달 내용 새로고침 + await this.loadUserInfo(); + // 테이블 새로고침 + if (typeof htmx !== 'undefined') { + htmx.trigger('#user-table', 'filterSubmit'); + } + } else { + alert(data.message || '복원에 실패했습니다.'); + } + } catch (error) { + console.error('Failed to restore user:', error); + alert('복원에 실패했습니다.'); + } + } + } +}; + +// DOM 로드 시 초기화 +document.addEventListener('DOMContentLoaded', () => { + UserModal.init(); +}); diff --git a/resources/views/components/context-menu.blade.php b/resources/views/components/context-menu.blade.php index e5c757e4..2d4d3567 100644 --- a/resources/views/components/context-menu.blade.php +++ b/resources/views/components/context-menu.blade.php @@ -25,6 +25,16 @@ class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex i 테넌트 수정 + + {{-- 구분선 --}}
@@ -49,4 +59,17 @@ class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex i 사용자 수정 + + {{-- 구분선 --}} +
+ + \ No newline at end of file diff --git a/resources/views/components/user-modal.blade.php b/resources/views/components/user-modal.blade.php new file mode 100644 index 00000000..774a8a57 --- /dev/null +++ b/resources/views/components/user-modal.blade.php @@ -0,0 +1,39 @@ +{{-- 사용자 정보 모달 --}} + diff --git a/resources/views/department-permissions/index.blade.php b/resources/views/department-permissions/index.blade.php index 7ca36320..434f5489 100644 --- a/resources/views/department-permissions/index.blade.php +++ b/resources/views/department-permissions/index.blade.php @@ -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 선택된 부서
- - - Guard: - - - -
+ @endforeach
@@ -50,21 +52,7 @@ class="role-button px-4 py-2 text-sm font-medium rounded-lg border transition-co 선택된 역할
- - - Guard: - - - -
+
+
+ +
+ + + +
+

템플릿: 새 테넌트 생성 시 메뉴 복사 원본 / 본사: 모든 테넌트 관리 가능

+
- - Guard: - + +
+ + +
@@ -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'); diff --git a/resources/views/users/create.blade.php b/resources/views/users/create.blade.php index 77a1f457..1b9967b6 100644 --- a/resources/views/users/create.blade.php +++ b/resources/views/users/create.blade.php @@ -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"> {{ $role->name }} + {{ $role->guard_name }} @endforeach
diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 70e420e6..26e7404f 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -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"> {{ $role->name }} + {{ $role->guard_name }} @endforeach diff --git a/resources/views/users/partials/modal-info.blade.php b/resources/views/users/partials/modal-info.blade.php new file mode 100644 index 00000000..0819b4ea --- /dev/null +++ b/resources/views/users/partials/modal-info.blade.php @@ -0,0 +1,199 @@ +{{-- 사용자 모달 - 기본 정보 --}} +
+ {{-- 삭제된 사용자 경고 배너 --}} + @if($user->deleted_at) +
+
+
+ + + +
+

이 사용자는 삭제되었습니다.

+

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

+
+
+ +
+
+ @endif + + {{-- 상단: 기본 정보 카드 --}} +
+
+ {{-- 좌측: 프로필 이미지/아이콘 --}} +
+ @if($user->profile_photo_path) + {{ $user->name }} + @else +
+ + {{ strtoupper(substr($user->name, 0, 1)) }} + +
+ @endif +
+ + {{-- 우측: 정보 테이블 --}} +
+
+

{{ $user->name }}

+ @if($user->is_super_admin) + 슈퍼 관리자 + @endif +
+ + + + + + + + + + + + + + + + + + + + + +
사용자 ID{{ $user->user_id ?? '-' }}이메일{{ $user->email }}
연락처{{ $user->phone ?? '-' }}상태 + @if($user->is_active) + 활성 + @else + 비활성 + @endif +
가입일{{ $user->created_at?->format('Y-m-d') ?? '-' }}최근 로그인{{ $user->last_login_at?->format('Y-m-d H:i') ?? '-' }}
+
+
+
+ + {{-- 2x2 그리드: 테넌트, 역할, 부서, 권한 --}} +
+ {{-- 소속 테넌트 --}} +
+

소속 테넌트

+ @if($user->tenants && $user->tenants->count() > 0) +
+ @foreach($user->tenants as $tenant) + + {{ $tenant->company_name }} + @if($tenant->pivot->is_default) + (기본) + @endif + + @endforeach +
+ @else +

소속된 테넌트 없음

+ @endif +
+ + {{-- 역할 정보 --}} +
+

역할

+ @if($user->userRoles && $user->userRoles->count() > 0) +
+ @foreach($user->userRoles as $userRole) + @if($userRole->role) + + {{ $userRole->role->name }} + + @endif + @endforeach +
+ @else +

할당된 역할 없음

+ @endif +
+ + {{-- 부서 정보 --}} +
+

부서

+ @if($user->departmentUsers && $user->departmentUsers->count() > 0) +
+ @foreach($user->departmentUsers as $departmentUser) + @if($departmentUser->department) + + {{ $departmentUser->department->name }} + @if($departmentUser->is_primary) + (주) + @endif + + @endif + @endforeach +
+ @else +

소속된 부서 없음

+ @endif +
+ + {{-- 권한 정보 --}} +
+

권한

+ @if(isset($user->web_permission_count) || isset($user->api_permission_count)) +
+
+
+ Web + {{ $user->web_permission_count ?? 0 }} +
+
+ API + {{ $user->api_permission_count ?? 0 }} +
+
+ + 관리 → + +
+ @else +

테넌트 선택 필요

+ @endif +
+
+ + {{-- 하단 버튼 --}} +
+ + @if(!$user->deleted_at) + + @endif + +
+
diff --git a/resources/views/users/partials/table.blade.php b/resources/views/users/partials/table.blade.php index 8e404588..fef983ff 100644 --- a/resources/views/users/partials/table.blade.php +++ b/resources/views/users/partials/table.blade.php @@ -5,20 +5,29 @@ ID 이름 이메일 - 연락처 - 테넌트 + 부서 + 역할 상태 작업 @forelse($users as $user) - + {{ $user->user_id ?? '-' }} -
{{ $user->name }}
+
+ {{ $user->name }} +
@if($user->is_super_admin) 슈퍼 관리자 @endif @@ -26,11 +35,36 @@ {{ $user->email }} - - {{ $user->phone ?? '-' }} + + @if($user->departmentUsers && $user->departmentUsers->count() > 0) +
+ @foreach($user->departmentUsers as $du) + + {{ $du->department?->name ?? '-' }} + + @endforeach +
+ @else + - + @endif - - {{ $user->currentTenant()?->company_name ?? '-' }} + + @if($user->userRoles && $user->userRoles->count() > 0) +
+ @foreach($user->userRoles as $ur) +
+
+ {{ $ur->role?->name ?? '-' }} +
+ @if($ur->role?->description) +
{{ $ur->role->description }}
+ @endif +
+ @endforeach +
+ @else + - + @endif @if($user->is_active) @@ -43,7 +77,7 @@ @endif - + @if($user->deleted_at)