diff --git a/src/main/java/com/gdsc_knu/official_homepage/controller/post/PostController.java b/src/main/java/com/gdsc_knu/official_homepage/controller/post/PostController.java index 6e8bcd76..7335bcab 100644 --- a/src/main/java/com/gdsc_knu/official_homepage/controller/post/PostController.java +++ b/src/main/java/com/gdsc_knu/official_homepage/controller/post/PostController.java @@ -104,7 +104,23 @@ public ResponseEntity> searchPosts(@RequestPar @RequestParam(value = "size", defaultValue = "20") int size) { return ResponseEntity.ok().body(postService.searchPostList(keyword, page, size)); } - + + @PostMapping("/{postId}/like") + @Operation(summary = "게시글 좋아요 API", description = "게시글에 좋아요를 누른다. 이미 좋아요를 누른 게시글이면 예외가 발생한다.") + public ResponseEntity likePost(@TokenMember JwtMemberDetail jwtMemberDetail, + @PathVariable("postId") Long postId) { + postService.likePost(jwtMemberDetail.getId(), postId); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{postId}/like") + @Operation(summary = "게시글 좋아요 취소 API", description = "게시글에 좋아요를 취소한다. 좋아요를 누르지 않은 게시글이면 예외가 발생한다.") + public ResponseEntity unlikePost(@TokenMember JwtMemberDetail jwtMemberDetail, + @PathVariable("postId") Long postId) { + postService.unlikePost(jwtMemberDetail.getId(), postId); + return ResponseEntity.ok().build(); + } + @GetMapping("trending") @Operation(summary = "카테고리별 인기글 5개 조회", description = "category가 null이면 전제를 조회한다.") public ResponseEntity> getTrendingPosts( diff --git a/src/main/java/com/gdsc_knu/official_homepage/dto/post/PostResponse.java b/src/main/java/com/gdsc_knu/official_homepage/dto/post/PostResponse.java index 20498bab..c84f2cba 100644 --- a/src/main/java/com/gdsc_knu/official_homepage/dto/post/PostResponse.java +++ b/src/main/java/com/gdsc_knu/official_homepage/dto/post/PostResponse.java @@ -43,7 +43,7 @@ public static Main from(Post post) { .category(post.getCategory().name()) .createAt(post.getPublishedAt()) .likeCount(post.getLikeCount()) - .commentCount(post.getCommentList().size()) + .commentCount(post.getCommentCount()) .sharedCount(post.getSharedCount()) .build(); @@ -70,8 +70,9 @@ public static class Detail { private int sharedCount; private boolean canDelete; private boolean canModify; + private boolean isLiked; - public static Detail from(Post post, AccessModel access) { + public static Detail from(Post post, AccessModel access, boolean isLiked) { int length = Math.min(post.getContent().length(), 20); return Detail.builder() .id(post.getId()) @@ -83,10 +84,11 @@ public static Detail from(Post post, AccessModel access) { .authorName(post.getMember().getName()) .createAt(post.getPublishedAt()) .likeCount(post.getLikeCount()) - .commentCount(post.getCommentList().size()) + .commentCount(post.getCommentCount()) .sharedCount(post.getSharedCount()) .canDelete(access.canDelete()) .canModify(access.canModify()) + .isLiked(isLiked) .build(); } diff --git a/src/main/java/com/gdsc_knu/official_homepage/entity/post/Post.java b/src/main/java/com/gdsc_knu/official_homepage/entity/post/Post.java index 55817572..431cd1a9 100644 --- a/src/main/java/com/gdsc_knu/official_homepage/entity/post/Post.java +++ b/src/main/java/com/gdsc_knu/official_homepage/entity/post/Post.java @@ -60,4 +60,20 @@ public void update(PostRequest.Update postRequest) { public boolean isSaved() { return this.status.equals(PostStatus.SAVED); } + + public void addCommentCount() { + this.commentCount++; + } + + public void subtractCommentCount(int deleteCount) { + this.commentCount =- deleteCount; + } + + public void addLikeCount() { + this.likeCount++; + } + + public void subtractLikeCount() { + this.likeCount--; + } } diff --git a/src/main/java/com/gdsc_knu/official_homepage/entity/post/PostLike.java b/src/main/java/com/gdsc_knu/official_homepage/entity/post/PostLike.java new file mode 100644 index 00000000..c2641fd3 --- /dev/null +++ b/src/main/java/com/gdsc_knu/official_homepage/entity/post/PostLike.java @@ -0,0 +1,29 @@ +package com.gdsc_knu.official_homepage.entity.post; + +import com.gdsc_knu.official_homepage.entity.Member; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PostLike { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + public static PostLike from(Post post, Member member) { + return PostLike.builder() + .post(post) + .member(member) + .build(); + } +} diff --git a/src/main/java/com/gdsc_knu/official_homepage/exception/ErrorCode.java b/src/main/java/com/gdsc_knu/official_homepage/exception/ErrorCode.java index 4ef183a3..5ef45d66 100644 --- a/src/main/java/com/gdsc_knu/official_homepage/exception/ErrorCode.java +++ b/src/main/java/com/gdsc_knu/official_homepage/exception/ErrorCode.java @@ -36,6 +36,8 @@ public enum ErrorCode { // Post POST_NOT_FOUND(404, HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다."), POST_FORBIDDEN(403, HttpStatus.FORBIDDEN, "게시글을 수정할 수 있는 권한이 없습니다."), + POST_ALREADY_LIKED(409, HttpStatus.CONFLICT, "이미 좋아요를 누른 게시글입니다."), + POST_NOT_LIKED(404, HttpStatus.NOT_FOUND, "좋아요를 누르지 않은 게시글입니다."), // Comment COMMENT_NOT_FOUND(404, HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다."), diff --git a/src/main/java/com/gdsc_knu/official_homepage/repository/PostLikeRepository.java b/src/main/java/com/gdsc_knu/official_homepage/repository/PostLikeRepository.java new file mode 100644 index 00000000..3be0b4de --- /dev/null +++ b/src/main/java/com/gdsc_knu/official_homepage/repository/PostLikeRepository.java @@ -0,0 +1,10 @@ +package com.gdsc_knu.official_homepage.repository; + +import com.gdsc_knu.official_homepage.entity.post.PostLike; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface PostLikeRepository extends JpaRepository { + Optional findByMemberIdAndPostId(Long memberId, Long postId); +} diff --git a/src/main/java/com/gdsc_knu/official_homepage/service/post/CommentService.java b/src/main/java/com/gdsc_knu/official_homepage/service/post/CommentService.java index 03fc4f05..f845e9e0 100644 --- a/src/main/java/com/gdsc_knu/official_homepage/service/post/CommentService.java +++ b/src/main/java/com/gdsc_knu/official_homepage/service/post/CommentService.java @@ -35,6 +35,7 @@ public void createComment(Long memberId, Long postId, CommentRequest.Create requ Comment parent = getParentComment(request.getGroupId()); Comment comment = Comment.from(request.getContent(), member, post, parent); commentRepository.save(comment); + post.addCommentCount(); } private Comment getParentComment(Long parentId) { @@ -91,6 +92,8 @@ public void deleteComment(Long memberId, Long commentId) { if (!comment.getAuthor().getId().equals(memberId)) { throw new CustomException(ErrorCode.COMMENT_FORBIDDEN); } + int deleteCount = 1 + comment.getReplies().size(); commentRepository.delete(comment); + comment.getPost().subtractCommentCount(deleteCount); } } diff --git a/src/main/java/com/gdsc_knu/official_homepage/service/post/PostService.java b/src/main/java/com/gdsc_knu/official_homepage/service/post/PostService.java index fa110af5..a7b96d8c 100644 --- a/src/main/java/com/gdsc_knu/official_homepage/service/post/PostService.java +++ b/src/main/java/com/gdsc_knu/official_homepage/service/post/PostService.java @@ -26,4 +26,8 @@ public interface PostService { PagingResponse searchPostList(String keyword, int page, int size); List getTrendingPosts(Category category, int size); + + void likePost(Long memberId, Long postId); + + void unlikePost(Long memberId, Long postId); } diff --git a/src/main/java/com/gdsc_knu/official_homepage/service/post/PostServiceImpl.java b/src/main/java/com/gdsc_knu/official_homepage/service/post/PostServiceImpl.java index 607f57b0..b89d7012 100644 --- a/src/main/java/com/gdsc_knu/official_homepage/service/post/PostServiceImpl.java +++ b/src/main/java/com/gdsc_knu/official_homepage/service/post/PostServiceImpl.java @@ -7,11 +7,13 @@ import com.gdsc_knu.official_homepage.entity.Member; import com.gdsc_knu.official_homepage.entity.enumeration.Role; import com.gdsc_knu.official_homepage.entity.post.Post; +import com.gdsc_knu.official_homepage.entity.post.PostLike; import com.gdsc_knu.official_homepage.entity.post.enumeration.Category; import com.gdsc_knu.official_homepage.entity.post.enumeration.PostStatus; import com.gdsc_knu.official_homepage.exception.CustomException; import com.gdsc_knu.official_homepage.exception.ErrorCode; import com.gdsc_knu.official_homepage.repository.MemberRepository; +import com.gdsc_knu.official_homepage.repository.PostLikeRepository; import com.gdsc_knu.official_homepage.repository.PostRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -31,6 +33,7 @@ public class PostServiceImpl implements PostService { private final PostRepository postRepository; private final MemberRepository memberRepository; + private final PostLikeRepository postLikeRepository; /** * 게시글 작성, 회원만 작성 가능 @@ -62,7 +65,8 @@ public PostResponse.Detail getPost(Long memberId, Long postId) { throw new CustomException(ErrorCode.POST_NOT_FOUND); } AccessModel access = getPostAccess(memberId, post.getMember().getId()); - return PostResponse.Detail.from(post, access); + boolean isLiked = memberId != 0L && postLikeRepository.findByMemberIdAndPostId(memberId, postId).isPresent(); + return PostResponse.Detail.from(post, access, isLiked); } /** @@ -142,6 +146,30 @@ public void deletePost(Long memberId, Long postId) { postRepository.delete(post); } + @Override + public void likePost(Long memberId, Long postId) { + postLikeRepository.findByMemberIdAndPostId(memberId, postId) + .ifPresent(postLike -> { + throw new CustomException(ErrorCode.POST_ALREADY_LIKED); + }); + Post post = postRepository.findById(postId) + .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + postLikeRepository.save(PostLike.from(post, member)); + post.addLikeCount(); + } + + @Override + public void unlikePost(Long memberId, Long postId) { + PostLike postLike = postLikeRepository.findByMemberIdAndPostId(memberId, postId) + .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_LIKED)); + postLikeRepository.delete(postLike); + Post post = postRepository.findById(postId) + .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); + post.subtractLikeCount(); + } + @Override @Transactional(readOnly = true) @Cacheable(value = "trending-post", key = "'Is '+#category", unless="#result.size()<5")