diff --git a/app/Http/Controllers/BoardController.php b/app/Http/Controllers/BoardController.php index e78cd77f..21b7a531 100644 --- a/app/Http/Controllers/BoardController.php +++ b/app/Http/Controllers/BoardController.php @@ -17,8 +17,9 @@ public function __construct( public function index(): View { $boardTypes = $this->boardService->getBoardTypes(); + $currentTenant = auth()->user()->currentTenant(); - return view('boards.index', compact('boardTypes')); + return view('boards.index', compact('boardTypes', 'currentTenant')); } /** diff --git a/app/Http/Controllers/PostController.php b/app/Http/Controllers/PostController.php index 7ecdcfa8..f8aa0c83 100644 --- a/app/Http/Controllers/PostController.php +++ b/app/Http/Controllers/PostController.php @@ -29,14 +29,16 @@ private function resolveBoard(string $boardCode, Request $request): Board // t 파라미터가 있으면 해당 테넌트의 게시판 조회 if ($tenantId) { - return Board::where('board_code', $boardCode) + return Board::with('tenant') + ->where('board_code', $boardCode) ->where('tenant_id', $tenantId) ->where('is_active', true) ->firstOrFail(); } // t 파라미터가 없으면: 시스템 게시판 우선, 그 다음 로그인 회원의 테넌트 게시판 - $board = Board::where('board_code', $boardCode) + $board = Board::with('tenant') + ->where('board_code', $boardCode) ->whereNull('tenant_id') // 시스템 게시판 ->where('is_active', true) ->first(); @@ -48,7 +50,8 @@ private function resolveBoard(string $boardCode, Request $request): Board // 시스템 게시판이 없으면 로그인 회원의 테넌트 게시판 $userTenantId = auth()->user()?->tenant_id; - return Board::where('board_code', $boardCode) + return Board::with('tenant') + ->where('board_code', $boardCode) ->where('tenant_id', $userTenantId) ->where('is_active', true) ->firstOrFail(); @@ -60,12 +63,17 @@ private function resolveBoard(string $boardCode, Request $request): Board public function index(string $boardCode, Request $request): View { $board = $this->resolveBoard($boardCode, $request); - $filters = $request->only(['search', 'is_notice']); - $posts = $this->postService->getPosts($board->id, $filters, 15); - $notices = $this->postService->getNotices($board->id, 5); - $stats = $this->postService->getBoardPostStats($board->id); + $filters = $request->only(['search', 'is_notice', 'trashed']); - return view('posts.index', compact('board', 'posts', 'notices', 'stats', 'filters')); + // 슈퍼관리자: 삭제된 게시물 포함 조회 + $isSuperAdmin = auth()->user()->hasRole('super-admin'); + $includeTrashed = $isSuperAdmin; + + $posts = $this->postService->getPosts($board->id, $filters, 15, $includeTrashed); + $notices = $this->postService->getNotices($board->id, 5); + $stats = $this->postService->getBoardPostStats($board->id, $includeTrashed); + + return view('posts.index', compact('board', 'posts', 'notices', 'stats', 'filters', 'isSuperAdmin')); } /** @@ -266,6 +274,46 @@ public function destroy(string $boardCode, Post $post, Request $request): Redire ->with('success', '게시글이 삭제되었습니다.'); } + /** + * 게시글 복원 (슈퍼관리자 전용) + */ + public function restore(string $boardCode, int $postId, Request $request): RedirectResponse + { + // 슈퍼관리자 권한 확인 + if (! auth()->user()->hasRole('super-admin')) { + abort(403); + } + + $board = $this->resolveBoard($boardCode, $request); + $post = Post::onlyTrashed()->where('board_id', $board->id)->findOrFail($postId); + + $this->postService->restorePost($post); + + return redirect() + ->route('boards.posts.index', [$board->board_code, 't' => $board->tenant_id]) + ->with('success', '게시글이 복원되었습니다.'); + } + + /** + * 게시글 영구 삭제 (슈퍼관리자 전용) + */ + public function forceDestroy(string $boardCode, int $postId, Request $request): RedirectResponse + { + // 슈퍼관리자 권한 확인 + if (! auth()->user()->hasRole('super-admin')) { + abort(403); + } + + $board = $this->resolveBoard($boardCode, $request); + $post = Post::withTrashed()->where('board_id', $board->id)->findOrFail($postId); + + $this->postService->forceDeletePost($post); + + return redirect() + ->route('boards.posts.index', [$board->board_code, 't' => $board->tenant_id]) + ->with('success', '게시글이 영구 삭제되었습니다.'); + } + // ========================================================================= // File Management // ========================================================================= diff --git a/app/Services/PostService.php b/app/Services/PostService.php index 2c6bfaa0..7d2c85bf 100644 --- a/app/Services/PostService.php +++ b/app/Services/PostService.php @@ -17,14 +17,21 @@ class PostService { /** * 게시글 목록 조회 (페이지네이션) + * + * @param bool $includeTrashed 슈퍼관리자용: 삭제된 게시물 포함 */ - public function getPosts(int $boardId, array $filters = [], int $perPage = 15): LengthAwarePaginator + public function getPosts(int $boardId, array $filters = [], int $perPage = 15, bool $includeTrashed = false): LengthAwarePaginator { $query = Post::query() ->ofBoard($boardId) ->with(['author', 'board']) ->published(); + // 슈퍼관리자: 삭제된 게시물 포함 + if ($includeTrashed) { + $query->withTrashed(); + } + // 검색 if (! empty($filters['search'])) { $search = $filters['search']; @@ -39,6 +46,15 @@ public function getPosts(int $boardId, array $filters = [], int $perPage = 15): $query->where('is_notice', $filters['is_notice']); } + // 삭제 상태 필터 + if (isset($filters['trashed'])) { + if ($filters['trashed'] === 'only') { + $query->onlyTrashed(); + } elseif ($filters['trashed'] === 'with') { + $query->withTrashed(); + } + } + // 정렬: 공지사항 먼저, 그 다음 최신순 $query->orderByDesc('is_notice') ->orderByDesc('created_at'); @@ -133,12 +149,25 @@ public function deletePost(Post $post): bool */ public function forceDeletePost(Post $post): bool { + // 첨부파일 영구 삭제 + $this->forceDeleteAllFiles($post); + // 커스텀 필드 값 삭제 $post->customFieldValues()->delete(); return $post->forceDelete(); } + /** + * 게시글 복원 + */ + public function restorePost(Post $post): bool + { + $post->deleted_by = null; + + return $post->restore(); + } + /** * 조회수 증가 */ @@ -196,14 +225,21 @@ public function getAdjacentPosts(Post $post): array /** * 게시판 통계 */ - public function getBoardPostStats(int $boardId): array + public function getBoardPostStats(int $boardId, bool $includeTrashed = false): array { - return [ + $stats = [ 'total' => Post::ofBoard($boardId)->count(), 'published' => Post::ofBoard($boardId)->published()->count(), 'notices' => Post::ofBoard($boardId)->notices()->count(), 'today' => Post::ofBoard($boardId)->whereDate('created_at', today())->count(), ]; + + // 슈퍼관리자용: 삭제된 게시물 통계 + if ($includeTrashed) { + $stats['trashed'] = Post::ofBoard($boardId)->onlyTrashed()->count(); + } + + return $stats; } // ========================================================================= diff --git a/resources/views/boards/index.blade.php b/resources/views/boards/index.blade.php index cac6b6b6..b4c8f718 100644 --- a/resources/views/boards/index.blade.php +++ b/resources/views/boards/index.blade.php @@ -1,11 +1,16 @@ @extends('layouts.app') -@section('title', '게시판 관리') +@section('title', ($currentTenant?->company_name ?? '') . ' 게시판 관리') @section('content')
-

게시판 관리

+

+ @if($currentTenant?->company_name) + {{ $currentTenant->company_name }} + @endif + 게시판 관리 +

+ 새 게시판 diff --git a/resources/views/posts/index.blade.php b/resources/views/posts/index.blade.php index 4d165de5..01ef395e 100644 --- a/resources/views/posts/index.blade.php +++ b/resources/views/posts/index.blade.php @@ -1,12 +1,17 @@ @extends('layouts.app') -@section('title', $board->name) +@section('title', ($board->tenant?->company_name ? $board->tenant->company_name . ' ' : '') . $board->name) @section('content')
-

{{ $board->name }}

+

+ @if($board->tenant?->company_name) + {{ $board->tenant->company_name }} + @endif + {{ $board->name }} +

@if($board->description)

{{ $board->description }}

@endif @@ -30,7 +35,7 @@ class="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-sm
-
+
전체 게시글
{{ number_format($stats['total']) }}
@@ -47,6 +52,12 @@ class="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-sm
게시 중
{{ number_format($stats['published']) }}
+ @if(isset($stats['trashed'])) +
+
삭제됨
+
{{ number_format($stats['trashed']) }}
+
+ @endif
@@ -79,13 +90,20 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc 작성자 작성일 조회 + @if($isSuperAdmin ?? false) + 관리 + @endif @forelse($posts as $post) - + - @if($post->is_notice) + @if($post->trashed()) + + 삭제 + + @elseif($post->is_notice) 공지 @@ -94,15 +112,20 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc @endif - - @if($post->is_secret) - - - - @endif - {{ $post->title }} - + @if($post->trashed()) + {{ $post->title }} + ({{ $post->deleted_at->format('Y-m-d') }} 삭제) + @else + + @if($post->is_secret) + + + + @endif + {{ $post->title }} + + @endif {{ $post->author?->name ?? '알 수 없음' }} @@ -113,6 +136,29 @@ class="text-gray-900 hover:text-blue-600 font-medium"> {{ number_format($post->views) }} + @if($isSuperAdmin ?? false) + + @if($post->trashed()) +
+
+ @csrf + +
+
+ @csrf + @method('DELETE') + +
+
+ @else + - + @endif + + @endif @empty diff --git a/routes/web.php b/routes/web.php index 5e31f01b..9c33cde3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -168,6 +168,10 @@ Route::put('/{post}', [PostController::class, 'update'])->name('update'); Route::delete('/{post}', [PostController::class, 'destroy'])->name('destroy'); + // 슈퍼관리자 전용: 복원/영구삭제 + Route::post('/{post}/restore', [PostController::class, 'restore'])->name('restore'); + Route::delete('/{post}/force', [PostController::class, 'forceDestroy'])->name('forceDestroy'); + // 파일 관리 Route::prefix('{post}/files')->name('files.')->group(function () { Route::get('/{fileId}/download', [PostController::class, 'downloadFile'])->name('download');