Skip to content

Commit 08acaf3

Browse files
authored
Feat: 게시글 좋아요/좋아요 취소 API 구현 (#201) (#203)
* Feat: 게시글 좋아요 API 구현 # Conflicts: # src/main/java/com/back/domain/board/comment/dto/CommentLikeResponse.java * Feat: 게시글 좋아요 취소 API 구현 * Test: 테스트 작성 * Docs: Swagger 문서 작성
1 parent 884ff2c commit 08acaf3

File tree

11 files changed

+901
-1
lines changed

11 files changed

+901
-1
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.back.domain.board.post.controller;
2+
3+
import com.back.domain.board.post.dto.PostLikeResponse;
4+
import com.back.domain.board.post.service.PostLikeService;
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}/like")
14+
@RequiredArgsConstructor
15+
public class PostLikeController implements PostLikeControllerDocs {
16+
private final PostLikeService postLikeService;
17+
18+
// 게시글 좋아요
19+
@PostMapping
20+
public ResponseEntity<RsData<PostLikeResponse>> likePost(
21+
@PathVariable Long postId,
22+
@AuthenticationPrincipal CustomUserDetails user
23+
) {
24+
PostLikeResponse response = postLikeService.likePost(postId, user.getUserId());
25+
return ResponseEntity
26+
.ok(RsData.success(
27+
"게시글 좋아요가 등록되었습니다.",
28+
response
29+
));
30+
}
31+
32+
// 게시글 좋아요 취소
33+
@DeleteMapping
34+
public ResponseEntity<RsData<PostLikeResponse>> cancelLikePost(
35+
@PathVariable Long postId,
36+
@AuthenticationPrincipal CustomUserDetails user
37+
) {
38+
PostLikeResponse response = postLikeService.cancelLikePost(postId, user.getUserId());
39+
return ResponseEntity
40+
.ok(RsData.success(
41+
"게시글 좋아요가 취소되었습니다.",
42+
response
43+
));
44+
}
45+
}
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
package com.back.domain.board.post.controller;
2+
3+
import com.back.domain.board.post.dto.PostLikeResponse;
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 = "Post Like API", description = "게시글 좋아요 관련 API")
17+
public interface PostLikeControllerDocs {
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_200",
33+
"message": "게시글 좋아요가 등록되었습니다.",
34+
"data": {
35+
"postId": 101,
36+
"likeCount": 11
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": "POST_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": "POST_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<PostLikeResponse>> likePost(
147+
@PathVariable Long postId,
148+
@AuthenticationPrincipal CustomUserDetails user
149+
);
150+
151+
@Operation(
152+
summary = "게시글 좋아요 취소",
153+
description = "로그인한 사용자가 특정 게시글의 좋아요를 취소합니다."
154+
)
155+
@ApiResponses({
156+
@ApiResponse(
157+
responseCode = "200",
158+
description = "게시글 좋아요 취소 성공",
159+
content = @Content(
160+
mediaType = "application/json",
161+
examples = @ExampleObject(value = """
162+
{
163+
"success": true,
164+
"code": "SUCCESS_200",
165+
"message": "게시글 좋아요가 취소되었습니다.",
166+
"data": {
167+
"postId": 101,
168+
"likeCount": 10
169+
}
170+
}
171+
""")
172+
)
173+
),
174+
@ApiResponse(
175+
responseCode = "400",
176+
description = "잘못된 요청 (파라미터 누락 등)",
177+
content = @Content(
178+
mediaType = "application/json",
179+
examples = @ExampleObject(value = """
180+
{
181+
"success": false,
182+
"code": "COMMON_400",
183+
"message": "잘못된 요청입니다.",
184+
"data": null
185+
}
186+
""")
187+
)
188+
),
189+
@ApiResponse(
190+
responseCode = "401",
191+
description = "인증 실패 (Access Token 없음/만료/잘못됨)",
192+
content = @Content(
193+
mediaType = "application/json",
194+
examples = {
195+
@ExampleObject(name = "토큰 없음", value = """
196+
{
197+
"success": false,
198+
"code": "AUTH_001",
199+
"message": "인증이 필요합니다.",
200+
"data": null
201+
}
202+
"""),
203+
@ExampleObject(name = "잘못된 토큰", value = """
204+
{
205+
"success": false,
206+
"code": "AUTH_002",
207+
"message": "유효하지 않은 액세스 토큰입니다.",
208+
"data": null
209+
}
210+
"""),
211+
@ExampleObject(name = "만료된 토큰", value = """
212+
{
213+
"success": false,
214+
"code": "AUTH_004",
215+
"message": "만료된 액세스 토큰입니다.",
216+
"data": null
217+
}
218+
""")
219+
}
220+
)
221+
),
222+
@ApiResponse(
223+
responseCode = "404",
224+
description = "존재하지 않는 사용자 / 게시글 / 좋아요 기록 없음",
225+
content = @Content(
226+
mediaType = "application/json",
227+
examples = {
228+
@ExampleObject(name = "존재하지 않는 사용자", value = """
229+
{
230+
"success": false,
231+
"code": "USER_001",
232+
"message": "존재하지 않는 사용자입니다.",
233+
"data": null
234+
}
235+
"""),
236+
@ExampleObject(name = "존재하지 않는 게시글", value = """
237+
{
238+
"success": false,
239+
"code": "POST_001",
240+
"message": "존재하지 않는 게시글입니다.",
241+
"data": null
242+
}
243+
"""),
244+
@ExampleObject(name = "좋아요 기록 없음", value = """
245+
{
246+
"success": false,
247+
"code": "POST_006",
248+
"message": "해당 게시글에 대한 좋아요 기록이 없습니다.",
249+
"data": null
250+
}
251+
""")
252+
}
253+
)
254+
),
255+
@ApiResponse(
256+
responseCode = "500",
257+
description = "서버 내부 오류",
258+
content = @Content(
259+
mediaType = "application/json",
260+
examples = @ExampleObject(value = """
261+
{
262+
"success": false,
263+
"code": "COMMON_500",
264+
"message": "서버 오류가 발생했습니다.",
265+
"data": null
266+
}
267+
""")
268+
)
269+
)
270+
})
271+
ResponseEntity<RsData<PostLikeResponse>> cancelLikePost(
272+
@PathVariable Long postId,
273+
@AuthenticationPrincipal CustomUserDetails user
274+
);
275+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.back.domain.board.post.dto;
2+
3+
import com.back.domain.board.post.entity.Post;
4+
5+
/**
6+
* 게시글 좋아요 응답 DTO
7+
*
8+
* @param postId 게시글 id
9+
* @param likeCount 좋아요 수
10+
*/
11+
public record PostLikeResponse(
12+
Long postId,
13+
Long likeCount
14+
) {
15+
public static PostLikeResponse from(Post post) {
16+
return new PostLikeResponse(
17+
post.getId(),
18+
post.getLikeCount()
19+
);
20+
}
21+
}

src/main/java/com/back/domain/board/post/entity/Post.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public class Post extends BaseEntity {
2222

2323
private String content;
2424

25+
// TODO: 추후 PostRepositoryImpl#searchPosts 로직 개선 필요, ERD에도 반영할 것
26+
@Column(nullable = false)
27+
private Long likeCount = 0L;
28+
2529
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
2630
private List<PostCategoryMapping> postCategoryMappings = new ArrayList<>();
2731

@@ -56,6 +60,18 @@ public void updateCategories(List<PostCategory> categories) {
5660
);
5761
}
5862

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+
}
74+
5975
// -------------------- 헬퍼 메서드 --------------------
6076
// 게시글에 연결된 카테고리 목록 조회
6177
public List<PostCategory> getCategories() {

src/main/java/com/back/domain/board/post/entity/PostLike.java

Lines changed: 3 additions & 1 deletion
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
13-
@NoArgsConstructor
1414
@Getter
15+
@NoArgsConstructor
16+
@AllArgsConstructor
1517
public class PostLike extends BaseEntity {
1618
@ManyToOne(fetch = FetchType.LAZY)
1719
@JoinColumn(name = "post_id")

0 commit comments

Comments
 (0)