Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions src/main/kotlin/com/petqua/application/order/OrderService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package com.petqua.application.order

import com.petqua.application.order.dto.OrderDetailReadQuery
import com.petqua.application.order.dto.OrderProductCommand
import com.petqua.application.order.dto.OrderReadQuery
import com.petqua.application.order.dto.SaveOrderCommand
import com.petqua.application.order.dto.SaveOrderResponse
import com.petqua.application.payment.infra.PaymentGatewayClient
import com.petqua.common.domain.dto.DEFAULT_LAST_VIEWED_ID
import com.petqua.common.domain.findByIdOrThrow
import com.petqua.common.util.getOrThrow
import com.petqua.common.util.throwExceptionWhen
Expand All @@ -16,6 +18,7 @@ import com.petqua.domain.order.OrderPayment
import com.petqua.domain.order.OrderPaymentRepository
import com.petqua.domain.order.OrderRepository
import com.petqua.domain.order.OrderShippingAddress
import com.petqua.domain.order.OrderStatus
import com.petqua.domain.order.ShippingAddress
import com.petqua.domain.order.ShippingAddressRepository
import com.petqua.domain.order.ShippingNumber
Expand All @@ -28,6 +31,7 @@ import com.petqua.domain.product.option.ProductOptionRepository
import com.petqua.domain.store.StoreRepository
import com.petqua.exception.order.OrderException
import com.petqua.exception.order.OrderExceptionType.EMPTY_SHIPPING_ADDRESS
import com.petqua.exception.order.OrderExceptionType.NOT_INVALID_ORDER_READ_QUERY
import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND
import com.petqua.exception.order.OrderExceptionType.PRODUCT_NOT_FOUND
import com.petqua.exception.order.OrderExceptionType.STORE_NOT_FOUND
Expand All @@ -37,6 +41,7 @@ import com.petqua.exception.product.ProductException
import com.petqua.exception.product.ProductExceptionType.NOT_FOUND_PRODUCT
import com.petqua.presentation.order.dto.OrderDetailResponse
import com.petqua.presentation.order.dto.OrderProductResponse
import com.petqua.presentation.order.dto.OrdersResponse
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

Expand Down Expand Up @@ -151,9 +156,51 @@ class OrderService(
}

private fun orderProductResponsesFromOrders(orders: List<Order>): List<OrderProductResponse> {
val statusByOrderId = orders.map { orderPaymentRepository.findOrderStatusByOrderId(it.id) }
.associateBy { orderPayment -> orderPayment.orderId }
.mapValues { it.value.status }
return orders.map { OrderProductResponse(it, statusByOrderId.getOrThrow(it.id)) }
val orderStatusByOrderId = orderStatusByOrders(orders)
return orders.map { OrderProductResponse(it, orderStatusByOrderId.getOrThrow(it.id)) }
}

@Transactional(readOnly = true)
fun readAll(query: OrderReadQuery): OrdersResponse {
validateOrderReadQuery(query)
val orders = orderRepository.findOrdersByMemberId(query.memberId, query.toOrderPaging())
val ordersByOrderNumber = orders.groupBy { it.orderNumber }
val orderDetails = ordersByOrderNumber.mapValues {
orderDetailResponseFromOrders(it.value)
}
return OrdersResponse.of(orderDetails.values.toList(), query.limit)
}

private fun validateOrderReadQuery(query: OrderReadQuery) {
if (query.lastViewedId == DEFAULT_LAST_VIEWED_ID) {
return
}

val order = orderRepository.findByIdOrThrow(query.lastViewedId)
throwExceptionWhen(order.orderNumber != query.lastViewedOrderNumber) {
throw OrderException(NOT_INVALID_ORDER_READ_QUERY)
}
}

private fun orderDetailResponseFromOrders(orders: List<Order>): OrderDetailResponse {
val orderStatusByOrderId = orderStatusByOrders(orders)
val representativeOrder = orders[0]
val orderProductResponses = orders.map {
val orderStatus = orderStatusByOrderId.getOrThrow(it.id)
OrderProductResponse(it, orderStatus)
}

return OrderDetailResponse(
orderNumber = representativeOrder.orderNumber.value,
orderedAt = representativeOrder.createdAt,
orderProducts = orderProductResponses,
totalAmount = representativeOrder.totalAmount,
)
}

private fun orderStatusByOrders(orders: List<Order>): Map<Long, OrderStatus> {
val orderIds = orders.map { it.id }
return orderPaymentRepository.findOrderStatusByOrderIds(orderIds)
.associateBy({ it.orderId }, { it.status })
}
}
45 changes: 45 additions & 0 deletions src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package com.petqua.application.order.dto

import com.petqua.common.domain.Money
import com.petqua.common.domain.dto.DEFAULT_LAST_VIEWED_ID
import com.petqua.common.util.throwExceptionWhen
import com.petqua.domain.delivery.DeliveryMethod
import com.petqua.domain.order.OrderNumber
import com.petqua.domain.order.OrderPaging
import com.petqua.domain.order.OrderProduct
import com.petqua.domain.order.ShippingNumber
import com.petqua.domain.product.ProductSnapshot
import com.petqua.domain.product.option.ProductOption
import com.petqua.domain.product.option.Sex
import com.petqua.exception.order.OrderException
import com.petqua.exception.order.OrderExceptionType.NOT_INVALID_ORDER_READ_QUERY
import io.swagger.v3.oas.annotations.media.Schema

data class SaveOrderCommand(
Expand Down Expand Up @@ -91,3 +96,43 @@ data class OrderDetailReadQuery(
}
}
}


data class OrderReadQuery internal constructor(
val memberId: Long,
val lastViewedId: Long,
val limit: Int,
val lastViewedOrderNumber: OrderNumber?,
) {

companion object {
fun of(
memberId: Long,
lastViewedId: Long,
limit: Int,
lastViewedOrderNumber: String?
): OrderReadQuery {
validateLastViewedIdAndOrderNumber(lastViewedId, lastViewedOrderNumber)
return OrderReadQuery(
memberId = memberId,
lastViewedId = lastViewedId,
limit = limit,
lastViewedOrderNumber = lastViewedOrderNumber?.let { OrderNumber(it) },
)
}

private fun validateLastViewedIdAndOrderNumber(lastViewedId: Long, lastViewedOrderNumber: String?) {
throwExceptionWhen(lastViewedId == DEFAULT_LAST_VIEWED_ID && lastViewedOrderNumber != null) {
throw OrderException(NOT_INVALID_ORDER_READ_QUERY)
}

throwExceptionWhen(lastViewedId != DEFAULT_LAST_VIEWED_ID && lastViewedOrderNumber == null) {
throw OrderException(NOT_INVALID_ORDER_READ_QUERY)
}
}
}

fun toOrderPaging(): OrderPaging {
return OrderPaging.of(lastViewedId, limit, lastViewedOrderNumber)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.petqua.domain.order

import com.petqua.common.domain.dto.DEFAULT_LAST_VIEWED_ID
import com.petqua.common.domain.dto.PADDING_FOR_HAS_NEXT_PAGE

private const val ORDER_PAGING_LIMIT_CEILING = 5

data class OrderPaging(
val lastViewedId: Long? = null,
val limit: Int = ORDER_PAGING_LIMIT_CEILING,
val lastViewedOrderNumber: OrderNumber? = null,
) {

companion object {
fun of(
lastViewedId: Long,
limit: Int,
lastViewedOrderNumber: OrderNumber?,
): OrderPaging {
val adjustedLastViewedId = if (lastViewedId == DEFAULT_LAST_VIEWED_ID) null else lastViewedId
val adjustedLimit = if (limit > ORDER_PAGING_LIMIT_CEILING) ORDER_PAGING_LIMIT_CEILING else limit
return OrderPaging(adjustedLastViewedId, adjustedLimit + PADDING_FOR_HAS_NEXT_PAGE, lastViewedOrderNumber)
}
}
}

interface OrderCustomRepository {

fun findOrdersByMemberId(
memberId: Long,
orderPaging: OrderPaging,
): List<Order>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.petqua.domain.order

import com.linecorp.kotlinjdsl.dsl.jpql.jpql
import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderContext
import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderer
import com.petqua.common.util.createQuery
import jakarta.persistence.EntityManager
import org.springframework.stereotype.Repository


@Repository
class OrderCustomRepositoryImpl(
private val entityManager: EntityManager,
private val jpqlRenderContext: JpqlRenderContext,
private val jpqlRenderer: JpqlRenderer,
) : OrderCustomRepository {

override fun findOrdersByMemberId(
memberId: Long,
orderPaging: OrderPaging,
): List<Order> {
val latestOrderNumbers = findLatestOrderNumbers(memberId, orderPaging)
if (latestOrderNumbers.isEmpty()) {
return emptyList()
}

val query = jpql {
select(
entity(Order::class),
).from(
entity(Order::class),
).where(
path(Order::orderNumber)(OrderNumber::value).`in`(latestOrderNumbers),
).orderBy(
path(Order::id).desc()
)
}
Comment on lines +22 to +37
Copy link
Member Author

Choose a reason for hiding this comment

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

주문을 기준으로 페이징을 하기 위해 서브쿼리가 필요했습니다.

  1. 조회 요청에 맞는 OrderNumber 조회
  2. OrderNumber에 해당하는 Order 조회

Kotlin-jdsl을 통해 1번에 해당하는 subquery를 작성하였는데요.
.asSubquery()를 통해 메인 쿼리에 추가하는 식으로 작성할 수 있습니다.

하지만 Kotlin-jdsl은 쿼리 자체에서 limit를 제공하지 않고, JPQL로 렌더될 때 setMaxResult()를 통해 조회 개수를 설정합니다.
결국 subquery에서 개수 제한을 사용하지 못해서 쿼리를 분리하여 사용했습니다. (방법이 있다면 알려주세요!)

JPQL을 직접 작성하려 했지만, 조회 조건이 동적으로 변하는 쿼리를 담아내기엔 어려움이 있어 2번의 쿼리로 구성했어요..!
리뷰하실때 참고하시면 좋을 것 같습니다

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 findLatestOrderNumbers(
memberId: Long,
paging: OrderPaging,
): List<String> {
val query = jpql(OrderDynamicJpqlGenerator) {
selectDistinct(
path(Order::orderNumber)(OrderNumber::value)
).from(
entity(Order::class)
).whereAnd(
path(Order::memberId).eq(memberId),
orderIdLt(paging.lastViewedId),
orderNumberNotEq(paging.lastViewedOrderNumber),
).orderBy(
path(Order::id).desc()
)
}

return entityManager.createQuery<String>(
query,
jpqlRenderContext,
jpqlRenderer,
paging.limit,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.petqua.domain.order

import com.linecorp.kotlinjdsl.dsl.jpql.Jpql
import com.linecorp.kotlinjdsl.dsl.jpql.JpqlDsl
import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.Predicate

class OrderDynamicJpqlGenerator : Jpql() {
companion object Constructor : JpqlDsl.Constructor<OrderDynamicJpqlGenerator> {
override fun newInstance(): OrderDynamicJpqlGenerator = OrderDynamicJpqlGenerator()
}

fun Jpql.orderNumberNotEq(orderNumber: OrderNumber?): Predicate? {
return orderNumber?.let { path(Order::orderNumber).notEqual(it) }
}

fun Jpql.orderIdLt(lastViewedId: Long?): Predicate? {
return lastViewedId?.let { path(Order::id).lt(it) }
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,19 @@ interface OrderPaymentRepository : JpaRepository<OrderPayment, Long> {

@Query("SELECT op FROM OrderPayment op WHERE op.orderId = :orderId ORDER BY op.id DESC LIMIT 1")
fun findOrderStatusByOrderId(orderId: Long): OrderPayment

@Query(
"""
SELECT op
FROM OrderPayment op
WHERE op.id IN (
SELECT MAX(op2.id)
FROM OrderPayment op2
WHERE op2.orderId IN :orderIds
GROUP BY op2.orderId
)
ORDER BY op.id DESC
"""
)
fun findOrderStatusByOrderIds(orderIds: List<Long>): List<OrderPayment>
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ fun OrderRepository.findByOrderNumberOrThrow(
return orders.ifEmpty { throw exceptionSupplier() }
}

interface OrderRepository : JpaRepository<Order, Long> {
interface OrderRepository : JpaRepository<Order, Long>, OrderCustomRepository {

fun findByOrderNumber(orderNumber: OrderNumber): List<Order>
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ enum class OrderExceptionType(
FORBIDDEN_ORDER(FORBIDDEN, "O30", "해당 주문에 대한 권한이 없습니다."),
ORDER_CAN_NOT_CANCEL(BAD_REQUEST, "O31", "취소할 수 없는 주문입니다."),
ORDER_CAN_NOT_PAY(BAD_REQUEST, "O32", "결제할 수 없는 주문입니다."),

NOT_INVALID_ORDER_READ_QUERY(BAD_REQUEST, "O40", "유효하지 않은 주문 조회 조건입니다."),
;

override fun httpStatus(): HttpStatus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.petqua.common.config.ACCESS_TOKEN_SECURITY_SCHEME_KEY
import com.petqua.domain.auth.Auth
import com.petqua.domain.auth.LoginMember
import com.petqua.presentation.order.dto.OrderDetailResponse
import com.petqua.presentation.order.dto.OrderReadRequest
import com.petqua.presentation.order.dto.OrdersResponse
import com.petqua.presentation.order.dto.SaveOrderRequest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
Expand Down Expand Up @@ -41,7 +43,7 @@ class OrderController(

@Operation(summary = "주문 상세 조회 API", description = "주문 상세를 조회합니다")
@ApiResponse(responseCode = "200", description = "주문 상세 조회 성공")
@GetMapping
@GetMapping("/detail")
fun readDetail(
@Auth loginMember: LoginMember,
@RequestParam orderNumber: String,
Expand All @@ -50,4 +52,16 @@ class OrderController(
val response = orderService.readDetail(query)
return ResponseEntity.ok(response)
}

@Operation(summary = "주문 내역 조회 API", description = "주문 내역을 조회합니다")
@ApiResponse(responseCode = "200", description = "주문 내역 조회 성공")
@GetMapping
fun readAll(
@Auth loginMember: LoginMember,
request: OrderReadRequest,
): ResponseEntity<OrdersResponse> {
val query = request.toQuery(loginMember)
val response = orderService.readAll(query)
return ResponseEntity.ok(response)
}
}
Loading