Skip to content

Commit fd6f8bf

Browse files
authored
Feat: 댓글 좋아요/좋아요 취소 API 구현 (#181) (#200)
* Feat: 댓글 좋아요 API 구현 * Feat: 댓글 좋아요 취소 API 구현 * Test: 테스트 작성 * Docs: Swagger 문서 작성
1 parent cdfb3f6 commit fd6f8bf

File tree

12 files changed

+967
-2
lines changed

12 files changed

+967
-2
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.back.domain.board.comment.controller;
2+
3+
import com.back.domain.board.comment.dto.CommentLikeResponse;
4+
import com.back.domain.board.comment.service.CommentLikeService;
5+
import com.back.global.common.dto.RsData;
6+
import com.back.global.security.user.CustomUserDetails;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
10+
import org.springframework.web.bind.annotation.*;
11+
12+
@RestController
13+
@RequestMapping("/api/posts/{postId}/comments/{commentId}/like")
14+
@RequiredArgsConstructor
15+
public class CommentLikeController implements CommentLikeControllerDocs {
16+
private final CommentLikeService commentLikeService;
17+
18+
// 댓글 좋아요
19+
@PostMapping
20+
public ResponseEntity<RsData<CommentLikeResponse>> likeComment(
21+
@PathVariable Long postId,
22+
@PathVariable Long commentId,
23+
@AuthenticationPrincipal CustomUserDetails user
24+
) {
25+
CommentLikeResponse response = commentLikeService.likeComment(commentId, user.getUserId());
26+
return ResponseEntity
27+
.ok(RsData.success(
28+
"댓글 좋아요가 등록되었습니다.",
29+
response
30+
));
31+
}
32+
33+
// 댓글 좋아요 취소
34+
@DeleteMapping
35+
public ResponseEntity<RsData<CommentLikeResponse>> cancelLikeComment(
36+
@PathVariable Long postId,
37+
@PathVariable Long commentId,
38+
@AuthenticationPrincipal CustomUserDetails user
39+
) {
40+
CommentLikeResponse response = commentLikeService.cancelLikeComment(commentId, user.getUserId());
41+
return ResponseEntity
42+
.ok(RsData.success(
43+
"댓글 좋아요가 취소되었습니다.",
44+
response
45+
));
46+
}
47+
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
package com.back.domain.board.comment.controller;
2+
3+
import com.back.domain.board.comment.dto.CommentLikeResponse;
4+
import com.back.global.common.dto.RsData;
5+
import com.back.global.security.user.CustomUserDetails;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.media.Content;
8+
import io.swagger.v3.oas.annotations.media.ExampleObject;
9+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
10+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
11+
import io.swagger.v3.oas.annotations.tags.Tag;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
14+
import org.springframework.web.bind.annotation.PathVariable;
15+
16+
@Tag(name = "Comment Like API", description = "댓글 좋아요 관련 API")
17+
public interface CommentLikeControllerDocs {
18+
19+
@Operation(
20+
summary = "댓글 좋아요 등록",
21+
description = "로그인한 사용자가 특정 댓글에 좋아요를 등록합니다."
22+
)
23+
@ApiResponses({
24+
@ApiResponse(
25+
responseCode = "200",
26+
description = "댓글 좋아요 등록 성공",
27+
content = @Content(
28+
mediaType = "application/json",
29+
examples = @ExampleObject(value = """
30+
{
31+
"success": true,
32+
"code": "SUCCESS_201",
33+
"message": "댓글 좋아요가 등록되었습니다.",
34+
"data": {
35+
"commentId": 25,
36+
"likeCount": 4
37+
}
38+
}
39+
""")
40+
)
41+
),
42+
@ApiResponse(
43+
responseCode = "400",
44+
description = "잘못된 요청 (파라미터 누락 등)",
45+
content = @Content(
46+
mediaType = "application/json",
47+
examples = @ExampleObject(value = """
48+
{
49+
"success": false,
50+
"code": "COMMON_400",
51+
"message": "잘못된 요청입니다.",
52+
"data": null
53+
}
54+
""")
55+
)
56+
),
57+
@ApiResponse(
58+
responseCode = "401",
59+
description = "인증 실패 (Access Token 없음/만료/잘못됨)",
60+
content = @Content(
61+
mediaType = "application/json",
62+
examples = {
63+
@ExampleObject(name = "토큰 없음", value = """
64+
{
65+
"success": false,
66+
"code": "AUTH_001",
67+
"message": "인증이 필요합니다.",
68+
"data": null
69+
}
70+
"""),
71+
@ExampleObject(name = "잘못된 토큰", value = """
72+
{
73+
"success": false,
74+
"code": "AUTH_002",
75+
"message": "유효하지 않은 액세스 토큰입니다.",
76+
"data": null
77+
}
78+
"""),
79+
@ExampleObject(name = "만료된 토큰", value = """
80+
{
81+
"success": false,
82+
"code": "AUTH_004",
83+
"message": "만료된 액세스 토큰입니다.",
84+
"data": null
85+
}
86+
""")
87+
}
88+
)
89+
),
90+
@ApiResponse(
91+
responseCode = "404",
92+
description = "존재하지 않는 사용자 또는 댓글",
93+
content = @Content(
94+
mediaType = "application/json",
95+
examples = {
96+
@ExampleObject(name = "존재하지 않는 사용자", value = """
97+
{
98+
"success": false,
99+
"code": "USER_001",
100+
"message": "존재하지 않는 사용자입니다.",
101+
"data": null
102+
}
103+
"""),
104+
@ExampleObject(name = "존재하지 않는 댓글", value = """
105+
{
106+
"success": false,
107+
"code": "COMMENT_001",
108+
"message": "존재하지 않는 댓글입니다.",
109+
"data": null
110+
}
111+
""")
112+
}
113+
)
114+
),
115+
@ApiResponse(
116+
responseCode = "409",
117+
description = "이미 좋아요한 댓글",
118+
content = @Content(
119+
mediaType = "application/json",
120+
examples = @ExampleObject(value = """
121+
{
122+
"success": false,
123+
"code": "COMMENT_005",
124+
"message": "이미 좋아요한 댓글입니다.",
125+
"data": null
126+
}
127+
""")
128+
)
129+
),
130+
@ApiResponse(
131+
responseCode = "500",
132+
description = "서버 내부 오류",
133+
content = @Content(
134+
mediaType = "application/json",
135+
examples = @ExampleObject(value = """
136+
{
137+
"success": false,
138+
"code": "COMMON_500",
139+
"message": "서버 오류가 발생했습니다.",
140+
"data": null
141+
}
142+
""")
143+
)
144+
)
145+
})
146+
ResponseEntity<RsData<CommentLikeResponse>> likeComment(
147+
@PathVariable Long postId,
148+
@PathVariable Long commentId,
149+
@AuthenticationPrincipal CustomUserDetails user
150+
);
151+
152+
@Operation(
153+
summary = "댓글 좋아요 취소",
154+
description = "로그인한 사용자가 특정 댓글에 등록한 좋아요를 취소합니다."
155+
)
156+
@ApiResponses({
157+
@ApiResponse(
158+
responseCode = "200",
159+
description = "댓글 좋아요 취소 성공",
160+
content = @Content(
161+
mediaType = "application/json",
162+
examples = @ExampleObject(value = """
163+
{
164+
"success": true,
165+
"code": "SUCCESS_200",
166+
"message": "댓글 좋아요가 취소되었습니다.",
167+
"data": {
168+
"commentId": 25,
169+
"likeCount": 3
170+
}
171+
}
172+
""")
173+
)
174+
),
175+
@ApiResponse(
176+
responseCode = "400",
177+
description = "잘못된 요청 (파라미터 누락 등)",
178+
content = @Content(
179+
mediaType = "application/json",
180+
examples = @ExampleObject(value = """
181+
{
182+
"success": false,
183+
"code": "COMMON_400",
184+
"message": "잘못된 요청입니다.",
185+
"data": null
186+
}
187+
""")
188+
)
189+
),
190+
@ApiResponse(
191+
responseCode = "401",
192+
description = "인증 실패 (Access Token 없음/만료/잘못됨)",
193+
content = @Content(
194+
mediaType = "application/json",
195+
examples = {
196+
@ExampleObject(name = "토큰 없음", value = """
197+
{
198+
"success": false,
199+
"code": "AUTH_001",
200+
"message": "인증이 필요합니다.",
201+
"data": null
202+
}
203+
"""),
204+
@ExampleObject(name = "잘못된 토큰", value = """
205+
{
206+
"success": false,
207+
"code": "AUTH_002",
208+
"message": "유효하지 않은 액세스 토큰입니다.",
209+
"data": null
210+
}
211+
"""),
212+
@ExampleObject(name = "만료된 토큰", value = """
213+
{
214+
"success": false,
215+
"code": "AUTH_004",
216+
"message": "만료된 액세스 토큰입니다.",
217+
"data": null
218+
}
219+
""")
220+
}
221+
)
222+
),
223+
@ApiResponse(
224+
responseCode = "404",
225+
description = "존재하지 않는 사용자, 댓글 또는 좋아요 기록 없음",
226+
content = @Content(
227+
mediaType = "application/json",
228+
examples = {
229+
@ExampleObject(name = "존재하지 않는 사용자", value = """
230+
{
231+
"success": false,
232+
"code": "USER_001",
233+
"message": "존재하지 않는 사용자입니다.",
234+
"data": null
235+
}
236+
"""),
237+
@ExampleObject(name = "존재하지 않는 댓글", value = """
238+
{
239+
"success": false,
240+
"code": "COMMENT_001",
241+
"message": "존재하지 않는 댓글입니다.",
242+
"data": null
243+
}
244+
"""),
245+
@ExampleObject(name = "좋아요 기록 없음", value = """
246+
{
247+
"success": false,
248+
"code": "COMMENT_006",
249+
"message": "해당 댓글에 대한 좋아요 기록이 없습니다.",
250+
"data": null
251+
}
252+
""")
253+
}
254+
)
255+
),
256+
@ApiResponse(
257+
responseCode = "500",
258+
description = "서버 내부 오류",
259+
content = @Content(
260+
mediaType = "application/json",
261+
examples = @ExampleObject(value = """
262+
{
263+
"success": false,
264+
"code": "COMMON_500",
265+
"message": "서버 오류가 발생했습니다.",
266+
"data": null
267+
}
268+
""")
269+
)
270+
)
271+
})
272+
ResponseEntity<RsData<CommentLikeResponse>> cancelLikeComment(
273+
@PathVariable Long postId,
274+
@PathVariable Long commentId,
275+
@AuthenticationPrincipal CustomUserDetails user
276+
);
277+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.back.domain.board.comment.dto;
2+
3+
import com.back.domain.board.comment.entity.Comment;
4+
5+
/**
6+
* 댓글 좋아요 응답 DTO
7+
*
8+
* @param commentId
9+
* @param likeCount
10+
*/
11+
public record CommentLikeResponse(
12+
Long commentId,
13+
Long likeCount
14+
) {
15+
public static CommentLikeResponse from(Comment comment) {
16+
return new CommentLikeResponse(
17+
comment.getId(),
18+
comment.getLikeCount()
19+
);
20+
}
21+
}

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ public class Comment extends BaseEntity {
2424

2525
private String content;
2626

27+
// TODO: 추후 CommentRepositoryImpl#getCommentsByPostId 로직 개선 필요, ERD에도 반영할 것
28+
@Column(nullable = false)
29+
private Long likeCount = 0L;
30+
2731
// 해당 댓글의 부모 댓글
2832
@ManyToOne(fetch = FetchType.LAZY)
2933
@JoinColumn(name = "parent_comment_id")
@@ -49,10 +53,22 @@ public Comment(Post post, User user, String content, Comment parent) {
4953
this.content = content;
5054
this.parent = parent;
5155
}
52-
56+
5357
// -------------------- 비즈니스 메서드 --------------------
5458
// 댓글 업데이트
5559
public void update(String content) {
5660
this.content = content;
5761
}
62+
63+
// 좋아요 수 증가
64+
public void increaseLikeCount() {
65+
this.likeCount++;
66+
}
67+
68+
// 좋아요 수 감소
69+
public void decreaseLikeCount() {
70+
if (this.likeCount > 0) {
71+
this.likeCount--;
72+
}
73+
}
5874
}

src/main/java/com/back/domain/board/comment/entity/CommentLike.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
import jakarta.persistence.FetchType;
77
import jakarta.persistence.JoinColumn;
88
import jakarta.persistence.ManyToOne;
9+
import lombok.AllArgsConstructor;
910
import lombok.Getter;
1011
import lombok.NoArgsConstructor;
1112

1213
@Entity
1314
@Getter
1415
@NoArgsConstructor
16+
@AllArgsConstructor
1517
public class CommentLike extends BaseEntity {
1618
@ManyToOne(fetch = FetchType.LAZY)
1719
@JoinColumn(name = "comment_id")

0 commit comments

Comments
 (0)