diff --git a/app/Http/Controllers/PostController.php b/app/Http/Controllers/PostController.php index f8aa0c83..80abb709 100644 --- a/app/Http/Controllers/PostController.php +++ b/app/Http/Controllers/PostController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Models\Boards\Board; +use App\Models\Boards\BoardComment; use App\Models\Boards\Post; use App\Services\PostService; use Illuminate\Http\JsonResponse; @@ -162,7 +163,10 @@ public function show(string $boardCode, Post $post, Request $request): View|Redi // 커스텀 필드 값 $customFieldValues = $post->getCustomFieldsArray(); - return view('posts.show', compact('board', 'post', 'adjacent', 'customFieldValues')); + // 댓글 목록 + $comments = $this->postService->getComments($post); + + return view('posts.show', compact('board', 'post', 'adjacent', 'customFieldValues', 'comments')); } /** @@ -428,4 +432,99 @@ public function deleteFile(string $boardCode, Post $post, int $fileId, Request $ 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', '댓글이 삭제되었습니다.'); + } } diff --git a/app/Models/Boards/BoardComment.php b/app/Models/Boards/BoardComment.php new file mode 100644 index 00000000..9fa0ce96 --- /dev/null +++ b/app/Models/Boards/BoardComment.php @@ -0,0 +1,71 @@ +belongsTo(Post::class, 'post_id'); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function parent(): BelongsTo + { + return $this->belongsTo(BoardComment::class, 'parent_id'); + } + + public function children(): HasMany + { + return $this->hasMany(BoardComment::class, 'parent_id') + ->where('status', 'active'); + } + + /** + * Alias for children() - used for eager loading + */ + public function replies(): HasMany + { + return $this->children(); + } +} \ No newline at end of file diff --git a/app/Models/Boards/Post.php b/app/Models/Boards/Post.php index db4a16be..465a4edb 100644 --- a/app/Models/Boards/Post.php +++ b/app/Models/Boards/Post.php @@ -145,6 +145,23 @@ public function files(): HasMany ->where('document_type', 'post'); } + /** + * 댓글 (활성 상태만) + */ + public function comments(): HasMany + { + return $this->hasMany(BoardComment::class, 'post_id') + ->where('status', 'active'); + } + + /** + * 모든 댓글 (상태 무관) + */ + public function allComments(): HasMany + { + return $this->hasMany(BoardComment::class, 'post_id'); + } + // ========================================================================= // Helper Methods // ========================================================================= @@ -208,4 +225,30 @@ public function canAccess(?User $user): bool return false; } + + /** + * 안전한 HTML 콘텐츠 반환 (XSS 방지) + * - 허용된 태그만 남기고 나머지 제거 + * - 위험한 속성 (onclick, onerror 등) 제거 + */ + public function getSafeHtmlContent(): string + { + $content = $this->content ?? ''; + + // 허용할 HTML 태그 + $allowedTags = '


' + .'