From dd91fa44300d624060f5641250e2e78dae61a913 Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 29 May 2024 17:41:05 +0900 Subject: [PATCH] =?UTF-8?q?5/29=20=EB=B0=B0=ED=8F=AC=20=EC=9E=91=EC=97=85?= =?UTF-8?q?=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../itracker/crawl/airpods/domain/AirPods.kt | 52 ++++++++++++ .../crawl/airpods/domain/AirPodsCategory.kt | 7 ++ .../crawl/airpods/domain/AirPodsPrice.kt | 35 ++++++++ .../crawl/airpods/domain/AirPodsPrices.kt | 16 ++++ .../domain/repository/AirPodsRepository.kt | 20 +++++ .../crawl/airpods/service/AirPodsService.kt | 24 ++++++ .../itracker/crawl/service/CrawlService.kt | 7 ++ .../crawl/service/mapper/CrawlMapper.kt | 11 ++- .../mapper/airpods/AirPodsGen2Mapper.kt | 48 +++++++++++ .../mapper/airpods/AirPodsGen3Mapper.kt | 48 +++++++++++ .../service/mapper/airpods/AirPodsMapper.kt | 36 ++++++++ .../mapper/airpods/AirPodsMappingComponent.kt | 11 +++ .../mapper/airpods/AirPodsMaxMapper.kt | 56 +++++++++++++ .../mapper/airpods/AirPodsProMapper.kt | 48 +++++++++++ .../service/response/AirPodsCrawlResponse.kt | 84 +++++++++++++++++++ .../service/response/MacCrawlResponse.kt | 2 +- .../service/response/MacbookCrawlResponse.kt | 5 -- .../crawl/service/vo/DefaultProduct.kt | 4 + .../schedule/controller/ScheduleController.kt | 1 + .../schedule/service/SchedulerService.kt | 14 +++- 20 files changed, 521 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPods.kt create mode 100644 src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPodsCategory.kt create mode 100644 src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPodsPrice.kt create mode 100644 src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPodsPrices.kt create mode 100644 src/main/kotlin/backend/itracker/crawl/airpods/domain/repository/AirPodsRepository.kt create mode 100644 src/main/kotlin/backend/itracker/crawl/airpods/service/AirPodsService.kt create mode 100644 src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsGen2Mapper.kt create mode 100644 src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsGen3Mapper.kt create mode 100644 src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsMapper.kt create mode 100644 src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsMappingComponent.kt create mode 100644 src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsMaxMapper.kt create mode 100644 src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsProMapper.kt create mode 100644 src/main/kotlin/backend/itracker/crawl/service/response/AirPodsCrawlResponse.kt diff --git a/src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPods.kt b/src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPods.kt new file mode 100644 index 0000000..f07eb9a --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPods.kt @@ -0,0 +1,52 @@ +package backend.itracker.crawl.airpods.domain + +import backend.itracker.crawl.common.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Embedded +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.Table + +@Entity +@Table(name = "airpods") +class AirPods( + val coupangId: Long, + val company: String, + val releaseYear: Int, + val generation: Int, + val canWirelessCharging: Boolean, + val chargingType: String, + val color: String, + + @Enumerated(EnumType.STRING) + val category: AirPodsCategory, + + @Column(columnDefinition = "TEXT") + val name: String, + + @Column(columnDefinition = "TEXT") + val productLink: String, + + @Column(columnDefinition = "TEXT") + val thumbnail: String, + + @Embedded + val prices: AirPodsPrices = AirPodsPrices(), + + id: Long = 0L +) : BaseEntity(id) { + + fun addAllPrices(prices: AirPodsPrices) { + prices.airPodsPrices.forEach(this::addPrice) + } + + fun addPrice(airPodsPrice: AirPodsPrice) { + prices.add(airPodsPrice) + airPodsPrice.changeAirPods(this) + } + + override fun toString(): String { + return "AirPods(coupangId=$coupangId, company='$company', releaseYear=$releaseYear, generation=$generation, canWirelessCharging=$canWirelessCharging, chargingType='$chargingType', color='$color', category=$category, name='$name', productLink='$productLink', thumbnail='$thumbnail')" + } +} diff --git a/src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPodsCategory.kt b/src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPodsCategory.kt new file mode 100644 index 0000000..28c0c2f --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPodsCategory.kt @@ -0,0 +1,7 @@ +package backend.itracker.crawl.airpods.domain + +enum class AirPodsCategory { + AIRPODS, + AIRPODS_PRO, + AIRPODS_MAX +} diff --git a/src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPodsPrice.kt b/src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPodsPrice.kt new file mode 100644 index 0000000..acf10d7 --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPodsPrice.kt @@ -0,0 +1,35 @@ +package backend.itracker.crawl.airpods.domain + +import backend.itracker.crawl.common.BaseEntity +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.ForeignKey +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import java.math.BigDecimal + +@Entity +@Table(name = "airpods_price") +class AirPodsPrice( + val discountPercentage: Int, + val basePrice: BigDecimal, + val currentPrice: BigDecimal, + val isOutOfStock: Boolean, + id: Long = 0L +) : BaseEntity(id) { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + name = "airpods_id", nullable = false, foreignKey = ForeignKey(name = "fk_airpods_price_airpods_id_ref_airpods_id") + ) + var airPods: AirPods? = null + + fun changeAirPods(airPods: AirPods) { + this.airPods = airPods + } + + override fun toString(): String { + return "AirPodsPrice(discountPercentage=$discountPercentage, basePrice=$basePrice, currentPrice=$currentPrice, isOutOfStock=$isOutOfStock)" + } +} diff --git a/src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPodsPrices.kt b/src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPodsPrices.kt new file mode 100644 index 0000000..fcb8033 --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/airpods/domain/AirPodsPrices.kt @@ -0,0 +1,16 @@ +package backend.itracker.crawl.airpods.domain + +import jakarta.persistence.CascadeType +import jakarta.persistence.Embeddable +import jakarta.persistence.OneToMany + +@Embeddable +class AirPodsPrices( + @OneToMany(mappedBy = "airPods", cascade = [CascadeType.PERSIST]) + val airPodsPrices: MutableList = mutableListOf() +) { + + fun add(targetAirPodsPrice: AirPodsPrice) { + airPodsPrices.add(targetAirPodsPrice) + } +} diff --git a/src/main/kotlin/backend/itracker/crawl/airpods/domain/repository/AirPodsRepository.kt b/src/main/kotlin/backend/itracker/crawl/airpods/domain/repository/AirPodsRepository.kt new file mode 100644 index 0000000..80ee018 --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/airpods/domain/repository/AirPodsRepository.kt @@ -0,0 +1,20 @@ +package backend.itracker.crawl.airpods.domain.repository + +import backend.itracker.crawl.airpods.domain.AirPods +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import java.util.* + +interface AirPodsRepository : JpaRepository { + + @Query( + """ + select a + from AirPods a + join fetch a.prices + where a.coupangId = :coupangId + """ + ) + fun findByCoupangId(@Param("coupangId") coupangId: Long): Optional +} diff --git a/src/main/kotlin/backend/itracker/crawl/airpods/service/AirPodsService.kt b/src/main/kotlin/backend/itracker/crawl/airpods/service/AirPodsService.kt new file mode 100644 index 0000000..50da857 --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/airpods/service/AirPodsService.kt @@ -0,0 +1,24 @@ +package backend.itracker.crawl.airpods.service + +import backend.itracker.crawl.airpods.domain.AirPods +import backend.itracker.crawl.airpods.domain.repository.AirPodsRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class AirPodsService( + private val airPodsRepository: AirPodsRepository +) { + + fun saveAll(airPodses: List) { + for (airPods in airPodses) { + val maybeAirPods = airPodsRepository.findByCoupangId(airPods.coupangId) + if (maybeAirPods.isEmpty) { + airPodsRepository.save(airPods) + continue + } + maybeAirPods.get().addAllPrices(airPods.prices) + } + } +} diff --git a/src/main/kotlin/backend/itracker/crawl/service/CrawlService.kt b/src/main/kotlin/backend/itracker/crawl/service/CrawlService.kt index 9fb4cc1..bb1a2b0 100644 --- a/src/main/kotlin/backend/itracker/crawl/service/CrawlService.kt +++ b/src/main/kotlin/backend/itracker/crawl/service/CrawlService.kt @@ -1,5 +1,6 @@ package backend.itracker.crawl.service +import backend.itracker.crawl.airpods.domain.AirPods import backend.itracker.crawl.ipad.domain.Ipad import backend.itracker.crawl.mac.domain.Mac import backend.itracker.crawl.macbook.domain.Macbook @@ -40,6 +41,12 @@ class CrawlService( return crawlMapper.toMac(products) } + fun crawlAirPods(): List { + val url = getCrawlUrl(CrawlTargetCategory.AIRPODS) + val products = crawler.crawl(url) + return crawlMapper.toAirPods(products) + } + private fun getCrawlUrl(category: CrawlTargetCategory): String { return "$CRAWL_URL${category.categoryId}" } diff --git a/src/main/kotlin/backend/itracker/crawl/service/mapper/CrawlMapper.kt b/src/main/kotlin/backend/itracker/crawl/service/mapper/CrawlMapper.kt index bd7ccee..e2afd92 100644 --- a/src/main/kotlin/backend/itracker/crawl/service/mapper/CrawlMapper.kt +++ b/src/main/kotlin/backend/itracker/crawl/service/mapper/CrawlMapper.kt @@ -1,8 +1,10 @@ package backend.itracker.crawl.service.mapper +import backend.itracker.crawl.airpods.domain.AirPods import backend.itracker.crawl.ipad.domain.Ipad import backend.itracker.crawl.mac.domain.Mac import backend.itracker.crawl.macbook.domain.Macbook +import backend.itracker.crawl.service.mapper.airpods.AirPodsMapper import backend.itracker.crawl.service.mapper.ipad.IpadMappers import backend.itracker.crawl.service.mapper.mac.MacMappers import backend.itracker.crawl.service.mapper.macbook.MacbookMappers @@ -17,7 +19,8 @@ class CrawlMapper( private val macbookMappers: MacbookMappers, private val ipadMappers: IpadMappers, private val appleWatchMappers: AppleWatchMappers, - private val macMappers: MacMappers + private val macMappers: MacMappers, + private val airPodsMapper: AirPodsMapper, ) { fun toMacbook(products: Map): List { @@ -43,4 +46,10 @@ class CrawlMapper( return macMappers.toDomain(filteredProducts) } + + fun toAirPods(products: Map): List { + val filteredProducts = products.values.filter { it.isAirPods() } + + return airPodsMapper.toDomain(filteredProducts) + } } diff --git a/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsGen2Mapper.kt b/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsGen2Mapper.kt new file mode 100644 index 0000000..5772394 --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsGen2Mapper.kt @@ -0,0 +1,48 @@ +package backend.itracker.crawl.service.mapper.airpods + +import backend.itracker.crawl.airpods.domain.AirPods +import backend.itracker.crawl.airpods.domain.AirPodsCategory +import backend.itracker.crawl.service.response.AirPodsCrawlResponse +import backend.itracker.crawl.service.vo.DefaultProduct +import org.springframework.stereotype.Component + +private const val AIR_PODS_GEN_2 = "AirPods 2세대" + +@Component +class AirPodsGen2Mapper : AirPodsMappingComponent { + + override fun supports(subCategory: String): Boolean { + return AIR_PODS_GEN_2 == subCategory + } + + override fun toDomain(product: DefaultProduct): AirPods { + val names = product.name.split(",") + .map { it.trim() } + .toList() + + val title = names[0].split(" ") + val company = title[0] + val releaseYear = "2019" + val category = AirPodsCategory.AIRPODS + val generation = title[2].replace("세대", "") + val canWirelessCharging = title[3] == "유선" + val chargingType = "Lighting 8-pin" + + return AirPodsCrawlResponse( + coupangId = product.productId, + name = product.name, + company = company, + releaseYear = releaseYear, + category = category, + generation = generation, + canWirelessCharging = canWirelessCharging, + chargingType = chargingType, + productLink = product.productLink, + thumbnail = product.thumbnailLink, + basePrice = product.price.basePrice, + discountPercentage = product.price.discountPercentage, + currentPrice = product.price.discountPrice, + isOutOfStock = product.price.isOutOfStock + ).toDomain() + } +} diff --git a/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsGen3Mapper.kt b/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsGen3Mapper.kt new file mode 100644 index 0000000..31e808d --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsGen3Mapper.kt @@ -0,0 +1,48 @@ +package backend.itracker.crawl.service.mapper.airpods + +import backend.itracker.crawl.airpods.domain.AirPods +import backend.itracker.crawl.airpods.domain.AirPodsCategory +import backend.itracker.crawl.service.response.AirPodsCrawlResponse +import backend.itracker.crawl.service.vo.DefaultProduct +import org.springframework.stereotype.Component + +private const val AIR_PODS_GEN_3 = "AirPods 3세대" + +@Component +class AirPodsGen3Mapper : AirPodsMappingComponent { + + override fun supports(subCategory: String): Boolean { + return AIR_PODS_GEN_3 == subCategory + } + + override fun toDomain(product: DefaultProduct): AirPods { + val names = product.name.split(",") + .map { it.trim() } + .toList() + + val title = names[0].split(" ") + val company = title[0] + val releaseYear = title[1] + val category = AirPodsCategory.AIRPODS + val generation = title[3].replace("세대", "") + val canWirelessCharging = title[4] != "유선" + val chargingType = "Lighting 8-pin" + + return AirPodsCrawlResponse( + coupangId = product.productId, + name = product.name, + company = company, + releaseYear = releaseYear, + category = category, + generation = generation, + canWirelessCharging = canWirelessCharging, + chargingType = chargingType, + productLink = product.productLink, + thumbnail = product.thumbnailLink, + basePrice = product.price.basePrice, + discountPercentage = product.price.discountPercentage, + currentPrice = product.price.discountPrice, + isOutOfStock = product.price.isOutOfStock + ).toDomain() + } +} diff --git a/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsMapper.kt b/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsMapper.kt new file mode 100644 index 0000000..6c3d404 --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsMapper.kt @@ -0,0 +1,36 @@ +package backend.itracker.crawl.service.mapper.airpods + +import backend.itracker.crawl.airpods.domain.AirPods +import backend.itracker.crawl.exception.CrawlException +import backend.itracker.crawl.service.vo.DefaultProduct +import org.springframework.stereotype.Component + +@Component +class AirPodsMapper( + private val airPodsMappers: List +) { + + fun toDomain(filteredProducts: List): List { + val airPodses = mutableListOf() + for (product in filteredProducts) { + try { + for (airPodsMapper in airPodsMappers) { + if (airPodsMapper.supports(product.subCategory)) { + airPodses.add(airPodsMapper.toDomain(product)) + } + } + } catch (e: Exception) { + throw CrawlException( + """ + |AirPods Mapping 중에 에러가 발생했습니다. + |name : ${product.name}, + |subcategory : ${product.subCategory}, + |error: ${e.stackTraceToString()} + |""".trimMargin() + ) + } + } + + return airPodses + } +} diff --git a/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsMappingComponent.kt b/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsMappingComponent.kt new file mode 100644 index 0000000..54cdd19 --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsMappingComponent.kt @@ -0,0 +1,11 @@ +package backend.itracker.crawl.service.mapper.airpods + +import backend.itracker.crawl.airpods.domain.AirPods +import backend.itracker.crawl.service.vo.DefaultProduct + +interface AirPodsMappingComponent { + + fun supports(subCategory: String): Boolean + + fun toDomain(product: DefaultProduct): AirPods +} diff --git a/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsMaxMapper.kt b/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsMaxMapper.kt new file mode 100644 index 0000000..0705b8c --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsMaxMapper.kt @@ -0,0 +1,56 @@ +package backend.itracker.crawl.service.mapper.airpods + +import backend.itracker.crawl.airpods.domain.AirPods +import backend.itracker.crawl.airpods.domain.AirPodsCategory +import backend.itracker.crawl.service.response.AirPodsCrawlResponse +import backend.itracker.crawl.service.vo.DefaultProduct +import org.springframework.stereotype.Component + +private const val AIR_PODS_MAX = "AirPods Max" + +@Component +class AirPodsMaxMapper : AirPodsMappingComponent { + + override fun supports(subCategory: String): Boolean { + return AIR_PODS_MAX == subCategory + } + + override fun toDomain(product: DefaultProduct): AirPods { + val names = product.name.split(",") + .map { it.trim() } + .toList() + + val title = names[0].split(" ") + val company = title[0] + val releaseYear = "2020" + val category = AirPodsCategory.AIRPODS_MAX + val generation = "1" + val canWirelessCharging = false + val chargingType = "Lighting 8-pin" + + val colors = names[1].split(" ") + val color = if (colors.size > 1) { + "${colors[0]} ${colors[1]}" + } else { + colors[0] + } + + return AirPodsCrawlResponse( + coupangId = product.productId, + name = product.name, + company = company, + releaseYear = releaseYear, + category = category, + color = color, + generation = generation, + canWirelessCharging = canWirelessCharging, + chargingType = chargingType, + productLink = product.productLink, + thumbnail = product.thumbnailLink, + basePrice = product.price.basePrice, + discountPercentage = product.price.discountPercentage, + currentPrice = product.price.discountPrice, + isOutOfStock = product.price.isOutOfStock + ).toDomain() + } +} diff --git a/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsProMapper.kt b/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsProMapper.kt new file mode 100644 index 0000000..779511e --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/service/mapper/airpods/AirPodsProMapper.kt @@ -0,0 +1,48 @@ +package backend.itracker.crawl.service.mapper.airpods + +import backend.itracker.crawl.airpods.domain.AirPods +import backend.itracker.crawl.airpods.domain.AirPodsCategory +import backend.itracker.crawl.service.response.AirPodsCrawlResponse +import backend.itracker.crawl.service.vo.DefaultProduct +import org.springframework.stereotype.Component + +private const val AIR_PODS_PRO_2 = "AirPods Pro2" + +@Component +class AirPodsProMapper : AirPodsMappingComponent { + + override fun supports(subCategory: String): Boolean { + return AIR_PODS_PRO_2 == subCategory + } + + override fun toDomain(product: DefaultProduct): AirPods { + val names = product.name.split(",") + .map { it.trim() } + .toList() + + val title = names[0].split(" ") + val company = title[0] + val releaseYear = title[1] + val category = AirPodsCategory.AIRPODS_PRO + val generation = title[4].replace("세대", "") + val canWirelessCharging = true + val chargingType = title[5] + + return AirPodsCrawlResponse( + coupangId = product.productId, + name = product.name, + company = company, + releaseYear = releaseYear, + category = category, + generation = generation, + canWirelessCharging = canWirelessCharging, + chargingType = chargingType, + productLink = product.productLink, + thumbnail = product.thumbnailLink, + basePrice = product.price.basePrice, + discountPercentage = product.price.discountPercentage, + currentPrice = product.price.discountPrice, + isOutOfStock = product.price.isOutOfStock + ).toDomain() + } +} diff --git a/src/main/kotlin/backend/itracker/crawl/service/response/AirPodsCrawlResponse.kt b/src/main/kotlin/backend/itracker/crawl/service/response/AirPodsCrawlResponse.kt new file mode 100644 index 0000000..b4c4d9d --- /dev/null +++ b/src/main/kotlin/backend/itracker/crawl/service/response/AirPodsCrawlResponse.kt @@ -0,0 +1,84 @@ +package backend.itracker.crawl.service.response + +import backend.itracker.crawl.airpods.domain.AirPods +import backend.itracker.crawl.airpods.domain.AirPodsCategory +import backend.itracker.crawl.airpods.domain.AirPodsPrice +import java.math.BigDecimal + +data class AirPodsCrawlResponse( + val coupangId: Long, + val name: String, + val company: String, + val releaseYear: Int, + val category: AirPodsCategory, + val color : String, + val generation: Int, + val canWirelessCharging: Boolean, + val chargingType: String, + val productLink: String, + val thumbnail: String, + val basePrice: BigDecimal, + val discountPercentage: Int, + val currentPrice: BigDecimal, + val isOutOfStock: Boolean +) { + constructor( + coupangId: Long, + name: String, + company: String, + releaseYear: String, + category: AirPodsCategory, + color: String = "", + generation: String, + canWirelessCharging: Boolean, + chargingType: String, + productLink: String, + thumbnail: String, + basePrice: BigDecimal, + discountPercentage: Int, + currentPrice: BigDecimal, + isOutOfStock: Boolean + ) : this( + coupangId = coupangId, + name = name, + color = color, + company = company, + releaseYear = releaseYear.toInt(), + category = category, + generation = generation.toInt(), + canWirelessCharging = canWirelessCharging, + chargingType = chargingType, + productLink = productLink, + thumbnail = thumbnail, + discountPercentage = discountPercentage, + basePrice = basePrice, + currentPrice = currentPrice, + isOutOfStock = isOutOfStock + ) + + fun toDomain(): AirPods { + return AirPods( + coupangId = coupangId, + name = name, + company = company, + color = color, + releaseYear = releaseYear, + category = category, + generation = generation, + canWirelessCharging = canWirelessCharging, + chargingType = chargingType, + productLink = productLink, + thumbnail = thumbnail + ).apply { + addPrice( + AirPodsPrice( + discountPercentage = discountPercentage, + basePrice = basePrice, + currentPrice = currentPrice, + isOutOfStock = isOutOfStock + ) + ) + } + } + +} diff --git a/src/main/kotlin/backend/itracker/crawl/service/response/MacCrawlResponse.kt b/src/main/kotlin/backend/itracker/crawl/service/response/MacCrawlResponse.kt index 8a82bd0..874baf5 100644 --- a/src/main/kotlin/backend/itracker/crawl/service/response/MacCrawlResponse.kt +++ b/src/main/kotlin/backend/itracker/crawl/service/response/MacCrawlResponse.kt @@ -5,7 +5,7 @@ import backend.itracker.crawl.mac.domain.MacCategory import backend.itracker.crawl.mac.domain.MacPrice import java.math.BigDecimal -class MacCrawlResponse( +data class MacCrawlResponse( val coupangId: Long, val company: String, val name: String, diff --git a/src/main/kotlin/backend/itracker/crawl/service/response/MacbookCrawlResponse.kt b/src/main/kotlin/backend/itracker/crawl/service/response/MacbookCrawlResponse.kt index fc38ce7..516e992 100644 --- a/src/main/kotlin/backend/itracker/crawl/service/response/MacbookCrawlResponse.kt +++ b/src/main/kotlin/backend/itracker/crawl/service/response/MacbookCrawlResponse.kt @@ -26,11 +26,6 @@ data class MacbookCrawlResponse( val thumbnail: String, val isOutOfStock: Boolean ) { -// companion object { -// fun from(product: DefaultProduct): MacbookCrawlResponse { -// return MacbookDesirializer.deserialize(product) -// } -// } fun toDomain(): Macbook { val macbook = Macbook( diff --git a/src/main/kotlin/backend/itracker/crawl/service/vo/DefaultProduct.kt b/src/main/kotlin/backend/itracker/crawl/service/vo/DefaultProduct.kt index 3ccf36f..3c989e6 100644 --- a/src/main/kotlin/backend/itracker/crawl/service/vo/DefaultProduct.kt +++ b/src/main/kotlin/backend/itracker/crawl/service/vo/DefaultProduct.kt @@ -44,4 +44,8 @@ data class DefaultProduct( name.contains("맥미니") || name.contains("Mac mini") } + + fun isAirPods(): Boolean { + return name.contains("에어팟") && !name.contains("케이스") + } } diff --git a/src/main/kotlin/backend/itracker/schedule/controller/ScheduleController.kt b/src/main/kotlin/backend/itracker/schedule/controller/ScheduleController.kt index 8c437fa..f67146b 100644 --- a/src/main/kotlin/backend/itracker/schedule/controller/ScheduleController.kt +++ b/src/main/kotlin/backend/itracker/schedule/controller/ScheduleController.kt @@ -19,6 +19,7 @@ class ScheduleController( CrawlTargetCategory.IPAD -> schedulerService.crawlIpad() CrawlTargetCategory.APPLE_WATCH -> schedulerService.crawlAppleWatch() CrawlTargetCategory.MAC -> schedulerService.crawlMac() + CrawlTargetCategory.AIRPODS -> schedulerService.crawlAirPods() else -> throw IllegalArgumentException("수동 업데이트를 지원하지 않는 카테고리 입니다.") } } diff --git a/src/main/kotlin/backend/itracker/schedule/service/SchedulerService.kt b/src/main/kotlin/backend/itracker/schedule/service/SchedulerService.kt index 55b7ffa..c229579 100644 --- a/src/main/kotlin/backend/itracker/schedule/service/SchedulerService.kt +++ b/src/main/kotlin/backend/itracker/schedule/service/SchedulerService.kt @@ -1,5 +1,6 @@ package backend.itracker.schedule.service +import backend.itracker.crawl.airpods.service.AirPodsService import backend.itracker.crawl.ipad.service.IpadService import backend.itracker.crawl.mac.service.MacService import backend.itracker.crawl.macbook.service.MacbookService @@ -21,7 +22,8 @@ class SchedulerService( private val macbookService: MacbookService, private val ipadService: IpadService, private val appleWatchService: AppleWatchService, - private val macService: MacService + private val macService: MacService, + private val airPodsService: AirPodsService ) { @Scheduled(cron = CRAWLING_TIME, zone = TIME_ZONE) @@ -63,4 +65,14 @@ class SchedulerService( } logger.info { "맥 크롤링 끝. 시간: $times" } } + + @Scheduled(cron = CRAWLING_TIME, zone = TIME_ZONE) + fun crawlAirPods() { + logger.info { "에어팟 크롤링 시작. " } + val times = measureTime { + val airPods = crawlService.crawlAirPods() + airPodsService.saveAll(airPods) + } + logger.info { "에어팟 크롤링 끝. 시간: $times" } + } }