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
@@ -0,0 +1,45 @@
package com.back.domain.board.post.controller;

import com.back.domain.board.post.dto.PostLikeResponse;
import com.back.domain.board.post.service.PostLikeService;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/posts/{postId}/like")
@RequiredArgsConstructor
public class PostLikeController implements PostLikeControllerDocs {
private final PostLikeService postLikeService;

// 게시글 좋아요
@PostMapping
public ResponseEntity<RsData<PostLikeResponse>> likePost(
@PathVariable Long postId,
@AuthenticationPrincipal CustomUserDetails user
) {
PostLikeResponse response = postLikeService.likePost(postId, user.getUserId());
return ResponseEntity
.ok(RsData.success(
"게시글 좋아요가 등록되었습니다.",
response
));
}

// 게시글 좋아요 취소
@DeleteMapping
public ResponseEntity<RsData<PostLikeResponse>> cancelLikePost(
@PathVariable Long postId,
@AuthenticationPrincipal CustomUserDetails user
) {
PostLikeResponse response = postLikeService.cancelLikePost(postId, user.getUserId());
return ResponseEntity
.ok(RsData.success(
"게시글 좋아요가 취소되었습니다.",
response
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package com.back.domain.board.post.controller;

import com.back.domain.board.post.dto.PostLikeResponse;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;

@Tag(name = "Post Like API", description = "게시글 좋아요 관련 API")
public interface PostLikeControllerDocs {

@Operation(
summary = "게시글 좋아요 등록",
description = "로그인한 사용자가 특정 게시글에 좋아요를 등록합니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "게시글 좋아요 등록 성공",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": true,
"code": "SUCCESS_200",
"message": "게시글 좋아요가 등록되었습니다.",
"data": {
"postId": 101,
"likeCount": 11
}
}
""")
)
),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청 (파라미터 누락 등)",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_400",
"message": "잘못된 요청입니다.",
"data": null
}
""")
)
),
@ApiResponse(
responseCode = "401",
description = "인증 실패 (Access Token 없음/만료/잘못됨)",
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
}
""")
}
)
),
@ApiResponse(
responseCode = "409",
description = "이미 좋아요한 게시글",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "POST_005",
"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<PostLikeResponse>> likePost(
@PathVariable Long postId,
@AuthenticationPrincipal CustomUserDetails user
);

@Operation(
summary = "게시글 좋아요 취소",
description = "로그인한 사용자가 특정 게시글의 좋아요를 취소합니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "게시글 좋아요 취소 성공",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": true,
"code": "SUCCESS_200",
"message": "게시글 좋아요가 취소되었습니다.",
"data": {
"postId": 101,
"likeCount": 10
}
}
""")
)
),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청 (파라미터 누락 등)",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_400",
"message": "잘못된 요청입니다.",
"data": null
}
""")
)
),
@ApiResponse(
responseCode = "401",
description = "인증 실패 (Access Token 없음/만료/잘못됨)",
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": "POST_006",
"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<PostLikeResponse>> cancelLikePost(
@PathVariable Long postId,
@AuthenticationPrincipal CustomUserDetails user
);
}
21 changes: 21 additions & 0 deletions src/main/java/com/back/domain/board/post/dto/PostLikeResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.back.domain.board.post.dto;

import com.back.domain.board.post.entity.Post;

/**
* 게시글 좋아요 응답 DTO
*
* @param postId 게시글 id
* @param likeCount 좋아요 수
*/
public record PostLikeResponse(
Long postId,
Long likeCount
) {
public static PostLikeResponse from(Post post) {
return new PostLikeResponse(
post.getId(),
post.getLikeCount()
);
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/back/domain/board/post/entity/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public class Post extends BaseEntity {

private String content;

// TODO: 추후 PostRepositoryImpl#searchPosts 로직 개선 필요, ERD에도 반영할 것
@Column(nullable = false)
private Long likeCount = 0L;

@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostCategoryMapping> postCategoryMappings = new ArrayList<>();

Expand Down Expand Up @@ -56,6 +60,18 @@ public void updateCategories(List<PostCategory> categories) {
);
}

// 좋아요 수 증가
public void increaseLikeCount() {
this.likeCount++;
}

// 좋아요 수 감소
public void decreaseLikeCount() {
if (this.likeCount > 0) {
this.likeCount--;
}
}

// -------------------- 헬퍼 메서드 --------------------
// 게시글에 연결된 카테고리 목록 조회
public List<PostCategory> getCategories() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class PostLike extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
Expand Down
Loading