Skip to content

Commit 0a63ccc

Browse files
authored
Feat: 댓글 생성 API 구현 (#152) (#158)
* Feat: 댓글 생성 API 구현 * Test: 테스트 작성 * Docs: Swagger 문서 작성
1 parent 2382a55 commit 0a63ccc

File tree

9 files changed

+597
-0
lines changed

9 files changed

+597
-0
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.back.domain.board.controller;
2+
3+
import com.back.domain.board.dto.CommentRequest;
4+
import com.back.domain.board.dto.CommentResponse;
5+
import com.back.domain.board.service.CommentService;
6+
import com.back.global.common.dto.RsData;
7+
import com.back.global.security.user.CustomUserDetails;
8+
import jakarta.validation.Valid;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.http.HttpStatus;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
13+
import org.springframework.web.bind.annotation.*;
14+
15+
@RestController
16+
@RequestMapping("/api/posts/{postId}/comments")
17+
@RequiredArgsConstructor
18+
public class CommentController implements CommentControllerDocs {
19+
private final CommentService commentService;
20+
21+
// 댓글 생성
22+
@PostMapping
23+
public ResponseEntity<RsData<CommentResponse>> createComment(
24+
@PathVariable Long postId,
25+
@RequestBody @Valid CommentRequest request,
26+
@AuthenticationPrincipal CustomUserDetails user
27+
) {
28+
CommentResponse response = commentService.createComment(postId, request, user.getUserId());
29+
return ResponseEntity
30+
.status(HttpStatus.CREATED)
31+
.body(RsData.success(
32+
"댓글이 생성되었습니다.",
33+
response
34+
));
35+
}
36+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.back.domain.board.controller;
2+
3+
import com.back.domain.board.dto.CommentRequest;
4+
import com.back.domain.board.dto.CommentResponse;
5+
import com.back.global.common.dto.RsData;
6+
import com.back.global.security.user.CustomUserDetails;
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.media.Content;
9+
import io.swagger.v3.oas.annotations.media.ExampleObject;
10+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
11+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
12+
import io.swagger.v3.oas.annotations.tags.Tag;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
15+
import org.springframework.web.bind.annotation.*;
16+
17+
@Tag(name = "Comment API", description = "댓글 관련 API")
18+
public interface CommentControllerDocs {
19+
20+
@Operation(
21+
summary = "댓글 생성",
22+
description = "로그인한 사용자가 특정 게시글에 댓글을 작성합니다."
23+
)
24+
@ApiResponses({
25+
@ApiResponse(
26+
responseCode = "201",
27+
description = "댓글 생성 성공",
28+
content = @Content(
29+
mediaType = "application/json",
30+
examples = @ExampleObject(value = """
31+
{
32+
"success": true,
33+
"code": "SUCCESS_200",
34+
"message": "댓글이 생성되었습니다.",
35+
"data": {
36+
"commentId": 25,
37+
"postId": 101,
38+
"author": {
39+
"id": 5,
40+
"nickname": "홍길동"
41+
},
42+
"content": "좋은 글 감사합니다!",
43+
"createdAt": "2025-09-22T11:30:00",
44+
"updatedAt": "2025-09-22T11:30:00"
45+
}
46+
}
47+
""")
48+
)
49+
),
50+
@ApiResponse(
51+
responseCode = "400",
52+
description = "잘못된 요청 (필드 누락 등)",
53+
content = @Content(
54+
mediaType = "application/json",
55+
examples = @ExampleObject(value = """
56+
{
57+
"success": false,
58+
"code": "COMMON_400",
59+
"message": "잘못된 요청입니다.",
60+
"data": null
61+
}
62+
""")
63+
)
64+
),
65+
@ApiResponse(
66+
responseCode = "401",
67+
description = "인증 실패 (토큰 없음/잘못됨/만료)",
68+
content = @Content(
69+
mediaType = "application/json",
70+
examples = {
71+
@ExampleObject(name = "토큰 없음", value = """
72+
{
73+
"success": false,
74+
"code": "AUTH_001",
75+
"message": "인증이 필요합니다.",
76+
"data": null
77+
}
78+
"""),
79+
@ExampleObject(name = "잘못된 토큰", value = """
80+
{
81+
"success": false,
82+
"code": "AUTH_002",
83+
"message": "유효하지 않은 액세스 토큰입니다.",
84+
"data": null
85+
}
86+
"""),
87+
@ExampleObject(name = "만료된 토큰", value = """
88+
{
89+
"success": false,
90+
"code": "AUTH_004",
91+
"message": "만료된 액세스 토큰입니다.",
92+
"data": null
93+
}
94+
""")
95+
}
96+
)
97+
),
98+
@ApiResponse(
99+
responseCode = "404",
100+
description = "존재하지 않는 사용자 또는 게시글",
101+
content = @Content(
102+
mediaType = "application/json",
103+
examples = {
104+
@ExampleObject(name = "존재하지 않는 사용자", value = """
105+
{
106+
"success": false,
107+
"code": "USER_001",
108+
"message": "존재하지 않는 사용자입니다.",
109+
"data": null
110+
}
111+
"""),
112+
@ExampleObject(name = "존재하지 않는 게시글", value = """
113+
{
114+
"success": false,
115+
"code": "POST_001",
116+
"message": "존재하지 않는 게시글입니다.",
117+
"data": null
118+
}
119+
""")
120+
}
121+
)
122+
),
123+
@ApiResponse(
124+
responseCode = "500",
125+
description = "서버 내부 오류",
126+
content = @Content(
127+
mediaType = "application/json",
128+
examples = @ExampleObject(value = """
129+
{
130+
"success": false,
131+
"code": "COMMON_500",
132+
"message": "서버 오류가 발생했습니다.",
133+
"data": null
134+
}
135+
""")
136+
)
137+
)
138+
})
139+
ResponseEntity<RsData<CommentResponse>> createComment(
140+
@PathVariable Long postId,
141+
@RequestBody CommentRequest request,
142+
@AuthenticationPrincipal CustomUserDetails user
143+
);
144+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.back.domain.board.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
5+
/**
6+
* 댓글 작성 및 수정을 위한 요청 DTO
7+
*
8+
* @param content 댓글 내용
9+
*/
10+
public record CommentRequest(
11+
@NotBlank String content
12+
) {
13+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.back.domain.board.dto;
2+
3+
import com.back.domain.board.entity.Comment;
4+
5+
import java.time.LocalDateTime;
6+
7+
/**
8+
* 댓글 응답 DTO
9+
*
10+
* @param commentId 댓글 Id
11+
* @param postId 게시글 Id
12+
* @param author 작성자 정보
13+
* @param content 댓글 내용
14+
* @param createdAt 댓글 생성 일시
15+
* @param updatedAt 댓글 수정 일시
16+
*/
17+
public record CommentResponse(
18+
Long commentId,
19+
Long postId,
20+
AuthorResponse author,
21+
String content,
22+
LocalDateTime createdAt,
23+
LocalDateTime updatedAt
24+
) {
25+
public static CommentResponse from(Comment comment) {
26+
return new CommentResponse(
27+
comment.getId(),
28+
comment.getPost().getId(),
29+
AuthorResponse.from(comment.getUser()),
30+
comment.getContent(),
31+
comment.getCreatedAt(),
32+
comment.getUpdatedAt()
33+
);
34+
}
35+
}

src/main/java/com/back/domain/board/entity/Comment.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,11 @@ public class Comment extends BaseEntity {
3434

3535
@OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true)
3636
private List<CommentLike> commentLikes = new ArrayList<>();
37+
38+
// -------------------- 생성자 --------------------
39+
public Comment(Post post, User user, String content) {
40+
this.post = post;
41+
this.user = user;
42+
this.content = content;
43+
}
3744
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.back.domain.board.repository;
2+
3+
import com.back.domain.board.entity.Comment;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.stereotype.Repository;
6+
7+
@Repository
8+
public interface CommentRepository extends JpaRepository<Comment, Long> {
9+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.back.domain.board.service;
2+
3+
import com.back.domain.board.dto.CommentRequest;
4+
import com.back.domain.board.dto.CommentResponse;
5+
import com.back.domain.board.entity.Comment;
6+
import com.back.domain.board.entity.Post;
7+
import com.back.domain.board.repository.CommentRepository;
8+
import com.back.domain.board.repository.PostRepository;
9+
import com.back.domain.user.entity.User;
10+
import com.back.domain.user.repository.UserRepository;
11+
import com.back.global.exception.CustomException;
12+
import com.back.global.exception.ErrorCode;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.stereotype.Service;
15+
import org.springframework.transaction.annotation.Transactional;
16+
17+
@Service
18+
@RequiredArgsConstructor
19+
@Transactional
20+
public class CommentService {
21+
private final CommentRepository commentRepository;
22+
private final UserRepository userRepository;
23+
private final PostRepository postRepository;
24+
25+
/**
26+
* 댓글 생성 서비스
27+
* 1. User 조회
28+
* 2. Post 조회
29+
* 3. Comment 생성
30+
* 4. Comment 저장 및 CommentResponse 반환
31+
*/
32+
public CommentResponse createComment(Long postId, CommentRequest request, Long userId) {
33+
// User 조회
34+
User user = userRepository.findById(userId)
35+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
36+
37+
// Post 조회
38+
Post post = postRepository.findById(postId)
39+
.orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND));
40+
41+
// Comment 생성
42+
Comment comment = new Comment(post, user, request.content());
43+
44+
// Comment 저장 및 응답 반환
45+
commentRepository.save(comment);
46+
return CommentResponse.from(comment);
47+
}
48+
}

0 commit comments

Comments
 (0)