From 5357fa51dcdc1d3cfd29258071ac1f0813b8be65 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 9 May 2024 20:40:48 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20Repository=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EC=9D=98=20=EB=A6=AC=EB=B7=B0=20=EB=82=B4=EC=97=AD=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/petqua/domain/order/OrderStatus.kt | 1 + .../domain/product/dto/ProductReviewDtos.kt | 47 ++++++ .../review/ProductReviewCustomRepository.kt | 5 +- .../ProductReviewCustomRepositoryImpl.kt | 37 ++++- .../ProductReviewCustomRepositoryImplTest.kt | 141 ++++++++++++++++++ 5 files changed, 229 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/petqua/domain/order/OrderStatus.kt b/src/main/kotlin/com/petqua/domain/order/OrderStatus.kt index 589f8162..b261cabe 100644 --- a/src/main/kotlin/com/petqua/domain/order/OrderStatus.kt +++ b/src/main/kotlin/com/petqua/domain/order/OrderStatus.kt @@ -9,6 +9,7 @@ enum class OrderStatus( ORDER_CONFIRMED("주문 확인"), CANCELED("주문 취소"), PAYMENT_CONFIRMED("결제 완료"), + PURCHASE_CONFIRMED("구매 확정"), DELIVERY_PREPARATION("배송 준비 중"), DELIVERING("배송 중"), DELIVERY_COMPLETED("배송 완료"), diff --git a/src/main/kotlin/com/petqua/domain/product/dto/ProductReviewDtos.kt b/src/main/kotlin/com/petqua/domain/product/dto/ProductReviewDtos.kt index 6765e31c..ef9d5185 100644 --- a/src/main/kotlin/com/petqua/domain/product/dto/ProductReviewDtos.kt +++ b/src/main/kotlin/com/petqua/domain/product/dto/ProductReviewDtos.kt @@ -1,7 +1,14 @@ package com.petqua.domain.product.dto +import com.petqua.domain.delivery.DeliveryMethod import com.petqua.domain.member.Member +import com.petqua.domain.order.Order +import com.petqua.domain.order.OrderPayment +import com.petqua.domain.order.OrderStatus +import com.petqua.domain.product.option.Sex import com.petqua.domain.product.review.ProductReview +import com.petqua.domain.product.review.ProductReviewContent +import com.petqua.domain.product.review.ProductReviewScore import com.petqua.domain.product.review.ProductReviewSorter import com.petqua.domain.product.review.ProductReviewSorter.REVIEW_DATE_DESC import java.time.LocalDateTime @@ -44,6 +51,46 @@ data class ProductReviewWithMemberResponse( ) } +data class MemberProductReviewResponse( + val reviewId: Long, + val memberId: Long, + val createdAt: LocalDateTime, + val orderStatus: OrderStatus, + val storeId: Long, + val storeName: String, + val productId: Long, + val productName: String, + val productThumbnailUrl: String, + val quantity: Int, + val sex: Sex, + val deliveryMethod: DeliveryMethod, + val score: ProductReviewScore, + val content: ProductReviewContent, + val recommendCount: Int, +) { + constructor( + productReview: ProductReview, + order: Order, + orderPayment: OrderPayment, + ) : this( + reviewId = productReview.id, + memberId = productReview.memberId, + createdAt = order.createdAt, + orderStatus = orderPayment.status, + storeId = order.orderProduct.storeId, + storeName = order.orderProduct.storeName, + productId = order.orderProduct.productId, + productName = order.orderProduct.productName, + productThumbnailUrl = order.orderProduct.thumbnailUrl, + quantity = order.orderProduct.quantity, + sex = order.orderProduct.sex, + deliveryMethod = order.orderProduct.deliveryMethod, + score = productReview.score, + content = productReview.content, + recommendCount = productReview.recommendCount, + ) +} + data class ProductReviewScoreWithCount( val score: Int, val count: Long, diff --git a/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepository.kt b/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepository.kt index facb5545..cebee59e 100644 --- a/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepository.kt +++ b/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepository.kt @@ -1,6 +1,7 @@ package com.petqua.domain.product.review import com.petqua.common.domain.dto.CursorBasedPaging +import com.petqua.domain.product.dto.MemberProductReviewResponse import com.petqua.domain.product.dto.ProductReviewReadCondition import com.petqua.domain.product.dto.ProductReviewScoreWithCount import com.petqua.domain.product.dto.ProductReviewWithMemberResponse @@ -9,8 +10,10 @@ interface ProductReviewCustomRepository { fun findAllByCondition( condition: ProductReviewReadCondition, - paging: CursorBasedPaging + paging: CursorBasedPaging, ): List fun findReviewScoresWithCount(productId: Long): List + + fun findMemberProductReviewBy(memberId: Long, paging: CursorBasedPaging): List } diff --git a/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepositoryImpl.kt b/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepositoryImpl.kt index e161e784..4ccaf12f 100644 --- a/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepositoryImpl.kt +++ b/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepositoryImpl.kt @@ -6,6 +6,10 @@ import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderer import com.petqua.common.domain.dto.CursorBasedPaging import com.petqua.common.util.createQuery import com.petqua.domain.member.Member +import com.petqua.domain.order.Order +import com.petqua.domain.order.OrderPayment +import com.petqua.domain.order.OrderProduct +import com.petqua.domain.product.dto.MemberProductReviewResponse import com.petqua.domain.product.dto.ProductReviewReadCondition import com.petqua.domain.product.dto.ProductReviewScoreWithCount import com.petqua.domain.product.dto.ProductReviewWithMemberResponse @@ -21,7 +25,7 @@ class ProductReviewCustomRepositoryImpl( override fun findAllByCondition( condition: ProductReviewReadCondition, - paging: CursorBasedPaging + paging: CursorBasedPaging, ): List { val query = jpql(ProductReviewDynamicJpqlGenerator) { @@ -69,4 +73,35 @@ class ProductReviewCustomRepositoryImpl( jpqlRenderer ) } + + override fun findMemberProductReviewBy( + memberId: Long, + paging: CursorBasedPaging, + ): List { + val query = jpql(ProductReviewDynamicJpqlGenerator) { + selectNew( + entity(ProductReview::class), + entity(Order::class), + entity(OrderPayment::class), + ).from( + entity(ProductReview::class), + join(Order::class).on( + path(ProductReview::memberId).eq(path(Order::memberId)) + .and(path(ProductReview::productId).eq(path(Order::orderProduct)(OrderProduct::productId))) + ), + join(OrderPayment::class).on(path(Order::id).eq(path(OrderPayment::orderId))) + ).whereAnd( + productReviewIdLt(paging.lastViewedId), + ).orderBy( + path(ProductReview::createdAt).desc(), + ) + } + + return entityManager.createQuery( + query, + jpqlRenderContext, + jpqlRenderer, + paging.limit + ) + } } diff --git a/src/test/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepositoryImplTest.kt b/src/test/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepositoryImplTest.kt index 77e056a3..255ec3e8 100644 --- a/src/test/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepositoryImplTest.kt +++ b/src/test/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepositoryImplTest.kt @@ -1,13 +1,21 @@ package com.petqua.domain.product.review import com.petqua.common.domain.dto.CursorBasedPaging +import com.petqua.domain.delivery.DeliveryMethod.SAFETY import com.petqua.domain.member.MemberRepository +import com.petqua.domain.order.OrderNumber +import com.petqua.domain.order.OrderPayment +import com.petqua.domain.order.OrderPaymentRepository +import com.petqua.domain.order.OrderRepository +import com.petqua.domain.order.OrderStatus.PURCHASE_CONFIRMED import com.petqua.domain.product.ProductRepository import com.petqua.domain.product.dto.ProductReviewReadCondition +import com.petqua.domain.product.option.Sex.FEMALE import com.petqua.domain.product.review.ProductReviewSorter.RECOMMEND_DESC import com.petqua.domain.store.StoreRepository import com.petqua.test.DataCleaner import com.petqua.test.fixture.member +import com.petqua.test.fixture.order import com.petqua.test.fixture.product import com.petqua.test.fixture.productReview import com.petqua.test.fixture.store @@ -18,6 +26,7 @@ import io.kotest.matchers.collections.shouldBeSortedWith import io.kotest.matchers.shouldBe import org.springframework.boot.test.context.SpringBootTest import java.math.BigDecimal +import java.math.BigDecimal.ONE @SpringBootTest class ProductReviewCustomRepositoryImplTest( @@ -25,6 +34,8 @@ class ProductReviewCustomRepositoryImplTest( private val productRepository: ProductRepository, private val storeRepository: StoreRepository, private val productReviewRepository: ProductReviewRepository, + private val orderRepository: OrderRepository, + private val orderPaymentRepository: OrderPaymentRepository, private val dataCleaner: DataCleaner, ) : BehaviorSpec({ @@ -228,6 +239,136 @@ class ProductReviewCustomRepositoryImplTest( } } + Given("사용자가 리뷰를 작성한 후 자신의 리뷰 내역을 조회할 때") { + val member = memberRepository.save(member(nickname = "쿠아")) + val store = storeRepository.save(store(name = "펫쿠아")) + val productA = productRepository.save( + product( + name = "상품A", + storeId = store.id, + discountPrice = BigDecimal.ZERO, + reviewCount = 0, + reviewTotalScore = 0 + ) + ) + val productB = productRepository.save( + product( + name = "상품B", + storeId = store.id, + discountPrice = BigDecimal.ZERO, + reviewCount = 0, + reviewTotalScore = 0 + ) + ) + val orderA = orderRepository.save( + order( + orderNumber = OrderNumber.from("202402211607020ORDERNUMBER"), + memberId = member.id, + storeId = store.id, + storeName = store.name, + quantity = 1, + totalAmount = ONE, + productId = productA.id, + productName = productA.name, + thumbnailUrl = productA.thumbnailUrl, + deliveryMethod = SAFETY, + sex = FEMALE, + ) + ) + val orderB = orderRepository.save( + order( + orderNumber = OrderNumber.from("202402211607021ORDERNUMBER"), + memberId = member.id, + storeId = store.id, + storeName = store.name, + quantity = 1, + totalAmount = ONE, + productId = productB.id, + productName = productB.name, + thumbnailUrl = productB.thumbnailUrl, + deliveryMethod = SAFETY, + sex = FEMALE, + ) + ) + orderPaymentRepository.saveAll( + listOf( + OrderPayment( + orderId = orderA.id, + status = PURCHASE_CONFIRMED + ), + OrderPayment( + orderId = orderB.id, + status = PURCHASE_CONFIRMED + ) + ) + ) + + val productReviewA = productReviewRepository.save( + productReview( + productId = productA.id, + reviewerId = member.id, + score = 5, + recommendCount = 1, + hasPhotos = false, + content = "상품A 정말 좋아요!" + ) + ) + val productReviewB = productReviewRepository.save( + productReview( + productId = productB.id, + reviewerId = member.id, + score = 5, + recommendCount = 1, + hasPhotos = false, + content = "상품B 정말 좋아요!" + ) + ) + + When("자신의 리뷰 내역을 조회하면") { + val reviews = productReviewRepository.findMemberProductReviewBy(member.id, CursorBasedPaging()) + + Then("리뷰가 최신순으로 반환된다") { + reviews.size shouldBe 2 + + val memberProductReviewB = reviews[0] + memberProductReviewB.reviewId shouldBe productReviewB.id + memberProductReviewB.memberId shouldBe productReviewB.memberId + memberProductReviewB.createdAt shouldBe orderB.createdAt + memberProductReviewB.orderStatus shouldBe PURCHASE_CONFIRMED + memberProductReviewB.storeId shouldBe orderB.orderProduct.storeId + memberProductReviewB.storeId shouldBe orderB.orderProduct.storeId + memberProductReviewB.storeName shouldBe orderB.orderProduct.storeName + memberProductReviewB.productId shouldBe orderB.orderProduct.productId + memberProductReviewB.productName shouldBe orderB.orderProduct.productName + memberProductReviewB.productThumbnailUrl shouldBe orderB.orderProduct.thumbnailUrl + memberProductReviewB.quantity shouldBe orderB.orderProduct.quantity + memberProductReviewB.sex shouldBe orderB.orderProduct.sex + memberProductReviewB.deliveryMethod shouldBe orderB.orderProduct.deliveryMethod + memberProductReviewB.score shouldBe productReviewB.score + memberProductReviewB.content.value shouldBe productReviewB.content.value + memberProductReviewB.recommendCount shouldBe productReviewB.recommendCount + + val memberProductReviewA = reviews[1] + memberProductReviewA.reviewId shouldBe productReviewA.id + memberProductReviewA.memberId shouldBe productReviewA.memberId + memberProductReviewA.createdAt shouldBe orderA.createdAt + memberProductReviewA.orderStatus shouldBe PURCHASE_CONFIRMED + memberProductReviewA.storeId shouldBe orderA.orderProduct.storeId + memberProductReviewA.storeId shouldBe orderA.orderProduct.storeId + memberProductReviewA.storeName shouldBe orderA.orderProduct.storeName + memberProductReviewA.productId shouldBe orderA.orderProduct.productId + memberProductReviewA.productName shouldBe orderA.orderProduct.productName + memberProductReviewA.productThumbnailUrl shouldBe orderA.orderProduct.thumbnailUrl + memberProductReviewA.quantity shouldBe orderA.orderProduct.quantity + memberProductReviewA.sex shouldBe orderA.orderProduct.sex + memberProductReviewA.deliveryMethod shouldBe orderA.orderProduct.deliveryMethod + memberProductReviewA.score shouldBe productReviewA.score + memberProductReviewA.content.value shouldBe productReviewA.content.value + memberProductReviewA.recommendCount shouldBe productReviewA.recommendCount + } + } + } + afterContainer { dataCleaner.clean() } From 65e6360fe4be94836ade9cc05c11e8e1cf5ec80a Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 9 May 2024 21:31:28 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20Service=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EC=9D=98=20=EB=A6=AC=EB=B7=B0=20=EB=82=B4=EC=97=AD=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/dto/ProductReviewDtos.kt | 157 ++++++++++++++++ .../review/ProductReviewFacadeService.kt | 6 + .../product/review/ProductReviewService.kt | 21 ++- .../domain/product/dto/ProductReviewDtos.kt | 2 +- .../review/ProductReviewCustomRepository.kt | 4 +- .../ProductReviewCustomRepositoryImpl.kt | 6 +- .../review/ProductReviewFacadeServiceTest.kt | 168 ++++++++++++++++++ 7 files changed, 354 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/com/petqua/application/product/dto/ProductReviewDtos.kt b/src/main/kotlin/com/petqua/application/product/dto/ProductReviewDtos.kt index 501de335..e72801b5 100644 --- a/src/main/kotlin/com/petqua/application/product/dto/ProductReviewDtos.kt +++ b/src/main/kotlin/com/petqua/application/product/dto/ProductReviewDtos.kt @@ -5,6 +5,7 @@ import com.petqua.common.domain.dto.DEFAULT_LAST_VIEWED_ID import com.petqua.common.domain.dto.PADDING_FOR_HAS_NEXT_PAGE import com.petqua.common.domain.dto.PAGING_LIMIT_CEILING import com.petqua.domain.auth.LoginMemberOrGuest +import com.petqua.domain.product.dto.MemberProductReview import com.petqua.domain.product.dto.ProductReviewReadCondition import com.petqua.domain.product.dto.ProductReviewWithMemberResponse import com.petqua.domain.product.review.ProductReview @@ -58,6 +59,17 @@ data class ProductReviewReadQuery( } } +data class MemberProductReviewReadQuery( + val memberId: Long, + val lastViewedId: Long = DEFAULT_LAST_VIEWED_ID, + val limit: Int = PAGING_LIMIT_CEILING, +) { + + fun toPaging(): CursorBasedPaging { + return CursorBasedPaging.of(lastViewedId, limit) + } +} + data class ProductReviewsResponse( val productReviews: List, @@ -245,3 +257,148 @@ data class ProductReviewStatisticsResponse( } } } + +data class MemberProductReviewsResponse( + val memberProductReviews: List, + + @Schema( + description = "다음 페이지 존재 여부", + example = "true" + ) + val hasNextPage: Boolean, +) { + companion object { + fun of(memberProductReviews: List, limit: Int): MemberProductReviewsResponse { + return if (memberProductReviews.size > limit) { + MemberProductReviewsResponse( + memberProductReviews.dropLast(PADDING_FOR_HAS_NEXT_PAGE), + hasNextPage = true + ) + } else { + MemberProductReviewsResponse(memberProductReviews, hasNextPage = false) + } + } + } +} + +data class MemberProductReviewResponse( + @Schema( + description = "상품 후기의 id", + example = "1" + ) + val reviewId: Long, + + @Schema( + description = "회원의 id", + example = "1" + ) + val memberId: Long, + + @Schema( + description = "주문 생성 날짜", + example = "2021-08-01T00:00:00" + ) + val createdAt: LocalDateTime, + + @Schema( + description = "주문 상태", + example = "PURCHASE_CONFIRMED" + ) + val orderStatus: String, + + @Schema( + description = "상품 판매점 id", + example = "1" + ) + val storeId: Long, + + @Schema( + description = "상품 판매점", + example = "S아쿠아" + ) + val storeName: String, + + @Schema( + description = "상품 id", + example = "1" + ) + val productId: Long, + + @Schema( + description = "상품 이름", + example = "알비노 풀레드 아시안 고정구피" + ) + val productName: String, + + @Schema( + description = "상품 썸네일 이미지", + example = "https://docs.petqua.co.kr/products/thumbnails/thumbnail1.jpeg" + ) + val productThumbnailUrl: String, + + @Schema( + description = "주문 수량", + example = "1" + ) + val quantity: Int, + + @Schema( + description = "성별", + example = "MALE", + allowableValues = ["MALE", "FEMALE", "HERMAPHRODITE"] + ) + val sex: String, + + @Schema( + description = "배송 방법(\"COMMON : 일반\", \"SAFETY : 안전\", \"PICK_UP : 직접\")", + example = "SAFETY", + allowableValues = ["COMMON", "SAFETY", "PICK_UP"] + ) + val deliveryMethod: String, + + @Schema( + description = "상품 후기 평점", + example = "5" + ) + val score: Int, + + @Schema( + description = "상품 후기 내용", + example = "아주 좋네요" + ) + val content: String, + + @Schema( + description = "상품 후기 추천 수", + example = "3" + ) + val recommendCount: Int, + + @Schema( + description = "상품 후기에 등록된 이미지 url 리스트", + example = "[\"http:/docs.petqua.co.kr/review1.jpg\", \"http:/docs.petqua.co.kr/review2.jpg\"]" + ) + val reviewImages: List, +) { + constructor( + memberProductReview: MemberProductReview, + reviewImages: List, + ) : this( + reviewId = memberProductReview.reviewId, + memberId = memberProductReview.memberId, + createdAt = memberProductReview.createdAt, + orderStatus = memberProductReview.orderStatus.name, + storeId = memberProductReview.storeId, + storeName = memberProductReview.storeName, + productId = memberProductReview.productId, + productName = memberProductReview.productName, + productThumbnailUrl = memberProductReview.productThumbnailUrl, + quantity = memberProductReview.quantity, + sex = memberProductReview.sex.name, + deliveryMethod = memberProductReview.deliveryMethod.name, + score = memberProductReview.score.value, + content = memberProductReview.content.value, + recommendCount = memberProductReview.recommendCount, + reviewImages = reviewImages, + ) +} diff --git a/src/main/kotlin/com/petqua/application/product/review/ProductReviewFacadeService.kt b/src/main/kotlin/com/petqua/application/product/review/ProductReviewFacadeService.kt index d05613e8..2cb5c4e3 100644 --- a/src/main/kotlin/com/petqua/application/product/review/ProductReviewFacadeService.kt +++ b/src/main/kotlin/com/petqua/application/product/review/ProductReviewFacadeService.kt @@ -1,5 +1,7 @@ package com.petqua.application.product.review +import com.petqua.application.product.dto.MemberProductReviewReadQuery +import com.petqua.application.product.dto.MemberProductReviewsResponse import com.petqua.application.product.dto.ProductReviewCreateCommand import com.petqua.application.product.dto.ProductReviewReadQuery import com.petqua.application.product.dto.ProductReviewStatisticsResponse @@ -23,6 +25,10 @@ class ProductReviewFacadeService( return productReviewService.readAll(query) } + fun readMemberProductReviews(query: MemberProductReviewReadQuery): MemberProductReviewsResponse { + return productReviewService.readMemberProductReviews(query) + } + fun readReviewCountStatistics(productId: Long): ProductReviewStatisticsResponse { return productReviewService.readReviewCountStatistics(productId) } diff --git a/src/main/kotlin/com/petqua/application/product/review/ProductReviewService.kt b/src/main/kotlin/com/petqua/application/product/review/ProductReviewService.kt index d96b630a..ce892baa 100644 --- a/src/main/kotlin/com/petqua/application/product/review/ProductReviewService.kt +++ b/src/main/kotlin/com/petqua/application/product/review/ProductReviewService.kt @@ -1,12 +1,14 @@ package com.petqua.application.product.review +import com.petqua.application.product.dto.MemberProductReviewReadQuery +import com.petqua.application.product.dto.MemberProductReviewResponse +import com.petqua.application.product.dto.MemberProductReviewsResponse import com.petqua.application.product.dto.ProductReviewReadQuery import com.petqua.application.product.dto.ProductReviewResponse import com.petqua.application.product.dto.ProductReviewStatisticsResponse import com.petqua.application.product.dto.ProductReviewsResponse import com.petqua.application.product.dto.UpdateReviewRecommendationCommand import com.petqua.common.domain.findByIdOrThrow -import com.petqua.domain.product.dto.ProductReviewWithMemberResponse import com.petqua.domain.product.review.ProductReview import com.petqua.domain.product.review.ProductReviewImage import com.petqua.domain.product.review.ProductReviewImageRepository @@ -42,7 +44,8 @@ class ProductReviewService( query.toCondition(), query.toPaging(), ) - val imagesByReview = getImagesByReview(reviewsByCondition) + val reviewIds = reviewsByCondition.map { it.id } + val imagesByReview = getImagesByReviewIds(reviewIds) val responses = reviewsByCondition.map { ProductReviewResponse(it, imagesByReview[it.id] ?: emptyList()) } @@ -59,12 +62,22 @@ class ProductReviewService( return ProductReviewsResponse.of(responses, query.limit) } - private fun getImagesByReview(reviewsByCondition: List): Map> { - val productReviewIds = reviewsByCondition.map { it.id } + private fun getImagesByReviewIds(productReviewIds: List): Map> { return productReviewImageRepository.findAllByProductReviewIdIn(productReviewIds).groupBy { it.productReviewId } .mapValues { it.value.map { image -> image.imageUrl } } } + fun readMemberProductReviews(query: MemberProductReviewReadQuery): MemberProductReviewsResponse { + val memberProductReviews = productReviewRepository.findMemberProductReviewBy(query.memberId, query.toPaging()) + val reviewIds = memberProductReviews.map { it.reviewId } + val imagesByReview = getImagesByReviewIds(reviewIds) + + val responses = memberProductReviews.map { + MemberProductReviewResponse(it, imagesByReview[it.reviewId] ?: emptyList()) + } + return MemberProductReviewsResponse.of(responses, query.limit) + } + @Transactional(readOnly = true) fun readReviewCountStatistics(productId: Long): ProductReviewStatisticsResponse { val reviewScoreWithCounts = productReviewRepository.findReviewScoresWithCount(productId) diff --git a/src/main/kotlin/com/petqua/domain/product/dto/ProductReviewDtos.kt b/src/main/kotlin/com/petqua/domain/product/dto/ProductReviewDtos.kt index ef9d5185..930b4409 100644 --- a/src/main/kotlin/com/petqua/domain/product/dto/ProductReviewDtos.kt +++ b/src/main/kotlin/com/petqua/domain/product/dto/ProductReviewDtos.kt @@ -51,7 +51,7 @@ data class ProductReviewWithMemberResponse( ) } -data class MemberProductReviewResponse( +data class MemberProductReview( val reviewId: Long, val memberId: Long, val createdAt: LocalDateTime, diff --git a/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepository.kt b/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepository.kt index cebee59e..6094c880 100644 --- a/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepository.kt +++ b/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepository.kt @@ -1,7 +1,7 @@ package com.petqua.domain.product.review import com.petqua.common.domain.dto.CursorBasedPaging -import com.petqua.domain.product.dto.MemberProductReviewResponse +import com.petqua.domain.product.dto.MemberProductReview import com.petqua.domain.product.dto.ProductReviewReadCondition import com.petqua.domain.product.dto.ProductReviewScoreWithCount import com.petqua.domain.product.dto.ProductReviewWithMemberResponse @@ -15,5 +15,5 @@ interface ProductReviewCustomRepository { fun findReviewScoresWithCount(productId: Long): List - fun findMemberProductReviewBy(memberId: Long, paging: CursorBasedPaging): List + fun findMemberProductReviewBy(memberId: Long, paging: CursorBasedPaging): List } diff --git a/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepositoryImpl.kt b/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepositoryImpl.kt index 4ccaf12f..82649cb7 100644 --- a/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepositoryImpl.kt +++ b/src/main/kotlin/com/petqua/domain/product/review/ProductReviewCustomRepositoryImpl.kt @@ -9,7 +9,7 @@ import com.petqua.domain.member.Member import com.petqua.domain.order.Order import com.petqua.domain.order.OrderPayment import com.petqua.domain.order.OrderProduct -import com.petqua.domain.product.dto.MemberProductReviewResponse +import com.petqua.domain.product.dto.MemberProductReview import com.petqua.domain.product.dto.ProductReviewReadCondition import com.petqua.domain.product.dto.ProductReviewScoreWithCount import com.petqua.domain.product.dto.ProductReviewWithMemberResponse @@ -77,9 +77,9 @@ class ProductReviewCustomRepositoryImpl( override fun findMemberProductReviewBy( memberId: Long, paging: CursorBasedPaging, - ): List { + ): List { val query = jpql(ProductReviewDynamicJpqlGenerator) { - selectNew( + selectNew( entity(ProductReview::class), entity(Order::class), entity(OrderPayment::class), diff --git a/src/test/kotlin/com/petqua/application/product/review/ProductReviewFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/product/review/ProductReviewFacadeServiceTest.kt index 04fc3a35..83c1dca6 100644 --- a/src/test/kotlin/com/petqua/application/product/review/ProductReviewFacadeServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/product/review/ProductReviewFacadeServiceTest.kt @@ -2,13 +2,30 @@ package com.petqua.application.product.review import com.amazonaws.services.s3.AmazonS3 import com.ninjasquad.springmockk.SpykBean +import com.petqua.application.product.dto.MemberProductReviewReadQuery import com.petqua.application.product.dto.ProductReviewCreateCommand import com.petqua.common.domain.findByIdOrThrow +import com.petqua.domain.delivery.DeliveryMethod +import com.petqua.domain.member.MemberRepository +import com.petqua.domain.order.OrderNumber +import com.petqua.domain.order.OrderPayment +import com.petqua.domain.order.OrderPaymentRepository +import com.petqua.domain.order.OrderRepository +import com.petqua.domain.order.OrderStatus.PURCHASE_CONFIRMED +import com.petqua.domain.product.ProductRepository +import com.petqua.domain.product.option.Sex import com.petqua.domain.product.review.ProductReviewImageRepository import com.petqua.domain.product.review.ProductReviewRepository +import com.petqua.domain.store.StoreRepository import com.petqua.exception.product.review.ProductReviewException import com.petqua.exception.product.review.ProductReviewExceptionType import com.petqua.test.DataCleaner +import com.petqua.test.fixture.member +import com.petqua.test.fixture.order +import com.petqua.test.fixture.product +import com.petqua.test.fixture.productReview +import com.petqua.test.fixture.productReviewImage +import com.petqua.test.fixture.store import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe @@ -18,12 +35,18 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE import org.springframework.http.MediaType import org.springframework.mock.web.MockMultipartFile +import java.math.BigDecimal @SpringBootTest(webEnvironment = NONE) class ProductReviewFacadeServiceTest( private val productReviewFacadeService: ProductReviewFacadeService, private val productReviewRepository: ProductReviewRepository, private val productReviewImageRepository: ProductReviewImageRepository, + private val orderRepository: OrderRepository, + private val memberRepository: MemberRepository, + private val storeRepository: StoreRepository, + private val productRepository: ProductRepository, + private val orderPaymentRepository: OrderPaymentRepository, private val dataCleaner: DataCleaner, @SpykBean @@ -225,6 +248,151 @@ class ProductReviewFacadeServiceTest( } } + Given("사용자가 리뷰를 작성한 후 자신의 리뷰 내역을 조회할 때") { + val member = memberRepository.save(member(nickname = "쿠아")) + val store = storeRepository.save(store(name = "펫쿠아")) + val productA = productRepository.save( + product( + name = "상품A", + storeId = store.id, + discountPrice = BigDecimal.ZERO, + reviewCount = 0, + reviewTotalScore = 0 + ) + ) + val productB = productRepository.save( + product( + name = "상품B", + storeId = store.id, + discountPrice = BigDecimal.ZERO, + reviewCount = 0, + reviewTotalScore = 0 + ) + ) + val orderA = orderRepository.save( + order( + orderNumber = OrderNumber.from("202402211607020ORDERNUMBER"), + memberId = member.id, + storeId = store.id, + storeName = store.name, + quantity = 1, + totalAmount = BigDecimal.ONE, + productId = productA.id, + productName = productA.name, + thumbnailUrl = productA.thumbnailUrl, + deliveryMethod = DeliveryMethod.SAFETY, + sex = Sex.FEMALE, + ) + ) + val orderB = orderRepository.save( + order( + orderNumber = OrderNumber.from("202402211607021ORDERNUMBER"), + memberId = member.id, + storeId = store.id, + storeName = store.name, + quantity = 1, + totalAmount = BigDecimal.ONE, + productId = productB.id, + productName = productB.name, + thumbnailUrl = productB.thumbnailUrl, + deliveryMethod = DeliveryMethod.SAFETY, + sex = Sex.FEMALE, + ) + ) + orderPaymentRepository.saveAll( + listOf( + OrderPayment( + orderId = orderA.id, + status = PURCHASE_CONFIRMED + ), + OrderPayment( + orderId = orderB.id, + status = PURCHASE_CONFIRMED + ) + ) + ) + + val productReviewA = productReviewRepository.save( + productReview( + productId = productA.id, + reviewerId = member.id, + score = 5, + recommendCount = 1, + hasPhotos = true, + content = "상품A 정말 좋아요!" + ) + ) + val productReviewB = productReviewRepository.save( + productReview( + productId = productB.id, + reviewerId = member.id, + score = 5, + recommendCount = 1, + hasPhotos = false, + content = "상품B 정말 좋아요!" + ) + ) + + productReviewImageRepository.saveAll( + listOf( + productReviewImage(imageUrl = "imageA1", productReviewId = productReviewA.id), + productReviewImage(imageUrl = "imageA2", productReviewId = productReviewA.id) + ) + ) + + When("회원의 Id를 입력해 조회하면") { + val memberProductReviewsResponse = productReviewFacadeService.readMemberProductReviews( + MemberProductReviewReadQuery( + memberId = member.id + ) + ) + + Then("리뷰 내역을 반환한다") { + val memberProductReviews = memberProductReviewsResponse.memberProductReviews + + memberProductReviews.size shouldBe 2 + + val memberProductReviewB = memberProductReviews[0] + memberProductReviewB.reviewId shouldBe productReviewB.id + memberProductReviewB.memberId shouldBe productReviewB.memberId + memberProductReviewB.createdAt shouldBe orderB.createdAt + memberProductReviewB.orderStatus shouldBe PURCHASE_CONFIRMED.name + memberProductReviewB.storeId shouldBe orderB.orderProduct.storeId + memberProductReviewB.storeId shouldBe orderB.orderProduct.storeId + memberProductReviewB.storeName shouldBe orderB.orderProduct.storeName + memberProductReviewB.productId shouldBe orderB.orderProduct.productId + memberProductReviewB.productName shouldBe orderB.orderProduct.productName + memberProductReviewB.productThumbnailUrl shouldBe orderB.orderProduct.thumbnailUrl + memberProductReviewB.quantity shouldBe orderB.orderProduct.quantity + memberProductReviewB.sex shouldBe orderB.orderProduct.sex.name + memberProductReviewB.deliveryMethod shouldBe orderB.orderProduct.deliveryMethod.name + memberProductReviewB.score shouldBe productReviewB.score.value + memberProductReviewB.content shouldBe productReviewB.content.value + memberProductReviewB.recommendCount shouldBe productReviewB.recommendCount + memberProductReviewB.reviewImages.size shouldBe 0 + + val memberProductReviewA = memberProductReviews[1] + memberProductReviewA.reviewId shouldBe productReviewA.id + memberProductReviewA.memberId shouldBe productReviewA.memberId + memberProductReviewA.createdAt shouldBe orderA.createdAt + memberProductReviewA.orderStatus shouldBe PURCHASE_CONFIRMED.name + memberProductReviewA.storeId shouldBe orderA.orderProduct.storeId + memberProductReviewA.storeId shouldBe orderA.orderProduct.storeId + memberProductReviewA.storeName shouldBe orderA.orderProduct.storeName + memberProductReviewA.productId shouldBe orderA.orderProduct.productId + memberProductReviewA.productName shouldBe orderA.orderProduct.productName + memberProductReviewA.productThumbnailUrl shouldBe orderA.orderProduct.thumbnailUrl + memberProductReviewA.quantity shouldBe orderA.orderProduct.quantity + memberProductReviewA.sex shouldBe orderA.orderProduct.sex.name + memberProductReviewA.deliveryMethod shouldBe orderA.orderProduct.deliveryMethod.name + memberProductReviewA.score shouldBe productReviewA.score.value + memberProductReviewA.content shouldBe productReviewA.content.value + memberProductReviewA.recommendCount shouldBe productReviewA.recommendCount + memberProductReviewA.reviewImages shouldBe listOf("imageA1", "imageA2") + } + } + } + afterContainer { dataCleaner.clean() } From 129d7152bb3c8efca0a813b79e6c339a46fd0115 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 9 May 2024 21:41:50 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EC=9D=98=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=82=B4=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/ProductReviewController.kt | 17 +- .../product/dto/ProductReviewDtos.kt | 26 ++- .../product/ProductReviewControllerSteps.kt | 22 +++ .../product/ProductReviewControllerTest.kt | 160 ++++++++++++++++++ 4 files changed, 223 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/petqua/presentation/product/ProductReviewController.kt b/src/main/kotlin/com/petqua/presentation/product/ProductReviewController.kt index 5a4826df..dc7a479b 100644 --- a/src/main/kotlin/com/petqua/presentation/product/ProductReviewController.kt +++ b/src/main/kotlin/com/petqua/presentation/product/ProductReviewController.kt @@ -1,5 +1,6 @@ package com.petqua.presentation.product +import com.petqua.application.product.dto.MemberProductReviewsResponse import com.petqua.application.product.dto.ProductReviewStatisticsResponse import com.petqua.application.product.dto.ProductReviewsResponse import com.petqua.application.product.review.ProductReviewFacadeService @@ -9,6 +10,7 @@ import com.petqua.domain.auth.LoginMember import com.petqua.domain.auth.LoginMemberOrGuest import com.petqua.presentation.product.dto.CreateReviewRequest import com.petqua.presentation.product.dto.ReadAllProductReviewsRequest +import com.petqua.presentation.product.dto.ReadMemberProductReviewsRequest import com.petqua.presentation.product.dto.UpdateReviewRecommendationRequest import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse @@ -63,7 +65,7 @@ class ProductReviewController( @PathVariable productId: Long, ): ResponseEntity { val responses = productReviewFacadeService.readAll( - request.toCommand( + request.toQuery( productId = productId, loginMemberOrGuest = loginMemberOrGuest, ) @@ -71,6 +73,19 @@ class ProductReviewController( return ResponseEntity.ok(responses) } + @Operation(summary = "내 후기 조회 API", description = "내가 작성한 상품의 후기를 조회합니다") + @ApiResponse(responseCode = "200", description = "상품 후기 조건 조회 성공") + @SecurityRequirement(name = ACCESS_TOKEN_SECURITY_SCHEME_KEY) + @GetMapping("/product-reviews/me") + fun readMemberProductReviews( + @Auth loginMember: LoginMember, + request: ReadMemberProductReviewsRequest, + ): ResponseEntity { + val query = request.toQuery(loginMember) + val response = productReviewFacadeService.readMemberProductReviews(query) + return ResponseEntity.ok(response) + } + @Operation(summary = "상품 후기 통계 조회 API", description = "상품의 후기 통계를 조회합니다") @ApiResponse(responseCode = "200", description = "상품 후기 통계 조회 성공") @GetMapping("/products/{productId}/review-statistics") diff --git a/src/main/kotlin/com/petqua/presentation/product/dto/ProductReviewDtos.kt b/src/main/kotlin/com/petqua/presentation/product/dto/ProductReviewDtos.kt index f7ab137e..bc58680b 100644 --- a/src/main/kotlin/com/petqua/presentation/product/dto/ProductReviewDtos.kt +++ b/src/main/kotlin/com/petqua/presentation/product/dto/ProductReviewDtos.kt @@ -1,9 +1,11 @@ package com.petqua.presentation.product.dto +import com.petqua.application.product.dto.MemberProductReviewReadQuery import com.petqua.application.product.dto.ProductReviewCreateCommand import com.petqua.application.product.dto.ProductReviewReadQuery import com.petqua.application.product.dto.UpdateReviewRecommendationCommand import com.petqua.common.domain.dto.PAGING_LIMIT_CEILING +import com.petqua.domain.auth.LoginMember import com.petqua.domain.auth.LoginMemberOrGuest import com.petqua.domain.product.review.ProductReviewSorter import com.petqua.domain.product.review.ProductReviewSorter.REVIEW_DATE_DESC @@ -72,7 +74,7 @@ data class ReadAllProductReviewsRequest( ) val limit: Int = PAGING_LIMIT_CEILING, ) { - fun toCommand(productId: Long, loginMemberOrGuest: LoginMemberOrGuest): ProductReviewReadQuery { + fun toQuery(productId: Long, loginMemberOrGuest: LoginMemberOrGuest): ProductReviewReadQuery { return ProductReviewReadQuery( productId = productId, loginMemberOrGuest = loginMemberOrGuest, @@ -85,6 +87,28 @@ data class ReadAllProductReviewsRequest( } } +data class ReadMemberProductReviewsRequest( + @Schema( + description = "마지막으로 조회한 후기의 Id", + example = "1", + ) + val lastViewedId: Long, + + @Schema( + description = "조회할 상품 개수", + defaultValue = "20" + ) + val limit: Int = PAGING_LIMIT_CEILING, +) { + fun toQuery(loginMember: LoginMember): MemberProductReviewReadQuery { + return MemberProductReviewReadQuery( + memberId = loginMember.memberId, + lastViewedId = lastViewedId, + limit = limit + ) + } +} + data class UpdateReviewRecommendationRequest( @Schema( description = "상품 후기 id", diff --git a/src/test/kotlin/com/petqua/presentation/product/ProductReviewControllerSteps.kt b/src/test/kotlin/com/petqua/presentation/product/ProductReviewControllerSteps.kt index 14b435bb..f1cae3ff 100644 --- a/src/test/kotlin/com/petqua/presentation/product/ProductReviewControllerSteps.kt +++ b/src/test/kotlin/com/petqua/presentation/product/ProductReviewControllerSteps.kt @@ -95,3 +95,25 @@ fun requestUpdateReviewRecommendation( response() } } + +fun requestReadMemberProductReviews( + lastViewedId: Long = -1, + limit: Int = 20, + accessToken: String, +): Response { + return Given { + log().all() + contentType(APPLICATION_JSON_VALUE) + auth().preemptive().oauth2(accessToken) + params( + "lastViewedId", lastViewedId, + "limit", limit, + ) + } When { + get("/product-reviews/me") + } Then { + log().all() + } Extract { + response() + } +} diff --git a/src/test/kotlin/com/petqua/presentation/product/ProductReviewControllerTest.kt b/src/test/kotlin/com/petqua/presentation/product/ProductReviewControllerTest.kt index 5351bdbc..e3e3cc82 100644 --- a/src/test/kotlin/com/petqua/presentation/product/ProductReviewControllerTest.kt +++ b/src/test/kotlin/com/petqua/presentation/product/ProductReviewControllerTest.kt @@ -2,12 +2,20 @@ package com.petqua.presentation.product import com.amazonaws.services.s3.AmazonS3 import com.ninjasquad.springmockk.SpykBean +import com.petqua.application.product.dto.MemberProductReviewsResponse import com.petqua.application.product.dto.ProductReviewStatisticsResponse import com.petqua.application.product.dto.ProductReviewsResponse import com.petqua.common.domain.findByIdOrThrow import com.petqua.common.exception.ExceptionResponse +import com.petqua.domain.delivery.DeliveryMethod import com.petqua.domain.member.MemberRepository +import com.petqua.domain.order.OrderNumber +import com.petqua.domain.order.OrderPayment +import com.petqua.domain.order.OrderPaymentRepository +import com.petqua.domain.order.OrderRepository +import com.petqua.domain.order.OrderStatus import com.petqua.domain.product.ProductRepository +import com.petqua.domain.product.option.Sex import com.petqua.domain.product.review.ProductReviewImageRepository import com.petqua.domain.product.review.ProductReviewRepository import com.petqua.domain.store.StoreRepository @@ -18,6 +26,7 @@ import com.petqua.presentation.product.dto.CreateReviewRequest import com.petqua.presentation.product.dto.UpdateReviewRecommendationRequest import com.petqua.test.ApiTestConfig import com.petqua.test.fixture.member +import com.petqua.test.fixture.order import com.petqua.test.fixture.product import com.petqua.test.fixture.productReview import com.petqua.test.fixture.productReviewImage @@ -32,6 +41,7 @@ import io.mockk.verify import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.CREATED import org.springframework.http.HttpStatus.NO_CONTENT +import org.springframework.http.HttpStatus.OK import org.springframework.http.MediaType.IMAGE_JPEG_VALUE import org.springframework.mock.web.MockMultipartFile import java.math.BigDecimal @@ -42,6 +52,8 @@ class ProductReviewControllerTest( private val storeRepository: StoreRepository, private val productReviewRepository: ProductReviewRepository, private val productReviewImageRepository: ProductReviewImageRepository, + private val orderRepository: OrderRepository, + private val orderPaymentRepository: OrderPaymentRepository, @SpykBean private val amazonS3: AmazonS3, @@ -472,5 +484,153 @@ class ProductReviewControllerTest( } } } + + Given("사용자가 리뷰를 작성한 후 자신의 리뷰 내역을 조회할 때") { + val accessToken = signInAsMember().accessToken + val memberId = getMemberIdByAccessToken(accessToken) + val store = storeRepository.save(store(name = "펫쿠아")) + val productA = productRepository.save( + product( + name = "상품A", + storeId = store.id, + discountPrice = BigDecimal.ZERO, + reviewCount = 0, + reviewTotalScore = 0 + ) + ) + val productB = productRepository.save( + product( + name = "상품B", + storeId = store.id, + discountPrice = BigDecimal.ZERO, + reviewCount = 0, + reviewTotalScore = 0 + ) + ) + val orderA = orderRepository.save( + order( + orderNumber = OrderNumber.from("202402211607020ORDERNUMBER"), + memberId = memberId, + storeId = store.id, + storeName = store.name, + quantity = 1, + totalAmount = BigDecimal.ONE, + productId = productA.id, + productName = productA.name, + thumbnailUrl = productA.thumbnailUrl, + deliveryMethod = DeliveryMethod.SAFETY, + sex = Sex.FEMALE, + ) + ) + val orderB = orderRepository.save( + order( + orderNumber = OrderNumber.from("202402211607021ORDERNUMBER"), + memberId = memberId, + storeId = store.id, + storeName = store.name, + quantity = 1, + totalAmount = BigDecimal.ONE, + productId = productB.id, + productName = productB.name, + thumbnailUrl = productB.thumbnailUrl, + deliveryMethod = DeliveryMethod.SAFETY, + sex = Sex.FEMALE, + ) + ) + orderPaymentRepository.saveAll( + listOf( + OrderPayment( + orderId = orderA.id, + status = OrderStatus.PURCHASE_CONFIRMED + ), + OrderPayment( + orderId = orderB.id, + status = OrderStatus.PURCHASE_CONFIRMED + ) + ) + ) + + val productReviewA = productReviewRepository.save( + productReview( + productId = productA.id, + reviewerId = memberId, + score = 5, + recommendCount = 1, + hasPhotos = true, + content = "상품A 정말 좋아요!" + ) + ) + val productReviewB = productReviewRepository.save( + productReview( + productId = productB.id, + reviewerId = memberId, + score = 5, + recommendCount = 1, + hasPhotos = false, + content = "상품B 정말 좋아요!" + ) + ) + + productReviewImageRepository.saveAll( + listOf( + productReviewImage(imageUrl = "imageA1", productReviewId = productReviewA.id), + productReviewImage(imageUrl = "imageA2", productReviewId = productReviewA.id) + ) + ) + + When("회원이 자신의 리뷰 내역을 조회하면") { + val response = requestReadMemberProductReviews(accessToken = accessToken) + + Then("200 OK 로 응답한다") { + response.statusCode shouldBe OK.value() + } + + Then("리뷰 내역을 응답한다") { + val memberProductReviewsResponse = response.`as`(MemberProductReviewsResponse::class.java) + + val memberProductReviews = memberProductReviewsResponse.memberProductReviews + + memberProductReviews.size shouldBe 2 + + val memberProductReviewB = memberProductReviews[0] + memberProductReviewB.reviewId shouldBe productReviewB.id + memberProductReviewB.memberId shouldBe productReviewB.memberId + memberProductReviewB.createdAt shouldBe orderB.createdAt + memberProductReviewB.orderStatus shouldBe OrderStatus.PURCHASE_CONFIRMED.name + memberProductReviewB.storeId shouldBe orderB.orderProduct.storeId + memberProductReviewB.storeId shouldBe orderB.orderProduct.storeId + memberProductReviewB.storeName shouldBe orderB.orderProduct.storeName + memberProductReviewB.productId shouldBe orderB.orderProduct.productId + memberProductReviewB.productName shouldBe orderB.orderProduct.productName + memberProductReviewB.productThumbnailUrl shouldBe orderB.orderProduct.thumbnailUrl + memberProductReviewB.quantity shouldBe orderB.orderProduct.quantity + memberProductReviewB.sex shouldBe orderB.orderProduct.sex.name + memberProductReviewB.deliveryMethod shouldBe orderB.orderProduct.deliveryMethod.name + memberProductReviewB.score shouldBe productReviewB.score.value + memberProductReviewB.content shouldBe productReviewB.content.value + memberProductReviewB.recommendCount shouldBe productReviewB.recommendCount + memberProductReviewB.reviewImages.size shouldBe 0 + + val memberProductReviewA = memberProductReviews[1] + memberProductReviewA.reviewId shouldBe productReviewA.id + memberProductReviewA.memberId shouldBe productReviewA.memberId + memberProductReviewA.createdAt shouldBe orderA.createdAt + memberProductReviewA.orderStatus shouldBe OrderStatus.PURCHASE_CONFIRMED.name + memberProductReviewA.storeId shouldBe orderA.orderProduct.storeId + memberProductReviewA.storeId shouldBe orderA.orderProduct.storeId + memberProductReviewA.storeName shouldBe orderA.orderProduct.storeName + memberProductReviewA.productId shouldBe orderA.orderProduct.productId + memberProductReviewA.productName shouldBe orderA.orderProduct.productName + memberProductReviewA.productThumbnailUrl shouldBe orderA.orderProduct.thumbnailUrl + memberProductReviewA.quantity shouldBe orderA.orderProduct.quantity + memberProductReviewA.sex shouldBe orderA.orderProduct.sex.name + memberProductReviewA.deliveryMethod shouldBe orderA.orderProduct.deliveryMethod.name + memberProductReviewA.score shouldBe productReviewA.score.value + memberProductReviewA.content shouldBe productReviewA.content.value + memberProductReviewA.recommendCount shouldBe productReviewA.recommendCount + memberProductReviewA.reviewImages shouldBe listOf("imageA1", "imageA2") + } + } + } } }