diff --git a/daepiro-api/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java b/daepiro-api/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java index 3a6c9867..e7e9ecd6 100644 --- a/daepiro-api/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java +++ b/daepiro-api/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java @@ -128,7 +128,7 @@ public CreateCommentResponse createComment(Long articleId, CreateCommentRequest Article article = articleRepository.findByIdFetchJoin(articleId) .orElseThrow(NotFoundArticleException::new); CommentEntity savedComment = commentRepository.save( - new CommentEntity(request.getContent(), article, member) + CommentEntity.of(request.getContent(), article, member) ); Member articleOwner = article.getArticleOwner(); diff --git a/daepiro-api/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java b/daepiro-api/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java index d7a4ab54..39e23d7f 100644 --- a/daepiro-api/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java +++ b/daepiro-api/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java @@ -54,7 +54,7 @@ public CreateChildCommentResponse createChildComment( CommentEntity parentComment = commentRepository.findById(parentCommentId) .orElseThrow(NotFoundCommentException::new); - CommentEntity childComment = commentRepository.save(new CommentEntity(request.getContent(), article, member)); + CommentEntity childComment = commentRepository.save(CommentEntity.of(request.getContent(), article, member)); childComment.updateParent(parentComment); Member owner = memberRepository.findById(parentComment.getAuthorId()) diff --git a/daepiro-api/src/main/java/com/numberone/backend/domain/like/service/LikeService.java b/daepiro-api/src/main/java/com/numberone/backend/domain/like/service/LikeService.java index 721072df..eda1ed13 100644 --- a/daepiro-api/src/main/java/com/numberone/backend/domain/like/service/LikeService.java +++ b/daepiro-api/src/main/java/com/numberone/backend/domain/like/service/LikeService.java @@ -13,12 +13,10 @@ import com.numberone.backend.domain.notification.entity.NotificationEntity; import com.numberone.backend.domain.notification.entity.NotificationTag; import com.numberone.backend.domain.notification.repository.NotificationRepository; +import com.numberone.backend.exception.notfound.*; import com.numberone.backend.provider.security.SecurityContextProvider; import com.numberone.backend.exception.conflict.AlreadyLikedException; import com.numberone.backend.exception.conflict.AlreadyUnLikedException; -import com.numberone.backend.exception.notfound.NotFoundApiException; -import com.numberone.backend.exception.notfound.NotFoundCommentException; -import com.numberone.backend.exception.notfound.NotFoundMemberException; import com.numberone.backend.provider.fcm.service.FcmMessageProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -43,17 +41,16 @@ public class LikeService { @Transactional public Integer increaseArticleLike(Long articleId) { - long principal = SecurityContextProvider.getAuthenticatedUserId(); - Member member = memberRepository.findById(principal) + Long memberId = SecurityContextProvider.getAuthenticatedUserId(); + Member member = memberRepository.findById(memberId) .orElseThrow(NotFoundMemberException::new); Article article = articleRepository.findByIdFetchJoin(articleId) - .orElseThrow(NotFoundApiException::new); - if (isAlreadyLikedArticle(member, articleId)) { - // 이미 좋아요를 누른 게시글입니다. + .orElseThrow(NotFoundArticleException::new); + + if (isAlreadyLikedArticle(memberId, articleId)) throw new AlreadyLikedException(); - } article.increaseLikeCount(); - articleLikeRepository.save(new ArticleLike(member, article)); + articleLikeRepository.save(ArticleLike.of(member, article)); Member articleOwner = article.getArticleOwner(); String memberName = member.getNickName() != null ? member.getNickName() : member.getRealName(); @@ -70,40 +67,35 @@ public Integer increaseArticleLike(Long articleId) { @Transactional public Integer decreaseArticleLike(Long articleId) { - long principal = SecurityContextProvider.getAuthenticatedUserId(); - Member member = memberRepository.findById(principal) + Long memberId = SecurityContextProvider.getAuthenticatedUserId(); + Member member = memberRepository.findById(memberId) .orElseThrow(NotFoundMemberException::new); Article article = articleRepository.findById(articleId) .orElseThrow(NotFoundApiException::new); - if (!isAlreadyLikedArticle(member, articleId)) { - // 좋아요를 누르지 않은 게시글이라 취소할 수 없습니다. + if (!isAlreadyLikedArticle(member.getId(), articleId)) throw new AlreadyUnLikedException(); - } - article.decreaseLikeCount(); // 사용자의 게시글 좋아요 목록에서 제거 - List articleLikeList = articleLikeRepository.findByMember(member); - articleLikeList.forEach(articleLike -> { - if (articleLike.getArticleId().equals(articleId)) - articleLikeRepository.delete(articleLike); - }); + article.decreaseLikeCount(); + ArticleLike articleLike = articleLikeRepository.findByMemberIdAndArticleId(memberId, articleId) + .orElseThrow(NotFoundArticleLikeException::new); + articleLikeRepository.delete(articleLike); return article.getLikeCount(); } @Transactional public Integer increaseCommentLike(Long commentId) { - long principal = SecurityContextProvider.getAuthenticatedUserId(); - Member member = memberRepository.findById(principal) + Long memberId = SecurityContextProvider.getAuthenticatedUserId(); + Member member = memberRepository.findById(memberId) .orElseThrow(NotFoundMemberException::new); CommentEntity commentEntity = commentRepository.findById(commentId) .orElseThrow(NotFoundCommentException::new); - if (isAlreadyLikedComment(member, commentId)) { - // 이미 좋아요를 누른 댓글입니다. + if (isAlreadyLikedComment(member.getId(), commentId)) throw new AlreadyLikedException(); - } + commentEntity.increaseLikeCount(); - commentLikeRepository.save(new CommentLike(member, commentEntity)); + commentLikeRepository.save(CommentLike.of(member, commentEntity)); Long ownerId = commentEntity.getAuthorId(); @@ -124,34 +116,27 @@ public Integer increaseCommentLike(Long commentId) { @Transactional public Integer decreaseCommentLike(Long commentId) { - long principal = SecurityContextProvider.getAuthenticatedUserId(); - Member member = memberRepository.findById(principal) + long memberId = SecurityContextProvider.getAuthenticatedUserId(); + Member member = memberRepository.findById(memberId) .orElseThrow(NotFoundMemberException::new); CommentEntity commentEntity = commentRepository.findById(commentId) .orElseThrow(NotFoundCommentException::new); - if (!isAlreadyLikedComment(member, commentId)) { - // 좋아요를 누르지 않은 댓글이라 좋아요를 취소할 수 없습니다. + if (!isAlreadyLikedComment(member.getId(), commentId)) throw new AlreadyUnLikedException(); - } + commentEntity.decreaseLikeCount(); - // 사용자의 댓글 좋아요 목록에서 제거 - List commentLikeList = commentLikeRepository.findByMember(member); - commentLikeList.forEach(commentLike -> { - if (commentLike.getCommentId().equals(commentId)) - commentLikeRepository.delete(commentLike); - }); + CommentLike commentLike = commentLikeRepository.findByMemberIdAndCommentId(memberId, commentId) + .orElseThrow(NotFoundCommentLikeException::new); + commentLikeRepository.delete(commentLike); return commentEntity.getLikeCount(); } - private boolean isAlreadyLikedArticle(Member member, Long articleId) { - return articleLikeRepository.findByMember(member).stream() - .anyMatch(articleLike -> articleLike.getArticleId().equals(articleId)); + private boolean isAlreadyLikedArticle(Long memberId, Long articleId) { + return articleLikeRepository.existsByMemberIdAndArticleId(memberId, articleId); } - private boolean isAlreadyLikedComment(Member member, Long commentId) { - return commentLikeRepository.findByMember(member).stream() - .anyMatch(commentLike -> commentLike.getCommentId().equals(commentId)); + private boolean isAlreadyLikedComment(Long memberId, Long commentId) { + return commentLikeRepository.existsByMemberIdAndCommentId(memberId, commentId); } - } diff --git a/daepiro-api/src/test/java/com/numberone/backend/domain/like/service/LikeServiceTest.java b/daepiro-api/src/test/java/com/numberone/backend/domain/like/service/LikeServiceTest.java new file mode 100644 index 00000000..aa372974 --- /dev/null +++ b/daepiro-api/src/test/java/com/numberone/backend/domain/like/service/LikeServiceTest.java @@ -0,0 +1,216 @@ +package com.numberone.backend.domain.like.service; + +import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.article.entity.ArticleTag; +import com.numberone.backend.domain.article.repository.ArticleRepository; +import com.numberone.backend.domain.comment.entity.CommentEntity; +import com.numberone.backend.domain.comment.repository.CommentRepository; +import com.numberone.backend.domain.like.entity.ArticleLike; +import com.numberone.backend.domain.like.entity.CommentLike; +import com.numberone.backend.domain.like.repository.ArticleLikeRepository; +import com.numberone.backend.domain.like.repository.CommentLikeRepository; +import com.numberone.backend.domain.member.entity.Member; +import com.numberone.backend.domain.member.repository.MemberRepository; +import com.numberone.backend.domain.notification.repository.NotificationRepository; +import com.numberone.backend.provider.fcm.service.FcmMessageProvider; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class LikeServiceTest { + @InjectMocks + private LikeService likeService; + @Mock + private ArticleLikeRepository articleLikeRepository; + @Mock + private ArticleRepository articleRepository; + @Mock + private MemberRepository memberRepository; + @Mock + private FcmMessageProvider fcmMessageProvider; + @Mock + private NotificationRepository notificationRepository; + @Mock + private CommentRepository commentRepository; + @Mock + private CommentLikeRepository commentLikeRepository; + + private Member loginMember; + + @BeforeEach + void setUp() { + setAuthentication(); + } + + @AfterEach + void tearDown() { + clearAuthentication(); + } + + @Test + @DisplayName("게시글의 좋아요를 설정한다") + void increaseArticleLike() { + // given + Member articleOwner = getDummyArticleOwner(); + given(articleOwner.getId()) + .willReturn(2L); + given(articleOwner.getFcmToken()) + .willReturn("fcmToken123"); + + Article article = getDummyArticle(articleOwner); + given(article.getId()) + .willReturn(1L); + given(articleRepository.findByIdFetchJoin(article.getId())) + .willReturn(Optional.of(article)); + + //when + likeService.increaseArticleLike(article.getId()); + + // then + assertThat(article.getLikeCount()).isEqualTo(1); + verify(articleLikeRepository, times(1)).save(any()); + verify(fcmMessageProvider, times(1)).sendFcm(eq(articleOwner.getFcmToken()), any(), any()); + verify(notificationRepository, times(1)).save(any()); + } + + @Test + @DisplayName("게시글의 좋아요를 취소한다") + void decreaseArticleLike() { + // given + Member articleOwner = getDummyArticleOwner(); + + Article article = getDummyArticle(articleOwner); + article.increaseLikeCount(); + article.increaseLikeCount(); + given(article.getId()) + .willReturn(1L); + given(articleRepository.findById(article.getId())) + .willReturn(Optional.of(article)); + + ArticleLike articleLike = ArticleLike.of(loginMember, article); + given(articleLikeRepository.existsByMemberIdAndArticleId(loginMember.getId(), article.getId())) + .willReturn(true); + given(articleLikeRepository.findByMemberIdAndArticleId(loginMember.getId(), article.getId())) + .willReturn(Optional.of(articleLike)); + + //when + likeService.decreaseArticleLike(article.getId()); + + // then + assertThat(article.getLikeCount()).isEqualTo(1); + verify(articleLikeRepository, times(1)).delete(any()); + } + + @Test + @DisplayName("댓글의 좋아요를 설정한다") + void increaseCommentLike() { + //given + Member articleOwner = getDummyArticleOwner(); + + Article article = getDummyArticle(articleOwner); + + Member commentOwner = getDummyCommentOwner(); + given(memberRepository.findById(commentOwner.getId())) + .willReturn(Optional.of(commentOwner)); + given(commentOwner.getFcmToken()) + .willReturn("fcmToken123"); + + CommentEntity commentEntity = getDummyComment(article, commentOwner); + given(commentEntity.getId()) + .willReturn(1L); + given(commentRepository.findById(commentEntity.getId())) + .willReturn(Optional.of(commentEntity)); + + //when + likeService.increaseCommentLike(commentEntity.getId()); + + //then + assertThat(commentEntity.getLikeCount()).isEqualTo(1); + verify(commentLikeRepository, times(1)).save(any()); + verify(fcmMessageProvider, times(1)).sendFcm(eq(commentOwner.getFcmToken()), any(), any()); + verify(notificationRepository, times(1)).save(any()); + } + + @Test + @DisplayName("댓글의 좋아요를 취소한다") + void decreaseCommentLike() { + //given + Member articleOwner = getDummyArticleOwner(); + + Article article = getDummyArticle(articleOwner); + + Member commentOwner = getDummyCommentOwner(); + + CommentEntity commentEntity = getDummyComment(article, commentOwner); + commentEntity.increaseLikeCount(); + commentEntity.increaseLikeCount(); + given(commentEntity.getId()) + .willReturn(1L); + given(commentRepository.findById(commentEntity.getId())) + .willReturn(Optional.of(commentEntity)); + + CommentLike commentLike = CommentLike.of(loginMember, commentEntity); + given(commentLikeRepository.existsByMemberIdAndCommentId(loginMember.getId(), commentEntity.getId())) + .willReturn(true); + given(commentLikeRepository.findByMemberIdAndCommentId(loginMember.getId(), commentEntity.getId())) + .willReturn(Optional.of(commentLike)); + + //when + likeService.decreaseCommentLike(commentEntity.getId()); + + //then + assertThat(commentEntity.getLikeCount()).isEqualTo(1); + verify(commentLikeRepository, times(1)).delete(any()); + } + + private Member getDummyCommentOwner() { + Member commentOwner = spy(Member.ofKakao(3456L)); + given(commentOwner.getId()) + .willReturn(3L); + return commentOwner; + } + + private Member getDummyArticleOwner() { + Member articleOwner = spy(Member.ofKakao(2345L)); + return articleOwner; + } + + private Article getDummyArticle(Member articleOwner) { + return spy(Article.of("title1", "content1", articleOwner, ArticleTag.LIFE)); + } + + private CommentEntity getDummyComment(Article article, Member commentOwner) { + return spy(CommentEntity.of("hello", article, commentOwner)); + } + + private void setAuthentication() { + loginMember = spy(Member.ofKakao(1234L)); + given(loginMember.getId()) + .willReturn(1L); + given(memberRepository.findById(loginMember.getId())) + .willReturn(Optional.of(loginMember)); + + Authentication authentication = UsernamePasswordAuthenticationToken.authenticated(loginMember.getId(), null, null); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + private void clearAuthentication() { + SecurityContextHolder.clearContext(); + } +} \ No newline at end of file diff --git a/daepiro-common/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java b/daepiro-common/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java index d05dc48b..7bbe7956 100644 --- a/daepiro-common/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java +++ b/daepiro-common/src/main/java/com/numberone/backend/exception/context/CustomExceptionContext.java @@ -42,6 +42,7 @@ public enum CustomExceptionContext implements ExceptionContext { // article 관련 예외 NOT_FOUND_ARTICLE("해당 게시글을 찾을 수 없습니다.", 8000), + NOT_FOUND_ARTICLE_LIKE("해당 게시글의 좋아요를 찾을 수 없습니다.", 8001), // article image 관련 예외 NOT_FOUND_ARTICLE_IMAGE("해당 이미지를 찾을 수 없습니다.", 9000), @@ -49,6 +50,7 @@ public enum CustomExceptionContext implements ExceptionContext { // comment 관련 예외 NOT_FOUND_COMMENT("해당 댓글을 찾을 수 없습니다.", 10000), + NOT_FOUND_COMMENT_LIKE("해당 댓글의 좋아요를 찾을 수 없습니다.", 10001), // like 관련 예외 ALREADY_LIKED_ERROR("이미 좋아요 처리된 엔티티입니다.", 11000), diff --git a/daepiro-common/src/main/java/com/numberone/backend/exception/notfound/NotFoundArticleLikeException.java b/daepiro-common/src/main/java/com/numberone/backend/exception/notfound/NotFoundArticleLikeException.java new file mode 100644 index 00000000..31e5f39d --- /dev/null +++ b/daepiro-common/src/main/java/com/numberone/backend/exception/notfound/NotFoundArticleLikeException.java @@ -0,0 +1,13 @@ +package com.numberone.backend.exception.notfound; + + +import com.numberone.backend.exception.context.CustomExceptionContext; +import com.numberone.backend.exception.context.ExceptionContext; + +import static com.numberone.backend.exception.context.CustomExceptionContext.NOT_FOUND_ARTICLE_LIKE; + +public class NotFoundArticleLikeException extends NotFoundException { + public NotFoundArticleLikeException() { + super(NOT_FOUND_ARTICLE_LIKE); + } +} diff --git a/daepiro-common/src/main/java/com/numberone/backend/exception/notfound/NotFoundCommentLikeException.java b/daepiro-common/src/main/java/com/numberone/backend/exception/notfound/NotFoundCommentLikeException.java new file mode 100644 index 00000000..fa99424a --- /dev/null +++ b/daepiro-common/src/main/java/com/numberone/backend/exception/notfound/NotFoundCommentLikeException.java @@ -0,0 +1,11 @@ +package com.numberone.backend.exception.notfound; + +import com.numberone.backend.exception.context.ExceptionContext; + +import static com.numberone.backend.exception.context.CustomExceptionContext.NOT_FOUND_COMMENT_LIKE; + +public class NotFoundCommentLikeException extends NotFoundException{ + public NotFoundCommentLikeException() { + super(NOT_FOUND_COMMENT_LIKE); + } +} diff --git a/daepiro-core/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java b/daepiro-core/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java index 24978a86..594586f4 100644 --- a/daepiro-core/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java +++ b/daepiro-core/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java @@ -5,6 +5,7 @@ import com.numberone.backend.domain.member.entity.Member; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; @@ -46,7 +47,8 @@ public class CommentEntity extends BaseTimeEntity { @OneToMany(mappedBy = "parent", orphanRemoval = true) private List childs = new ArrayList<>(); - public CommentEntity(String content, Article article, Member author) { + @Builder + private CommentEntity(String content, Article article, Member author) { this.depth = 0; this.content = content; this.article = article; @@ -68,4 +70,11 @@ public void decreaseLikeCount() { } } + public static CommentEntity of(String content, Article article, Member author) { + return CommentEntity.builder() + .content(content) + .article(article) + .author(author) + .build(); + } } diff --git a/daepiro-core/src/main/java/com/numberone/backend/domain/like/entity/ArticleLike.java b/daepiro-core/src/main/java/com/numberone/backend/domain/like/entity/ArticleLike.java index 53f0dbe9..00a1cf8a 100644 --- a/daepiro-core/src/main/java/com/numberone/backend/domain/like/entity/ArticleLike.java +++ b/daepiro-core/src/main/java/com/numberone/backend/domain/like/entity/ArticleLike.java @@ -5,6 +5,7 @@ import com.numberone.backend.domain.member.entity.Member; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; @@ -26,8 +27,16 @@ public class ArticleLike extends BaseTimeEntity { private Long articleId; - public ArticleLike(Member member, Article article){ + @Builder + private ArticleLike(Member member, Article article){ this.member = member; this.articleId = article.getId(); } + + public static ArticleLike of(Member member, Article article) { + return ArticleLike.builder() + .member(member) + .article(article) + .build(); + } } diff --git a/daepiro-core/src/main/java/com/numberone/backend/domain/like/entity/CommentLike.java b/daepiro-core/src/main/java/com/numberone/backend/domain/like/entity/CommentLike.java index 36759115..d0fea743 100644 --- a/daepiro-core/src/main/java/com/numberone/backend/domain/like/entity/CommentLike.java +++ b/daepiro-core/src/main/java/com/numberone/backend/domain/like/entity/CommentLike.java @@ -5,6 +5,7 @@ import com.numberone.backend.domain.member.entity.Member; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; @@ -26,8 +27,16 @@ public class CommentLike extends BaseTimeEntity { private Long commentId; - public CommentLike(Member member, CommentEntity comment) { + @Builder + private CommentLike(Member member, CommentEntity comment) { this.member = member; this.commentId = comment.getId(); } + + public static CommentLike of(Member member, CommentEntity comment) { + return CommentLike.builder() + .member(member) + .comment(comment) + .build(); + } } diff --git a/daepiro-core/src/main/java/com/numberone/backend/domain/like/repository/ArticleLikeRepository.java b/daepiro-core/src/main/java/com/numberone/backend/domain/like/repository/ArticleLikeRepository.java index 390e5ecf..2a38754a 100644 --- a/daepiro-core/src/main/java/com/numberone/backend/domain/like/repository/ArticleLikeRepository.java +++ b/daepiro-core/src/main/java/com/numberone/backend/domain/like/repository/ArticleLikeRepository.java @@ -5,9 +5,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface ArticleLikeRepository extends JpaRepository { - List findByMember(Member member); + Optional findByMemberIdAndArticleId(Long memberId, Long articleId); + boolean existsByMemberIdAndArticleId(Long memberId, Long articleId); } diff --git a/daepiro-core/src/main/java/com/numberone/backend/domain/like/repository/CommentLikeRepository.java b/daepiro-core/src/main/java/com/numberone/backend/domain/like/repository/CommentLikeRepository.java index c6ee3d8f..fe1dbd10 100644 --- a/daepiro-core/src/main/java/com/numberone/backend/domain/like/repository/CommentLikeRepository.java +++ b/daepiro-core/src/main/java/com/numberone/backend/domain/like/repository/CommentLikeRepository.java @@ -1,11 +1,16 @@ package com.numberone.backend.domain.like.repository; +import com.numberone.backend.domain.like.entity.ArticleLike; import com.numberone.backend.domain.like.entity.CommentLike; import com.numberone.backend.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface CommentLikeRepository extends JpaRepository { List findByMember(Member member); + Optional findByMemberIdAndCommentId(Long memberId, Long commentId); + + boolean existsByMemberIdAndCommentId(Long memberId, Long commentId); }