diff --git a/app/Http/Controllers/Api/Admin/DepartmentController.php b/app/Http/Controllers/Api/Admin/DepartmentController.php index 66bc3345..b0bcd10d 100644 --- a/app/Http/Controllers/Api/Admin/DepartmentController.php +++ b/app/Http/Controllers/Api/Admin/DepartmentController.php @@ -178,6 +178,54 @@ public function restore(Request $request, int $id): JsonResponse ]); } + /** + * 일괄 삭제 (Soft Delete) + */ + public function bulkDelete(Request $request): JsonResponse + { + $validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']); + $deleted = 0; + foreach ($validated['ids'] as $id) { + if ($this->departmentService->deleteDepartment($id)) { + $deleted++; + } + } + + return response()->json(['success' => true, 'message' => "{$deleted}개 부서가 삭제되었습니다.", 'deleted' => $deleted]); + } + + /** + * 일괄 복원 + */ + public function bulkRestore(Request $request): JsonResponse + { + $validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']); + $restored = 0; + foreach ($validated['ids'] as $id) { + if ($this->departmentService->restoreDepartment($id)) { + $restored++; + } + } + + return response()->json(['success' => true, 'message' => "{$restored}개 부서가 복원되었습니다.", 'restored' => $restored]); + } + + /** + * 일괄 영구삭제 (슈퍼관리자 전용) + */ + public function bulkForceDelete(Request $request): JsonResponse + { + $validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']); + $deleted = 0; + foreach ($validated['ids'] as $id) { + if ($this->departmentService->forceDeleteDepartment($id)) { + $deleted++; + } + } + + return response()->json(['success' => true, 'message' => "{$deleted}개 부서가 영구 삭제되었습니다.", 'deleted' => $deleted]); + } + /** * 부서 영구 삭제 */ diff --git a/app/Http/Controllers/Api/Admin/RoleController.php b/app/Http/Controllers/Api/Admin/RoleController.php index 54769600..04d12f8f 100644 --- a/app/Http/Controllers/Api/Admin/RoleController.php +++ b/app/Http/Controllers/Api/Admin/RoleController.php @@ -115,6 +115,25 @@ public function update(UpdateRoleRequest $request, int $id): JsonResponse ]); } + /** + * 일괄 삭제 + */ + public function bulkDelete(Request $request): JsonResponse + { + $validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']); + $deleted = 0; + foreach ($validated['ids'] as $id) { + try { + $this->roleService->deleteRole($id); + $deleted++; + } catch (\Exception $e) { + continue; + } + } + + return response()->json(['success' => true, 'message' => "{$deleted}개 역할이 삭제되었습니다.", 'deleted' => $deleted]); + } + /** * 역할 삭제 (Soft Delete) */ diff --git a/app/Http/Controllers/Api/Admin/UserController.php b/app/Http/Controllers/Api/Admin/UserController.php index 020680f4..e0b99b2c 100644 --- a/app/Http/Controllers/Api/Admin/UserController.php +++ b/app/Http/Controllers/Api/Admin/UserController.php @@ -294,6 +294,57 @@ public function forceDestroy(Request $request, int $id): JsonResponse ]); } + /** + * 일괄 삭제 (Soft Delete) + */ + public function bulkDelete(Request $request): JsonResponse + { + $validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']); + $deleted = 0; + foreach ($validated['ids'] as $id) { + if ($this->userService->deleteUser($id)) { + $deleted++; + } + } + + return response()->json(['success' => true, 'message' => "{$deleted}명의 사용자가 삭제되었습니다.", 'deleted' => $deleted]); + } + + /** + * 일괄 복원 + */ + public function bulkRestore(Request $request): JsonResponse + { + $validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']); + $restored = 0; + foreach ($validated['ids'] as $id) { + if ($this->userService->restoreUser($id)) { + $restored++; + } + } + + return response()->json(['success' => true, 'message' => "{$restored}명의 사용자가 복원되었습니다.", 'restored' => $restored]); + } + + /** + * 일괄 영구삭제 (슈퍼관리자 전용) + */ + public function bulkForceDelete(Request $request): JsonResponse + { + $validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']); + $deleted = 0; + foreach ($validated['ids'] as $id) { + try { + $this->userService->forceDeleteUser($id); + $deleted++; + } catch (\Exception $e) { + continue; + } + } + + return response()->json(['success' => true, 'message' => "{$deleted}명의 사용자가 영구 삭제되었습니다.", 'deleted' => $deleted]); + } + /** * DEV 사이트 자동 로그인 토큰 생성 * MNG → DEV 자동 로그인용 One-Time Token 발급 diff --git a/resources/views/departments/index.blade.php b/resources/views/departments/index.blade.php index f48eb6eb..1a947acd 100644 --- a/resources/views/departments/index.blade.php +++ b/resources/views/departments/index.blade.php @@ -6,9 +6,31 @@

부서 관리

- - + 새 부서 - +
+ + + + + @if(auth()->user()?->is_super_admin) + + + @endif + + + 새 부서 + +
@@ -114,5 +136,78 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> }); }); }; + + // ===== 일괄 작업 ===== + window.toggleSelectAll = function(headerCheckbox) { + document.querySelectorAll('.bulk-checkbox').forEach(cb => cb.checked = headerCheckbox.checked); + updateBulkButtonState(); + }; + + window.updateBulkButtonState = function() { + const checked = document.querySelectorAll('.bulk-checkbox:checked'); + let activeCount = 0, deletedCount = 0; + checked.forEach(cb => { + if (cb.dataset.deleted === '1') { deletedCount++; } else { activeCount++; } + }); + + const deleteBtn = document.getElementById('bulkDeleteBtn'); + const restoreBtn = document.getElementById('bulkRestoreBtn'); + const forceDeleteBtn = document.getElementById('bulkForceDeleteBtn'); + + if (deleteBtn) { document.getElementById('deleteCount').textContent = activeCount; deleteBtn.disabled = activeCount === 0; } + if (restoreBtn) { document.getElementById('restoreCount').textContent = deletedCount; restoreBtn.disabled = deletedCount === 0; } + if (forceDeleteBtn) { document.getElementById('forceDeleteCount').textContent = deletedCount; forceDeleteBtn.disabled = deletedCount === 0; } + }; + + window.bulkDelete = function() { + const ids = Array.from(document.querySelectorAll('.bulk-checkbox:checked')) + .filter(cb => cb.dataset.deleted !== '1').map(cb => parseInt(cb.value)); + if (ids.length === 0) { showToast('삭제할 부서를 선택해주세요.', 'warning'); return; } + + showDeleteConfirm(`${ids.length}개 부서`, () => { + fetch('/api/admin/departments/bulk-delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' }, + body: JSON.stringify({ ids }) + }).then(r => r.json()).then(data => { + showToast(data.message || '삭제 완료', data.success ? 'success' : 'error'); + htmx.trigger('#department-table', 'filterSubmit'); + }).catch(() => showToast('삭제 중 오류 발생', 'error')); + }); + }; + + window.bulkRestore = function() { + const ids = Array.from(document.querySelectorAll('.bulk-checkbox:checked')) + .filter(cb => cb.dataset.deleted === '1').map(cb => parseInt(cb.value)); + if (ids.length === 0) { showToast('복원할 부서를 선택해주세요.', 'warning'); return; } + + showConfirm(`선택한 ${ids.length}개의 부서를 복원하시겠습니까?`, () => { + fetch('/api/admin/departments/bulk-restore', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' }, + body: JSON.stringify({ ids }) + }).then(r => r.json()).then(data => { + showToast(data.message || '복원 완료', data.success ? 'success' : 'error'); + htmx.trigger('#department-table', 'filterSubmit'); + }).catch(() => showToast('복원 중 오류 발생', 'error')); + }, { title: '부서 복원', icon: 'question' }); + }; + + window.bulkForceDelete = function() { + const ids = Array.from(document.querySelectorAll('.bulk-checkbox:checked')) + .filter(cb => cb.dataset.deleted === '1').map(cb => parseInt(cb.value)); + if (ids.length === 0) { showToast('영구삭제할 부서를 선택해주세요.', 'warning'); return; } + + showPermanentDeleteConfirm(`${ids.length}개 부서`, () => { + fetch('/api/admin/departments/bulk-force-delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' }, + body: JSON.stringify({ ids }) + }).then(r => r.json()).then(data => { + showToast(data.message || '영구삭제 완료', data.success ? 'success' : 'error'); + htmx.trigger('#department-table', 'filterSubmit'); + }).catch(() => showToast('영구삭제 중 오류 발생', 'error')); + }); + }; @endpush \ No newline at end of file diff --git a/resources/views/departments/partials/table.blade.php b/resources/views/departments/partials/table.blade.php index f1b95749..e628df47 100644 --- a/resources/views/departments/partials/table.blade.php +++ b/resources/views/departments/partials/table.blade.php @@ -3,6 +3,9 @@ + @@ -14,6 +17,12 @@ @forelse($departments as $department) + @@ -44,30 +53,37 @@ - @empty - diff --git a/resources/views/roles/index.blade.php b/resources/views/roles/index.blade.php index 35a2019a..589e6179 100644 --- a/resources/views/roles/index.blade.php +++ b/resources/views/roles/index.blade.php @@ -6,9 +6,17 @@

역할 관리

- - + 새 역할 - +
+ + + + + 새 역할 + +
@@ -51,7 +59,6 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
- @endsection @push('scripts') @@ -77,5 +84,36 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> }); }); }; + + // ===== 일괄 작업 ===== + window.toggleSelectAll = function(headerCheckbox) { + document.querySelectorAll('.bulk-checkbox').forEach(cb => cb.checked = headerCheckbox.checked); + updateBulkButtonState(); + }; + + window.updateBulkButtonState = function() { + const checked = document.querySelectorAll('.bulk-checkbox:checked'); + const deleteBtn = document.getElementById('bulkDeleteBtn'); + if (deleteBtn) { + document.getElementById('deleteCount').textContent = checked.length; + deleteBtn.disabled = checked.length === 0; + } + }; + + window.bulkDelete = function() { + const ids = Array.from(document.querySelectorAll('.bulk-checkbox:checked')).map(cb => parseInt(cb.value)); + if (ids.length === 0) { showToast('삭제할 역할을 선택해주세요.', 'warning'); return; } + + showDeleteConfirm(`${ids.length}개 역할`, () => { + fetch('/api/admin/roles/bulk-delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' }, + body: JSON.stringify({ ids }) + }).then(r => r.json()).then(data => { + showToast(data.message || '삭제 완료', data.success ? 'success' : 'error'); + htmx.trigger('#role-table', 'filterSubmit'); + }).catch(() => showToast('삭제 중 오류 발생', 'error')); + }); + }; @endpush \ No newline at end of file diff --git a/resources/views/roles/partials/table.blade.php b/resources/views/roles/partials/table.blade.php index f3d4e24b..1e9ae3c3 100644 --- a/resources/views/roles/partials/table.blade.php +++ b/resources/views/roles/partials/table.blade.php @@ -8,6 +8,9 @@
+ + 부서 코드 부서명 상위 부서
+ + {{ $department->code }} {{ $department->sort_order }} + @if($department->trashed()) - - - @if(auth()->user()?->is_super_admin) - - @endif +
+ + @if(auth()->user()?->is_super_admin) + + @endif +
@else - - 수정 - - +
+ + 수정 + + +
@endif
+ 부서가 없습니다.
+ @if($isAllTenants) @@ -23,6 +26,11 @@ @forelse($roles as $role) + @@ -71,7 +79,7 @@ class="text-red-600 hover:text-red-900"> @empty - diff --git a/resources/views/tenants/partials/table.blade.php b/resources/views/tenants/partials/table.blade.php index 64cfe480..ee51ea64 100644 --- a/resources/views/tenants/partials/table.blade.php +++ b/resources/views/tenants/partials/table.blade.php @@ -15,7 +15,7 @@ - + @@ -95,43 +95,37 @@ {{ $tenant->storage_used_formatted }} - @if($tenant->deleted_at) - {{-- 삭제된 항목 --}} - - - @else - {{-- 활성 항목 --}} - - - @endif @empty - diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php index d409e6d4..ca55f319 100644 --- a/resources/views/users/index.blade.php +++ b/resources/views/users/index.blade.php @@ -6,9 +6,31 @@

사용자 관리

- - + 새 사용자 - +
+ + + + + @if(auth()->user()?->is_super_admin) + + + @endif + + + 새 사용자 + +
@@ -149,5 +171,78 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> }); }, { title: 'DEV 사이트 접속', icon: 'question' }); }; + + // ===== 일괄 작업 ===== + window.toggleSelectAll = function(headerCheckbox) { + document.querySelectorAll('.bulk-checkbox').forEach(cb => cb.checked = headerCheckbox.checked); + updateBulkButtonState(); + }; + + window.updateBulkButtonState = function() { + const checked = document.querySelectorAll('.bulk-checkbox:checked'); + let activeCount = 0, deletedCount = 0; + checked.forEach(cb => { + if (cb.dataset.deleted === '1') { deletedCount++; } else { activeCount++; } + }); + + const deleteBtn = document.getElementById('bulkDeleteBtn'); + const restoreBtn = document.getElementById('bulkRestoreBtn'); + const forceDeleteBtn = document.getElementById('bulkForceDeleteBtn'); + + if (deleteBtn) { document.getElementById('deleteCount').textContent = activeCount; deleteBtn.disabled = activeCount === 0; } + if (restoreBtn) { document.getElementById('restoreCount').textContent = deletedCount; restoreBtn.disabled = deletedCount === 0; } + if (forceDeleteBtn) { document.getElementById('forceDeleteCount').textContent = deletedCount; forceDeleteBtn.disabled = deletedCount === 0; } + }; + + window.bulkDelete = function() { + const ids = Array.from(document.querySelectorAll('.bulk-checkbox:checked')) + .filter(cb => cb.dataset.deleted !== '1').map(cb => parseInt(cb.value)); + if (ids.length === 0) { showToast('삭제할 사용자를 선택해주세요.', 'warning'); return; } + + showDeleteConfirm(`${ids.length}명 사용자`, () => { + fetch('/api/admin/users/bulk-delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' }, + body: JSON.stringify({ ids }) + }).then(r => r.json()).then(data => { + showToast(data.message || '삭제 완료', data.success ? 'success' : 'error'); + htmx.trigger('#user-table', 'filterSubmit'); + }).catch(() => showToast('삭제 중 오류 발생', 'error')); + }); + }; + + window.bulkRestore = function() { + const ids = Array.from(document.querySelectorAll('.bulk-checkbox:checked')) + .filter(cb => cb.dataset.deleted === '1').map(cb => parseInt(cb.value)); + if (ids.length === 0) { showToast('복원할 사용자를 선택해주세요.', 'warning'); return; } + + showConfirm(`선택한 ${ids.length}명의 사용자를 복원하시겠습니까?`, () => { + fetch('/api/admin/users/bulk-restore', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' }, + body: JSON.stringify({ ids }) + }).then(r => r.json()).then(data => { + showToast(data.message || '복원 완료', data.success ? 'success' : 'error'); + htmx.trigger('#user-table', 'filterSubmit'); + }).catch(() => showToast('복원 중 오류 발생', 'error')); + }, { title: '사용자 복원', icon: 'question' }); + }; + + window.bulkForceDelete = function() { + const ids = Array.from(document.querySelectorAll('.bulk-checkbox:checked')) + .filter(cb => cb.dataset.deleted === '1').map(cb => parseInt(cb.value)); + if (ids.length === 0) { showToast('영구삭제할 사용자를 선택해주세요.', 'warning'); return; } + + showPermanentDeleteConfirm(`${ids.length}명 사용자`, () => { + fetch('/api/admin/users/bulk-force-delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' }, + body: JSON.stringify({ ids }) + }).then(r => r.json()).then(data => { + showToast(data.message || '영구삭제 완료', data.success ? 'success' : 'error'); + htmx.trigger('#user-table', 'filterSubmit'); + }).catch(() => showToast('영구삭제 중 오류 발생', 'error')); + }); + }; @endpush \ No newline at end of file diff --git a/resources/views/users/partials/table.blade.php b/resources/views/users/partials/table.blade.php index 0de5b388..10ccd638 100644 --- a/resources/views/users/partials/table.blade.php +++ b/resources/views/users/partials/table.blade.php @@ -3,10 +3,12 @@
+ + ID테넌트
+ + {{ $role->id }}
+ 등록된 역할이 없습니다.
메뉴 역할 사용량관리관리
- - - @if(auth()->user()?->is_super_admin) - + + @if($tenant->deleted_at) +
+ + @if(auth()->user()?->is_super_admin) + + @endif +
@else - - +
+ + 수정 + + +
@endif
- - 수정 - - - -
+ 등록된 테넌트가 없습니다.
+ - @@ -19,13 +21,20 @@ + - - - diff --git a/routes/api.php b/routes/api.php index 1decaeb3..4d5395ad 100644 --- a/routes/api.php +++ b/routes/api.php @@ -267,6 +267,7 @@ // 역할 관리 API Route::prefix('roles')->name('roles.')->group(function () { Route::get('/', [RoleController::class, 'index'])->name('index'); + Route::post('/bulk-delete', [RoleController::class, 'bulkDelete'])->name('bulkDelete'); Route::post('/', [RoleController::class, 'store'])->name('store'); Route::get('/{id}', [RoleController::class, 'show'])->name('show'); Route::put('/{id}', [RoleController::class, 'update'])->name('update'); @@ -276,6 +277,8 @@ // 부서 관리 API Route::prefix('departments')->name('departments.')->group(function () { Route::get('/', [DepartmentController::class, 'index'])->name('index'); + Route::post('/bulk-delete', [DepartmentController::class, 'bulkDelete'])->name('bulkDelete'); + Route::post('/bulk-restore', [DepartmentController::class, 'bulkRestore'])->name('bulkRestore'); Route::post('/', [DepartmentController::class, 'store'])->name('store'); Route::get('/{id}', [DepartmentController::class, 'show'])->name('show'); Route::put('/{id}', [DepartmentController::class, 'update'])->name('update'); @@ -286,6 +289,7 @@ // 슈퍼관리자 전용 액션 (영구삭제) Route::middleware('super.admin')->group(function () { + Route::post('/bulk-force-delete', [DepartmentController::class, 'bulkForceDelete'])->name('bulkForceDelete'); Route::delete('/{id}/force', [DepartmentController::class, 'forceDelete'])->name('forceDelete'); }); }); @@ -293,6 +297,8 @@ // 사용자 관리 API Route::prefix('users')->name('users.')->group(function () { Route::get('/', [UserController::class, 'index'])->name('index'); + Route::post('/bulk-delete', [UserController::class, 'bulkDelete'])->name('bulkDelete'); + Route::post('/bulk-restore', [UserController::class, 'bulkRestore'])->name('bulkRestore'); Route::post('/', [UserController::class, 'store'])->name('store'); Route::get('/{id}', [UserController::class, 'show'])->name('show'); Route::put('/{id}', [UserController::class, 'update'])->name('update'); @@ -306,6 +312,7 @@ // 슈퍼관리자 전용 액션 (영구삭제) Route::middleware('super.admin')->group(function () { + Route::post('/bulk-force-delete', [UserController::class, 'bulkForceDelete'])->name('bulkForceDelete'); Route::delete('/{id}/force', [UserController::class, 'forceDestroy'])->name('forceDestroy'); });
+ + ID 이름 테넌트이메일 부서 역할 재직
+ + {{ $user->user_id ?? '-' }} +
{{ $user->name }}
+
{{ $user->email }}
@if($user->is_super_admin && auth()->user()?->is_super_admin) 슈퍼 관리자 @endif @@ -47,9 +56,6 @@ - @endif
- {{ $user->email }} - @if($user->departmentUsers && $user->departmentUsers->count() > 0)
@@ -102,45 +108,48 @@ @endif
+ @php - // 슈퍼관리자 보호: 일반관리자가 슈퍼관리자를 수정/삭제할 수 없음 $canModify = ! $user->is_super_admin || auth()->user()?->is_super_admin; @endphp @if($user->deleted_at) - - @if($canModify) - - @endif - @if(auth()->user()?->is_super_admin) - - @endif - @if(!$canModify && !auth()->user()?->is_super_admin) - 삭제됨 - @endif +
+ @if($canModify) + + @endif + @if(auth()->user()?->is_super_admin) + + @endif + @if(!$canModify && !auth()->user()?->is_super_admin) + 삭제됨 + @endif +
@elseif($canModify) - - - - 수정 - - +
+ +
+ + 수정 + + +
+
@else - 수정 불가 @endif