Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: 봉달 목록 조회 api #38

Merged
merged 12 commits into from
Feb 2, 2024
Merged
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.petqua.application.cart

import com.petqua.application.cart.dto.CartProductResponse
import com.petqua.application.cart.dto.DeleteCartProductCommand
import com.petqua.application.cart.dto.SaveCartProductCommand
import com.petqua.application.cart.dto.UpdateCartProductOptionCommand
import com.petqua.common.domain.existByIdOrThrow
import com.petqua.common.domain.findByIdOrThrow
import com.petqua.domain.cart.CartProduct
import com.petqua.domain.cart.CartProductRepository
import com.petqua.domain.cart.DeliveryMethod
import com.petqua.domain.member.MemberRepository
import com.petqua.domain.product.ProductRepository
import com.petqua.domain.product.dto.ProductResponse
import com.petqua.exception.cart.CartProductException
import com.petqua.exception.cart.CartProductExceptionType.DUPLICATED_PRODUCT
import com.petqua.exception.cart.CartProductExceptionType.NOT_FOUND_CART_PRODUCT
Expand Down Expand Up @@ -76,4 +79,21 @@ class CartProductService(
cartProduct.validateOwner(command.memberId)
cartProductRepository.delete(cartProduct)
}

@Transactional(readOnly = true)
fun readAll(memberId: Long): List<CartProductResponse> {
memberRepository.existByIdOrThrow(memberId, MemberException(NOT_FOUND_MEMBER))
val cartProducts = cartProductRepository.findAllByMemberId(memberId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헛 저 찜 목록 조회할 때 이거까지 한 번에 조인해서 가져오게했는데, 혹시 cartProducts를 미리 조회해오신 이유가 있으신가요?

Copy link
Member Author

@TaeyeonRoyce TaeyeonRoyce Feb 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Product가 제거된 cartProduct가 조회 되지 않을 것 같아서 분리했었습니다.
근데 outer join 하고 product가 null인 걸로 삭제 판단해도 될 것 같네요!
커넥션을 줄이는 방향으로 수정해보겠습니다!

val products = productByIds(cartProducts)
val findAll = productRepository.findAll()
TaeyeonRoyce marked this conversation as resolved.
Show resolved Hide resolved
return cartProducts.map {
products[it.id]?.let { product -> CartProductResponse.of(it, product) }
?: CartProductResponse.fromDeletedProduct(it)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

장바구니 전체 가격은 클라이언트에서 계산하는 걸까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 협의해보고 적용하겠습니다!


private fun productByIds(cartProducts: List<CartProduct>): Map<Long, ProductResponse> {
val productIdsFromCart = cartProducts.map { it.productId }
return productRepository.findAllProductResponseByIdIn(productIdsFromCart).associateBy { it.id }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.petqua.application.cart.dto
import com.petqua.domain.cart.CartProduct
import com.petqua.domain.cart.CartProductQuantity
import com.petqua.domain.cart.DeliveryMethod
import com.petqua.domain.product.dto.ProductResponse

data class SaveCartProductCommand(
val memberId: Long,
Expand Down Expand Up @@ -35,3 +36,56 @@ data class DeleteCartProductCommand(
val memberId: Long,
val cartProductId: Long,
)

data class CartProductResponse(
val id: Long,
val storeName: String,
val productId: Long,
val productName: String,
val productThumbnailUrl: String,
val productPrice: Int,
val productDiscountRate: Int,
val productDiscountPrice: Int,
val quantity: Int,
val isMale: Boolean,
val deliveryMethod: String,
val isOnSale: Boolean,
) {

companion object {
fun of(cartProduct: CartProduct, productResponse: ProductResponse): CartProductResponse {
return CartProductResponse(
id = cartProduct.id,
storeName = productResponse.storeName,
productId = productResponse.id,
productName = productResponse.name,
productThumbnailUrl = productResponse.thumbnailUrl,
productPrice = productResponse.price,
productDiscountRate = productResponse.discountRate,
productDiscountPrice = productResponse.discountPrice,
quantity = cartProduct.quantity.value,
isMale = cartProduct.isMale,
deliveryMethod = cartProduct.deliveryMethod.name,
isOnSale = true,
)
}


fun fromDeletedProduct(cartProduct: CartProduct): CartProductResponse {
return CartProductResponse(
id = cartProduct.id,
storeName = "",
productId = cartProduct.productId,
productName = "",
productThumbnailUrl = "",
productPrice = 0,
productDiscountRate = 0,
productDiscountPrice = 0,
quantity = cartProduct.quantity.value,
isMale = cartProduct.isMale,
deliveryMethod = cartProduct.deliveryMethod.name,
isOnSale = false,
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 디자인이 아직 안정해졌다고 해서 임의로 보내는 값인가요?.?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 경우에는 [품절]로 표기 하신다고 했어요!
client 입장에서 봉달 목록 data를 한번에 조회하게 되는데 품절인 상품과 그렇지 않은 상품의 응답 스키마가 달라지면 복잡해질 것 같더라고요.
그래서 그냥 임의 데이터로 채워 넣고 isOnSale로 품절 여부를 판단할 수 있게 두었습니다!

}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/com/petqua/common/util/EntityManagerUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ inline fun <reified T> EntityManager.createQuery(
.resultList
}

inline fun <reified T> EntityManager.createQuery(
query: SelectQuery<*>,
context: JpqlRenderContext,
renderer: JpqlRenderer,
): List<T> {
val rendered = renderer.render(query, context)
return this.createQuery(rendered.query, T::class.java)
.apply { rendered.params.forEach { (name, value) -> setParameter(name, value) } }
.resultList
}

inline fun <reified T> EntityManager.createCountQuery(
query: SelectQuery<*>,
context: JpqlRenderContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ interface CartProductRepository : JpaRepository<CartProduct, Long> {
deliveryMethod: DeliveryMethod
): CartProduct?

fun findAllByIdIn(ids: List<Long>): List<CartProduct>
fun findAllByMemberId(id: Long): List<CartProduct>
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ interface ProductCustomRepository {
fun findAllByCondition(condition: ProductReadCondition, paging: ProductPaging): List<ProductResponse>

fun countByCondition(condition: ProductReadCondition): Int

fun findAllProductResponseByIdIn(ids: List<Long>): List<ProductResponse>
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,26 @@ class ProductCustomRepositoryImpl(
jpqlRenderer
)
}

override fun findAllProductResponseByIdIn(ids: List<Long>): List<ProductResponse> {
val query = jpql {
selectNew<ProductResponse>(
entity(Product::class),
path(Store::name)
).from(
entity(Product::class),
join(Store::class).on(path(Product::storeId).eq(path(Store::id))),
).where(
predicateByIds(ids)
)
}
Comment on lines +101 to +111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멋지게 잘 쓰셨네요!
혹시 데이터 순서도 명시하면 어떨까요?


return entityManager.createQuery(
query,
jpqlRenderContext,
jpqlRenderer
)
}

private fun Jpql.predicateByIds(ids: List<Long>) = if (ids.isEmpty()) null else path(Product::id).`in`(ids)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package com.petqua.presentation.cart

import com.petqua.application.cart.CartProductService
import com.petqua.application.cart.dto.DeleteCartProductCommand
import com.petqua.application.cart.dto.CartProductResponse
import com.petqua.application.product.dto.ProductDetailResponse
import com.petqua.domain.auth.Auth
import com.petqua.domain.auth.LoginMember
import com.petqua.presentation.cart.dto.SaveCartProductRequest
import com.petqua.presentation.cart.dto.UpdateCartProductOptionRequest
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
Expand Down Expand Up @@ -60,4 +62,12 @@ class CartProductController(
cartProductService.delete(command)
return ResponseEntity.noContent().build()
}

@GetMapping
fun readAll(
@Auth loginMember: LoginMember,
): ResponseEntity<List<CartProductResponse>> {
val responses = cartProductService.readAll(loginMember.memberId)
return ResponseEntity.ok(responses)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.petqua.domain.cart.DeliveryMethod.COMMON
import com.petqua.domain.cart.DeliveryMethod.SAFETY
import com.petqua.domain.member.MemberRepository
import com.petqua.domain.product.ProductRepository
import com.petqua.domain.store.StoreRepository
import com.petqua.exception.cart.CartProductException
import com.petqua.exception.cart.CartProductExceptionType.DUPLICATED_PRODUCT
import com.petqua.exception.cart.CartProductExceptionType.FORBIDDEN_CART_PRODUCT
Expand All @@ -22,6 +23,7 @@ import com.petqua.test.DataCleaner
import com.petqua.test.fixture.cartProduct
import com.petqua.test.fixture.member
import com.petqua.test.fixture.product
import com.petqua.test.fixture.store
import io.kotest.assertions.assertSoftly
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.BehaviorSpec
Expand All @@ -35,6 +37,7 @@ class CartProductServiceTest(
private val cartProductRepository: CartProductRepository,
private val productRepository: ProductRepository,
private val memberRepository: MemberRepository,
private val storeRepository: StoreRepository,
private val dataCleaner: DataCleaner,
) : BehaviorSpec({

Expand Down Expand Up @@ -173,16 +176,16 @@ class CartProductServiceTest(
cartProduct(
memberId = memberId,
productId = productId,
isMale = true,
deliveryMethod = COMMON
isMale = false,
deliveryMethod = SAFETY
)
)
val command = UpdateCartProductOptionCommand(
cartProductId = cartProduct.id,
memberId = memberId,
quantity = CartProductQuantity(3),
isMale = true,
deliveryMethod = COMMON,
isMale = false,
deliveryMethod = SAFETY,
)
Then("예외가 발생 한다") {
shouldThrow<CartProductException> {
Expand Down Expand Up @@ -251,6 +254,58 @@ class CartProductServiceTest(
}
}

Given("봉달 상품 조회시") {
val store = storeRepository.save(store(name = "store"))
val productAId = productRepository.save(product(storeId = store.id)).id
val productBId = productRepository.save(product(storeId = store.id)).id
val productCId = productRepository.save(product(storeId = store.id)).id
val memberId = memberRepository.save(member()).id
cartProductRepository.saveAll(
listOf(
cartProduct(memberId = memberId, productId = productAId),
cartProduct(memberId = memberId, productId = productBId),
cartProduct(memberId = memberId, productId = productCId),
)
)

When("봉달 상품이 있는 회원이 조회 하는 경우") {
val result = cartProductService.readAll(memberId)

Then("봉달 상품 리스트를 반환 한다") {
result.size shouldBe 3
}
}

When("봉달 상품이 없는 회원이 조회 하는 경우") {
val newMemberId = memberRepository.save(member()).id
val results = cartProductService.readAll(newMemberId)

Then("빈 리스트를 반환 한다") {
results.size shouldBe 0
}
}

When("봉달에 담아둔 상품이 삭제된 경우") {
productRepository.deleteById(productAId)
val results = cartProductService.readAll(memberId)

Then("빈 리스트를 반환 한다") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 Then 글 잘못된 것 같아요!

assertSoftly(results) {
size shouldBe 3
find { it.productId == productAId }!!.isOnSale shouldBe false
}
}
}

When("존재 하지 않는 회원이 조회 하는 경우") {
Then("예외가 발생 한다") {
shouldThrow<MemberException> {
cartProductService.readAll(Long.MIN_VALUE)
}.exceptionType() shouldBe NOT_FOUND_MEMBER
}
}
}

afterContainer {
dataCleaner.clean()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,22 @@ class ProductCustomRepositoryImplTest(
}
}

Given("다중 id로 ProductResponse를 조회 할 때") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

val product1 = productRepository.save(product(name = "상품1", storeId = store.id))
val product2 = productRepository.save(product(name = "상품2", storeId = store.id))

When("id 목록을 입력하면") {
val products = productRepository.findAllProductResponseByIdIn(listOf(product1.id, product2.id))

Then("해당 id의 ProductResponse를 반환한다") {
products shouldContainExactly listOf(
ProductResponse(product1, store.name),
ProductResponse(product2, store.name),
)
}
}
}

afterContainer {
dataCleaner.clean()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,18 @@ fun requestDeleteCartProduct(
response()
}
}

fun requestReadAllCartProducts(
accessToken: String
): Response {
return Given {
log().all()
.header(HttpHeaders.AUTHORIZATION, accessToken)
} When {
get("/carts")
} Then {
log().all()
} Extract {
response()
}
}
Loading
Loading