From 37d87c5caf616f12f1c982b7878268975a5c825e Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 31 May 2024 23:44:47 +0900 Subject: [PATCH] =?UTF-8?q?5/31=20=EB=B0=B0=ED=8F=AC=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/MacbookRepository.kt | 10 ++++ .../repository/MacbookRepositoryCustom.kt | 8 ++++ .../repository/MacbookRepositoryImpl.kt | 34 ++++++++++++++ .../crawl/macbook/service/MacbookService.kt | 15 ++++++ .../logging/GlobalRestControllerAdvice.kt | 16 +++++++ .../tracker/controller/ProductController.kt | 15 ++++++ .../tracker/controller/request/PageParams.kt | 18 ++++++++ .../tracker/controller/response/Pages.kt | 33 ++++++++++++- .../tracker/service/ProductService.kt | 13 ++++++ .../tracker/service/handler/MacbookHandler.kt | 46 +++++++++---------- .../tracker/service/handler/ProductHandler.kt | 8 ++++ .../response/product/MacbookResponse.kt | 33 ++++++++++++- src/main/resources/application.yml | 2 +- 13 files changed, 223 insertions(+), 28 deletions(-) create mode 100644 src/main/kotlin/backend/itracker/tracker/controller/request/PageParams.kt diff --git a/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepository.kt b/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepository.kt index 44ce782..e0537db 100644 --- a/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepository.kt +++ b/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepository.kt @@ -20,4 +20,14 @@ interface MacbookRepository: JpaRepository, MacbookRepositoryCust """ ) fun findAllFetchByProductCategory(@Param("category") crawlTargetCategory: ProductCategory): List + + @Query( + """ + select m + from Macbook m + join fetch m.prices + where m.id = :id + """ + ) + fun findAllPricesByMacbookId(@Param("id") id: Long) } diff --git a/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepositoryCustom.kt b/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepositoryCustom.kt index 58fc538..4bc43d0 100644 --- a/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepositoryCustom.kt +++ b/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepositoryCustom.kt @@ -3,6 +3,8 @@ package backend.itracker.crawl.macbook.domain.repository import backend.itracker.crawl.common.ProductCategory import backend.itracker.crawl.macbook.domain.Macbook import backend.itracker.crawl.macbook.service.dto.MacbookFilterCondition +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable interface MacbookRepositoryCustom { @@ -10,4 +12,10 @@ interface MacbookRepositoryCustom { productCategory: ProductCategory, filterCondition: MacbookFilterCondition ): List + + fun findAllProductsByFilter( + category: ProductCategory, + filterCondition: MacbookFilterCondition, + pageable: Pageable + ): PageImpl } diff --git a/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepositoryImpl.kt b/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepositoryImpl.kt index 1fdd4e0..c54a545 100644 --- a/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepositoryImpl.kt +++ b/src/main/kotlin/backend/itracker/crawl/macbook/domain/repository/MacbookRepositoryImpl.kt @@ -6,6 +6,8 @@ import backend.itracker.crawl.macbook.domain.QMacbook.macbook import backend.itracker.crawl.macbook.service.dto.MacbookFilterCondition import com.querydsl.core.types.Predicate import com.querydsl.jpa.impl.JPAQueryFactory +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable class MacbookRepositoryImpl( private val jpaQueryFactory: JPAQueryFactory @@ -27,6 +29,38 @@ class MacbookRepositoryImpl( ).fetch() } + override fun findAllProductsByFilter( + category: ProductCategory, + filterCondition: MacbookFilterCondition, + pageable: Pageable + ): PageImpl { + val contents = jpaQueryFactory.selectFrom(macbook) + .where( + equalSize(filterCondition.size), + equalColor(filterCondition.color), + equalChip(filterCondition.processor), + equalStorage(filterCondition.storage), + equalMemory(filterCondition.memory), + equalCategory(category) + ).offset(pageable.offset) + .limit(pageable.pageSize.toLong()) + .fetch() + + val total = jpaQueryFactory.selectFrom(macbook) + .where( + equalSize(filterCondition.size), + equalColor(filterCondition.color), + equalChip(filterCondition.processor), + equalStorage(filterCondition.storage), + equalMemory(filterCondition.memory), + equalCategory(category) + ).fetch() + .count() + .toLong() + + return PageImpl(contents, pageable, total) + } + private fun equalSize( size: Int? ): Predicate? { diff --git a/src/main/kotlin/backend/itracker/crawl/macbook/service/MacbookService.kt b/src/main/kotlin/backend/itracker/crawl/macbook/service/MacbookService.kt index c0ad073..2abe848 100644 --- a/src/main/kotlin/backend/itracker/crawl/macbook/service/MacbookService.kt +++ b/src/main/kotlin/backend/itracker/crawl/macbook/service/MacbookService.kt @@ -4,6 +4,8 @@ import backend.itracker.crawl.common.ProductCategory import backend.itracker.crawl.macbook.domain.Macbook import backend.itracker.crawl.macbook.domain.repository.MacbookRepository import backend.itracker.crawl.macbook.service.dto.MacbookFilterCondition +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -38,4 +40,17 @@ class MacbookService( ): List { return macbookRepository.findAllByFilterCondition(productCategory, filterCondition) } + + @Transactional(readOnly = true) + fun findAllProductsByFilter( + category: ProductCategory, + macbookFilterCondition: MacbookFilterCondition, + pageable: Pageable + ): Page { + val pageMacbooks = + macbookRepository.findAllProductsByFilter(category, macbookFilterCondition, pageable) + pageMacbooks.forEach { macbookRepository.findAllPricesByMacbookId(it.id) } + + return pageMacbooks + } } diff --git a/src/main/kotlin/backend/itracker/logging/GlobalRestControllerAdvice.kt b/src/main/kotlin/backend/itracker/logging/GlobalRestControllerAdvice.kt index c8ede15..4622f6d 100644 --- a/src/main/kotlin/backend/itracker/logging/GlobalRestControllerAdvice.kt +++ b/src/main/kotlin/backend/itracker/logging/GlobalRestControllerAdvice.kt @@ -1,5 +1,6 @@ package backend.itracker.logging +import org.springframework.beans.BeanInstantiationException import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice @@ -8,6 +9,21 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep @RestControllerAdvice class GlobalRestControllerAdvice : ResponseEntityExceptionHandler() { + @ExceptionHandler(BeanInstantiationException::class) + fun handleBeanInstantiationException(e: BeanInstantiationException): ResponseEntity { + return when (val ex = e.cause) { + is IllegalArgumentException -> { + logger.info("사용자 입력 예외입니다. message : ", ex) + ResponseEntity.badRequest().body("message = ${ex.message}") + } + + else -> { + logger.error("예상치 못한 예외입니다. message : ", ex) + ResponseEntity.internalServerError().body("message = ${ex?.message}") + } + } + } + @ExceptionHandler(IllegalArgumentException::class) fun handleIllealArgumentException(e: IllegalArgumentException): ResponseEntity { logger.info("사용자 입력 예외입니다. message : ", e) diff --git a/src/main/kotlin/backend/itracker/tracker/controller/ProductController.kt b/src/main/kotlin/backend/itracker/tracker/controller/ProductController.kt index af41954..c528284 100644 --- a/src/main/kotlin/backend/itracker/tracker/controller/ProductController.kt +++ b/src/main/kotlin/backend/itracker/tracker/controller/ProductController.kt @@ -1,6 +1,7 @@ package backend.itracker.tracker.controller import backend.itracker.crawl.common.ProductCategory +import backend.itracker.tracker.controller.request.PageParams import backend.itracker.tracker.controller.response.CategoryResponses import backend.itracker.tracker.controller.response.Pages import backend.itracker.tracker.controller.response.SinglePage @@ -9,8 +10,10 @@ import backend.itracker.tracker.service.response.filter.CommonFilterModel import backend.itracker.tracker.service.response.product.CommonProductModel import backend.itracker.tracker.service.vo.Limit import backend.itracker.tracker.service.vo.ProductFilter +import org.springframework.data.domain.PageRequest import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -44,4 +47,16 @@ class ProductController( val filter = productService.findFilter(category, ProductFilter(filterConditon)) return ResponseEntity.ok(SinglePage(filter)) } + + @GetMapping("/api/v1/products/{category}/search") + fun findFilterdMacbookAir( + @PathVariable category: ProductCategory, + @RequestParam filterCondition: Map, + @ModelAttribute pageParams: PageParams, + ): ResponseEntity> { + val pageProducts = + productService.findFilteredProducts(category, ProductFilter(filterCondition), PageRequest.of(pageParams.offset, pageParams.limit)) + + return ResponseEntity.ok(Pages.withPagination(pageProducts)) + } } diff --git a/src/main/kotlin/backend/itracker/tracker/controller/request/PageParams.kt b/src/main/kotlin/backend/itracker/tracker/controller/request/PageParams.kt new file mode 100644 index 0000000..36d18dc --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/controller/request/PageParams.kt @@ -0,0 +1,18 @@ +package backend.itracker.tracker.controller.request + +class PageParams( + private var page: Int = 1, + var limit: Int = 8 +) { + + init { + require(page >= 1) { throw IllegalArgumentException("page는 1이상 입력해주세요. page=$page") } + require(limit >= 1) { throw IllegalArgumentException("limit는 1이상 입력해주세요. limit=$limit") } + } + + val offset: Int + get() = page - 1 +} + + + diff --git a/src/main/kotlin/backend/itracker/tracker/controller/response/Pages.kt b/src/main/kotlin/backend/itracker/tracker/controller/response/Pages.kt index 93b9558..ebb1fa8 100644 --- a/src/main/kotlin/backend/itracker/tracker/controller/response/Pages.kt +++ b/src/main/kotlin/backend/itracker/tracker/controller/response/Pages.kt @@ -1,9 +1,38 @@ package backend.itracker.tracker.controller.response +import org.springframework.data.domain.Page + +private const val DEFAULT_PAGE_OFFSET = 1 + data class Pages( - val data: List -) + val data: List, + val pageInfo: PageInfo = PageInfo() +) { + + companion object { + fun withPagination(pagesData: Page) = Pages( + data = pagesData.content, + pageInfo = PageInfo.from(pagesData) + ) + } + +} data class SinglePage( val data: T ) + +data class PageInfo( + val currentPage: Int = 0, + val lastPage: Int = 0, + val elementSize: Int = 0 +) { + + companion object { + fun from(pagesData: Page) = PageInfo( + currentPage = pagesData.number + DEFAULT_PAGE_OFFSET, + lastPage = pagesData.totalPages, + elementSize = pagesData.numberOfElements + ) + } +} diff --git a/src/main/kotlin/backend/itracker/tracker/service/ProductService.kt b/src/main/kotlin/backend/itracker/tracker/service/ProductService.kt index ecb2696..e5edbb7 100644 --- a/src/main/kotlin/backend/itracker/tracker/service/ProductService.kt +++ b/src/main/kotlin/backend/itracker/tracker/service/ProductService.kt @@ -6,6 +6,8 @@ import backend.itracker.tracker.service.response.filter.CommonFilterModel import backend.itracker.tracker.service.response.product.CommonProductModel import backend.itracker.tracker.service.vo.Limit import backend.itracker.tracker.service.vo.ProductFilter +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service @Service @@ -32,4 +34,15 @@ class ProductService( return productHandler.findFilter(productCategory, productFilter) } + + fun findFilteredProducts( + category: ProductCategory, + productFilter: ProductFilter, + pageable: PageRequest + ): Page { + val productHandler = productHandlers.find { it.supports(category) } + ?: throw IllegalArgumentException("핸들러가 지원하지 않는 카테고리 입니다. category: $category") + + return productHandler.findFilteredProductsOrderByDiscountRate(category, productFilter, pageable) + } } diff --git a/src/main/kotlin/backend/itracker/tracker/service/handler/MacbookHandler.kt b/src/main/kotlin/backend/itracker/tracker/service/handler/MacbookHandler.kt index 47219d5..5c9a978 100644 --- a/src/main/kotlin/backend/itracker/tracker/service/handler/MacbookHandler.kt +++ b/src/main/kotlin/backend/itracker/tracker/service/handler/MacbookHandler.kt @@ -8,6 +8,9 @@ import backend.itracker.tracker.service.response.filter.MacbookFilterResponse import backend.itracker.tracker.service.response.product.CommonProductModel import backend.itracker.tracker.service.response.product.MacbookResponse import backend.itracker.tracker.service.vo.ProductFilter +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Component @@ -26,30 +29,8 @@ class MacbookHandler( limit: Int ): List { val macbooks = macbookService.findAllFetchByProductCategory(productCategory) - return macbooks.map { - val koreanCategory = when (it.category) { - ProductCategory.MACBOOK_AIR -> "맥북 에어" - ProductCategory.MACBOOK_PRO -> "맥북 프로" - else -> "" - } - MacbookResponse( - id = it.id, - title = "${it.company} ${it.releaseYear} $koreanCategory ${it.size}", - category = it.category.name.lowercase(), - size = it.size, - discountPercentage = it.findDiscountPercentage(), - chip = it.chip, - cpu = "${it.cpu} CPU", - gpu = "${it.gpu} GPU", - storage = "${it.storage} SSD 저장 장치", - memory = "${it.memory} 통합 메모리", - color = it.color, - currentPrice = it.findCurrentPrice(), - label = "역대최저가", - imageUrl = it.thumbnail, - isOutOfStock = it.isOutOfStock() - ) - }.sortedBy { it.discountPercentage } + return macbooks.map { MacbookResponse.of(it) } + .sortedBy { it.discountPercentage } .take(limit) } @@ -61,4 +42,21 @@ class MacbookHandler( return MacbookFilterResponse.from(macbooks) } + + override fun findFilteredProductsOrderByDiscountRate( + category: ProductCategory, + filter: ProductFilter, + pageable: Pageable, + ): Page { + val pageMacbooks = macbookService.findAllProductsByFilter( + category, + MacbookFilterCondition(filter.value), + pageable + ) + + val contents = pageMacbooks.content.map { MacbookResponse.of(it) } + .sortedBy { it.discountPercentage } + + return PageImpl(contents, pageMacbooks.pageable, pageMacbooks.totalElements) + } } diff --git a/src/main/kotlin/backend/itracker/tracker/service/handler/ProductHandler.kt b/src/main/kotlin/backend/itracker/tracker/service/handler/ProductHandler.kt index 857ae05..1db9dd5 100644 --- a/src/main/kotlin/backend/itracker/tracker/service/handler/ProductHandler.kt +++ b/src/main/kotlin/backend/itracker/tracker/service/handler/ProductHandler.kt @@ -4,6 +4,8 @@ import backend.itracker.crawl.common.ProductCategory import backend.itracker.tracker.service.response.filter.CommonFilterModel import backend.itracker.tracker.service.response.product.CommonProductModel import backend.itracker.tracker.service.vo.ProductFilter +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable interface ProductHandler { @@ -13,4 +15,10 @@ interface ProductHandler { fun findTopDiscountPercentageProducts(productCategory: ProductCategory, limit: Int): List fun findFilter(productCategory: ProductCategory, filterCondition: ProductFilter): CommonFilterModel + + fun findFilteredProductsOrderByDiscountRate( + category: ProductCategory, + filter: ProductFilter, + pageable: Pageable, + ): Page } diff --git a/src/main/kotlin/backend/itracker/tracker/service/response/product/MacbookResponse.kt b/src/main/kotlin/backend/itracker/tracker/service/response/product/MacbookResponse.kt index 51a51f0..e919586 100644 --- a/src/main/kotlin/backend/itracker/tracker/service/response/product/MacbookResponse.kt +++ b/src/main/kotlin/backend/itracker/tracker/service/response/product/MacbookResponse.kt @@ -1,5 +1,7 @@ package backend.itracker.tracker.service.response.product +import backend.itracker.crawl.common.ProductCategory +import backend.itracker.crawl.macbook.domain.Macbook import java.math.BigDecimal data class MacbookResponse( @@ -18,5 +20,34 @@ data class MacbookResponse( val label: String, val imageUrl: String, val isOutOfStock: Boolean -) : CommonProductModel +) : CommonProductModel { + + companion object { + fun of(macbook: Macbook): MacbookResponse { + val koreanCategory = when (macbook.category) { + ProductCategory.MACBOOK_AIR -> "맥북 에어" + ProductCategory.MACBOOK_PRO -> "맥북 프로" + else -> "" + } + + return MacbookResponse( + id = macbook.id, + title = "${macbook.company} ${macbook.releaseYear} $koreanCategory ${macbook.size}", + category = macbook.category.name.lowercase(), + size = macbook.size, + discountPercentage = macbook.findDiscountPercentage(), + chip = macbook.chip, + cpu = "${macbook.cpu} CPU", + gpu = "${macbook.gpu} GPU", + storage = "${macbook.storage} SSD 저장 장치", + memory = "${macbook.memory} 통합 메모리", + color = macbook.color, + currentPrice = macbook.findCurrentPrice(), + label = "역대최저가", + imageUrl = macbook.thumbnail, + isOutOfStock = macbook.isOutOfStock() + ) + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 764e60a..bcbbf99 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,7 +3,7 @@ spring: name: itracker jpa: hibernate: - ddl-auto: create + ddl-auto: validate properties: hibernate: default_batch_fetch_size: 100