diff --git a/backend/JiShop/src/main/java/com/jishop/common/exception/ErrorType.java b/backend/JiShop/src/main/java/com/jishop/common/exception/ErrorType.java index b8e4f4da..6e911303 100644 --- a/backend/JiShop/src/main/java/com/jishop/common/exception/ErrorType.java +++ b/backend/JiShop/src/main/java/com/jishop/common/exception/ErrorType.java @@ -29,6 +29,7 @@ public enum ErrorType { REVIEW_DUPLICATE(HttpStatus.CONFLICT, "이미 리뷰를 작성했습니다."), RATING_OUT_OF_RANGE(HttpStatus.BAD_REQUEST, "별점은 1~5점을 해야한다."), REVIEW_NOT_FOUND(HttpStatus.BAD_REQUEST, "작성하신 리뷰가 없습니다."), + ALREADY_LIVIEW_LIKED(HttpStatus.CONFLICT, "이미 리뷰 좋아요를 했습니다."), // NOTICE NOTICE_NOT_FOUND(HttpStatus.NOT_FOUND, "공지사항이 존재하지 않습니다."), diff --git a/backend/JiShop/src/main/java/com/jishop/order/domain/OrderDetail.java b/backend/JiShop/src/main/java/com/jishop/order/domain/OrderDetail.java index 3079b09a..7cb54597 100644 --- a/backend/JiShop/src/main/java/com/jishop/order/domain/OrderDetail.java +++ b/backend/JiShop/src/main/java/com/jishop/order/domain/OrderDetail.java @@ -52,12 +52,12 @@ public OrderDetail(Order order, String orderNumber, SaleProduct saleProduct, int this.discountPrice = discountPrice; } - public static OrderDetail from(Order order, SaleProduct saleProduct, int quantity){ + public static OrderDetail from(Order order, SaleProduct saleProduct, int quantity) { int orderPrice = saleProduct.getProduct().getProductInfo().getOriginPrice(); int paymentPrice = saleProduct.getProduct().getProductInfo().getDiscountPrice(); int discountPrice = orderPrice - paymentPrice; - if(saleProduct.getOption() != null){ + if (saleProduct.getOption() != null) { int optionExtra = saleProduct.getOption().getOptionExtra(); paymentPrice += optionExtra; orderPrice += optionExtra; @@ -74,4 +74,7 @@ public static OrderDetail from(Order order, SaleProduct saleProduct, int quantit .build(); } + public String getProductSummary() { + return saleProduct.getProductSummary(quantity); + } } diff --git a/backend/JiShop/src/main/java/com/jishop/product/domain/embed/CategoryInfo.java b/backend/JiShop/src/main/java/com/jishop/product/domain/embed/CategoryInfo.java index 7ff4473f..18d26bde 100644 --- a/backend/JiShop/src/main/java/com/jishop/product/domain/embed/CategoryInfo.java +++ b/backend/JiShop/src/main/java/com/jishop/product/domain/embed/CategoryInfo.java @@ -18,7 +18,7 @@ public class CategoryInfo { @Column(name = "s_cat_id") private Long sCatId; - public CategoryInfo(Category category, Long lCatId, Long mCatId, Long sCatId) { + public CategoryInfo(Long lCatId, Long mCatId, Long sCatId) { this.lCatId = lCatId; this.mCatId = mCatId; this.sCatId = sCatId; diff --git a/backend/JiShop/src/main/java/com/jishop/review/domain/Review.java b/backend/JiShop/src/main/java/com/jishop/review/domain/Review.java index 9e100137..badfc15f 100644 --- a/backend/JiShop/src/main/java/com/jishop/review/domain/Review.java +++ b/backend/JiShop/src/main/java/com/jishop/review/domain/Review.java @@ -76,7 +76,7 @@ public void increaseLikeCount() { } public void decreaseLikeCount() { - likeCount--; + likeCount--; } public void updateReview(UpdateReviewRequest updateReviewRequest) { @@ -85,7 +85,23 @@ public void updateReview(UpdateReviewRequest updateReviewRequest) { this.tag = updateReviewRequest.tag(); } - public ImageUrls getImageUrls() { - return Optional.ofNullable(imageUrls).orElse(new ImageUrls()); + public List getImageUrls() { + return Optional.ofNullable(imageUrls) + .orElse(new ImageUrls()) + .getImages(); + } + + private String[] splitProductSummary() { + return productSummary.split(";"); + } + + public String getOptionString() { + String[] split = splitProductSummary(); + + if (split.length == 3) { + return split[2]; + } + + return null; } } diff --git a/backend/JiShop/src/main/java/com/jishop/review/dto/MyPageReviewResponse.java b/backend/JiShop/src/main/java/com/jishop/review/dto/MyPageReviewResponse.java index 3f1fd424..461e3fd9 100644 --- a/backend/JiShop/src/main/java/com/jishop/review/dto/MyPageReviewResponse.java +++ b/backend/JiShop/src/main/java/com/jishop/review/dto/MyPageReviewResponse.java @@ -32,8 +32,7 @@ public static MyPageReviewResponse from(Review review) { review.getContent(), review.getLikeCount(), review.getProduct().getId(), - review.getImageUrls().getImages() - .stream().findFirst().orElse(null), + review.getImageUrls().stream().findFirst().orElse(null), review.getOrderDetail().getCreatedAt(), ProductSummary.from(review.getProduct()), option diff --git a/backend/JiShop/src/main/java/com/jishop/review/dto/ReviewImageResponse.java b/backend/JiShop/src/main/java/com/jishop/review/dto/ReviewImageResponse.java index 4eb81d85..093c9dd1 100644 --- a/backend/JiShop/src/main/java/com/jishop/review/dto/ReviewImageResponse.java +++ b/backend/JiShop/src/main/java/com/jishop/review/dto/ReviewImageResponse.java @@ -9,7 +9,7 @@ public record ReviewImageResponse( public static ReviewImageResponse from(Review review) { return new ReviewImageResponse( review.getId(), - review.getImageUrls().getImages().get(0) + review.getImageUrls().get(0) ); } } diff --git a/backend/JiShop/src/main/java/com/jishop/review/dto/ReviewWithOutUserResponse.java b/backend/JiShop/src/main/java/com/jishop/review/dto/ReviewWithOutUserResponse.java index 48c371c2..11dedd41 100644 --- a/backend/JiShop/src/main/java/com/jishop/review/dto/ReviewWithOutUserResponse.java +++ b/backend/JiShop/src/main/java/com/jishop/review/dto/ReviewWithOutUserResponse.java @@ -31,7 +31,7 @@ public static ReviewWithOutUserResponse from(Review review) { review.getRating(), review.getContent(), review.getLikeCount(), - review.getImageUrls().getImages(), + review.getImageUrls(), review.getCreatedAt().toLocalDate(), option, user.getName() diff --git a/backend/JiShop/src/main/java/com/jishop/review/dto/ReviewWithUserResponse.java b/backend/JiShop/src/main/java/com/jishop/review/dto/ReviewWithUserResponse.java index a6fcaf8f..4015e74d 100644 --- a/backend/JiShop/src/main/java/com/jishop/review/dto/ReviewWithUserResponse.java +++ b/backend/JiShop/src/main/java/com/jishop/review/dto/ReviewWithUserResponse.java @@ -21,19 +21,15 @@ public record ReviewWithUserResponse( ) { public static ReviewWithUserResponse from(Review review, Boolean isLike) { User user = review.getUser(); - String[] split = review.getProductSummary().split(";"); - String option = null; - if (split.length == 3) { - option = split[1]; - } + return new ReviewWithUserResponse( review.getTag(), review.getRating(), review.getContent(), review.getLikeCount(), - review.getImageUrls().getImages(), + review.getImageUrls(), review.getCreatedAt().toLocalDate(), - option, + review.getOptionString(), isLike, user.getName() ); diff --git a/backend/JiShop/src/main/java/com/jishop/review/repository/LikeReviewRepository.java b/backend/JiShop/src/main/java/com/jishop/review/repository/LikeReviewRepository.java index b76e4883..d496dcb6 100644 --- a/backend/JiShop/src/main/java/com/jishop/review/repository/LikeReviewRepository.java +++ b/backend/JiShop/src/main/java/com/jishop/review/repository/LikeReviewRepository.java @@ -13,4 +13,6 @@ public interface LikeReviewRepository extends JpaRepository { @Modifying @Query("delete from LikeReview lr where lr.review = :review and lr.user = :user") int deleteByReviewAndUser(@Param(value = "review") Review review, @Param(value = "user") User user); + + boolean existsLikeReviewByUserAndReview(User user, Review review); } diff --git a/backend/JiShop/src/main/java/com/jishop/review/service/ReviewServiceImpl.java b/backend/JiShop/src/main/java/com/jishop/review/service/ReviewServiceImpl.java index 423160e6..4498a7c7 100644 --- a/backend/JiShop/src/main/java/com/jishop/review/service/ReviewServiceImpl.java +++ b/backend/JiShop/src/main/java/com/jishop/review/service/ReviewServiceImpl.java @@ -19,15 +19,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.web.PagedModel; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @Slf4j @Service @Transactional @@ -64,7 +61,7 @@ public Long createReview(ReviewRequest reviewRequest, User user) { reviewProduct.increaseRating(reviewRequest.rating()); - String productSummary = makeProductSummar(saleProduct, orderDetail); + String productSummary = orderDetail.getProductSummary(); try { Review review = reviewRepository.save(reviewRequest.toEntity(reviewRequest.images(), product, orderDetail, user, productSummary)); @@ -74,19 +71,6 @@ public Long createReview(ReviewRequest reviewRequest, User user) { } } - private String makeProductSummar(SaleProduct saleProduct, OrderDetail orderDetail) { - if (saleProduct.getOption() == null) { - return String.format("%s;%s", - saleProduct.getName(), - orderDetail.getQuantity()); - } - - return String.format("%s;%s;%s", - saleProduct.getName(), - saleProduct.getOption().getOptionValue(), - orderDetail.getQuantity()); - } - @Override public PagedModel getProductReviews(Long productId, Pageable pageable) { @@ -116,7 +100,6 @@ public PagedModel getMyPageReviews(Long userId, Pageable p @Override public ReviewWithOutUserResponse getDetailReview(Long reviewId) { - return reviewRepository.findByReviewIdWithUser(reviewId) .map(ReviewWithOutUserResponse::from) .orElseThrow(() -> new DomainException(ErrorType.REVIEW_NOT_FOUND)); @@ -124,7 +107,6 @@ public ReviewWithOutUserResponse getDetailReview(Long reviewId) { @Override public ReviewWithUserResponse getDetailReview(Long reviewId, User user) { - return reviewRepository.findReviewsWithUserLike(reviewId, user.getId()) .map(object -> { Object[] result = (Object[]) object; @@ -136,16 +118,14 @@ public ReviewWithUserResponse getDetailReview(Long reviewId, User user) { @Override public PagedModel getMyPageReviewWrite(User user, Pageable pageable) { - return new PagedModel<>(reviewRepository .findByMyPageReviewWrite(user.getId(), pageable)); } @Override public Slice getReviewImages(Pageable pageable) { - Slice reviews = reviewRepository.findByAllWithImage(pageable) + return reviewRepository.findByAllWithImage(pageable) .map(ReviewImageResponse::from); - return reviews; } @@ -160,6 +140,10 @@ public void likeReview(LikerIdRequest likerIdRequest, Long reviewId) { () -> new DomainException(ErrorType.USER_NOT_FOUND) ); + if(likeReviewRepository.existsLikeReviewByUserAndReview(liker, review)) { + throw new DomainException(ErrorType.ALREADY_LIVIEW_LIKED); + } + LikeReview likeReview = LikeReview.builder() .user(liker) .review(review) diff --git a/backend/JiShop/src/main/java/com/jishop/saleproduct/domain/SaleProduct.java b/backend/JiShop/src/main/java/com/jishop/saleproduct/domain/SaleProduct.java index 7f1a5668..8d72e1ee 100644 --- a/backend/JiShop/src/main/java/com/jishop/saleproduct/domain/SaleProduct.java +++ b/backend/JiShop/src/main/java/com/jishop/saleproduct/domain/SaleProduct.java @@ -19,13 +19,6 @@ public class SaleProduct extends BaseEntity { private String name; -// private String image; -// -// private int quantity; -// -// @ColumnDefault("0") -// private Double productRating; - @JoinColumn(name = "product_id") @ManyToOne(fetch = FetchType.LAZY) private Product product; @@ -43,4 +36,17 @@ public SaleProduct(Product product, Option option, String name) { this.option = option; this.name = name; } + + public String getProductSummary(int quantity) { + if (option == null) { + return String.format("%s;%s", + name, + quantity); + } + + return String.format("%s;%s;%s", + name, + quantity, + option.getOptionValue()); + } } \ No newline at end of file diff --git a/backend/JiShop/src/main/resources/config b/backend/JiShop/src/main/resources/config index 04f54a4a..4cfe2636 160000 --- a/backend/JiShop/src/main/resources/config +++ b/backend/JiShop/src/main/resources/config @@ -1 +1 @@ -Subproject commit 04f54a4aa1734aeeab6c21bb705b1568e2c39788 +Subproject commit 4cfe2636a840bcba5c09dd7a116229acfc7d421d diff --git a/backend/JiShop/src/test/java/com/jishop/review/ReviewIntegrationTest.java b/backend/JiShop/src/test/java/com/jishop/review/ReviewIntegrationTest.java index 81fa3eee..12c52f05 100644 --- a/backend/JiShop/src/test/java/com/jishop/review/ReviewIntegrationTest.java +++ b/backend/JiShop/src/test/java/com/jishop/review/ReviewIntegrationTest.java @@ -21,13 +21,22 @@ import com.jishop.product.domain.Labels; import com.jishop.product.domain.Product; import com.jishop.product.domain.SaleStatus; +import com.jishop.product.domain.embed.CategoryInfo; +import com.jishop.product.domain.embed.ImageUrl; +import com.jishop.product.domain.embed.ProductInfo; +import com.jishop.product.domain.embed.Status; import com.jishop.product.repository.ProductRepository; import com.jishop.review.domain.tag.Tag; +import com.jishop.review.dto.LikerIdRequest; import com.jishop.review.dto.ReviewRequest; -import com.jishop.review.service.ReviewService; +import com.jishop.review.dto.ReviewWithUserResponse; import com.jishop.saleproduct.domain.SaleProduct; import com.jishop.saleproduct.repository.SaleProductRepository; -import org.junit.jupiter.api.*; +import org.junit.Assert; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -37,7 +46,6 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; @@ -46,6 +54,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Transactional @@ -59,15 +69,15 @@ public class ReviewIntegrationTest { @Autowired private SaleProductRepository saleProductRepository; - @Autowired - private OptionRepository optionRepository; - @Autowired private CategoryRepository categoryRepository; @Autowired private ProductRepository productRepository; + @Autowired + private OptionRepository optionRepository; + @Autowired private OrderRepository orderRepository; @@ -92,9 +102,11 @@ void init() { categoryRepository.save(category1); categoryRepository.save(category2); - Product product = new Product(category, 5000L, 5010L, 5100L, "MALL-001", "테스트 상품", "테스트 상품 설명", 10000, 8000, 20, LocalDateTime.now() - , false, SaleStatus.SELLING, DiscountStatus.NONE, true, "테스트 브랜드", 0, Labels.SPECIAL_PRICE, - "main.jpg", "image1.jpg", "image2.jpg", "image3.jpg", "image4.jpg", "detail.jpg", 0); + ProductInfo productInfo = new ProductInfo("테스트 상품", "14154", LocalDateTime.now(), "테스트 브랜드", "설명", 1000, 0, 0); + CategoryInfo categoryInfo = new CategoryInfo(5000L, 5010L, 5100L); + Status status = new Status(false, SaleStatus.SELLING, Labels.SPECIAL_PRICE, false, DiscountStatus.NONE); + ImageUrl imageUrl = new ImageUrl("main.jpg", "image1.jpg", "image2.jpg", "image3.jpg", "image4.jpg", "detail.jpg"); + Product product = new Product(productInfo, categoryInfo, status, imageUrl, category, 0, 0); productRepository.save(product); @@ -104,7 +116,6 @@ void init() { SaleProduct saleProduct = new SaleProduct(product, option, "화이트 점퍼"); saleProductRepository.save(saleProduct); - OrderRequest sampleOrderRequest = createSampleOrderRequest(); User user = User.builder() @@ -121,7 +132,22 @@ void init() { .adAgreement(false) // 광고 수신 동의 .build(); + User user1 = User.builder() + .loginId("testuser@example.com") // 로그인 아이디 (이메일 또는 소셜 ID) + .password("testPassword123!") // 비밀번호 (소셜 로그인은 null 가능) + .name("홍순이") // 이름 + .birthDate("1995-05-01") // 생년월일 + .gender("M") // 성별 (M/F) + .phone("010-1234-5678") // 휴대폰 번호 + .provider(LoginType.LOCAL) // 로그인 타입 (enum 값) + .ageAgreement(true) // 만 14세 이상 동의 + .useAgreement(true) // 서비스 이용 약관 동의 + .picAgreement(true) // 개인정보 처리 방침 동의 + .adAgreement(false) // 광고 수신 동의 + .build(); + userRepository.save(user); + userRepository.save(user1); Order order = Order.from(sampleOrderRequest, user, "1"); OrderDetail orderDetail = OrderDetail.from(order, saleProduct, 3); @@ -134,18 +160,27 @@ void init() { } @Test - @DisplayName("리뷰 작성") + @DisplayName("리뷰 작성 하고 리뷰 확인") void createReview() throws Exception { // given - ReviewRequest request = new ReviewRequest(1L, "굳굳", null, Tag.RECOMMENDED, 3); + ReviewRequest request = reviewFixture(); User user = userRepository.findById(1L).orElseThrow(IllegalStateException::new); loginResolver(user); // when Long result = 리뷰작성(request); - // then - Assertions.assertNotNull(result); + MvcResult mvcResult = mvc.perform(get("/reviews/{reviewId}/detail", result) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + String content = mvcResult.getResponse().getContentAsString(); + ReviewWithUserResponse response = objectMapper.readValue(content, ReviewWithUserResponse.class); + + Assert.assertEquals(response.name(), user.getName()); + Assert.assertEquals(response.content(), request.content()); + Assert.assertEquals(response.option(), "화이트/FREE"); } @@ -153,18 +188,43 @@ void createReview() throws Exception { @DisplayName("한번한 리뷰 작성 또 작성할때 에러 발생.") void duplicate_review() throws Exception { // given - ReviewRequest request = new ReviewRequest(1L, "굳굳", null, Tag.RECOMMENDED, 3); + ReviewRequest request = reviewFixture(); User user = userRepository.findById(1L).orElseThrow(IllegalStateException::new); loginResolver(user); 리뷰작성(request); //when - mvc.perform(MockMvcRequestBuilders.post("/reviews") + mvc.perform(post("/reviews") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isConflict()); } + @Test + @DisplayName("리뷰 좋아요 2번이상 하기") + void likeReview_Throw_error() throws Exception { + // given + User user = userRepository.findById(1L).orElseThrow(IllegalStateException::new); + loginResolver(user); + ReviewRequest request = reviewFixture(); + Long reviewId = 리뷰작성(request); + User liker = userRepository.findById(2L).orElseThrow(IllegalStateException::new); + LikerIdRequest likerid = new LikerIdRequest(liker.getId()); + + //when + mvc.perform(post("/reviews/{reviewId}/likes", reviewId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(likerid))) + .andExpect(status().isOk()); + + //then + mvc.perform(post("/reviews/{reviewId}/likes", reviewId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(likerid))) + .andExpect(status().isConflict()); + + } + private OrderRequest createSampleOrderRequest() { // 주소 정보 생성 @@ -186,6 +246,10 @@ private OrderRequest createSampleOrderRequest() { return new OrderRequest(addressRequest, orderDetailRequestList); } + private ReviewRequest reviewFixture() { + return new ReviewRequest(1L, "굳굳", null, Tag.RECOMMENDED, 3); + } + private void loginResolver(User user) throws Exception { given(currentUserResolver.supportsParameter(any())) .willReturn(true); @@ -195,7 +259,7 @@ private void loginResolver(User user) throws Exception { private Long 리뷰작성(ReviewRequest request) throws Exception { - MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.post("/reviews") + MvcResult mvcResult = mvc.perform(post("/reviews") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk())