fix: [users] 슈퍼관리자 보호 기능 복원 라우트 수정
- routes/api.php: 8개 엔티티의 restore 라우트를 super.admin 미들웨어 밖으로 이동 - tenants, departments, users, menus, boards - pm/projects, pm/tasks, pm/issues - UserService.canAccessUser(): withTrashed() 적용하여 soft-deleted 사용자 권한 체크 가능 - UserPermissionService.canModifyUser(): withTrashed() 적용 (일관성 유지) 권한 정책: - 복원 (Restore): 일반관리자 가능 - 영구삭제 (Force Delete): 슈퍼관리자 전용 버그 수정: - 302 Found 에러 해결 (미들웨어 블로킹) - soft-deleted 사용자 복원 시 권한 체크 실패 해결 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,14 @@ public function index(Request $request): JsonResponse
|
|||||||
*/
|
*/
|
||||||
public function show(int $id): JsonResponse
|
public function show(int $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 정보 조회 불가
|
||||||
|
if (! $this->userService->canAccessUser($id)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '사용자를 찾을 수 없습니다.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
$user = $this->userService->getUserById($id);
|
$user = $this->userService->getUserById($id);
|
||||||
|
|
||||||
if (! $user) {
|
if (! $user) {
|
||||||
@@ -165,6 +173,14 @@ public function destroy(int $id): JsonResponse
|
|||||||
*/
|
*/
|
||||||
public function restore(Request $request, int $id): JsonResponse
|
public function restore(Request $request, int $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 복원 불가 (존재하지 않는 것처럼)
|
||||||
|
if (! $this->userService->canAccessUser($id)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '사용자를 찾을 수 없습니다.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
$this->userService->restoreUser($id);
|
$this->userService->restoreUser($id);
|
||||||
|
|
||||||
// HTMX 요청 시 테이블 새로고침 트리거
|
// HTMX 요청 시 테이블 새로고침 트리거
|
||||||
@@ -187,6 +203,14 @@ public function restore(Request $request, int $id): JsonResponse
|
|||||||
*/
|
*/
|
||||||
public function modal(Request $request, int $id): JsonResponse
|
public function modal(Request $request, int $id): JsonResponse
|
||||||
{
|
{
|
||||||
|
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 정보 조회 불가
|
||||||
|
if (! $this->userService->canAccessUser($id)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '사용자를 찾을 수 없습니다.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
$user = $this->userService->getUserForModal($id);
|
$user = $this->userService->getUserForModal($id);
|
||||||
|
|
||||||
if (! $user) {
|
if (! $user) {
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ public function getMatrix(Request $request)
|
|||||||
return view('user-permissions.partials.empty-state');
|
return view('user-permissions.partials.empty-state');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 권한 조회 불가
|
||||||
|
if (! $this->userPermissionService->canModifyUser($userId)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '슈퍼관리자의 권한은 조회할 수 없습니다.',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
// 사용자의 tenant_id로 메뉴 필터링
|
// 사용자의 tenant_id로 메뉴 필터링
|
||||||
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
||||||
|
|
||||||
@@ -89,6 +97,14 @@ public function toggle(Request $request)
|
|||||||
$guardName = $this->getValidatedGuardName($request);
|
$guardName = $this->getValidatedGuardName($request);
|
||||||
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
||||||
|
|
||||||
|
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 권한 수정 불가
|
||||||
|
if (! $this->userPermissionService->canModifyUser($userId)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '슈퍼관리자의 권한은 수정할 수 없습니다.',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
$newValue = $this->userPermissionService->togglePermission(
|
$newValue = $this->userPermissionService->togglePermission(
|
||||||
$userId,
|
$userId,
|
||||||
$menuId,
|
$menuId,
|
||||||
@@ -117,6 +133,14 @@ public function allowAll(Request $request)
|
|||||||
$guardName = $this->getValidatedGuardName($request);
|
$guardName = $this->getValidatedGuardName($request);
|
||||||
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
||||||
|
|
||||||
|
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 권한 수정 불가
|
||||||
|
if (! $this->userPermissionService->canModifyUser($userId)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '슈퍼관리자의 권한은 수정할 수 없습니다.',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
$this->userPermissionService->allowAllPermissions($userId, $tenantId, $guardName);
|
$this->userPermissionService->allowAllPermissions($userId, $tenantId, $guardName);
|
||||||
|
|
||||||
// 전체 매트릭스 다시 로드
|
// 전체 매트릭스 다시 로드
|
||||||
@@ -139,6 +163,14 @@ public function denyAll(Request $request)
|
|||||||
$guardName = $this->getValidatedGuardName($request);
|
$guardName = $this->getValidatedGuardName($request);
|
||||||
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
||||||
|
|
||||||
|
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 권한 수정 불가
|
||||||
|
if (! $this->userPermissionService->canModifyUser($userId)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '슈퍼관리자의 권한은 수정할 수 없습니다.',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
$this->userPermissionService->denyAllPermissions($userId, $tenantId, $guardName);
|
$this->userPermissionService->denyAllPermissions($userId, $tenantId, $guardName);
|
||||||
|
|
||||||
// 전체 매트릭스 다시 로드
|
// 전체 매트릭스 다시 로드
|
||||||
@@ -161,6 +193,14 @@ public function reset(Request $request)
|
|||||||
$guardName = $this->getValidatedGuardName($request);
|
$guardName = $this->getValidatedGuardName($request);
|
||||||
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
$tenantId = $this->getEffectiveTenantId($request, $userId);
|
||||||
|
|
||||||
|
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자 권한 수정 불가
|
||||||
|
if (! $this->userPermissionService->canModifyUser($userId)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '슈퍼관리자의 권한은 수정할 수 없습니다.',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
$this->userPermissionService->resetToDefaultPermissions($userId, $tenantId, $guardName);
|
$this->userPermissionService->resetToDefaultPermissions($userId, $tenantId, $guardName);
|
||||||
|
|
||||||
// 전체 매트릭스 다시 로드
|
// 전체 매트릭스 다시 로드
|
||||||
|
|||||||
@@ -41,17 +41,17 @@ public function create(): View
|
|||||||
*/
|
*/
|
||||||
public function edit(int $id): View
|
public function edit(int $id): View
|
||||||
{
|
{
|
||||||
|
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자에 접근 불가 (존재하지 않는 것처럼)
|
||||||
|
if (! $this->userService->canAccessUser($id)) {
|
||||||
|
abort(404, '사용자를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
$user = $this->userService->getUserById($id);
|
$user = $this->userService->getUserById($id);
|
||||||
|
|
||||||
if (! $user) {
|
if (! $user) {
|
||||||
abort(404, '사용자를 찾을 수 없습니다.');
|
abort(404, '사용자를 찾을 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 슈퍼관리자 보호: 일반관리자가 슈퍼관리자를 수정하려는 경우 차단
|
|
||||||
if ($user->is_super_admin && ! auth()->user()?->is_super_admin) {
|
|
||||||
abort(403, '슈퍼관리자는 수정할 수 없습니다.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$tenantId = session('selected_tenant_id');
|
$tenantId = session('selected_tenant_id');
|
||||||
|
|
||||||
// 역할/부서 목록 (테넌트별)
|
// 역할/부서 목록 (테넌트별)
|
||||||
|
|||||||
@@ -691,21 +691,49 @@ public function hasPermission(int $userId, int $menuId, string $permissionType,
|
|||||||
->exists();
|
->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 대상 사용자가 슈퍼관리자인지 검증 (일반관리자는 슈퍼관리자 수정 불가)
|
||||||
|
*
|
||||||
|
* @param int $targetUserId 대상 사용자 ID
|
||||||
|
* @return bool true면 수정 가능, false면 수정 불가
|
||||||
|
*/
|
||||||
|
public function canModifyUser(int $targetUserId): bool
|
||||||
|
{
|
||||||
|
// withTrashed()를 사용하여 일관성 유지
|
||||||
|
$targetUser = User::withTrashed()->find($targetUserId);
|
||||||
|
$currentUser = auth()->user();
|
||||||
|
|
||||||
|
// 대상 사용자가 슈퍼관리자이고 현재 사용자가 슈퍼관리자가 아니면 수정 불가
|
||||||
|
if ($targetUser?->is_super_admin && ! $currentUser?->is_super_admin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 테넌트별 사용자 목록 조회 (권한 개수 포함)
|
* 테넌트별 사용자 목록 조회 (권한 개수 포함)
|
||||||
|
* 일반관리자는 슈퍼관리자가 목록에서 제외됨
|
||||||
*
|
*
|
||||||
* @param int $tenantId 테넌트 ID
|
* @param int $tenantId 테넌트 ID
|
||||||
* @return \Illuminate\Support\Collection 사용자 목록
|
* @return \Illuminate\Support\Collection 사용자 목록
|
||||||
*/
|
*/
|
||||||
public function getUsersByTenant(int $tenantId): \Illuminate\Support\Collection
|
public function getUsersByTenant(int $tenantId): \Illuminate\Support\Collection
|
||||||
{
|
{
|
||||||
$users = User::whereHas('tenants', function ($query) use ($tenantId) {
|
$currentUser = auth()->user();
|
||||||
|
|
||||||
|
$query = User::whereHas('tenants', function ($query) use ($tenantId) {
|
||||||
$query->where('tenants.id', $tenantId)
|
$query->where('tenants.id', $tenantId)
|
||||||
->where('user_tenants.is_active', true);
|
->where('user_tenants.is_active', true);
|
||||||
})
|
})
|
||||||
->where('is_active', true)
|
->where('is_active', true);
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
// 일반관리자는 슈퍼관리자를 볼 수 없음
|
||||||
|
if (! $currentUser?->is_super_admin) {
|
||||||
|
$query->where('is_super_admin', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = $query->orderBy('name')->get();
|
||||||
|
|
||||||
// 각 사용자별 권한 개수 계산
|
// 각 사용자별 권한 개수 계산
|
||||||
$now = now();
|
$now = now();
|
||||||
|
|||||||
@@ -6,10 +6,15 @@
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\UserRole;
|
use App\Models\UserRole;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
class UserService
|
class UserService
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ArchiveService $archiveService
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 목록 조회 (페이지네이션)
|
* 사용자 목록 조회 (페이지네이션)
|
||||||
*/
|
*/
|
||||||
@@ -18,6 +23,11 @@ public function getUsers(array $filters = [], int $perPage = 15): LengthAwarePag
|
|||||||
$tenantId = session('selected_tenant_id');
|
$tenantId = session('selected_tenant_id');
|
||||||
$query = User::query()->withTrashed();
|
$query = User::query()->withTrashed();
|
||||||
|
|
||||||
|
// 슈퍼관리자 보호: 일반관리자는 슈퍼관리자를 볼 수 없음
|
||||||
|
if (! auth()->user()?->is_super_admin) {
|
||||||
|
$query->where('is_super_admin', false);
|
||||||
|
}
|
||||||
|
|
||||||
// 역할/부서 관계 eager loading (테넌트별)
|
// 역할/부서 관계 eager loading (테넌트별)
|
||||||
if ($tenantId) {
|
if ($tenantId) {
|
||||||
$query->with([
|
$query->with([
|
||||||
@@ -223,15 +233,25 @@ public function restoreUser(int $id): bool
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 영구 삭제 (슈퍼관리자 전용)
|
* 사용자 영구 삭제 (슈퍼관리자 전용)
|
||||||
|
*
|
||||||
|
* 1. 사용자 데이터를 아카이브에 저장
|
||||||
|
* 2. 관련 데이터 삭제
|
||||||
|
* 3. 사용자 영구 삭제
|
||||||
*/
|
*/
|
||||||
public function forceDeleteUser(int $id): bool
|
public function forceDeleteUser(int $id): bool
|
||||||
{
|
{
|
||||||
$user = User::withTrashed()->findOrFail($id);
|
$user = User::withTrashed()->findOrFail($id);
|
||||||
|
|
||||||
// 관련 데이터 먼저 삭제
|
return DB::transaction(function () use ($user) {
|
||||||
$user->tenants()->detach(); // user_tenants 관계 삭제
|
// 1. 아카이브에 저장 (복원 가능하도록)
|
||||||
|
$this->archiveService->archiveUser($user);
|
||||||
|
|
||||||
return $user->forceDelete();
|
// 2. 관련 데이터 삭제
|
||||||
|
$user->tenants()->detach(); // user_tenants 관계 삭제
|
||||||
|
|
||||||
|
// 3. 사용자 영구 삭제
|
||||||
|
return $user->forceDelete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -368,6 +388,11 @@ public function getActiveUsers()
|
|||||||
$tenantId = session('selected_tenant_id');
|
$tenantId = session('selected_tenant_id');
|
||||||
$query = User::query()->where('is_active', true);
|
$query = User::query()->where('is_active', true);
|
||||||
|
|
||||||
|
// 슈퍼관리자 보호: 일반관리자는 슈퍼관리자를 볼 수 없음
|
||||||
|
if (! auth()->user()?->is_super_admin) {
|
||||||
|
$query->where('is_super_admin', false);
|
||||||
|
}
|
||||||
|
|
||||||
// 테넌트 필터링 (user_tenants pivot을 통한 필터링)
|
// 테넌트 필터링 (user_tenants pivot을 통한 필터링)
|
||||||
if ($tenantId) {
|
if ($tenantId) {
|
||||||
$query->whereHas('tenants', function ($q) use ($tenantId) {
|
$query->whereHas('tenants', function ($q) use ($tenantId) {
|
||||||
@@ -377,4 +402,24 @@ public function getActiveUsers()
|
|||||||
|
|
||||||
return $query->orderBy('name')->get();
|
return $query->orderBy('name')->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 슈퍼관리자 보호: 일반관리자가 슈퍼관리자에 접근할 수 있는지 확인
|
||||||
|
*
|
||||||
|
* @param int $targetUserId 대상 사용자 ID
|
||||||
|
* @return bool true면 접근 가능, false면 접근 불가
|
||||||
|
*/
|
||||||
|
public function canAccessUser(int $targetUserId): bool
|
||||||
|
{
|
||||||
|
// withTrashed()를 사용하여 soft-deleted 사용자도 확인 (복원 시 필요)
|
||||||
|
$targetUser = User::withTrashed()->find($targetUserId);
|
||||||
|
$currentUser = auth()->user();
|
||||||
|
|
||||||
|
// 대상 사용자가 슈퍼관리자이고 현재 사용자가 슈퍼관리자가 아니면 접근 불가
|
||||||
|
if ($targetUser?->is_super_admin && ! $currentUser?->is_super_admin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
174
claudedocs/super-admin-protection.md
Normal file
174
claudedocs/super-admin-protection.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Super Admin Protection Feature
|
||||||
|
|
||||||
|
**날짜:** 2025-12-01
|
||||||
|
**작업자:** Claude Code
|
||||||
|
**요청:** 슈퍼관리자 보호 및 복원/영구삭제 권한 분리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 요구사항
|
||||||
|
|
||||||
|
### 1.1 슈퍼관리자 보호
|
||||||
|
- 일반관리자는 슈퍼관리자를 **볼 수 없음** (목록에서 숨김)
|
||||||
|
- 일반관리자는 슈퍼관리자를 **수정/삭제할 수 없음**
|
||||||
|
- 슈퍼관리자만 다른 슈퍼관리자를 관리 가능
|
||||||
|
|
||||||
|
### 1.2 복원/영구삭제 권한 분리
|
||||||
|
- **복원 (Restore)**: 일반관리자도 가능
|
||||||
|
- **영구삭제 (Force Delete)**: 슈퍼관리자 전용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 구현 내용
|
||||||
|
|
||||||
|
### 2.1 라우트 수정 (`routes/api.php`)
|
||||||
|
|
||||||
|
8개 엔티티의 restore 라우트를 `super.admin` 미들웨어 밖으로 이동:
|
||||||
|
|
||||||
|
| 엔티티 | 라인 | 복원 라우트 | 영구삭제 라우트 |
|
||||||
|
|--------|------|-------------|-----------------|
|
||||||
|
| Tenants | 42-48 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| Departments | 76-82 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| Users | 93-99 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| Menus | 117-123 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| Boards | 151-157 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| PM Projects | 234-240 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| PM Tasks | 260-266 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
| PM Issues | 292-298 | `POST /{id}/restore` | `DELETE /{id}/force` (super.admin) |
|
||||||
|
|
||||||
|
**패턴:**
|
||||||
|
```php
|
||||||
|
// 복원 (일반관리자 가능)
|
||||||
|
Route::post('/{id}/restore', [Controller::class, 'restore'])->name('restore');
|
||||||
|
|
||||||
|
// 슈퍼관리자 전용 액션 (영구삭제)
|
||||||
|
Route::middleware('super.admin')->group(function () {
|
||||||
|
Route::delete('/{id}/force', [Controller::class, 'forceDestroy'])->name('forceDestroy');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 서비스 레이어 수정
|
||||||
|
|
||||||
|
#### `app/Services/UserService.php`
|
||||||
|
```php
|
||||||
|
public function canAccessUser(int $targetUserId): bool
|
||||||
|
{
|
||||||
|
// withTrashed()를 사용하여 soft-deleted 사용자도 확인 (복원 시 필요)
|
||||||
|
$targetUser = User::withTrashed()->find($targetUserId);
|
||||||
|
$currentUser = auth()->user();
|
||||||
|
|
||||||
|
// 대상 사용자가 슈퍼관리자이고 현재 사용자가 슈퍼관리자가 아니면 접근 불가
|
||||||
|
if ($targetUser?->is_super_admin && ! $currentUser?->is_super_admin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `app/Services/UserPermissionService.php`
|
||||||
|
```php
|
||||||
|
public function canModifyUser(int $targetUserId): bool
|
||||||
|
{
|
||||||
|
// withTrashed()를 사용하여 일관성 유지
|
||||||
|
$targetUser = User::withTrashed()->find($targetUserId);
|
||||||
|
$currentUser = auth()->user();
|
||||||
|
|
||||||
|
if ($targetUser?->is_super_admin && ! $currentUser?->is_super_admin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**핵심 수정**: `User::find()` → `User::withTrashed()->find()`
|
||||||
|
- Soft-deleted 사용자도 조회 가능하게 변경
|
||||||
|
- 복원 작업 시 권한 체크가 정상 작동
|
||||||
|
|
||||||
|
### 2.3 뷰 레이어 수정
|
||||||
|
|
||||||
|
6개 테이블 뷰에 권한별 버튼 표시 로직 적용:
|
||||||
|
|
||||||
|
| 파일 | 복원 버튼 | 영구삭제 버튼 |
|
||||||
|
|------|-----------|---------------|
|
||||||
|
| `users/partials/table.blade.php` | `$canModify` 체크 | `is_super_admin` 체크 |
|
||||||
|
| `users/partials/modal-info.blade.php` | 슈퍼관리자이거나 대상이 일반사용자 | - |
|
||||||
|
| `departments/partials/table.blade.php` | 항상 표시 | `is_super_admin` 체크 |
|
||||||
|
| `menus/partials/table.blade.php` | 항상 표시 | `is_super_admin` 체크 |
|
||||||
|
| `boards/partials/table.blade.php` | 항상 표시 | `is_super_admin` 체크 |
|
||||||
|
| `tenants/partials/table.blade.php` | 항상 표시 | `is_super_admin` 체크 |
|
||||||
|
| `project-management/projects/partials/table.blade.php` | 항상 표시 | `is_super_admin` 체크 |
|
||||||
|
|
||||||
|
**Blade 패턴:**
|
||||||
|
```blade
|
||||||
|
@if($item->deleted_at)
|
||||||
|
<!-- 삭제된 항목 - 복원은 일반관리자도 가능, 영구삭제는 슈퍼관리자만 -->
|
||||||
|
<button onclick="confirmRestore({{ $item->id }}, '{{ $item->name }}')">
|
||||||
|
복원
|
||||||
|
</button>
|
||||||
|
@if(auth()->user()?->is_super_admin)
|
||||||
|
<button onclick="confirmForceDelete({{ $item->id }}, '{{ $item->name }}')">
|
||||||
|
영구삭제
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 수정된 파일 목록
|
||||||
|
|
||||||
|
### 라우트
|
||||||
|
- `routes/api.php` - 8개 엔티티 restore 라우트 분리
|
||||||
|
|
||||||
|
### 서비스
|
||||||
|
- `app/Services/UserService.php` - `canAccessUser()` withTrashed 적용
|
||||||
|
- `app/Services/UserPermissionService.php` - `canModifyUser()` withTrashed 적용
|
||||||
|
|
||||||
|
### 뷰 (Blade)
|
||||||
|
- `resources/views/users/partials/table.blade.php`
|
||||||
|
- `resources/views/users/partials/modal-info.blade.php`
|
||||||
|
- `resources/views/departments/partials/table.blade.php`
|
||||||
|
- `resources/views/menus/partials/table.blade.php`
|
||||||
|
- `resources/views/boards/partials/table.blade.php`
|
||||||
|
- `resources/views/tenants/partials/table.blade.php`
|
||||||
|
- `resources/views/project-management/projects/partials/table.blade.php`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 테스트 시나리오
|
||||||
|
|
||||||
|
### 4.1 일반관리자 테스트
|
||||||
|
- [ ] 사용자 목록에서 슈퍼관리자가 보이지 않음
|
||||||
|
- [ ] 삭제된 사용자 복원 가능
|
||||||
|
- [ ] 삭제된 부서/메뉴/게시판/테넌트/프로젝트 복원 가능
|
||||||
|
- [ ] 영구삭제 버튼이 보이지 않음
|
||||||
|
- [ ] 슈퍼관리자 수정/삭제 불가 (API 레벨)
|
||||||
|
|
||||||
|
### 4.2 슈퍼관리자 테스트
|
||||||
|
- [ ] 모든 사용자 조회 가능 (슈퍼관리자 포함)
|
||||||
|
- [ ] 삭제된 항목 복원 가능
|
||||||
|
- [ ] 영구삭제 가능
|
||||||
|
- [ ] 다른 슈퍼관리자 관리 가능
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 이슈 해결
|
||||||
|
|
||||||
|
### 5.1 302 Found 에러
|
||||||
|
**문제**: 일반관리자가 복원 API 호출 시 302 리다이렉트 발생
|
||||||
|
**원인**: restore 라우트가 `super.admin` 미들웨어 내부에 있었음
|
||||||
|
**해결**: restore 라우트를 미들웨어 밖으로 이동
|
||||||
|
|
||||||
|
### 5.2 Soft-deleted 사용자 권한 체크 실패
|
||||||
|
**문제**: `User::find()`가 soft-deleted 사용자를 조회하지 못함
|
||||||
|
**원인**: Eloquent 기본 동작으로 soft-deleted 레코드 제외
|
||||||
|
**해결**: `User::withTrashed()->find()` 사용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 관련 문서
|
||||||
|
|
||||||
|
- `claudedocs/archive-restore-feature-analysis.md` - 아카이브/복원 기능 분석
|
||||||
|
- `CURRENT_WORKS.md` - 작업 히스토리
|
||||||
@@ -69,14 +69,12 @@ class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
||||||
@if($board->trashed())
|
@if($board->trashed())
|
||||||
<!-- 삭제된 항목 - 슈퍼관리자만 복구/영구삭제 가능 -->
|
<!-- 삭제된 항목 - 복원은 일반관리자도 가능, 영구삭제는 슈퍼관리자만 -->
|
||||||
@if(auth()->user()?->is_super_admin)
|
|
||||||
<button onclick="confirmRestore({{ $board->id }}, '{{ $board->name }}')"
|
<button onclick="confirmRestore({{ $board->id }}, '{{ $board->name }}')"
|
||||||
class="text-green-600 hover:text-green-900">복원</button>
|
class="text-green-600 hover:text-green-900">복원</button>
|
||||||
|
@if(auth()->user()?->is_super_admin)
|
||||||
<button onclick="confirmForceDelete({{ $board->id }}, '{{ $board->name }}')"
|
<button onclick="confirmForceDelete({{ $board->id }}, '{{ $board->name }}')"
|
||||||
class="text-red-600 hover:text-red-900">영구삭제</button>
|
class="text-red-600 hover:text-red-900">영구삭제</button>
|
||||||
@else
|
|
||||||
<span class="text-gray-400 text-xs">삭제됨</span>
|
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
<!-- 일반 항목 액션 -->
|
<!-- 일반 항목 액션 -->
|
||||||
|
|||||||
@@ -44,16 +44,14 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
@if($department->trashed())
|
@if($department->trashed())
|
||||||
<!-- 삭제된 항목 - 슈퍼관리자만 복구/영구삭제 가능 -->
|
<!-- 삭제된 항목 - 복원은 일반관리자도 가능, 영구삭제는 슈퍼관리자만 -->
|
||||||
@if(auth()->user()?->is_super_admin)
|
|
||||||
<button onclick="confirmRestore({{ $department->id }}, '{{ $department->name }}')" class="text-green-600 hover:text-green-900 mr-3">
|
<button onclick="confirmRestore({{ $department->id }}, '{{ $department->name }}')" class="text-green-600 hover:text-green-900 mr-3">
|
||||||
복원
|
복원
|
||||||
</button>
|
</button>
|
||||||
|
@if(auth()->user()?->is_super_admin)
|
||||||
<button onclick="confirmForceDelete({{ $department->id }}, '{{ $department->name }}')" class="text-red-600 hover:text-red-900">
|
<button onclick="confirmForceDelete({{ $department->id }}, '{{ $department->name }}')" class="text-red-600 hover:text-red-900">
|
||||||
영구삭제
|
영구삭제
|
||||||
</button>
|
</button>
|
||||||
@else
|
|
||||||
<span class="text-gray-400 text-xs">삭제됨</span>
|
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
<a href="{{ route('departments.edit', $department->id) }}" class="text-blue-600 hover:text-blue-900 mr-3">
|
<a href="{{ route('departments.edit', $department->id) }}" class="text-blue-600 hover:text-blue-900 mr-3">
|
||||||
|
|||||||
@@ -95,18 +95,16 @@ class="relative inline-flex h-4 w-8 items-center rounded-full transition-colors
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
@if($menu->deleted_at)
|
@if($menu->deleted_at)
|
||||||
<!-- 삭제된 항목 - 슈퍼관리자만 복구/영구삭제 가능 -->
|
<!-- 삭제된 항목 - 복원은 일반관리자도 가능, 영구삭제는 슈퍼관리자만 -->
|
||||||
@if(auth()->user()?->is_super_admin)
|
|
||||||
<button onclick="confirmRestore({{ $menu->id }}, '{{ $menu->name }}')"
|
<button onclick="confirmRestore({{ $menu->id }}, '{{ $menu->name }}')"
|
||||||
class="text-green-600 hover:text-green-900 mr-3">
|
class="text-green-600 hover:text-green-900 mr-3">
|
||||||
복원
|
복원
|
||||||
</button>
|
</button>
|
||||||
|
@if(auth()->user()?->is_super_admin)
|
||||||
<button onclick="confirmForceDelete({{ $menu->id }}, '{{ $menu->name }}')"
|
<button onclick="confirmForceDelete({{ $menu->id }}, '{{ $menu->name }}')"
|
||||||
class="text-red-600 hover:text-red-900">
|
class="text-red-600 hover:text-red-900">
|
||||||
영구삭제
|
영구삭제
|
||||||
</button>
|
</button>
|
||||||
@else
|
|
||||||
<span class="text-gray-400 text-xs">삭제됨</span>
|
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
<!-- 활성 항목 -->
|
<!-- 활성 항목 -->
|
||||||
|
|||||||
@@ -92,8 +92,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-4 whitespace-nowrap text-center">
|
<td class="px-4 py-4 whitespace-nowrap text-center">
|
||||||
@if($project->deleted_at)
|
@if($project->deleted_at)
|
||||||
<!-- 삭제된 항목 - 슈퍼관리자만 복구/영구삭제 가능 -->
|
<!-- 삭제된 항목 - 복원은 일반관리자도 가능, 영구삭제는 슈퍼관리자만 -->
|
||||||
@if(auth()->user()?->is_super_admin)
|
|
||||||
<div class="flex justify-center gap-1">
|
<div class="flex justify-center gap-1">
|
||||||
<button onclick="confirmRestore({{ $project->id }}, '{{ $project->name }}')"
|
<button onclick="confirmRestore({{ $project->id }}, '{{ $project->name }}')"
|
||||||
class="p-1.5 text-green-600 hover:text-green-900 hover:bg-green-50 rounded" title="복원">
|
class="p-1.5 text-green-600 hover:text-green-900 hover:bg-green-50 rounded" title="복원">
|
||||||
@@ -101,16 +100,15 @@ class="p-1.5 text-green-600 hover:text-green-900 hover:bg-green-50 rounded" titl
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
@if(auth()->user()?->is_super_admin)
|
||||||
<button onclick="confirmForceDelete({{ $project->id }}, '{{ $project->name }}')"
|
<button onclick="confirmForceDelete({{ $project->id }}, '{{ $project->name }}')"
|
||||||
class="p-1.5 text-red-600 hover:text-red-900 hover:bg-red-50 rounded" title="영구삭제">
|
class="p-1.5 text-red-600 hover:text-red-900 hover:bg-red-50 rounded" title="영구삭제">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@else
|
|
||||||
<span class="text-gray-400 text-xs">삭제됨</span>
|
|
||||||
@endif
|
|
||||||
@else
|
@else
|
||||||
<!-- 활성 항목 -->
|
<!-- 활성 항목 -->
|
||||||
<div class="flex justify-center gap-1">
|
<div class="flex justify-center gap-1">
|
||||||
|
|||||||
@@ -84,18 +84,16 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" onclick="event.stopPropagation()">
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" onclick="event.stopPropagation()">
|
||||||
@if($tenant->deleted_at)
|
@if($tenant->deleted_at)
|
||||||
<!-- 삭제된 항목 - 슈퍼관리자만 복구/영구삭제 가능 -->
|
<!-- 삭제된 항목 - 복원은 일반관리자도 가능, 영구삭제는 슈퍼관리자만 -->
|
||||||
@if(auth()->user()?->is_super_admin)
|
|
||||||
<button onclick="confirmRestore({{ $tenant->id }}, '{{ $tenant->company_name }}')"
|
<button onclick="confirmRestore({{ $tenant->id }}, '{{ $tenant->company_name }}')"
|
||||||
class="text-green-600 hover:text-green-900 mr-3">
|
class="text-green-600 hover:text-green-900 mr-3">
|
||||||
복원
|
복원
|
||||||
</button>
|
</button>
|
||||||
|
@if(auth()->user()?->is_super_admin)
|
||||||
<button onclick="confirmForceDelete({{ $tenant->id }}, '{{ $tenant->company_name }}')"
|
<button onclick="confirmForceDelete({{ $tenant->id }}, '{{ $tenant->company_name }}')"
|
||||||
class="text-red-600 hover:text-red-900">
|
class="text-red-600 hover:text-red-900">
|
||||||
영구삭제
|
영구삭제
|
||||||
</button>
|
</button>
|
||||||
@else
|
|
||||||
<span class="text-gray-400 text-xs">삭제됨</span>
|
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
<!-- 활성 항목 -->
|
<!-- 활성 항목 -->
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if(auth()->user()?->is_super_admin)
|
{{-- 슈퍼관리자이거나, 대상이 일반사용자면 복원 가능 --}}
|
||||||
|
@if(auth()->user()?->is_super_admin || ! $user->is_super_admin)
|
||||||
<button type="button"
|
<button type="button"
|
||||||
onclick="event.stopPropagation(); UserModal.restoreUser()"
|
onclick="event.stopPropagation(); UserModal.restoreUser()"
|
||||||
class="px-3 py-1.5 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors">
|
class="px-3 py-1.5 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors">
|
||||||
@@ -167,10 +168,12 @@ class="w-20 h-20 rounded-full object-cover">
|
|||||||
<span class="text-sm font-semibold text-gray-900">{{ $user->api_permission_count ?? 0 }}</span>
|
<span class="text-sm font-semibold text-gray-900">{{ $user->api_permission_count ?? 0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if(! $user->is_super_admin || auth()->user()?->is_super_admin)
|
||||||
<a href="{{ route('user-permissions.index') }}?user_id={{ $user->id }}"
|
<a href="{{ route('user-permissions.index') }}?user_id={{ $user->id }}"
|
||||||
class="text-xs text-blue-600 hover:text-blue-800 hover:underline">
|
class="text-xs text-blue-600 hover:text-blue-800 hover:underline">
|
||||||
관리 →
|
관리 →
|
||||||
</a>
|
</a>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<p class="text-xs text-gray-400">테넌트 선택 필요</p>
|
<p class="text-xs text-gray-400">테넌트 선택 필요</p>
|
||||||
|
|||||||
@@ -83,17 +83,20 @@
|
|||||||
$canModify = ! $user->is_super_admin || auth()->user()?->is_super_admin;
|
$canModify = ! $user->is_super_admin || auth()->user()?->is_super_admin;
|
||||||
@endphp
|
@endphp
|
||||||
@if($user->deleted_at)
|
@if($user->deleted_at)
|
||||||
<!-- 삭제된 항목 - 슈퍼관리자만 복구/영구삭제 가능 -->
|
<!-- 삭제된 항목 - 복원은 일반관리자도 가능, 영구삭제는 슈퍼관리자만 -->
|
||||||
@if(auth()->user()?->is_super_admin)
|
@if($canModify)
|
||||||
<button onclick="confirmRestore({{ $user->id }}, '{{ $user->name }}')"
|
<button onclick="confirmRestore({{ $user->id }}, '{{ $user->name }}')"
|
||||||
class="text-green-600 hover:text-green-900 mr-3">
|
class="text-green-600 hover:text-green-900 mr-3">
|
||||||
복원
|
복원
|
||||||
</button>
|
</button>
|
||||||
|
@endif
|
||||||
|
@if(auth()->user()?->is_super_admin)
|
||||||
<button onclick="confirmForceDelete({{ $user->id }}, '{{ $user->name }}')"
|
<button onclick="confirmForceDelete({{ $user->id }}, '{{ $user->name }}')"
|
||||||
class="text-red-600 hover:text-red-900">
|
class="text-red-600 hover:text-red-900">
|
||||||
영구삭제
|
영구삭제
|
||||||
</button>
|
</button>
|
||||||
@else
|
@endif
|
||||||
|
@if(!$canModify && !auth()->user()?->is_super_admin)
|
||||||
<span class="text-gray-400 text-xs">삭제됨</span>
|
<span class="text-gray-400 text-xs">삭제됨</span>
|
||||||
@endif
|
@endif
|
||||||
@elseif($canModify)
|
@elseif($canModify)
|
||||||
|
|||||||
@@ -39,9 +39,11 @@
|
|||||||
Route::put('/{id}', [TenantController::class, 'update'])->name('update');
|
Route::put('/{id}', [TenantController::class, 'update'])->name('update');
|
||||||
Route::delete('/{id}', [TenantController::class, 'destroy'])->name('destroy');
|
Route::delete('/{id}', [TenantController::class, 'destroy'])->name('destroy');
|
||||||
|
|
||||||
// 슈퍼관리자 전용 액션
|
// 복원 (일반관리자 가능)
|
||||||
|
Route::post('/{id}/restore', [TenantController::class, 'restore'])->name('restore');
|
||||||
|
|
||||||
|
// 슈퍼관리자 전용 액션 (영구삭제)
|
||||||
Route::middleware('super.admin')->group(function () {
|
Route::middleware('super.admin')->group(function () {
|
||||||
Route::post('/{id}/restore', [TenantController::class, 'restore'])->name('restore');
|
|
||||||
Route::delete('/{id}/force', [TenantController::class, 'forceDestroy'])->name('forceDestroy');
|
Route::delete('/{id}/force', [TenantController::class, 'forceDestroy'])->name('forceDestroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,9 +73,11 @@
|
|||||||
Route::put('/{id}', [DepartmentController::class, 'update'])->name('update');
|
Route::put('/{id}', [DepartmentController::class, 'update'])->name('update');
|
||||||
Route::delete('/{id}', [DepartmentController::class, 'destroy'])->name('destroy');
|
Route::delete('/{id}', [DepartmentController::class, 'destroy'])->name('destroy');
|
||||||
|
|
||||||
// 슈퍼관리자 전용 액션
|
// 복원 (일반관리자 가능)
|
||||||
|
Route::post('/{id}/restore', [DepartmentController::class, 'restore'])->name('restore');
|
||||||
|
|
||||||
|
// 슈퍼관리자 전용 액션 (영구삭제)
|
||||||
Route::middleware('super.admin')->group(function () {
|
Route::middleware('super.admin')->group(function () {
|
||||||
Route::post('/{id}/restore', [DepartmentController::class, 'restore'])->name('restore');
|
|
||||||
Route::delete('/{id}/force', [DepartmentController::class, 'forceDelete'])->name('forceDelete');
|
Route::delete('/{id}/force', [DepartmentController::class, 'forceDelete'])->name('forceDelete');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -86,9 +90,11 @@
|
|||||||
Route::put('/{id}', [UserController::class, 'update'])->name('update');
|
Route::put('/{id}', [UserController::class, 'update'])->name('update');
|
||||||
Route::delete('/{id}', [UserController::class, 'destroy'])->name('destroy');
|
Route::delete('/{id}', [UserController::class, 'destroy'])->name('destroy');
|
||||||
|
|
||||||
// 슈퍼관리자 전용 액션
|
// 복원 (일반관리자 가능 - 슈퍼관리자 복원은 컨트롤러에서 차단)
|
||||||
|
Route::post('/{id}/restore', [UserController::class, 'restore'])->name('restore');
|
||||||
|
|
||||||
|
// 슈퍼관리자 전용 액션 (영구삭제)
|
||||||
Route::middleware('super.admin')->group(function () {
|
Route::middleware('super.admin')->group(function () {
|
||||||
Route::post('/{id}/restore', [UserController::class, 'restore'])->name('restore');
|
|
||||||
Route::delete('/{id}/force', [UserController::class, 'forceDestroy'])->name('forceDestroy');
|
Route::delete('/{id}/force', [UserController::class, 'forceDestroy'])->name('forceDestroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,9 +114,11 @@
|
|||||||
Route::put('/{id}', [MenuController::class, 'update'])->name('update');
|
Route::put('/{id}', [MenuController::class, 'update'])->name('update');
|
||||||
Route::delete('/{id}', [MenuController::class, 'destroy'])->name('destroy');
|
Route::delete('/{id}', [MenuController::class, 'destroy'])->name('destroy');
|
||||||
|
|
||||||
// 슈퍼관리자 전용 액션
|
// 복원 (일반관리자 가능)
|
||||||
|
Route::post('/{id}/restore', [MenuController::class, 'restore'])->name('restore');
|
||||||
|
|
||||||
|
// 슈퍼관리자 전용 액션 (영구삭제)
|
||||||
Route::middleware('super.admin')->group(function () {
|
Route::middleware('super.admin')->group(function () {
|
||||||
Route::post('/{id}/restore', [MenuController::class, 'restore'])->name('restore');
|
|
||||||
Route::delete('/{id}/force', [MenuController::class, 'forceDestroy'])->name('forceDestroy');
|
Route::delete('/{id}/force', [MenuController::class, 'forceDestroy'])->name('forceDestroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,9 +148,11 @@
|
|||||||
Route::put('/{id}', [BoardController::class, 'update'])->name('update');
|
Route::put('/{id}', [BoardController::class, 'update'])->name('update');
|
||||||
Route::delete('/{id}', [BoardController::class, 'destroy'])->name('destroy');
|
Route::delete('/{id}', [BoardController::class, 'destroy'])->name('destroy');
|
||||||
|
|
||||||
// 슈퍼관리자 전용 액션
|
// 복원 (일반관리자 가능)
|
||||||
|
Route::post('/{id}/restore', [BoardController::class, 'restore'])->name('restore');
|
||||||
|
|
||||||
|
// 슈퍼관리자 전용 액션 (영구삭제)
|
||||||
Route::middleware('super.admin')->group(function () {
|
Route::middleware('super.admin')->group(function () {
|
||||||
Route::post('/{id}/restore', [BoardController::class, 'restore'])->name('restore');
|
|
||||||
Route::delete('/{id}/force', [BoardController::class, 'forceDestroy'])->name('forceDestroy');
|
Route::delete('/{id}/force', [BoardController::class, 'forceDestroy'])->name('forceDestroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -221,9 +231,11 @@
|
|||||||
Route::put('/{id}', [PmProjectController::class, 'update'])->name('update');
|
Route::put('/{id}', [PmProjectController::class, 'update'])->name('update');
|
||||||
Route::delete('/{id}', [PmProjectController::class, 'destroy'])->name('destroy');
|
Route::delete('/{id}', [PmProjectController::class, 'destroy'])->name('destroy');
|
||||||
|
|
||||||
// 슈퍼관리자 전용 액션
|
// 복원 (일반관리자 가능)
|
||||||
|
Route::post('/{id}/restore', [PmProjectController::class, 'restore'])->name('restore');
|
||||||
|
|
||||||
|
// 슈퍼관리자 전용 액션 (영구삭제)
|
||||||
Route::middleware('super.admin')->group(function () {
|
Route::middleware('super.admin')->group(function () {
|
||||||
Route::post('/{id}/restore', [PmProjectController::class, 'restore'])->name('restore');
|
|
||||||
Route::delete('/{id}/force', [PmProjectController::class, 'forceDestroy'])->name('forceDestroy');
|
Route::delete('/{id}/force', [PmProjectController::class, 'forceDestroy'])->name('forceDestroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -245,9 +257,11 @@
|
|||||||
Route::put('/{id}', [PmTaskController::class, 'update'])->name('update');
|
Route::put('/{id}', [PmTaskController::class, 'update'])->name('update');
|
||||||
Route::delete('/{id}', [PmTaskController::class, 'destroy'])->name('destroy');
|
Route::delete('/{id}', [PmTaskController::class, 'destroy'])->name('destroy');
|
||||||
|
|
||||||
// 슈퍼관리자 전용 액션
|
// 복원 (일반관리자 가능)
|
||||||
|
Route::post('/{id}/restore', [PmTaskController::class, 'restore'])->name('restore');
|
||||||
|
|
||||||
|
// 슈퍼관리자 전용 액션 (영구삭제)
|
||||||
Route::middleware('super.admin')->group(function () {
|
Route::middleware('super.admin')->group(function () {
|
||||||
Route::post('/{id}/restore', [PmTaskController::class, 'restore'])->name('restore');
|
|
||||||
Route::delete('/{id}/force', [PmTaskController::class, 'forceDestroy'])->name('forceDestroy');
|
Route::delete('/{id}/force', [PmTaskController::class, 'forceDestroy'])->name('forceDestroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -275,9 +289,11 @@
|
|||||||
Route::put('/{id}', [PmIssueController::class, 'update'])->name('update');
|
Route::put('/{id}', [PmIssueController::class, 'update'])->name('update');
|
||||||
Route::delete('/{id}', [PmIssueController::class, 'destroy'])->name('destroy');
|
Route::delete('/{id}', [PmIssueController::class, 'destroy'])->name('destroy');
|
||||||
|
|
||||||
// 슈퍼관리자 전용 액션
|
// 복원 (일반관리자 가능)
|
||||||
|
Route::post('/{id}/restore', [PmIssueController::class, 'restore'])->name('restore');
|
||||||
|
|
||||||
|
// 슈퍼관리자 전용 액션 (영구삭제)
|
||||||
Route::middleware('super.admin')->group(function () {
|
Route::middleware('super.admin')->group(function () {
|
||||||
Route::post('/{id}/restore', [PmIssueController::class, 'restore'])->name('restore');
|
|
||||||
Route::delete('/{id}/force', [PmIssueController::class, 'forceDestroy'])->name('forceDestroy');
|
Route::delete('/{id}/force', [PmIssueController::class, 'forceDestroy'])->name('forceDestroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user