사용자 관리 복구 및 영구삭제 기능 추가

- UserService: audit 컬럼 처리 추가 (created_by, updated_by, deleted_by)
- UserService: restoreUser(), forceDeleteUser() 메서드 추가
- UserController: restore(), forceDestroy() 엔드포인트 추가
- 권한 체크: 영구삭제는 슈퍼관리자만 가능
- UI: 삭제된 사용자에 복원/영구삭제 버튼 추가
- Routes: restore, forceDestroy 라우트 추가
- UserService::getUsers()에 withTrashed() 추가
This commit is contained in:
2025-11-24 19:30:36 +09:00
parent 0c86b390ad
commit 894055786e
5 changed files with 139 additions and 7 deletions

View File

@@ -140,4 +140,56 @@ public function destroy(int $id): JsonResponse
], 500);
}
}
/**
* 사용자 복원
*/
public function restore(Request $request, int $id): JsonResponse
{
$this->userService->restoreUser($id);
// HTMX 요청 시 테이블 새로고침 트리거
if ($request->header('HX-Request')) {
return response()->json([
'success' => true,
'message' => '사용자가 복원되었습니다.',
'action' => 'refresh',
]);
}
return response()->json([
'success' => true,
'message' => '사용자가 복원되었습니다.',
]);
}
/**
* 사용자 영구 삭제 (슈퍼관리자 전용)
*/
public function forceDestroy(Request $request, int $id): JsonResponse
{
// 슈퍼관리자 권한 체크
if (!auth()->user()?->is_super_admin) {
return response()->json([
'success' => false,
'message' => '권한이 없습니다.',
], 403);
}
$this->userService->forceDeleteUser($id);
// HTMX 요청 시 테이블 새로고침 트리거
if ($request->header('HX-Request')) {
return response()->json([
'success' => true,
'message' => '사용자가 영구 삭제되었습니다.',
'action' => 'refresh',
]);
}
return response()->json([
'success' => true,
'message' => '사용자가 영구 삭제되었습니다.',
]);
}
}

View File

@@ -14,7 +14,7 @@ class UserService
public function getUsers(array $filters = [], int $perPage = 15): LengthAwarePaginator
{
$tenantId = session('selected_tenant_id');
$query = User::query();
$query = User::query()->withTrashed();
// 테넌트 필터링 (user_tenants pivot을 통한 필터링)
if ($tenantId) {
@@ -23,6 +23,15 @@ public function getUsers(array $filters = [], int $perPage = 15): LengthAwarePag
});
}
// Soft Delete 필터
if (isset($filters['trashed'])) {
if ($filters['trashed'] === 'only') {
$query->onlyTrashed();
} elseif ($filters['trashed'] === 'with') {
$query->withTrashed();
}
}
// 검색 필터
if (! empty($filters['search'])) {
$search = $filters['search'];
@@ -125,6 +134,28 @@ public function deleteUser(int $id): bool
return $user->delete();
}
/**
* 사용자 복원
*/
public function restoreUser(int $id): bool
{
$user = User::onlyTrashed()->findOrFail($id);
return $user->restore();
}
/**
* 사용자 영구 삭제 (슈퍼관리자 전용)
*/
public function forceDeleteUser(int $id): bool
{
$user = User::withTrashed()->findOrFail($id);
// 관련 데이터 먼저 삭제
$user->tenants()->detach(); // user_tenants 관계 삭제
return $user->forceDelete();
}
/**
* 활성 사용자 목록 조회 (드롭다운용)
*/

View File

@@ -88,5 +88,35 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
});
}
};
// 복원 확인
window.confirmRestore = function(id, name) {
if (confirm(`"${name}" 사용자를 복원하시겠습니까?`)) {
htmx.ajax('POST', `/api/admin/users/${id}/restore`, {
target: '#user-table',
swap: 'none',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
}).then(() => {
htmx.trigger('#user-table', 'filterSubmit');
});
}
};
// 영구삭제 확인
window.confirmForceDelete = function(id, name) {
if (confirm(`⚠️ 경고: "${name}" 사용자를 영구 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다!`)) {
htmx.ajax('DELETE', `/api/admin/users/${id}/force`, {
target: '#user-table',
swap: 'none',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
}).then(() => {
htmx.trigger('#user-table', 'filterSubmit');
});
}
};
</script>
@endpush

View File

@@ -44,12 +44,27 @@
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<a href="{{ route('users.edit', $user->id) }}" class="text-blue-600 hover:text-blue-900 mr-3">
수정
</a>
<button onclick="confirmDelete({{ $user->id }}, '{{ $user->name }}')" class="text-red-600 hover:text-red-900">
삭제
</button>
@if($user->deleted_at)
<!-- 삭제된 항목 -->
<button onclick="confirmRestore({{ $user->id }}, '{{ $user->name }}')"
class="text-green-600 hover:text-green-900 mr-3">
복원
</button>
@if(auth()->user()?->is_super_admin)
<button onclick="confirmForceDelete({{ $user->id }}, '{{ $user->name }}')"
class="text-red-600 hover:text-red-900">
영구삭제
</button>
@endif
@else
<!-- 활성 항목 -->
<a href="{{ route('users.edit', $user->id) }}" class="text-blue-600 hover:text-blue-900 mr-3">
수정
</a>
<button onclick="confirmDelete({{ $user->id }}, '{{ $user->name }}')" class="text-red-600 hover:text-red-900">
삭제
</button>
@endif
</td>
</tr>
@empty

View File

@@ -59,5 +59,9 @@
Route::get('/{id}', [UserController::class, 'show'])->name('show');
Route::put('/{id}', [UserController::class, 'update'])->name('update');
Route::delete('/{id}', [UserController::class, 'destroy'])->name('destroy');
// 추가 액션
Route::post('/{id}/restore', [UserController::class, 'restore'])->name('restore');
Route::delete('/{id}/force', [UserController::class, 'forceDestroy'])->name('forceDestroy');
});
});