Skip to content

Commit 8472c64

Browse files
committed
feat: 상품 아이템 조회 API 구현
1 parent 78a2c68 commit 8472c64

File tree

7 files changed

+173
-0
lines changed

7 files changed

+173
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.nilgil.commerce.common
2+
3+
import jakarta.persistence.EntityNotFoundException
4+
import org.springframework.http.HttpStatus
5+
import org.springframework.web.bind.annotation.ControllerAdvice
6+
import org.springframework.web.bind.annotation.ExceptionHandler
7+
import org.springframework.web.bind.annotation.ResponseStatus
8+
9+
@ControllerAdvice
10+
class GlobalExceptionHandler {
11+
@ExceptionHandler(EntityNotFoundException::class)
12+
@ResponseStatus(HttpStatus.NOT_FOUND)
13+
fun handleEntityNotFoundException(ex: EntityNotFoundException) {
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.nilgil.commerce.product
2+
3+
import org.springframework.data.jpa.repository.JpaRepository
4+
5+
interface ProductImageRepository : JpaRepository<ProductImage, Long> {
6+
fun findByProductAndType(
7+
product: Product,
8+
type: ProductImageType,
9+
): ProductImage?
10+
11+
fun findAllByProductInAndType(
12+
products: List<Product>,
13+
type: ProductImageType,
14+
): List<ProductImage>
15+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.nilgil.commerce.product
2+
3+
import org.springframework.web.bind.annotation.GetMapping
4+
import org.springframework.web.bind.annotation.PathVariable
5+
import org.springframework.web.bind.annotation.RequestParam
6+
import org.springframework.web.bind.annotation.RestController
7+
8+
@RestController
9+
class ProductItemController(
10+
private val service: ProductItemService,
11+
) {
12+
@GetMapping("/product-items/{id}")
13+
fun getProductItem(
14+
@PathVariable id: Long,
15+
): ProductItemResponse = service.getProductItem(id)
16+
17+
@GetMapping("/product-items")
18+
fun getProductItems(
19+
@RequestParam ids: List<Long>,
20+
): List<ProductItemResponse> = service.getProductItems(ids)
21+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.nilgil.commerce.product
2+
3+
import org.springframework.data.jpa.repository.EntityGraph
4+
import org.springframework.data.jpa.repository.JpaRepository
5+
6+
interface ProductItemOptionElementRepository : JpaRepository<ProductItemOptionElement, Long> {
7+
@EntityGraph(attributePaths = ["optionElement.option"])
8+
fun findAllWithOptionDetailsByItem(item: ProductItem): List<ProductItemOptionElement>
9+
10+
@EntityGraph(attributePaths = ["optionElement.option"])
11+
fun findAllWithOptionDetailsByItemIn(items: List<ProductItem>): List<ProductItemOptionElement>
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.nilgil.commerce.product
2+
3+
import org.springframework.data.jpa.repository.EntityGraph
4+
import org.springframework.data.jpa.repository.JpaRepository
5+
6+
interface ProductItemRepository : JpaRepository<ProductItem, Long> {
7+
@EntityGraph(attributePaths = ["product"])
8+
fun findWithProductById(productItemId: Long): ProductItem?
9+
10+
@EntityGraph(attributePaths = ["product"])
11+
fun findWithProductByIdIn(productItemIds: List<Long>): List<ProductItem>
12+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.nilgil.commerce.product
2+
3+
data class ProductItemResponse(
4+
val id: Long,
5+
val productName: String,
6+
val options: Map<String, String>,
7+
val thumbnailImageUrl: String?,
8+
val price: Int,
9+
val stock: Int,
10+
) {
11+
companion object {
12+
fun from(
13+
productItem: ProductItem,
14+
options: Map<String, String>,
15+
thumbnailUrl: String?,
16+
): ProductItemResponse =
17+
ProductItemResponse(
18+
id = productItem.id,
19+
productName = productItem.product.name,
20+
options = options,
21+
thumbnailImageUrl = thumbnailUrl,
22+
price = productItem.getAdjustedPrice(),
23+
stock = productItem.stock,
24+
)
25+
}
26+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.nilgil.commerce.product
2+
3+
import jakarta.persistence.EntityNotFoundException
4+
import org.springframework.stereotype.Service
5+
6+
@Service
7+
class ProductItemService(
8+
private val productItemRepository: ProductItemRepository,
9+
private val productImageRepository: ProductImageRepository,
10+
private val productItemOptionElementRepository: ProductItemOptionElementRepository,
11+
) {
12+
fun getProductItem(id: Long): ProductItemResponse {
13+
val productItem = findProductItemOrThrow(id)
14+
val options = findOptionMap(productItem)
15+
val thumbnailUrl = findThumbnailUrlOrNull(productItem.product)
16+
17+
return ProductItemResponse.from(
18+
productItem = productItem,
19+
options = options,
20+
thumbnailUrl = thumbnailUrl,
21+
)
22+
}
23+
24+
fun getProductItems(ids: List<Long>): List<ProductItemResponse> {
25+
val productItems = productItemRepository.findWithProductByIdIn(ids)
26+
if (productItems.isEmpty()) {
27+
return emptyList()
28+
}
29+
30+
val thumbnailUrlMap = getProductIdThumbnailUrlMap(productItems)
31+
val optionElementsMap = getProductItemIdOptionElementsMap(productItems)
32+
33+
val responseMap =
34+
productItems.associateBy(
35+
keySelector = { it.id },
36+
valueTransform = { item ->
37+
ProductItemResponse.from(
38+
productItem = item,
39+
options = optionElementsMap[item.id] ?: emptyMap(),
40+
thumbnailUrl = thumbnailUrlMap[item.product.id],
41+
)
42+
},
43+
)
44+
45+
return ids.mapNotNull { responseMap[it] }
46+
}
47+
48+
private fun findProductItemOrThrow(id: Long): ProductItem =
49+
productItemRepository.findWithProductById(id)
50+
?: throw EntityNotFoundException("상품 아이템을 찾을 수 없습니다. id: $id")
51+
52+
private fun findOptionMap(item: ProductItem): Map<String, String> =
53+
productItemOptionElementRepository
54+
.findAllWithOptionDetailsByItem(item)
55+
.associate { it.optionElement.option.name to it.optionElement.name }
56+
57+
private fun findThumbnailUrlOrNull(product: Product): String? =
58+
productImageRepository.findByProductAndType(product, ProductImageType.THUMBNAIL)?.url
59+
60+
private fun getProductIdThumbnailUrlMap(productItems: List<ProductItem>): Map<Long, String> {
61+
val products = productItems.map { productItem -> productItem.product }
62+
return productImageRepository
63+
.findAllByProductInAndType(products, ProductImageType.THUMBNAIL)
64+
.associate { it.product.id to it.url }
65+
}
66+
67+
private fun getProductItemIdOptionElementsMap(productItems: List<ProductItem>): Map<Long, Map<String, String>> =
68+
productItemOptionElementRepository
69+
.findAllWithOptionDetailsByItemIn(productItems)
70+
.groupBy { it.item.id }
71+
.mapValues { it.value.associate { it -> it.optionElement.option.name to it.optionElement.name } }
72+
}

0 commit comments

Comments
 (0)