From 8d45aa033b22d1e6a7d0b96c10c0c659ca70f0e7 Mon Sep 17 00:00:00 2001 From: Younggil An Date: Thu, 31 Jul 2025 23:47:15 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=A0=84=EC=97=AD=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nilgil/commerce/common/error/CoreError.kt | 16 ++++++ .../commerce/common/error/CoreException.kt | 10 ++++ .../commerce/common/error/ErrorResponse.kt | 14 ++++++ .../nilgil/commerce/common/error/ErrorType.kt | 9 ++++ .../common/error/GlobalExceptionHandler.kt | 50 +++++++++++++++++++ 5 files changed, 99 insertions(+) create mode 100644 src/main/kotlin/com/nilgil/commerce/common/error/CoreError.kt create mode 100644 src/main/kotlin/com/nilgil/commerce/common/error/CoreException.kt create mode 100644 src/main/kotlin/com/nilgil/commerce/common/error/ErrorResponse.kt create mode 100644 src/main/kotlin/com/nilgil/commerce/common/error/ErrorType.kt create mode 100644 src/main/kotlin/com/nilgil/commerce/common/error/GlobalExceptionHandler.kt diff --git a/src/main/kotlin/com/nilgil/commerce/common/error/CoreError.kt b/src/main/kotlin/com/nilgil/commerce/common/error/CoreError.kt new file mode 100644 index 0000000..96d53cc --- /dev/null +++ b/src/main/kotlin/com/nilgil/commerce/common/error/CoreError.kt @@ -0,0 +1,16 @@ +package com.nilgil.commerce.common.error + +import org.springframework.http.HttpStatus + +enum class CoreError( + override val status: HttpStatus, + override val code: String, + override val message: String, +) : ErrorType { + UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "C001", "알 수 없는 오류가 발생했습니다."), + VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "C002", "요청 값이 올바르지 않습니다."), + AUTHENTICATION_ERROR(HttpStatus.UNAUTHORIZED, "C003", "인증되지 않은 사용자입니다."), + AUTHORIZATION_ERROR(HttpStatus.FORBIDDEN, "C004", "접근 권한이 없습니다."), + NOT_FOUND_ERROR(HttpStatus.NOT_FOUND, "C005", "리소스를 찾을 수 없습니다."), + CONFLICT_ERROR(HttpStatus.CONFLICT, "C006", "충돌되는 리소스가 존재합니다."), +} diff --git a/src/main/kotlin/com/nilgil/commerce/common/error/CoreException.kt b/src/main/kotlin/com/nilgil/commerce/common/error/CoreException.kt new file mode 100644 index 0000000..308f675 --- /dev/null +++ b/src/main/kotlin/com/nilgil/commerce/common/error/CoreException.kt @@ -0,0 +1,10 @@ +package com.nilgil.commerce.common.error + +import org.springframework.boot.logging.LogLevel + +data class CoreException( + val type: ErrorType = CoreError.UNKNOWN_ERROR, + override val cause: Throwable? = null, + val logLevel: LogLevel = LogLevel.ERROR, + val detail: Any? = null, +) : RuntimeException(type.message, cause) diff --git a/src/main/kotlin/com/nilgil/commerce/common/error/ErrorResponse.kt b/src/main/kotlin/com/nilgil/commerce/common/error/ErrorResponse.kt new file mode 100644 index 0000000..438581c --- /dev/null +++ b/src/main/kotlin/com/nilgil/commerce/common/error/ErrorResponse.kt @@ -0,0 +1,14 @@ +package com.nilgil.commerce.common.error + +data class ErrorResponse( + val code: String, + val message: String, + val detail: Any? = null, +) + +fun CoreException.toResponse(): ErrorResponse = + ErrorResponse( + code = this.type.code, + message = this.type.message, + detail = this.detail, + ) diff --git a/src/main/kotlin/com/nilgil/commerce/common/error/ErrorType.kt b/src/main/kotlin/com/nilgil/commerce/common/error/ErrorType.kt new file mode 100644 index 0000000..941ba51 --- /dev/null +++ b/src/main/kotlin/com/nilgil/commerce/common/error/ErrorType.kt @@ -0,0 +1,9 @@ +package com.nilgil.commerce.common.error + +import org.springframework.http.HttpStatus + +interface ErrorType { + val status: HttpStatus + val code: String + val message: String +} diff --git a/src/main/kotlin/com/nilgil/commerce/common/error/GlobalExceptionHandler.kt b/src/main/kotlin/com/nilgil/commerce/common/error/GlobalExceptionHandler.kt new file mode 100644 index 0000000..4f88f24 --- /dev/null +++ b/src/main/kotlin/com/nilgil/commerce/common/error/GlobalExceptionHandler.kt @@ -0,0 +1,50 @@ +package com.nilgil.commerce.common.error + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.boot.logging.LogLevel +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice + +@RestControllerAdvice +class GlobalExceptionHandler { + private val log: Logger = LoggerFactory.getLogger(javaClass) + + @ExceptionHandler(CoreException::class) + fun handleCoreException(e: CoreException): ResponseEntity { + log.logOnLevel( + level = e.logLevel, + message = "[ERR-${e.type.code}] ${e.message}", + throwable = e, + ) + return ResponseEntity(e.toResponse(), e.type.status) + } + + @ExceptionHandler(Exception::class) + fun handleException(e: Exception): ResponseEntity = + handleCoreException(CoreException(type = CoreError.UNKNOWN_ERROR, cause = e)) + + @ExceptionHandler(IllegalArgumentException::class) + fun handleIllegalArgumentException(e: Exception): ResponseEntity = + handleCoreException(CoreException(type = CoreError.VALIDATION_ERROR, cause = e)) + + @ExceptionHandler(IllegalStateException::class) + fun handleIllegalStateException(e: Exception): ResponseEntity = + handleCoreException(CoreException(type = CoreError.CONFLICT_ERROR, cause = e)) +} + +private fun Logger.logOnLevel( + level: LogLevel, + message: String, + throwable: Throwable? = null, +) { + when (level) { + LogLevel.ERROR -> error(message, throwable) + LogLevel.WARN -> warn(message, throwable) + LogLevel.INFO -> info(message, throwable) + LogLevel.DEBUG -> debug(message, throwable) + LogLevel.TRACE -> trace(message, throwable) + else -> error(message, throwable) + } +} From 0d4d886a2b5ad9ccafac913b9c996c122c46fb66 Mon Sep 17 00:00:00 2001 From: Younggil An Date: Fri, 1 Aug 2025 00:54:11 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=A3=BC=EB=AC=B8=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=A0=84=ED=99=98=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=97=90=20CoreException=20=EB=8F=84=EC=9E=85=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/nilgil/commerce/order/Order.kt | 21 ++++++++-- .../com/nilgil/commerce/order/OrderError.kt | 12 ++++++ .../com/nilgil/commerce/order/OrderTest.kt | 39 ++++++++++++------- 3 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 src/main/kotlin/com/nilgil/commerce/order/OrderError.kt diff --git a/src/main/kotlin/com/nilgil/commerce/order/Order.kt b/src/main/kotlin/com/nilgil/commerce/order/Order.kt index 021ab46..5a4ba40 100644 --- a/src/main/kotlin/com/nilgil/commerce/order/Order.kt +++ b/src/main/kotlin/com/nilgil/commerce/order/Order.kt @@ -1,6 +1,7 @@ package com.nilgil.commerce.order import com.nilgil.commerce.common.BaseEntity +import com.nilgil.commerce.common.error.CoreException import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.EnumType @@ -22,7 +23,10 @@ class Order( fun pay(paymentId: Long) { if (this.status != OrderStatus.CREATED) { - throw IllegalStateException("결제를 진행할 수 없는 상태입니다.") + throw CoreException( + type = OrderError.INVALID_STATUS_TRANSITION, + detail = "결제를 진행할 수 없는 상태입니다.", + ) } this.paymentId = paymentId this.status = OrderStatus.PAID @@ -30,21 +34,30 @@ class Order( fun complete() { if (this.status != OrderStatus.PAID) { - throw IllegalStateException("완료 처리할 수 없는 상태입니다.") + throw CoreException( + type = OrderError.INVALID_STATUS_TRANSITION, + detail = "완료 처리할 수 없는 상태입니다.", + ) } this.status = OrderStatus.COMPLETED } fun cancel() { if (this.status != OrderStatus.CREATED && this.status != OrderStatus.PAID) { - throw IllegalStateException("취소할 수 없는 상태입니다.") + throw CoreException( + type = OrderError.INVALID_STATUS_TRANSITION, + detail = "취소할 수 없는 상태입니다.", + ) } this.status = OrderStatus.CANCELLED } fun returnOrder() { if (this.status != OrderStatus.COMPLETED) { - throw IllegalStateException("반품할 수 없는 상태입니다.") + throw CoreException( + type = OrderError.INVALID_STATUS_TRANSITION, + detail = "반품할 수 없는 상태입니다.", + ) } this.status = OrderStatus.RETURNED } diff --git a/src/main/kotlin/com/nilgil/commerce/order/OrderError.kt b/src/main/kotlin/com/nilgil/commerce/order/OrderError.kt new file mode 100644 index 0000000..744b1f1 --- /dev/null +++ b/src/main/kotlin/com/nilgil/commerce/order/OrderError.kt @@ -0,0 +1,12 @@ +package com.nilgil.commerce.order + +import com.nilgil.commerce.common.error.ErrorType +import org.springframework.http.HttpStatus + +enum class OrderError( + override val status: HttpStatus, + override val code: String, + override val message: String, +) : ErrorType { + INVALID_STATUS_TRANSITION(HttpStatus.CONFLICT, "O001", "주문 상태를 변경할 수 없습니다."), +} diff --git a/src/test/kotlin/com/nilgil/commerce/order/OrderTest.kt b/src/test/kotlin/com/nilgil/commerce/order/OrderTest.kt index 25d64c2..3fa7930 100644 --- a/src/test/kotlin/com/nilgil/commerce/order/OrderTest.kt +++ b/src/test/kotlin/com/nilgil/commerce/order/OrderTest.kt @@ -1,5 +1,6 @@ package com.nilgil.commerce.order +import com.nilgil.commerce.common.error.CoreException import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.DisplayName @@ -37,14 +38,17 @@ class OrderTest { } @Test - fun `CREATED 상태가 아니면 IllegalStateException이 발생한다`() { + fun `CREATED 상태가 아니면 INVALID_STATUS_TRANSITION 에러가 발생한다`() { // given val order = OrderFixtures.anOrder() order.pay(paymentId = 123L) // when, then assertThatThrownBy { order.pay(paymentId = 456L) } - .isInstanceOf(IllegalStateException::class.java) + .isInstanceOf(CoreException::class.java) + .extracting { it as CoreException } + .extracting(CoreException::type) + .isEqualTo(OrderError.INVALID_STATUS_TRANSITION) } } @@ -65,13 +69,16 @@ class OrderTest { } @Test - fun `PAID 상태가 아니면 IllegalStateException이 발생한다`() { + fun `PAID 상태가 아니면 INVALID_STATUS_TRANSITION 에러가 발생한다`() { // given val order = OrderFixtures.anOrder() // when, then assertThatThrownBy { order.complete() } - .isInstanceOf(IllegalStateException::class.java) + .isInstanceOf(CoreException::class.java) + .extracting { it as CoreException } + .extracting(CoreException::type) + .isEqualTo(OrderError.INVALID_STATUS_TRANSITION) } } @@ -104,7 +111,7 @@ class OrderTest { } @Test - fun `COMPLETED 상태이면 IllegalStateException이 발생한다`() { + fun `COMPLETED 상태이면 INVALID_STATUS_TRANSITION 에러가 발생한다`() { // given val order = OrderFixtures.anOrder() order.pay(123L) @@ -112,12 +119,14 @@ class OrderTest { // when, then assertThatThrownBy { order.cancel() } - .isInstanceOf(IllegalStateException::class.java) - .hasMessage("취소할 수 없는 상태입니다.") + .isInstanceOf(CoreException::class.java) + .extracting { it as CoreException } + .extracting(CoreException::type) + .isEqualTo(OrderError.INVALID_STATUS_TRANSITION) } @Test - fun `RETURNED 상태이면 IllegalStateException이 발생한다`() { + fun `RETURNED 상태이면 INVALID_STATUS_TRANSITION 에러가 발생한다`() { // given val order = OrderFixtures.anOrder() order.pay(123L) @@ -126,8 +135,10 @@ class OrderTest { // when, then assertThatThrownBy { order.cancel() } - .isInstanceOf(IllegalStateException::class.java) - .hasMessage("취소할 수 없는 상태입니다.") + .isInstanceOf(CoreException::class.java) + .extracting { it as CoreException } + .extracting(CoreException::type) + .isEqualTo(OrderError.INVALID_STATUS_TRANSITION) } } @@ -149,14 +160,16 @@ class OrderTest { } @Test - fun `COMPLETED 상태가 아니면 IllegalStateException이 발생한다`() { + fun `COMPLETED 상태가 아니면 INVALID_STATUS_TRANSITION 에러가 발생한다`() { // given val order = OrderFixtures.anOrder() // when, then assertThatThrownBy { order.returnOrder() } - .isInstanceOf(IllegalStateException::class.java) - .hasMessage("반품할 수 없는 상태입니다.") + .isInstanceOf(CoreException::class.java) + .extracting { it as CoreException } + .extracting(CoreException::type) + .isEqualTo(OrderError.INVALID_STATUS_TRANSITION) } } } From 56ced04e718c6d155223f117a13fe03f7b25f9ca Mon Sep 17 00:00:00 2001 From: Younggil An Date: Thu, 24 Jul 2025 23:29:30 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EB=A1=9C=EC=BB=AC=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=99=98=EA=B2=BD=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20DB=20=EC=97=B0=EB=8F=99=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + build.gradle.kts | 1 + compose.yaml | 10 ++++++++++ src/main/resources/application.yml | 11 +++++++++++ 4 files changed, 23 insertions(+) create mode 100644 compose.yaml diff --git a/.gitignore b/.gitignore index 8e7f0b6..e15afb0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +data.sql application.properties HELP.md .gradle diff --git a/build.gradle.kts b/build.gradle.kts index feb9480..bc856b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { runtimeOnly("com.h2database:h2") runtimeOnly("com.mysql:mysql-connector-j") annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") + developmentOnly("org.springframework.boot:spring-boot-docker-compose") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("io.kotest:kotest-runner-junit5:5.8.0") testImplementation("com.appmattus.fixture:fixture:1.2.0") diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..11561d8 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,10 @@ +services: + mysql: + image: 'mysql:latest' + environment: + - 'MYSQL_DATABASE=gcm' + - 'MYSQL_ROOT_PASSWORD=verysecret' + - 'MYSQL_USER=nilgil' + - 'MYSQL_PASSWORD=secret' + ports: + - '3306:3306' diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3558351..4c7b39e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,2 +1,13 @@ spring: application.name: generic-commerce + jpa: + open-in-view: false + hibernate: + ddl-auto: create + show-sql: true + defer-datasource-initialization: true + properties: + hibernate: + format_sql: true + sql.init.mode: always + docker.compose.lifecycle-management: start_only From 9a4099989cbefa9bbfc325cede881eb02ea02ce4 Mon Sep 17 00:00:00 2001 From: Younggil An Date: Sat, 26 Jul 2025 14:20:42 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=ED=85=9C=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/ProductImageRepository.kt | 15 ++++ .../commerce/product/ProductItemController.kt | 21 +++++ .../ProductItemOptionElementRepository.kt | 12 +++ .../commerce/product/ProductItemRepository.kt | 12 +++ .../commerce/product/ProductItemResponse.kt | 26 +++++++ .../commerce/product/ProductItemService.kt | 76 +++++++++++++++++++ src/main/resources/application.yml | 1 + 7 files changed, 163 insertions(+) create mode 100644 src/main/kotlin/com/nilgil/commerce/product/ProductImageRepository.kt create mode 100644 src/main/kotlin/com/nilgil/commerce/product/ProductItemController.kt create mode 100644 src/main/kotlin/com/nilgil/commerce/product/ProductItemOptionElementRepository.kt create mode 100644 src/main/kotlin/com/nilgil/commerce/product/ProductItemRepository.kt create mode 100644 src/main/kotlin/com/nilgil/commerce/product/ProductItemResponse.kt create mode 100644 src/main/kotlin/com/nilgil/commerce/product/ProductItemService.kt diff --git a/src/main/kotlin/com/nilgil/commerce/product/ProductImageRepository.kt b/src/main/kotlin/com/nilgil/commerce/product/ProductImageRepository.kt new file mode 100644 index 0000000..fa8571a --- /dev/null +++ b/src/main/kotlin/com/nilgil/commerce/product/ProductImageRepository.kt @@ -0,0 +1,15 @@ +package com.nilgil.commerce.product + +import org.springframework.data.jpa.repository.JpaRepository + +interface ProductImageRepository : JpaRepository { + fun findByProductAndType( + product: Product, + type: ProductImageType, + ): ProductImage? + + fun findAllByProductInAndType( + products: List, + type: ProductImageType, + ): List +} diff --git a/src/main/kotlin/com/nilgil/commerce/product/ProductItemController.kt b/src/main/kotlin/com/nilgil/commerce/product/ProductItemController.kt new file mode 100644 index 0000000..7d12f0c --- /dev/null +++ b/src/main/kotlin/com/nilgil/commerce/product/ProductItemController.kt @@ -0,0 +1,21 @@ +package com.nilgil.commerce.product + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +class ProductItemController( + private val service: ProductItemService, +) { + @GetMapping("/product-items/{id}") + fun getProductItem( + @PathVariable id: Long, + ): ProductItemResponse = service.getProductItem(id) + + @GetMapping("/product-items") + fun getProductItems( + @RequestParam ids: List, + ): List = service.getProductItems(ids) +} diff --git a/src/main/kotlin/com/nilgil/commerce/product/ProductItemOptionElementRepository.kt b/src/main/kotlin/com/nilgil/commerce/product/ProductItemOptionElementRepository.kt new file mode 100644 index 0000000..c877be2 --- /dev/null +++ b/src/main/kotlin/com/nilgil/commerce/product/ProductItemOptionElementRepository.kt @@ -0,0 +1,12 @@ +package com.nilgil.commerce.product + +import org.springframework.data.jpa.repository.EntityGraph +import org.springframework.data.jpa.repository.JpaRepository + +interface ProductItemOptionElementRepository : JpaRepository { + @EntityGraph(attributePaths = ["optionElement.option"]) + fun findAllWithOptionDetailsByItem(item: ProductItem): List + + @EntityGraph(attributePaths = ["optionElement.option"]) + fun findAllWithOptionDetailsByItemIn(items: List): List +} diff --git a/src/main/kotlin/com/nilgil/commerce/product/ProductItemRepository.kt b/src/main/kotlin/com/nilgil/commerce/product/ProductItemRepository.kt new file mode 100644 index 0000000..9b24e1a --- /dev/null +++ b/src/main/kotlin/com/nilgil/commerce/product/ProductItemRepository.kt @@ -0,0 +1,12 @@ +package com.nilgil.commerce.product + +import org.springframework.data.jpa.repository.EntityGraph +import org.springframework.data.jpa.repository.JpaRepository + +interface ProductItemRepository : JpaRepository { + @EntityGraph(attributePaths = ["product"]) + fun findWithProductById(productItemId: Long): ProductItem? + + @EntityGraph(attributePaths = ["product"]) + fun findWithProductByIdIn(productItemIds: List): List +} diff --git a/src/main/kotlin/com/nilgil/commerce/product/ProductItemResponse.kt b/src/main/kotlin/com/nilgil/commerce/product/ProductItemResponse.kt new file mode 100644 index 0000000..704cba9 --- /dev/null +++ b/src/main/kotlin/com/nilgil/commerce/product/ProductItemResponse.kt @@ -0,0 +1,26 @@ +package com.nilgil.commerce.product + +data class ProductItemResponse( + val id: Long, + val productName: String, + val options: Map, + val thumbnailImageUrl: String?, + val price: Int, + val stock: Int, +) { + companion object { + fun from( + productItem: ProductItem, + options: Map, + thumbnailUrl: String?, + ): ProductItemResponse = + ProductItemResponse( + id = productItem.id, + productName = productItem.product.name, + options = options, + thumbnailImageUrl = thumbnailUrl, + price = productItem.getAdjustedPrice(), + stock = productItem.stock, + ) + } +} diff --git a/src/main/kotlin/com/nilgil/commerce/product/ProductItemService.kt b/src/main/kotlin/com/nilgil/commerce/product/ProductItemService.kt new file mode 100644 index 0000000..38594aa --- /dev/null +++ b/src/main/kotlin/com/nilgil/commerce/product/ProductItemService.kt @@ -0,0 +1,76 @@ +package com.nilgil.commerce.product + +import com.nilgil.commerce.common.error.CoreError +import com.nilgil.commerce.common.error.CoreException +import org.springframework.stereotype.Service + +@Service +class ProductItemService( + private val productItemRepository: ProductItemRepository, + private val productImageRepository: ProductImageRepository, + private val productItemOptionElementRepository: ProductItemOptionElementRepository, +) { + fun getProductItem(id: Long): ProductItemResponse { + val productItem = findProductItemOrThrow(id) + val options = findOptionMap(productItem) + val thumbnailUrl = findThumbnailUrlOrNull(productItem.product) + + return ProductItemResponse.from( + productItem = productItem, + options = options, + thumbnailUrl = thumbnailUrl, + ) + } + + fun getProductItems(ids: List): List { + val productItems = productItemRepository.findWithProductByIdIn(ids) + if (productItems.isEmpty()) { + return emptyList() + } + + val thumbnailUrlMap = getProductIdThumbnailUrlMap(productItems) + val optionElementsMap = getProductItemIdOptionElementsMap(productItems) + + val responseMap = + productItems.associateBy( + keySelector = { it.id }, + valueTransform = { item -> + ProductItemResponse.from( + productItem = item, + options = optionElementsMap[item.id] ?: emptyMap(), + thumbnailUrl = thumbnailUrlMap[item.product.id], + ) + }, + ) + + return ids.mapNotNull { responseMap[it] } + } + + private fun findProductItemOrThrow(id: Long): ProductItem = + productItemRepository.findWithProductById(id) + ?: throw CoreException( + type = CoreError.NOT_FOUND_ERROR, + detail = "상품 아이템을 찾을 수 없습니다. id: $id", + ) + + private fun findOptionMap(item: ProductItem): Map = + productItemOptionElementRepository + .findAllWithOptionDetailsByItem(item) + .associate { it.optionElement.option.name to it.optionElement.name } + + private fun findThumbnailUrlOrNull(product: Product): String? = + productImageRepository.findByProductAndType(product, ProductImageType.THUMBNAIL)?.url + + private fun getProductIdThumbnailUrlMap(productItems: List): Map { + val products = productItems.map { productItem -> productItem.product } + return productImageRepository + .findAllByProductInAndType(products, ProductImageType.THUMBNAIL) + .associate { it.product.id to it.url } + } + + private fun getProductItemIdOptionElementsMap(productItems: List): Map> = + productItemOptionElementRepository + .findAllWithOptionDetailsByItemIn(productItems) + .groupBy { it.item.id } + .mapValues { it.value.associate { it -> it.optionElement.option.name to it.optionElement.name } } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4c7b39e..87edbe8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,5 +9,6 @@ spring: properties: hibernate: format_sql: true + default_batch_fetch_size: 100 sql.init.mode: always docker.compose.lifecycle-management: start_only From 1330c15612964d3206b1b56de82ce6fdac9dd59e Mon Sep 17 00:00:00 2001 From: Younggil An Date: Sun, 27 Jul 2025 17:35:59 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore:=20docker-compose=20mysql=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EA=B0=92=20=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + compose.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index e15afb0..19014da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.env data.sql application.properties HELP.md diff --git a/compose.yaml b/compose.yaml index 11561d8..7a643e4 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,9 +2,9 @@ services: mysql: image: 'mysql:latest' environment: - - 'MYSQL_DATABASE=gcm' - - 'MYSQL_ROOT_PASSWORD=verysecret' - - 'MYSQL_USER=nilgil' - - 'MYSQL_PASSWORD=secret' + - MYSQL_DATABASE=${MYSQL_DATABASE} + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_USER=${MYSQL_USER} + - MYSQL_PASSWORD=${MYSQL_PASSWORD} ports: - '3306:3306'