query('t'); // t 파라미터가 있으면 해당 테넌트의 게시판 조회 if ($tenantId) { return Board::with('tenant') ->where('board_code', $boardCode) ->where('tenant_id', $tenantId) ->where('is_active', true) ->firstOrFail(); } // t 파라미터가 없으면: 시스템 게시판 우선, 그 다음 로그인 회원의 테넌트 게시판 $board = Board::with('tenant') ->where('board_code', $boardCode) ->whereNull('tenant_id') // 시스템 게시판 ->where('is_active', true) ->first(); if ($board) { return $board; } // 시스템 게시판이 없으면 로그인 회원의 테넌트 게시판 $userTenantId = auth()->user()?->tenant_id; return Board::with('tenant') ->where('board_code', $boardCode) ->where('tenant_id', $userTenantId) ->where('is_active', true) ->firstOrFail(); } /** * 게시글 목록 */ public function index(string $boardCode, Request $request): View { $board = $this->resolveBoard($boardCode, $request); $filters = $request->only(['search', 'is_notice', 'trashed']); // 슈퍼관리자: 삭제된 게시물 포함 조회 $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')); } /** * 게시글 작성 폼 */ public function create(string $boardCode, Request $request): View { $board = $this->resolveBoard($boardCode, $request); $fields = $board->fields; return view('posts.create', compact('board', 'fields')); } /** * 게시글 저장 */ public function store(string $boardCode, Request $request): RedirectResponse { $board = $this->resolveBoard($boardCode, $request); $validated = $request->validate([ 'title' => 'required|string|max:255', 'content' => 'nullable|string', 'is_notice' => 'nullable|boolean', 'is_secret' => 'nullable|boolean', 'files' => 'nullable|array', 'files.*' => 'file|max:'.($board->max_file_size), ]); // 커스텀 필드 값 $customFields = $request->input('custom_fields', []); // 커스텀 필드 유효성 검사 foreach ($board->fields as $field) { if ($field->is_required && empty($customFields[$field->field_key])) { return back() ->withInput() ->withErrors([$field->field_key => "{$field->name}은(는) 필수입니다."]); } } $validated['is_notice'] = $request->boolean('is_notice'); $validated['is_secret'] = $request->boolean('is_secret'); $post = $this->postService->createPost($board, $validated, $customFields); // 파일 업로드 처리 if ($request->hasFile('files') && $board->allow_files) { try { $this->postService->uploadFiles($post, $request->file('files')); } catch (\Exception $e) { // 파일 업로드 실패해도 게시글은 저장됨 - 경고 메시지만 표시 return redirect() ->route('boards.posts.show', [$board->board_code, $post, 't' => $board->tenant_id]) ->with('warning', '게시글이 작성되었으나 일부 파일 업로드에 실패했습니다: '.$e->getMessage()); } } return redirect() ->route('boards.posts.show', [$board->board_code, $post, 't' => $board->tenant_id]) ->with('success', '게시글이 작성되었습니다.'); } /** * 게시글 상세 */ public function show(string $boardCode, Post $post, Request $request): View|RedirectResponse { $board = $this->resolveBoard($boardCode, $request); // 게시판 일치 확인 if ($post->board_id !== $board->id) { abort(404); } // 비밀글 접근 권한 확인 if (! $post->canAccess(auth()->user())) { return back()->with('error', '비밀글은 작성자만 볼 수 있습니다.'); } // 조회수 증가 $this->postService->incrementViews($post); // 이전/다음 글 $adjacent = $this->postService->getAdjacentPosts($post); // 커스텀 필드 값 $customFieldValues = $post->getCustomFieldsArray(); // 댓글 목록 $comments = $this->postService->getComments($post); return view('posts.show', compact('board', 'post', 'adjacent', 'customFieldValues', 'comments')); } /** * 게시글 수정 폼 */ public function edit(string $boardCode, Post $post, Request $request): View|RedirectResponse { $board = $this->resolveBoard($boardCode, $request); // 게시판 일치 확인 if ($post->board_id !== $board->id) { abort(404); } // 수정 권한 확인 (작성자 또는 관리자) if ($post->user_id !== auth()->id() && ! auth()->user()->hasRole(['admin', 'super-admin'])) { return back()->with('error', '수정 권한이 없습니다.'); } $fields = $board->fields; $customFieldValues = $post->getCustomFieldsArray(); return view('posts.edit', compact('board', 'post', 'fields', 'customFieldValues')); } /** * 게시글 수정 저장 */ public function update(string $boardCode, Post $post, Request $request): RedirectResponse { $board = $this->resolveBoard($boardCode, $request); // 게시판 일치 확인 if ($post->board_id !== $board->id) { abort(404); } // 수정 권한 확인 if ($post->user_id !== auth()->id() && ! auth()->user()->hasRole(['admin', 'super-admin'])) { return back()->with('error', '수정 권한이 없습니다.'); } $validated = $request->validate([ 'title' => 'required|string|max:255', 'content' => 'nullable|string', 'is_notice' => 'nullable|boolean', 'is_secret' => 'nullable|boolean', 'files' => 'nullable|array', 'files.*' => 'file|max:'.($board->max_file_size), ]); // 커스텀 필드 값 $customFields = $request->input('custom_fields', []); // 커스텀 필드 유효성 검사 foreach ($board->fields as $field) { if ($field->is_required && empty($customFields[$field->field_key])) { return back() ->withInput() ->withErrors([$field->field_key => "{$field->name}은(는) 필수입니다."]); } } $validated['is_notice'] = $request->boolean('is_notice'); $validated['is_secret'] = $request->boolean('is_secret'); $this->postService->updatePost($post, $validated, $customFields); // 파일 업로드 처리 if ($request->hasFile('files') && $board->allow_files) { try { $this->postService->uploadFiles($post, $request->file('files')); } catch (\Exception $e) { return redirect() ->route('boards.posts.show', [$board->board_code, $post, 't' => $board->tenant_id]) ->with('warning', '게시글이 수정되었으나 일부 파일 업로드에 실패했습니다: '.$e->getMessage()); } } return redirect() ->route('boards.posts.show', [$board->board_code, $post, 't' => $board->tenant_id]) ->with('success', '게시글이 수정되었습니다.'); } /** * 게시글 삭제 */ public function destroy(string $boardCode, Post $post, Request $request): RedirectResponse { $board = $this->resolveBoard($boardCode, $request); // 게시판 일치 확인 if ($post->board_id !== $board->id) { abort(404); } // 삭제 권한 확인 if ($post->user_id !== auth()->id() && ! auth()->user()->hasRole(['admin', 'super-admin'])) { return back()->with('error', '삭제 권한이 없습니다.'); } // 첨부 파일 삭제 $this->postService->deleteAllFiles($post); $this->postService->deletePost($post); return redirect() ->route('boards.posts.index', [$board->board_code, 't' => $board->tenant_id]) ->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 // ========================================================================= /** * 파일 업로드 (AJAX) */ public function uploadFiles(string $boardCode, Post $post, Request $request): JsonResponse { $board = $this->resolveBoard($boardCode, $request); // 게시판 일치 확인 if ($post->board_id !== $board->id) { return response()->json(['success' => false, 'message' => '게시글을 찾을 수 없습니다.'], 404); } // 수정 권한 확인 if ($post->user_id !== auth()->id() && ! auth()->user()->hasRole(['admin', 'super-admin'])) { return response()->json(['success' => false, 'message' => '권한이 없습니다.'], 403); } // 파일 첨부 허용 여부 if (! $board->allow_files) { return response()->json(['success' => false, 'message' => '이 게시판은 파일 첨부가 허용되지 않습니다.'], 400); } $request->validate([ 'files' => 'required|array', 'files.*' => 'file|max:'.($board->max_file_size), ]); try { $uploadedFiles = $this->postService->uploadFiles($post, $request->file('files')); return response()->json([ 'success' => true, 'message' => count($uploadedFiles).'개의 파일이 업로드되었습니다.', 'files' => collect($uploadedFiles)->map(fn ($file) => [ 'id' => $file->id, 'name' => $file->display_name ?? $file->original_name, 'size' => $file->getFormattedSize(), 'type' => $file->file_type, ]), ]); } catch (\Exception $e) { return response()->json(['success' => false, 'message' => $e->getMessage()], 400); } } /** * 파일 다운로드 */ public function downloadFile(string $boardCode, Post $post, int $fileId, Request $request): BinaryFileResponse|StreamedResponse|RedirectResponse { $board = $this->resolveBoard($boardCode, $request); // 게시판 일치 확인 if ($post->board_id !== $board->id) { abort(404); } // 비밀글 접근 권한 확인 if (! $post->canAccess(auth()->user())) { return back()->with('error', '파일에 접근할 수 없습니다.'); } return $this->postService->downloadFile($post, $fileId); } /** * 파일 미리보기 (이미지 인라인 표시) */ public function previewFile(string $boardCode, Post $post, int $fileId, Request $request): BinaryFileResponse|StreamedResponse|RedirectResponse { $board = $this->resolveBoard($boardCode, $request); // 게시판 일치 확인 if ($post->board_id !== $board->id) { abort(404); } // 비밀글 접근 권한 확인 if (! $post->canAccess(auth()->user())) { return back()->with('error', '파일에 접근할 수 없습니다.'); } return $this->postService->previewFile($post, $fileId); } /** * 파일 삭제 (AJAX) */ public function deleteFile(string $boardCode, Post $post, int $fileId, Request $request): JsonResponse { $board = $this->resolveBoard($boardCode, $request); // 게시판 일치 확인 if ($post->board_id !== $board->id) { return response()->json(['success' => false, 'message' => '게시글을 찾을 수 없습니다.'], 404); } // 삭제 권한 확인 (작성자 또는 관리자) if ($post->user_id !== auth()->id() && ! auth()->user()->hasRole(['admin', 'super-admin'])) { return response()->json(['success' => false, 'message' => '권한이 없습니다.'], 403); } try { $this->postService->deleteFile($post, $fileId); return response()->json(['success' => true, 'message' => '파일이 삭제되었습니다.']); } catch (\Exception $e) { return response()->json(['success' => false, 'message' => $e->getMessage()], 400); } } // ========================================================================= // Comment Management // ========================================================================= /** * 댓글 작성 */ public function storeComment(string $boardCode, Post $post, Request $request): RedirectResponse { $board = $this->resolveBoard($boardCode, $request); // 게시판 일치 확인 if ($post->board_id !== $board->id) { abort(404); } // 비밀글 접근 권한 확인 if (! $post->canAccess(auth()->user())) { return back()->with('error', '댓글을 작성할 수 없습니다.'); } $validated = $request->validate([ 'content' => 'required|string|max:1000', 'parent_id' => 'nullable|integer|exists:board_comments,id', ]); $this->postService->createComment($post, $validated); return redirect() ->route('boards.posts.show', [$board->board_code, $post, 't' => $board->tenant_id]) ->with('success', '댓글이 작성되었습니다.'); } /** * 댓글 수정 */ public function updateComment(string $boardCode, Post $post, BoardComment $comment, Request $request): RedirectResponse { $board = $this->resolveBoard($boardCode, $request); // 게시판 일치 확인 if ($post->board_id !== $board->id) { abort(404); } // 댓글-게시글 일치 확인 if ($comment->post_id !== $post->id) { abort(404); } // 수정 권한 확인 (작성자 또는 관리자) if ($comment->user_id !== auth()->id() && ! auth()->user()->hasRole(['admin', 'super-admin'])) { return back()->with('error', '수정 권한이 없습니다.'); } $validated = $request->validate([ 'content' => 'required|string|max:1000', ]); $this->postService->updateComment($comment, $validated); return redirect() ->route('boards.posts.show', [$board->board_code, $post, 't' => $board->tenant_id]) ->with('success', '댓글이 수정되었습니다.'); } /** * 댓글 삭제 */ public function destroyComment(string $boardCode, Post $post, BoardComment $comment, Request $request): RedirectResponse { $board = $this->resolveBoard($boardCode, $request); // 게시판 일치 확인 if ($post->board_id !== $board->id) { abort(404); } // 댓글-게시글 일치 확인 if ($comment->post_id !== $post->id) { abort(404); } // 삭제 권한 확인 (작성자 또는 관리자) if ($comment->user_id !== auth()->id() && ! auth()->user()->hasRole(['admin', 'super-admin'])) { return back()->with('error', '삭제 권한이 없습니다.'); } $this->postService->deleteComment($comment); return redirect() ->route('boards.posts.show', [$board->board_code, $post, 't' => $board->tenant_id]) ->with('success', '댓글이 삭제되었습니다.'); } }