From ac4e1d959ad2a8ec5f46c6ef184253f8e8550bdc Mon Sep 17 00:00:00 2001 From: kimdohyung Date: Tue, 2 Apr 2024 20:03:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EC=83=9D=EC=84=B1=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/reviewlike/ReviewLikeController.java | 29 ++++++++++++ .../reviewlike/ReviewLikeService.java | 45 +++++++++++++++++++ .../domain/reviewlike/ReviewLike.java | 40 +++++++++++++++++ .../repository/ReviewLikeRepository.java | 16 +++++++ 4 files changed, 130 insertions(+) create mode 100644 src/main/java/com/jisungin/api/reviewlike/ReviewLikeController.java create mode 100644 src/main/java/com/jisungin/application/reviewlike/ReviewLikeService.java create mode 100644 src/main/java/com/jisungin/domain/reviewlike/ReviewLike.java create mode 100644 src/main/java/com/jisungin/domain/reviewlike/repository/ReviewLikeRepository.java diff --git a/src/main/java/com/jisungin/api/reviewlike/ReviewLikeController.java b/src/main/java/com/jisungin/api/reviewlike/ReviewLikeController.java new file mode 100644 index 0000000..caa4f71 --- /dev/null +++ b/src/main/java/com/jisungin/api/reviewlike/ReviewLikeController.java @@ -0,0 +1,29 @@ +package com.jisungin.api.reviewlike; + +import com.jisungin.api.ApiResponse; +import com.jisungin.api.oauth.Auth; +import com.jisungin.api.oauth.AuthContext; +import com.jisungin.application.reviewlike.ReviewLikeService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequestMapping("/v1/reviews") +@RequiredArgsConstructor +@RestController +public class ReviewLikeController { + + private final ReviewLikeService reviewLikeService; + + @PostMapping("/{reviewId}/likes") + public ApiResponse likeReview( + @PathVariable Long reviewId, + @Auth AuthContext authContext + ) { + reviewLikeService.createReviewLike(authContext.getUserId(), reviewId); + return ApiResponse.ok(null); + } + +} diff --git a/src/main/java/com/jisungin/application/reviewlike/ReviewLikeService.java b/src/main/java/com/jisungin/application/reviewlike/ReviewLikeService.java new file mode 100644 index 0000000..1992394 --- /dev/null +++ b/src/main/java/com/jisungin/application/reviewlike/ReviewLikeService.java @@ -0,0 +1,45 @@ +package com.jisungin.application.reviewlike; + +import com.jisungin.domain.review.Review; +import com.jisungin.domain.review.repository.ReviewRepository; +import com.jisungin.domain.reviewlike.ReviewLike; +import com.jisungin.domain.reviewlike.repository.ReviewLikeRepository; +import com.jisungin.domain.user.User; +import com.jisungin.domain.user.repository.UserRepository; +import com.jisungin.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.jisungin.exception.ErrorCode.*; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class ReviewLikeService { + + private final ReviewLikeRepository reviewLikeRepository; + + private final UserRepository userRepository; + + private final ReviewRepository reviewRepository; + + @Transactional + public void createReviewLike(Long userId, Long reviewId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(USER_NOT_FOUND)); + + Review review = reviewRepository.findById(reviewId) + .orElseThrow(() -> new BusinessException(REVIEW_NOT_FOUND)); + + // 좋아요가 이미 존재할 경우, 400 에러 + if (reviewLikeRepository.findByUserAndReview(user, review).isPresent()) { + throw new BusinessException(LIKE_EXIST); + } + + // 없는 경우, 리뷰 좋아요 저장 + ReviewLike reviewLike = ReviewLike.likeReview(user, review); + reviewLikeRepository.save(reviewLike); + } + +} diff --git a/src/main/java/com/jisungin/domain/reviewlike/ReviewLike.java b/src/main/java/com/jisungin/domain/reviewlike/ReviewLike.java new file mode 100644 index 0000000..bbaf1b7 --- /dev/null +++ b/src/main/java/com/jisungin/domain/reviewlike/ReviewLike.java @@ -0,0 +1,40 @@ +package com.jisungin.domain.reviewlike; + +import com.jisungin.domain.review.Review; +import com.jisungin.domain.user.User; +import jakarta.persistence.*; +import lombok.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@ToString +public class ReviewLike { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "review_like_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "review_id") + private Review review; + + @Builder + private ReviewLike(User user, Review review) { + this.user = user; + this.review = review; + } + + public static ReviewLike likeReview(User user, Review review) { + return ReviewLike.builder() + .user(user) + .review(review) + .build(); + } + +} diff --git a/src/main/java/com/jisungin/domain/reviewlike/repository/ReviewLikeRepository.java b/src/main/java/com/jisungin/domain/reviewlike/repository/ReviewLikeRepository.java new file mode 100644 index 0000000..4d099bc --- /dev/null +++ b/src/main/java/com/jisungin/domain/reviewlike/repository/ReviewLikeRepository.java @@ -0,0 +1,16 @@ +package com.jisungin.domain.reviewlike.repository; + +import com.jisungin.domain.review.Review; +import com.jisungin.domain.reviewlike.ReviewLike; +import com.jisungin.domain.user.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface ReviewLikeRepository extends JpaRepository { + + Optional findByUserAndReview(User user, Review review); + +} From fa08403a159604072b68f2bfae6e4a5e6d3733ac Mon Sep 17 00:00:00 2001 From: kimdohyung Date: Tue, 2 Apr 2024 20:05:24 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test:=20=EB=A6=AC=EB=B7=B0=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EC=83=9D=EC=84=B1=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/jisungin/ControllerTestSupport.java | 8 +- .../reviewlike/ReviewLikeControllerTest.java | 29 +++++ .../reviewlike/ReviewLikeServiceTest.java | 123 ++++++++++++++++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/jisungin/api/reviewlike/ReviewLikeControllerTest.java create mode 100644 src/test/java/com/jisungin/application/reviewlike/ReviewLikeServiceTest.java diff --git a/src/test/java/com/jisungin/ControllerTestSupport.java b/src/test/java/com/jisungin/ControllerTestSupport.java index 7e70976..d5d51d0 100644 --- a/src/test/java/com/jisungin/ControllerTestSupport.java +++ b/src/test/java/com/jisungin/ControllerTestSupport.java @@ -6,6 +6,7 @@ import com.jisungin.api.commentlike.CommentLikeController; import com.jisungin.api.oauth.AuthContext; import com.jisungin.api.review.ReviewController; +import com.jisungin.api.reviewlike.ReviewLikeController; import com.jisungin.api.talkroom.TalkRoomController; import com.jisungin.api.talkroomlike.TalkRoomLikeController; import com.jisungin.api.user.UserController; @@ -14,6 +15,7 @@ import com.jisungin.application.comment.CommentService; import com.jisungin.application.commentlike.CommentLikeService; import com.jisungin.application.review.ReviewService; +import com.jisungin.application.reviewlike.ReviewLikeService; import com.jisungin.application.talkroom.TalkRoomService; import com.jisungin.application.talkroomlike.TalkRoomLikeService; import com.jisungin.application.user.UserService; @@ -29,7 +31,8 @@ TalkRoomLikeController.class, CommentLikeController.class, UserController.class, - BookController.class + BookController.class, + ReviewLikeController.class }) public abstract class ControllerTestSupport { @@ -66,4 +69,7 @@ public abstract class ControllerTestSupport { @MockBean protected BestSellerService bestSellerService; + @MockBean + protected ReviewLikeService reviewLikeService; + } diff --git a/src/test/java/com/jisungin/api/reviewlike/ReviewLikeControllerTest.java b/src/test/java/com/jisungin/api/reviewlike/ReviewLikeControllerTest.java new file mode 100644 index 0000000..ef09fe4 --- /dev/null +++ b/src/test/java/com/jisungin/api/reviewlike/ReviewLikeControllerTest.java @@ -0,0 +1,29 @@ +package com.jisungin.api.reviewlike; + +import com.jisungin.ControllerTestSupport; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class ReviewLikeControllerTest extends ControllerTestSupport { + + @DisplayName("유저가 리뷰 좋아요를 누른다.") + @Test + void likeReview() throws Exception { + //given + Long reviewId = 1L; + + //when //then + mockMvc.perform(post("/v1/reviews/{reviewId}/likes", reviewId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")) + .andDo(print()); + } + +} \ No newline at end of file diff --git a/src/test/java/com/jisungin/application/reviewlike/ReviewLikeServiceTest.java b/src/test/java/com/jisungin/application/reviewlike/ReviewLikeServiceTest.java new file mode 100644 index 0000000..67d96a3 --- /dev/null +++ b/src/test/java/com/jisungin/application/reviewlike/ReviewLikeServiceTest.java @@ -0,0 +1,123 @@ +package com.jisungin.application.reviewlike; + +import com.jisungin.ServiceTestSupport; +import com.jisungin.domain.book.Book; +import com.jisungin.domain.book.repository.BookRepository; +import com.jisungin.domain.oauth.OauthId; +import com.jisungin.domain.oauth.OauthType; +import com.jisungin.domain.review.Review; +import com.jisungin.domain.review.repository.ReviewRepository; +import com.jisungin.domain.reviewlike.ReviewLike; +import com.jisungin.domain.reviewlike.repository.ReviewLikeRepository; +import com.jisungin.domain.user.User; +import com.jisungin.domain.user.repository.UserRepository; +import com.jisungin.exception.BusinessException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +class ReviewLikeServiceTest extends ServiceTestSupport { + + @Autowired + private UserRepository userRepository; + + @Autowired + private BookRepository bookRepository; + + @Autowired + private ReviewRepository reviewRepository; + + @Autowired + private ReviewLikeRepository reviewLikeRepository; + + @Autowired + private ReviewLikeService reviewLikeService; + + @AfterEach + void tearDown() { + reviewLikeRepository.deleteAllInBatch(); + reviewRepository.deleteAllInBatch(); + bookRepository.deleteAllInBatch(); + userRepository.deleteAllInBatch(); + } + + @DisplayName("사용자가 리뷰 좋아요를 누른다.") + @Test + void likeReview() { + //given + User user = userRepository.save(createUser("1")); + Book book = bookRepository.save(createBook()); + Review review = reviewRepository.save(createReview(user, book)); + + //when + reviewLikeService.createReviewLike(user.getId(), review.getId()); + + //then + List reviewLike = reviewLikeRepository.findAll(); + + assertThat(reviewLike).hasSize(1); + assertThat(reviewLike.get(0).getUser().getId()).isEqualTo(user.getId()); + assertThat(reviewLike.get(0).getReview().getId()).isEqualTo(review.getId()); + } + + @DisplayName("좋아요가 중복되면 예외 처리한다.") + @Test + void likeReviewWithReLike() { + //given + User user = userRepository.save(createUser("1")); + Book book = bookRepository.save(createBook()); + Review review = reviewRepository.save(createReview(user, book)); + ReviewLike reviewLike = reviewLikeRepository.save(createReviewLike(user, review)); + + //when //then + assertThatThrownBy(() -> reviewLikeService.createReviewLike(user.getId(), review.getId())) + .isInstanceOf(BusinessException.class) + .hasMessage("이미 좋아요를 눌렀습니다."); + + } + + private static ReviewLike createReviewLike(User user, Review review) { + return ReviewLike.likeReview(user, review); + } + + private static User createUser(String oauthId) { + return User.builder() + .name("김도형") + .profileImage("image") + .oauthId( + OauthId.builder() + .oauthId(oauthId) + .oauthType(OauthType.KAKAO) + .build() + ) + .build(); + } + + private static Book createBook() { + return Book.builder() + .title("제목") + .content("내용") + .authors("김도형") + .isbn("123456") + .publisher("지성인") + .dateTime(LocalDateTime.of(2024, 1, 1, 0, 0)) + .imageUrl("image") + .build(); + } + + private static Review createReview(User user, Book book) { + return Review.builder() + .user(user) + .book(book) + .content("내용") + .rating(4.5) + .build(); + } + +} \ No newline at end of file From 6cb1021671de7c9ce47d2b03877e78fca7ec934b Mon Sep 17 00:00:00 2001 From: kimdohyung Date: Tue, 2 Apr 2024 20:36:00 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EC=82=AD=EC=A0=9C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/reviewlike/ReviewLikeController.java | 16 +++++++++++----- .../reviewlike/ReviewLikeService.java | 16 +++++++++++++++- .../java/com/jisungin/exception/ErrorCode.java | 3 ++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/jisungin/api/reviewlike/ReviewLikeController.java b/src/main/java/com/jisungin/api/reviewlike/ReviewLikeController.java index caa4f71..500ee86 100644 --- a/src/main/java/com/jisungin/api/reviewlike/ReviewLikeController.java +++ b/src/main/java/com/jisungin/api/reviewlike/ReviewLikeController.java @@ -5,10 +5,7 @@ import com.jisungin.api.oauth.AuthContext; import com.jisungin.application.reviewlike.ReviewLikeService; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RequestMapping("/v1/reviews") @RequiredArgsConstructor @@ -22,7 +19,16 @@ public ApiResponse likeReview( @PathVariable Long reviewId, @Auth AuthContext authContext ) { - reviewLikeService.createReviewLike(authContext.getUserId(), reviewId); + reviewLikeService.likeReview(authContext.getUserId(), reviewId); + return ApiResponse.ok(null); + } + + @DeleteMapping("/{reviewId}/likes") + public ApiResponse unlikeReview( + @PathVariable Long reviewId, + @Auth AuthContext authContext + ) { + reviewLikeService.unlikeReview(authContext.getUserId(), reviewId); return ApiResponse.ok(null); } diff --git a/src/main/java/com/jisungin/application/reviewlike/ReviewLikeService.java b/src/main/java/com/jisungin/application/reviewlike/ReviewLikeService.java index 1992394..23c0646 100644 --- a/src/main/java/com/jisungin/application/reviewlike/ReviewLikeService.java +++ b/src/main/java/com/jisungin/application/reviewlike/ReviewLikeService.java @@ -25,7 +25,7 @@ public class ReviewLikeService { private final ReviewRepository reviewRepository; @Transactional - public void createReviewLike(Long userId, Long reviewId) { + public void likeReview(Long userId, Long reviewId) { User user = userRepository.findById(userId) .orElseThrow(() -> new BusinessException(USER_NOT_FOUND)); @@ -42,4 +42,18 @@ public void createReviewLike(Long userId, Long reviewId) { reviewLikeRepository.save(reviewLike); } + @Transactional + public void unlikeReview(Long userId, Long reviewId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(USER_NOT_FOUND)); + + Review review = reviewRepository.findById(reviewId) + .orElseThrow(() -> new BusinessException(REVIEW_NOT_FOUND)); + + ReviewLike reviewLike = reviewLikeRepository.findByUserAndReview(user, review) + .orElseThrow(() -> new BusinessException(REVIEW_LIKE_NOT_FOUND)); + + reviewLikeRepository.delete(reviewLike); + } + } diff --git a/src/main/java/com/jisungin/exception/ErrorCode.java b/src/main/java/com/jisungin/exception/ErrorCode.java index e6b14c0..8ca14b7 100644 --- a/src/main/java/com/jisungin/exception/ErrorCode.java +++ b/src/main/java/com/jisungin/exception/ErrorCode.java @@ -21,7 +21,8 @@ public enum ErrorCode { TALK_ROOM_LIKE_NOT_FOUND(404, "토크방 좋아요를 찾을 수 없습니다."), LIKE_EXIST(400, "이미 좋아요를 눌렀습니다."), REQUEST_TIME_OUT(408, "요청 시간이 만료 되었습니다."), - COMMENT_LIKE_NOT_FOUND(404, "의견 좋아요를 찾을 수 없습니다."); + COMMENT_LIKE_NOT_FOUND(404, "의견 좋아요를 찾을 수 없습니다."), + REVIEW_LIKE_NOT_FOUND(404, "리뷰 좋아요를 찾을 수 없습니다."); private final int code; From c06f3a869a1d3927a6d232b0d1916bbcc312e1fe Mon Sep 17 00:00:00 2001 From: kimdohyung Date: Tue, 2 Apr 2024 20:36:34 +0900 Subject: [PATCH 4/4] =?UTF-8?q?test:=20=EB=A6=AC=EB=B7=B0=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EC=82=AD=EC=A0=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reviewlike/ReviewLikeControllerTest.java | 15 +++++++ .../reviewlike/ReviewLikeServiceTest.java | 39 +++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/jisungin/api/reviewlike/ReviewLikeControllerTest.java b/src/test/java/com/jisungin/api/reviewlike/ReviewLikeControllerTest.java index ef09fe4..9b98a9e 100644 --- a/src/test/java/com/jisungin/api/reviewlike/ReviewLikeControllerTest.java +++ b/src/test/java/com/jisungin/api/reviewlike/ReviewLikeControllerTest.java @@ -26,4 +26,19 @@ void likeReview() throws Exception { .andDo(print()); } + @DisplayName("유저가 리뷰 좋아요를 취소한다.") + @Test + void unlikeReview() throws Exception { + //given + Long reviewId = 1L; + + //when //then + mockMvc.perform(delete("/v1/reviews/{reviewId}/likes", reviewId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("OK")) + .andDo(print()); + } + } \ No newline at end of file diff --git a/src/test/java/com/jisungin/application/reviewlike/ReviewLikeServiceTest.java b/src/test/java/com/jisungin/application/reviewlike/ReviewLikeServiceTest.java index 67d96a3..5c96b88 100644 --- a/src/test/java/com/jisungin/application/reviewlike/ReviewLikeServiceTest.java +++ b/src/test/java/com/jisungin/application/reviewlike/ReviewLikeServiceTest.java @@ -56,7 +56,7 @@ void likeReview() { Review review = reviewRepository.save(createReview(user, book)); //when - reviewLikeService.createReviewLike(user.getId(), review.getId()); + reviewLikeService.likeReview(user.getId(), review.getId()); //then List reviewLike = reviewLikeRepository.findAll(); @@ -66,7 +66,7 @@ void likeReview() { assertThat(reviewLike.get(0).getReview().getId()).isEqualTo(review.getId()); } - @DisplayName("좋아요가 중복되면 예외 처리한다.") + @DisplayName("리뷰 좋아요가 중복되면 예외가 발생한다.") @Test void likeReviewWithReLike() { //given @@ -76,12 +76,45 @@ void likeReviewWithReLike() { ReviewLike reviewLike = reviewLikeRepository.save(createReviewLike(user, review)); //when //then - assertThatThrownBy(() -> reviewLikeService.createReviewLike(user.getId(), review.getId())) + assertThatThrownBy(() -> reviewLikeService.likeReview(user.getId(), review.getId())) .isInstanceOf(BusinessException.class) .hasMessage("이미 좋아요를 눌렀습니다."); } + @DisplayName("사용자가 리뷰 좋아요를 취소한다.") + @Test + void unlikeReview() { + //given + User user = userRepository.save(createUser("1")); + Book book = bookRepository.save(createBook()); + Review review = reviewRepository.save(createReview(user, book)); + ReviewLike reviewLike = reviewLikeRepository.save(createReviewLike(user, review)); + + //when + reviewLikeService.unlikeReview(user.getId(), review.getId()); + + //then + List reviewLikes = reviewLikeRepository.findAll(); + + assertThat(reviewLikes).isEmpty(); + } + + @DisplayName("존재하지 않는 리뷰 좋아요를 취소하면 예외가 발생한다.") + @Test + void unlikeReviewWithNotExist() { + //given + User user = userRepository.save(createUser("1")); + Book book = bookRepository.save(createBook()); + Review review = reviewRepository.save(createReview(user, book)); + + //when //then + assertThatThrownBy(() -> reviewLikeService.unlikeReview(user.getId(), review.getId())) + .isInstanceOf(BusinessException.class) + .hasMessage("리뷰 좋아요를 찾을 수 없습니다."); + + } + private static ReviewLike createReviewLike(User user, Review review) { return ReviewLike.likeReview(user, review); }