Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.back.domain.board.comment.dto.CommentListResponse;
import com.back.domain.board.comment.dto.CommentRequest;
import com.back.domain.board.comment.dto.CommentResponse;
import com.back.domain.board.comment.dto.ReplyResponse;
import com.back.domain.board.common.dto.PageResponse;
import com.back.domain.board.comment.service.CommentService;
import com.back.global.common.dto.RsData;
Expand Down Expand Up @@ -86,4 +87,21 @@ public ResponseEntity<RsData<Void>> deleteComment(
null
));
}

// 대댓글 생성
@PostMapping("/{commentId}/replies")
public ResponseEntity<RsData<ReplyResponse>> createReply(
@PathVariable Long postId,
@PathVariable Long commentId,
@RequestBody @Valid CommentRequest request,
@AuthenticationPrincipal CustomUserDetails user
) {
ReplyResponse response = commentService.createReply(postId, commentId, request, user.getUserId());
return ResponseEntity
.status(HttpStatus.CREATED)
.body(RsData.success(
"대댓글이 생성되었습니다.",
response
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.back.domain.board.comment.dto.CommentListResponse;
import com.back.domain.board.comment.dto.CommentRequest;
import com.back.domain.board.comment.dto.CommentResponse;
import com.back.domain.board.comment.dto.ReplyResponse;
import com.back.domain.board.common.dto.PageResponse;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
Expand Down Expand Up @@ -509,4 +510,157 @@ ResponseEntity<RsData<Void>> deleteComment(
@PathVariable Long commentId,
@AuthenticationPrincipal CustomUserDetails user
);

@Operation(
summary = "대댓글 생성",
description = "로그인한 사용자가 특정 게시글의 댓글에 대댓글을 작성합니다. (대댓글은 1단계까지만 허용됩니다.)"
)
@ApiResponses({
@ApiResponse(
responseCode = "201",
description = "대댓글 생성 성공",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": true,
"code": "SUCCESS_200",
"message": "대댓글이 생성되었습니다.",
"data": {
"replyId": 45,
"postId": 101,
"parentId": 25,
"author": {
"id": 7,
"nickname": "이몽룡"
},
"content": "저도 동의합니다!",
"createdAt": "2025-09-22T13:30:00",
"updatedAt": "2025-09-22T13:30:00"
}
}
""")
)
),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청 또는 depth 제한 초과",
content = @Content(
mediaType = "application/json",
examples = {
@ExampleObject(name = "필드 누락", value = """
{
"success": false,
"code": "COMMON_400",
"message": "잘못된 요청입니다.",
"data": null
}
"""),
@ExampleObject(name = "부모 댓글 불일치", value = """
{
"success": false,
"code": "COMMENT_003",
"message": "부모 댓글이 해당 게시글에 속하지 않습니다.",
"data": null
}
"""),
@ExampleObject(name = "depth 초과", value = """
{
"success": false,
"code": "COMMENT_004",
"message": "대댓글은 한 단계까지만 작성할 수 있습니다.",
"data": null
}
""")
}
)
),
@ApiResponse(
responseCode = "401",
description = "인증 실패 (토큰 없음/잘못됨/만료)",
content = @Content(
mediaType = "application/json",
examples = {
@ExampleObject(name = "토큰 없음", value = """
{
"success": false,
"code": "AUTH_001",
"message": "인증이 필요합니다.",
"data": null
}
"""),
@ExampleObject(name = "잘못된 토큰", value = """
{
"success": false,
"code": "AUTH_002",
"message": "유효하지 않은 액세스 토큰입니다.",
"data": null
}
"""),
@ExampleObject(name = "만료된 토큰", value = """
{
"success": false,
"code": "AUTH_004",
"message": "만료된 액세스 토큰입니다.",
"data": null
}
""")
}
)
),
@ApiResponse(
responseCode = "404",
description = "존재하지 않는 사용자 / 게시글 / 댓글",
content = @Content(
mediaType = "application/json",
examples = {
@ExampleObject(name = "존재하지 않는 사용자", value = """
{
"success": false,
"code": "USER_001",
"message": "존재하지 않는 사용자입니다.",
"data": null
}
"""),
@ExampleObject(name = "존재하지 않는 게시글", value = """
{
"success": false,
"code": "POST_001",
"message": "존재하지 않는 게시글입니다.",
"data": null
}
"""),
@ExampleObject(name = "존재하지 않는 댓글", value = """
{
"success": false,
"code": "COMMENT_001",
"message": "존재하지 않는 댓글입니다.",
"data": null
}
""")
}
)
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_500",
"message": "서버 오류가 발생했습니다.",
"data": null
}
""")
)
)
})
ResponseEntity<RsData<ReplyResponse>> createReply(
@PathVariable Long postId,
@PathVariable Long commentId,
@RequestBody CommentRequest request,
@AuthenticationPrincipal CustomUserDetails user
);
}
39 changes: 39 additions & 0 deletions src/main/java/com/back/domain/board/comment/dto/ReplyResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.back.domain.board.comment.dto;

import com.back.domain.board.comment.entity.Comment;
import com.back.domain.board.common.dto.AuthorResponse;

import java.time.LocalDateTime;

/**
* 대댓글 응답 DTO
*
* @param commentId 댓글 Id
* @param postId 게시글 Id
* @param parentId 부모 댓글 Id
* @param author 작성자 정보
* @param content 댓글 내용
* @param createdAt 댓글 생성 일시
* @param updatedAt 댓글 수정 일시
*/
public record ReplyResponse(
Long commentId,
Long postId,
Long parentId,
AuthorResponse author,
String content,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
public static ReplyResponse from(Comment comment) {
return new ReplyResponse(
comment.getId(),
comment.getPost().getId(),
comment.getParent().getId(),
AuthorResponse.from(comment.getUser()),
comment.getContent(),
comment.getCreatedAt(),
comment.getUpdatedAt()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.back.domain.board.comment.dto.CommentListResponse;
import com.back.domain.board.comment.dto.CommentRequest;
import com.back.domain.board.comment.dto.CommentResponse;
import com.back.domain.board.comment.dto.ReplyResponse;
import com.back.domain.board.common.dto.PageResponse;
import com.back.domain.board.comment.entity.Comment;
import com.back.domain.board.post.entity.Post;
Expand Down Expand Up @@ -120,4 +121,44 @@ public void deleteComment(Long postId, Long commentId, Long userId) {

commentRepository.delete(comment);
}

/**
* 대댓글 생성 서비스
* 1. User 조회
* 2. Post 조회
* 3. 부모 Comment 조회
* 4. 부모 및 depth 검증
* 5. 자식 Comment 생성
* 6. Comment 저장 및 ReplyResponse 반환
*/
public ReplyResponse createReply(Long postId, Long parentCommentId, CommentRequest request, Long userId) {
// User 조회
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

// Post 조회
Post post = postRepository.findById(postId)
.orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND));

// 부모 Comment 조회
Comment parent = commentRepository.findById(parentCommentId)
.orElseThrow(() -> new CustomException(ErrorCode.COMMENT_NOT_FOUND));

// 부모의 게시글 일치 검증
if (!parent.getPost().getId().equals(postId)) {
throw new CustomException(ErrorCode.COMMENT_PARENT_MISMATCH);
}

// depth 검증: 부모가 이미 대댓글이면 예외
if (parent.getParent() != null) {
throw new CustomException(ErrorCode.COMMENT_DEPTH_EXCEEDED);
}

// 자식 Comment 생성
Comment reply = new Comment(post, user, request.content(), parent);

// 저장 및 응답 반환
commentRepository.save(reply);
return ReplyResponse.from(reply);
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/back/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public enum ErrorCode {
CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_003", "존재하지 않는 카테고리입니다."),
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT_001", "존재하지 않는 댓글입니다."),
COMMENT_NO_PERMISSION(HttpStatus.FORBIDDEN, "COMMENT_002", "댓글 작성자만 수정/삭제할 수 있습니다."),
COMMENT_PARENT_MISMATCH(HttpStatus.BAD_REQUEST, "COMMENT_003", "부모 댓글이 해당 게시글에 속하지 않습니다."),
COMMENT_DEPTH_EXCEEDED(HttpStatus.BAD_REQUEST, "COMMENT_004", "대댓글은 한 단계까지만 작성할 수 있습니다."),

// ======================== 공통 에러 ========================
BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON_400", "잘못된 요청입니다."),
Expand Down
Loading