Skip to content

Commit c3ba75c

Browse files
authored
Merge branch 'dev' into Feat/129
2 parents 33dfc47 + 37729d2 commit c3ba75c

File tree

7 files changed

+706
-0
lines changed

7 files changed

+706
-0
lines changed

src/main/java/com/back/domain/board/comment/controller/CommentController.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.back.domain.board.comment.dto.CommentListResponse;
44
import com.back.domain.board.comment.dto.CommentRequest;
55
import com.back.domain.board.comment.dto.CommentResponse;
6+
import com.back.domain.board.comment.dto.ReplyResponse;
67
import com.back.domain.board.common.dto.PageResponse;
78
import com.back.domain.board.comment.service.CommentService;
89
import com.back.global.common.dto.RsData;
@@ -86,4 +87,21 @@ public ResponseEntity<RsData<Void>> deleteComment(
8687
null
8788
));
8889
}
90+
91+
// 대댓글 생성
92+
@PostMapping("/{commentId}/replies")
93+
public ResponseEntity<RsData<ReplyResponse>> createReply(
94+
@PathVariable Long postId,
95+
@PathVariable Long commentId,
96+
@RequestBody @Valid CommentRequest request,
97+
@AuthenticationPrincipal CustomUserDetails user
98+
) {
99+
ReplyResponse response = commentService.createReply(postId, commentId, request, user.getUserId());
100+
return ResponseEntity
101+
.status(HttpStatus.CREATED)
102+
.body(RsData.success(
103+
"대댓글이 생성되었습니다.",
104+
response
105+
));
106+
}
89107
}

src/main/java/com/back/domain/board/comment/controller/CommentControllerDocs.java

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.back.domain.board.comment.dto.CommentListResponse;
44
import com.back.domain.board.comment.dto.CommentRequest;
55
import com.back.domain.board.comment.dto.CommentResponse;
6+
import com.back.domain.board.comment.dto.ReplyResponse;
67
import com.back.domain.board.common.dto.PageResponse;
78
import com.back.global.common.dto.RsData;
89
import com.back.global.security.user.CustomUserDetails;
@@ -509,4 +510,157 @@ ResponseEntity<RsData<Void>> deleteComment(
509510
@PathVariable Long commentId,
510511
@AuthenticationPrincipal CustomUserDetails user
511512
);
513+
514+
@Operation(
515+
summary = "대댓글 생성",
516+
description = "로그인한 사용자가 특정 게시글의 댓글에 대댓글을 작성합니다. (대댓글은 1단계까지만 허용됩니다.)"
517+
)
518+
@ApiResponses({
519+
@ApiResponse(
520+
responseCode = "201",
521+
description = "대댓글 생성 성공",
522+
content = @Content(
523+
mediaType = "application/json",
524+
examples = @ExampleObject(value = """
525+
{
526+
"success": true,
527+
"code": "SUCCESS_200",
528+
"message": "대댓글이 생성되었습니다.",
529+
"data": {
530+
"replyId": 45,
531+
"postId": 101,
532+
"parentId": 25,
533+
"author": {
534+
"id": 7,
535+
"nickname": "이몽룡"
536+
},
537+
"content": "저도 동의합니다!",
538+
"createdAt": "2025-09-22T13:30:00",
539+
"updatedAt": "2025-09-22T13:30:00"
540+
}
541+
}
542+
""")
543+
)
544+
),
545+
@ApiResponse(
546+
responseCode = "400",
547+
description = "잘못된 요청 또는 depth 제한 초과",
548+
content = @Content(
549+
mediaType = "application/json",
550+
examples = {
551+
@ExampleObject(name = "필드 누락", value = """
552+
{
553+
"success": false,
554+
"code": "COMMON_400",
555+
"message": "잘못된 요청입니다.",
556+
"data": null
557+
}
558+
"""),
559+
@ExampleObject(name = "부모 댓글 불일치", value = """
560+
{
561+
"success": false,
562+
"code": "COMMENT_003",
563+
"message": "부모 댓글이 해당 게시글에 속하지 않습니다.",
564+
"data": null
565+
}
566+
"""),
567+
@ExampleObject(name = "depth 초과", value = """
568+
{
569+
"success": false,
570+
"code": "COMMENT_004",
571+
"message": "대댓글은 한 단계까지만 작성할 수 있습니다.",
572+
"data": null
573+
}
574+
""")
575+
}
576+
)
577+
),
578+
@ApiResponse(
579+
responseCode = "401",
580+
description = "인증 실패 (토큰 없음/잘못됨/만료)",
581+
content = @Content(
582+
mediaType = "application/json",
583+
examples = {
584+
@ExampleObject(name = "토큰 없음", value = """
585+
{
586+
"success": false,
587+
"code": "AUTH_001",
588+
"message": "인증이 필요합니다.",
589+
"data": null
590+
}
591+
"""),
592+
@ExampleObject(name = "잘못된 토큰", value = """
593+
{
594+
"success": false,
595+
"code": "AUTH_002",
596+
"message": "유효하지 않은 액세스 토큰입니다.",
597+
"data": null
598+
}
599+
"""),
600+
@ExampleObject(name = "만료된 토큰", value = """
601+
{
602+
"success": false,
603+
"code": "AUTH_004",
604+
"message": "만료된 액세스 토큰입니다.",
605+
"data": null
606+
}
607+
""")
608+
}
609+
)
610+
),
611+
@ApiResponse(
612+
responseCode = "404",
613+
description = "존재하지 않는 사용자 / 게시글 / 댓글",
614+
content = @Content(
615+
mediaType = "application/json",
616+
examples = {
617+
@ExampleObject(name = "존재하지 않는 사용자", value = """
618+
{
619+
"success": false,
620+
"code": "USER_001",
621+
"message": "존재하지 않는 사용자입니다.",
622+
"data": null
623+
}
624+
"""),
625+
@ExampleObject(name = "존재하지 않는 게시글", value = """
626+
{
627+
"success": false,
628+
"code": "POST_001",
629+
"message": "존재하지 않는 게시글입니다.",
630+
"data": null
631+
}
632+
"""),
633+
@ExampleObject(name = "존재하지 않는 댓글", value = """
634+
{
635+
"success": false,
636+
"code": "COMMENT_001",
637+
"message": "존재하지 않는 댓글입니다.",
638+
"data": null
639+
}
640+
""")
641+
}
642+
)
643+
),
644+
@ApiResponse(
645+
responseCode = "500",
646+
description = "서버 내부 오류",
647+
content = @Content(
648+
mediaType = "application/json",
649+
examples = @ExampleObject(value = """
650+
{
651+
"success": false,
652+
"code": "COMMON_500",
653+
"message": "서버 오류가 발생했습니다.",
654+
"data": null
655+
}
656+
""")
657+
)
658+
)
659+
})
660+
ResponseEntity<RsData<ReplyResponse>> createReply(
661+
@PathVariable Long postId,
662+
@PathVariable Long commentId,
663+
@RequestBody CommentRequest request,
664+
@AuthenticationPrincipal CustomUserDetails user
665+
);
512666
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.back.domain.board.comment.dto;
2+
3+
import com.back.domain.board.comment.entity.Comment;
4+
import com.back.domain.board.common.dto.AuthorResponse;
5+
6+
import java.time.LocalDateTime;
7+
8+
/**
9+
* 대댓글 응답 DTO
10+
*
11+
* @param commentId 댓글 Id
12+
* @param postId 게시글 Id
13+
* @param parentId 부모 댓글 Id
14+
* @param author 작성자 정보
15+
* @param content 댓글 내용
16+
* @param createdAt 댓글 생성 일시
17+
* @param updatedAt 댓글 수정 일시
18+
*/
19+
public record ReplyResponse(
20+
Long commentId,
21+
Long postId,
22+
Long parentId,
23+
AuthorResponse author,
24+
String content,
25+
LocalDateTime createdAt,
26+
LocalDateTime updatedAt
27+
) {
28+
public static ReplyResponse from(Comment comment) {
29+
return new ReplyResponse(
30+
comment.getId(),
31+
comment.getPost().getId(),
32+
comment.getParent().getId(),
33+
AuthorResponse.from(comment.getUser()),
34+
comment.getContent(),
35+
comment.getCreatedAt(),
36+
comment.getUpdatedAt()
37+
);
38+
}
39+
}

src/main/java/com/back/domain/board/comment/service/CommentService.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.back.domain.board.comment.dto.CommentListResponse;
44
import com.back.domain.board.comment.dto.CommentRequest;
55
import com.back.domain.board.comment.dto.CommentResponse;
6+
import com.back.domain.board.comment.dto.ReplyResponse;
67
import com.back.domain.board.common.dto.PageResponse;
78
import com.back.domain.board.comment.entity.Comment;
89
import com.back.domain.board.post.entity.Post;
@@ -120,4 +121,44 @@ public void deleteComment(Long postId, Long commentId, Long userId) {
120121

121122
commentRepository.delete(comment);
122123
}
124+
125+
/**
126+
* 대댓글 생성 서비스
127+
* 1. User 조회
128+
* 2. Post 조회
129+
* 3. 부모 Comment 조회
130+
* 4. 부모 및 depth 검증
131+
* 5. 자식 Comment 생성
132+
* 6. Comment 저장 및 ReplyResponse 반환
133+
*/
134+
public ReplyResponse createReply(Long postId, Long parentCommentId, CommentRequest request, Long userId) {
135+
// User 조회
136+
User user = userRepository.findById(userId)
137+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
138+
139+
// Post 조회
140+
Post post = postRepository.findById(postId)
141+
.orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND));
142+
143+
// 부모 Comment 조회
144+
Comment parent = commentRepository.findById(parentCommentId)
145+
.orElseThrow(() -> new CustomException(ErrorCode.COMMENT_NOT_FOUND));
146+
147+
// 부모의 게시글 일치 검증
148+
if (!parent.getPost().getId().equals(postId)) {
149+
throw new CustomException(ErrorCode.COMMENT_PARENT_MISMATCH);
150+
}
151+
152+
// depth 검증: 부모가 이미 대댓글이면 예외
153+
if (parent.getParent() != null) {
154+
throw new CustomException(ErrorCode.COMMENT_DEPTH_EXCEEDED);
155+
}
156+
157+
// 자식 Comment 생성
158+
Comment reply = new Comment(post, user, request.content(), parent);
159+
160+
// 저장 및 응답 반환
161+
commentRepository.save(reply);
162+
return ReplyResponse.from(reply);
163+
}
123164
}

src/main/java/com/back/global/exception/ErrorCode.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ public enum ErrorCode {
9191
CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_003", "존재하지 않는 카테고리입니다."),
9292
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT_001", "존재하지 않는 댓글입니다."),
9393
COMMENT_NO_PERMISSION(HttpStatus.FORBIDDEN, "COMMENT_002", "댓글 작성자만 수정/삭제할 수 있습니다."),
94+
COMMENT_PARENT_MISMATCH(HttpStatus.BAD_REQUEST, "COMMENT_003", "부모 댓글이 해당 게시글에 속하지 않습니다."),
95+
COMMENT_DEPTH_EXCEEDED(HttpStatus.BAD_REQUEST, "COMMENT_004", "대댓글은 한 단계까지만 작성할 수 있습니다."),
9496

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

0 commit comments

Comments
 (0)