From 41e7240508f230cb08d484577de714e79883737f Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 21 Jun 2024 17:40:10 +0900 Subject: [PATCH] =?UTF-8?q?6/21=20=EB=B0=B0=ED=8F=AC=20=EC=9E=91=EC=97=85?= =?UTF-8?q?=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/AirPodsRepository.kt | 16 +++++- .../crawl/airpods/service/AirPodsService.kt | 5 ++ .../domain/repository/MacbookRepository.kt | 10 ++++ .../crawl/macbook/service/MacbookService.kt | 5 ++ .../crawl/service/mapper/ipad/IpadMappers.kt | 3 - .../tracker/config/ArgumentResolverConfig.kt | 9 ++- .../infra/oauth/kakao/KakaoMemberClient.kt | 2 +- .../oauth/kakao/dto/KakaoMemberResponse.kt | 2 +- .../member/controller/FavoriteController.kt | 53 ++++++++++++++++++ .../member/controller/MemberController.kt | 2 +- .../request/FavoritePatchRequest.kt | 19 +++++++ .../controller/response/MyInfoResponse.kt | 2 +- .../tracker/member/domain/AuthType.kt | 5 ++ .../tracker/member/domain/Favorite.kt | 32 +++++++++++ .../tracker/member/domain/FavoriteProduct.kt | 20 +++++++ .../{oauth => member/domain}/Member.kt | 19 ++++++- .../domain/repository/FavoriteRepository.kt | 29 ++++++++++ .../tracker/member/service/FavoriteService.kt | 46 +++++++++++++++ .../tracker/member/service/MemberService.kt | 2 +- .../service/handler/FavoriteComposite.kt | 22 ++++++++ .../service/handler/FavoriteHandleable.kt | 12 ++++ .../handler/airpods/FavoriteAirpodsHandler.kt | 30 ++++++++++ .../handler/macbook/FavoriteMacbookHandler.kt | 30 ++++++++++ .../response/AirpodsFavoriteResponse.kt | 50 +++++++++++++++++ .../response/CommonFavoriteProductModel.kt | 9 +++ .../response/MacbookFavoriteResponse.kt | 56 +++++++++++++++++++ .../tracker/member/service/vo/FavoriteInfo.kt | 8 +++ .../tracker/oauth/client/OauthMemberClient.kt | 2 +- .../oauth/repository/MemberRepository.kt | 2 +- .../tracker/oauth/service/OauthService.kt | 2 +- .../product/controller/ProductController.kt | 6 +- .../tracker/product/handler/AirPodsHandler.kt | 18 ++++-- .../tracker/product/handler/MacbookHandler.kt | 16 +++++- .../product/handler/ProductHandleable.kt | 4 +- .../product/handler/ProductHandlerImpl.kt | 11 ++-- .../response/product/CommonProductModel.kt | 1 + .../product/airpods/AirPodsDetailResponse.kt | 8 ++- .../product/macbook/MacbookDetailResponse.kt | 5 +- .../tracker/product/vo/ProductInfo.kt | 8 +++ .../AnonymousMemberArgumentResolver.kt | 33 +++++++++++ .../itracker/tracker/resolver/LoginMember.kt | 4 ++ .../resolver/LoginMemberArgumentResolver.kt | 34 +++++++++++ .../tracker/resolver/OauthArgumentResolver.kt | 2 +- .../assured/ProductMacbookAssuredTest.kt | 2 +- 44 files changed, 621 insertions(+), 35 deletions(-) create mode 100644 src/main/kotlin/backend/itracker/tracker/member/controller/FavoriteController.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/controller/request/FavoritePatchRequest.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/domain/AuthType.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/domain/Favorite.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/domain/FavoriteProduct.kt rename src/main/kotlin/backend/itracker/tracker/{oauth => member/domain}/Member.kt (64%) create mode 100644 src/main/kotlin/backend/itracker/tracker/member/domain/repository/FavoriteRepository.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/service/FavoriteService.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/service/handler/FavoriteComposite.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/service/handler/FavoriteHandleable.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/service/handler/airpods/FavoriteAirpodsHandler.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/service/handler/macbook/FavoriteMacbookHandler.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/service/handler/response/AirpodsFavoriteResponse.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/service/handler/response/CommonFavoriteProductModel.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/service/handler/response/MacbookFavoriteResponse.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/member/service/vo/FavoriteInfo.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/product/vo/ProductInfo.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/resolver/AnonymousMemberArgumentResolver.kt create mode 100644 src/main/kotlin/backend/itracker/tracker/resolver/LoginMemberArgumentResolver.kt 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 index a3bce1b..9c52fa2 100644 --- a/src/main/kotlin/backend/itracker/crawl/airpods/domain/repository/AirPodsRepository.kt +++ b/src/main/kotlin/backend/itracker/crawl/airpods/domain/repository/AirPodsRepository.kt @@ -17,7 +17,7 @@ interface AirPodsRepository : JpaRepository { """ select a from AirPods a - join fetch a.prices.airPodsPrices + left join fetch a.prices.airPodsPrices where a.coupangId = :coupangId """ ) @@ -27,7 +27,7 @@ interface AirPodsRepository : JpaRepository { """ select a from AirPods a - join fetch a.prices.airPodsPrices + left join fetch a.prices.airPodsPrices """ ) fun findAllFetch(): List @@ -36,11 +36,21 @@ interface AirPodsRepository : JpaRepository { """ select a from AirPods a - join fetch a.prices.airPodsPrices + left join fetch a.prices.airPodsPrices where a.id = :id """ ) fun findByIdAllFetch(@Param("id") id: Long): Optional fun findByIdBetween(startId: Long, endId: Long): List + + @Query( + """ + select a + from AirPods a + left join fetch a.prices.airPodsPrices + where a.id in :ids + """ + ) + fun findAllInIds(@Param("ids") ids: List): List } diff --git a/src/main/kotlin/backend/itracker/crawl/airpods/service/AirPodsService.kt b/src/main/kotlin/backend/itracker/crawl/airpods/service/AirPodsService.kt index a60148d..463b8ae 100644 --- a/src/main/kotlin/backend/itracker/crawl/airpods/service/AirPodsService.kt +++ b/src/main/kotlin/backend/itracker/crawl/airpods/service/AirPodsService.kt @@ -46,4 +46,9 @@ class AirPodsService( fun findByIdBetween(startId: Long, endId: Long): List { return airPodsRepository.findByIdBetween(startId, endId) } + + @Transactional(readOnly = true) + fun findAllInIds(ids: List): List { + return airPodsRepository.findAllInIds(ids) + } } 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 b5e626f..7967eb3 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 @@ -37,4 +37,14 @@ interface MacbookRepository: JpaRepository, MacbookRepositoryCust fun findAllPricesByMacbookId(@Param("id") id: Long): Optional fun findByIdBetween(startId: Long, endId: Long): List + + @Query( + """ + select m + from Macbook m + left join fetch m.prices.macbookPrices + where m.id in :ids + """ + ) + fun findAllInIds(@Param("ids") ids: List): List } 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 9fa1ce0..11ac50a 100644 --- a/src/main/kotlin/backend/itracker/crawl/macbook/service/MacbookService.kt +++ b/src/main/kotlin/backend/itracker/crawl/macbook/service/MacbookService.kt @@ -68,4 +68,9 @@ class MacbookService( fun findByIdBetween(startId: Long, endId: Long): List { return macbookRepository.findByIdBetween(startId, endId) } + + @Transactional(readOnly = true) + fun findAllInIds(ids: List): List { + return macbookRepository.findAllInIds(ids) + } } diff --git a/src/main/kotlin/backend/itracker/crawl/service/mapper/ipad/IpadMappers.kt b/src/main/kotlin/backend/itracker/crawl/service/mapper/ipad/IpadMappers.kt index c0ff94e..9c1dcf0 100644 --- a/src/main/kotlin/backend/itracker/crawl/service/mapper/ipad/IpadMappers.kt +++ b/src/main/kotlin/backend/itracker/crawl/service/mapper/ipad/IpadMappers.kt @@ -3,11 +3,8 @@ package backend.itracker.crawl.service.mapper.ipad import backend.itracker.crawl.exception.CrawlException import backend.itracker.crawl.ipad.domain.Ipad import backend.itracker.crawl.service.vo.DefaultProduct -import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.stereotype.Component -private val logger = KotlinLogging.logger {} - @Component class IpadMappers( private val ipadMappers: List diff --git a/src/main/kotlin/backend/itracker/tracker/config/ArgumentResolverConfig.kt b/src/main/kotlin/backend/itracker/tracker/config/ArgumentResolverConfig.kt index 4f90a26..f993bce 100644 --- a/src/main/kotlin/backend/itracker/tracker/config/ArgumentResolverConfig.kt +++ b/src/main/kotlin/backend/itracker/tracker/config/ArgumentResolverConfig.kt @@ -1,16 +1,19 @@ package backend.itracker.tracker.config -import backend.itracker.tracker.resolver.OauthArgumentResolver +import backend.itracker.tracker.resolver.AnonymousMemberArgumentResolver +import backend.itracker.tracker.resolver.LoginMemberArgumentResolver import org.springframework.context.annotation.Configuration import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Configuration class ArgumentResolverConfig( - private val oauthArgumentResolver: OauthArgumentResolver + private val loginMemberArgumentResolver: LoginMemberArgumentResolver, + private val anonymousMemberArgumentResolver: AnonymousMemberArgumentResolver, ) : WebMvcConfigurer { override fun addArgumentResolvers(resolvers: MutableList) { - resolvers.add(oauthArgumentResolver) + resolvers.add(loginMemberArgumentResolver) + resolvers.add(anonymousMemberArgumentResolver) } } diff --git a/src/main/kotlin/backend/itracker/tracker/infra/oauth/kakao/KakaoMemberClient.kt b/src/main/kotlin/backend/itracker/tracker/infra/oauth/kakao/KakaoMemberClient.kt index 20a858f..e206002 100644 --- a/src/main/kotlin/backend/itracker/tracker/infra/oauth/kakao/KakaoMemberClient.kt +++ b/src/main/kotlin/backend/itracker/tracker/infra/oauth/kakao/KakaoMemberClient.kt @@ -1,7 +1,7 @@ package backend.itracker.tracker.infra.oauth.kakao import backend.itracker.tracker.infra.oauth.kakao.client.KakaoApiClient -import backend.itracker.tracker.oauth.Member +import backend.itracker.tracker.member.domain.Member import backend.itracker.tracker.oauth.OauthServerType import backend.itracker.tracker.oauth.RedirectType import backend.itracker.tracker.oauth.client.OauthMemberClient diff --git a/src/main/kotlin/backend/itracker/tracker/infra/oauth/kakao/dto/KakaoMemberResponse.kt b/src/main/kotlin/backend/itracker/tracker/infra/oauth/kakao/dto/KakaoMemberResponse.kt index 7f9b276..1d38171 100644 --- a/src/main/kotlin/backend/itracker/tracker/infra/oauth/kakao/dto/KakaoMemberResponse.kt +++ b/src/main/kotlin/backend/itracker/tracker/infra/oauth/kakao/dto/KakaoMemberResponse.kt @@ -1,6 +1,6 @@ package backend.itracker.tracker.infra.oauth.kakao.dto -import backend.itracker.tracker.oauth.Member +import backend.itracker.tracker.member.domain.Member import backend.itracker.tracker.oauth.OauthId import backend.itracker.tracker.oauth.OauthServerType import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy diff --git a/src/main/kotlin/backend/itracker/tracker/member/controller/FavoriteController.kt b/src/main/kotlin/backend/itracker/tracker/member/controller/FavoriteController.kt new file mode 100644 index 0000000..76c88ab --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/controller/FavoriteController.kt @@ -0,0 +1,53 @@ +package backend.itracker.tracker.member.controller + +import backend.itracker.tracker.common.request.PageParams +import backend.itracker.tracker.common.response.Pages +import backend.itracker.tracker.member.controller.request.FavoritePatchRequest +import backend.itracker.tracker.member.domain.Favorite +import backend.itracker.tracker.member.domain.FavoriteProduct +import backend.itracker.tracker.member.domain.Member +import backend.itracker.tracker.member.service.FavoriteService +import backend.itracker.tracker.member.service.handler.response.CommonFavoriteProductModel +import backend.itracker.tracker.resolver.LoginMember +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.PatchMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +private const val FAVORITE_DEFAULT_SIZE = 6 + +@RestController +class FavoriteController( + private val favoriteService: FavoriteService +) { + + @PatchMapping("/api/v1/favorites") + fun patchFavorites( + @LoginMember member: Member, + @RequestBody request: FavoritePatchRequest + ): ResponseEntity { + val favoriteInfo = request.toFavoriteInfo() + val favorite = Favorite( + member = member, + product = FavoriteProduct(productId = favoriteInfo.productId, productCategory = favoriteInfo.category) + ) + favoriteService.patchFavorite(favorite) + + return ResponseEntity.noContent().build() + } + + @GetMapping("/api/v1/favorites") + fun findAllFavorites( + @LoginMember member: Member, + @ModelAttribute pageParams: PageParams, + ): ResponseEntity> { + val favorites = favoriteService.findAllFavoritesByMember( + member, PageRequest.of(pageParams.offset, FAVORITE_DEFAULT_SIZE) + ) + + return ResponseEntity.ok(Pages.withPagination(favorites)) + } +} diff --git a/src/main/kotlin/backend/itracker/tracker/member/controller/MemberController.kt b/src/main/kotlin/backend/itracker/tracker/member/controller/MemberController.kt index 0c6459b..2f51ddd 100644 --- a/src/main/kotlin/backend/itracker/tracker/member/controller/MemberController.kt +++ b/src/main/kotlin/backend/itracker/tracker/member/controller/MemberController.kt @@ -2,7 +2,7 @@ package backend.itracker.tracker.member.controller import backend.itracker.tracker.common.response.SingleData import backend.itracker.tracker.member.controller.response.MyInfoResponse -import backend.itracker.tracker.oauth.Member +import backend.itracker.tracker.member.domain.Member import backend.itracker.tracker.resolver.LoginMember import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping diff --git a/src/main/kotlin/backend/itracker/tracker/member/controller/request/FavoritePatchRequest.kt b/src/main/kotlin/backend/itracker/tracker/member/controller/request/FavoritePatchRequest.kt new file mode 100644 index 0000000..00281c9 --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/controller/request/FavoritePatchRequest.kt @@ -0,0 +1,19 @@ +package backend.itracker.tracker.member.controller.request + +import backend.itracker.crawl.common.ProductCategory +import backend.itracker.tracker.member.service.vo.FavoriteInfo + +data class FavoritePatchRequest( + val productId: Long, + val productCategory: String +) { + + fun toFavoriteInfo() : FavoriteInfo + { + return FavoriteInfo( + productId = productId, + category = ProductCategory.valueOf(productCategory.uppercase()) + ) + } +} + diff --git a/src/main/kotlin/backend/itracker/tracker/member/controller/response/MyInfoResponse.kt b/src/main/kotlin/backend/itracker/tracker/member/controller/response/MyInfoResponse.kt index 93e117c..f2c20e3 100644 --- a/src/main/kotlin/backend/itracker/tracker/member/controller/response/MyInfoResponse.kt +++ b/src/main/kotlin/backend/itracker/tracker/member/controller/response/MyInfoResponse.kt @@ -1,6 +1,6 @@ package backend.itracker.tracker.member.controller.response -import backend.itracker.tracker.oauth.Member +import backend.itracker.tracker.member.domain.Member data class MyInfoResponse( val id: Long, diff --git a/src/main/kotlin/backend/itracker/tracker/member/domain/AuthType.kt b/src/main/kotlin/backend/itracker/tracker/member/domain/AuthType.kt new file mode 100644 index 0000000..ebfd4bc --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/domain/AuthType.kt @@ -0,0 +1,5 @@ +package backend.itracker.tracker.member.domain + +enum class AuthType { + USER, ADMIN; +} diff --git a/src/main/kotlin/backend/itracker/tracker/member/domain/Favorite.kt b/src/main/kotlin/backend/itracker/tracker/member/domain/Favorite.kt new file mode 100644 index 0000000..836832c --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/domain/Favorite.kt @@ -0,0 +1,32 @@ +package backend.itracker.tracker.member.domain + +import backend.itracker.crawl.common.BaseEntity +import jakarta.persistence.Embedded +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import jakarta.persistence.UniqueConstraint + +@Entity +@Table( + name = "favorite", + uniqueConstraints = [ + UniqueConstraint( + name = "favorite_product_unique", + columnNames = ["member_id", "product_id", "product_category"] + ) + ] +) +class Favorite( + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + val member: Member, + + @Embedded + val product: FavoriteProduct, + + id: Long = 0 +) : BaseEntity(id) diff --git a/src/main/kotlin/backend/itracker/tracker/member/domain/FavoriteProduct.kt b/src/main/kotlin/backend/itracker/tracker/member/domain/FavoriteProduct.kt new file mode 100644 index 0000000..1516595 --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/domain/FavoriteProduct.kt @@ -0,0 +1,20 @@ +package backend.itracker.tracker.member.domain + +import backend.itracker.crawl.common.ProductCategory +import jakarta.persistence.Embeddable +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated + +@Embeddable +class FavoriteProduct( + + val productId: Long, + + @Enumerated(EnumType.STRING) + val productCategory: ProductCategory, +) { + + override fun toString(): String { + return "FavoriteProduct(productId=$productId, productCategory=$productCategory)" + } +} diff --git a/src/main/kotlin/backend/itracker/tracker/oauth/Member.kt b/src/main/kotlin/backend/itracker/tracker/member/domain/Member.kt similarity index 64% rename from src/main/kotlin/backend/itracker/tracker/oauth/Member.kt rename to src/main/kotlin/backend/itracker/tracker/member/domain/Member.kt index ce25793..36a6dee 100644 --- a/src/main/kotlin/backend/itracker/tracker/oauth/Member.kt +++ b/src/main/kotlin/backend/itracker/tracker/member/domain/Member.kt @@ -1,11 +1,15 @@ -package backend.itracker.tracker.oauth +package backend.itracker.tracker.member.domain import backend.itracker.crawl.common.BaseEntity +import backend.itracker.tracker.oauth.OauthId +import backend.itracker.tracker.oauth.OauthServerType import jakarta.persistence.Column import jakarta.persistence.Embedded import jakarta.persistence.Entity import jakarta.persistence.EnumType import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.OneToMany import jakarta.persistence.Table import jakarta.persistence.UniqueConstraint @@ -29,6 +33,9 @@ class Member( @Enumerated(EnumType.STRING) val authType: AuthType = AuthType.USER, + @OneToMany(fetch = FetchType.LAZY, mappedBy = "member") + val favorites: List = mutableListOf(), + id: Long = 0L ) : BaseEntity(id) { @@ -37,7 +44,17 @@ class Member( this.profileImage = target.profileImage } + fun isAnonymous() = this.id == 0L + override fun toString(): String { return "Member(id='$id' oauthId=$oauthId, nickname='$nickname', profileImage='$profileImage', authType=$authType)" } + + companion object { + fun anonymous() = Member( + oauthId = OauthId("anonymous", OauthServerType.KAKAO), + nickname = "익명", + profileImage = "익명", + ) + } } diff --git a/src/main/kotlin/backend/itracker/tracker/member/domain/repository/FavoriteRepository.kt b/src/main/kotlin/backend/itracker/tracker/member/domain/repository/FavoriteRepository.kt new file mode 100644 index 0000000..2a7d025 --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/domain/repository/FavoriteRepository.kt @@ -0,0 +1,29 @@ +package backend.itracker.tracker.member.domain.repository + +import backend.itracker.tracker.member.domain.Favorite +import backend.itracker.tracker.member.domain.FavoriteProduct +import backend.itracker.tracker.member.domain.Member +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +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 FavoriteRepository : JpaRepository { + + @Query( + """ + select f + from Favorite f + where f.member.id = :memberId + and f.product = :product + """ + ) + fun findByFavorite( + @Param("memberId") memberId: Long, + @Param("product") product: FavoriteProduct, + ): Optional + + fun findAllByMember(member: Member, pageable: Pageable): Page +} diff --git a/src/main/kotlin/backend/itracker/tracker/member/service/FavoriteService.kt b/src/main/kotlin/backend/itracker/tracker/member/service/FavoriteService.kt new file mode 100644 index 0000000..da8b03c --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/service/FavoriteService.kt @@ -0,0 +1,46 @@ +package backend.itracker.tracker.member.service + +import backend.itracker.tracker.member.domain.Favorite +import backend.itracker.tracker.member.domain.Member +import backend.itracker.tracker.member.domain.repository.FavoriteRepository +import backend.itracker.tracker.member.service.handler.FavoriteComposite +import backend.itracker.tracker.member.service.handler.response.CommonFavoriteProductModel +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class FavoriteService( + private val favoriteRepository: FavoriteRepository, + private val favoriteComposite: FavoriteComposite, +) { + + fun patchFavorite(favorite: Favorite) { + val maybeFavorite = + favoriteRepository.findByFavorite(favorite.member.id, favorite.product) + + maybeFavorite.ifPresentOrElse( + { favoriteRepository.delete(it) }, + { favoriteRepository.save(favorite) } + ) + } + + @Transactional(readOnly = true) + fun findAllFavoritesByMember( + member: Member, pageable: Pageable + ): Page { + val pageFavorites = favoriteRepository.findAllByMember(member, pageable) + val favorites = pageFavorites.content + .groupBy { it.product.productCategory } + val productsContents = favorites.flatMap { (productCategory, favorites) -> + favoriteComposite.findAllByIds(productCategory, favorites) + } + .sortedBy { it.createdAt } + .reversed().toList() + + return PageImpl(productsContents, pageFavorites.pageable, productsContents.size.toLong()) + } +} diff --git a/src/main/kotlin/backend/itracker/tracker/member/service/MemberService.kt b/src/main/kotlin/backend/itracker/tracker/member/service/MemberService.kt index a4e78af..be6ca76 100644 --- a/src/main/kotlin/backend/itracker/tracker/member/service/MemberService.kt +++ b/src/main/kotlin/backend/itracker/tracker/member/service/MemberService.kt @@ -1,6 +1,6 @@ package backend.itracker.tracker.member.service -import backend.itracker.tracker.oauth.Member +import backend.itracker.tracker.member.domain.Member import backend.itracker.tracker.oauth.OauthId import backend.itracker.tracker.oauth.repository.MemberRepository import backend.itracker.tracker.oauth.repository.getByOauthId diff --git a/src/main/kotlin/backend/itracker/tracker/member/service/handler/FavoriteComposite.kt b/src/main/kotlin/backend/itracker/tracker/member/service/handler/FavoriteComposite.kt new file mode 100644 index 0000000..9a23dce --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/service/handler/FavoriteComposite.kt @@ -0,0 +1,22 @@ +package backend.itracker.tracker.member.service.handler + +import backend.itracker.crawl.common.ProductCategory +import backend.itracker.tracker.member.domain.Favorite +import backend.itracker.tracker.member.service.handler.response.CommonFavoriteProductModel +import org.springframework.stereotype.Component + +@Component +class FavoriteComposite( + private val favoriteHandlers: List +) { + + fun findAllByIds( + category: ProductCategory, + favorites: List + ) : List { + val favoriteHandler = favoriteHandlers.find { it.supports(category) } + ?: throw IllegalArgumentException("핸들러가 지원하지 않는 카테고리 입니다. category: $category") + + return favoriteHandler.findAllByIds(favorites) + } +} diff --git a/src/main/kotlin/backend/itracker/tracker/member/service/handler/FavoriteHandleable.kt b/src/main/kotlin/backend/itracker/tracker/member/service/handler/FavoriteHandleable.kt new file mode 100644 index 0000000..97108e2 --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/service/handler/FavoriteHandleable.kt @@ -0,0 +1,12 @@ +package backend.itracker.tracker.member.service.handler + +import backend.itracker.crawl.common.ProductCategory +import backend.itracker.tracker.member.domain.Favorite +import backend.itracker.tracker.member.service.handler.response.CommonFavoriteProductModel + +interface FavoriteHandleable { + + fun supports(category: ProductCategory): Boolean + + fun findAllByIds(favorites: List): List +} diff --git a/src/main/kotlin/backend/itracker/tracker/member/service/handler/airpods/FavoriteAirpodsHandler.kt b/src/main/kotlin/backend/itracker/tracker/member/service/handler/airpods/FavoriteAirpodsHandler.kt new file mode 100644 index 0000000..a0f50ea --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/service/handler/airpods/FavoriteAirpodsHandler.kt @@ -0,0 +1,30 @@ +package backend.itracker.tracker.member.service.handler.airpods + +import backend.itracker.crawl.airpods.service.AirPodsService +import backend.itracker.crawl.common.ProductCategory +import backend.itracker.tracker.member.domain.Favorite +import backend.itracker.tracker.member.service.handler.FavoriteHandleable +import backend.itracker.tracker.member.service.handler.response.AirpodsFavoriteResponse +import backend.itracker.tracker.member.service.handler.response.CommonFavoriteProductModel +import org.springframework.stereotype.Component + +@Component +class FavoriteAirpodsHandler( + private val airpodsService: AirPodsService +) : FavoriteHandleable { + override fun supports(category: ProductCategory): Boolean { + return ProductCategory.AIRPODS == category + } + + override fun findAllByIds(favorites: List): List { + val airpods = airpodsService.findAllInIds(favorites.map { it.product.productId } + .toList()) + + return airpods.map { airpod -> + val favorite = favorites.find { it.product.productId == airpod.id } + ?: throw IllegalStateException("찜한 상품을 찾을 수 없습니다.") + + AirpodsFavoriteResponse.of(airpod, favorite) + } + } +} diff --git a/src/main/kotlin/backend/itracker/tracker/member/service/handler/macbook/FavoriteMacbookHandler.kt b/src/main/kotlin/backend/itracker/tracker/member/service/handler/macbook/FavoriteMacbookHandler.kt new file mode 100644 index 0000000..8e5b76d --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/service/handler/macbook/FavoriteMacbookHandler.kt @@ -0,0 +1,30 @@ +package backend.itracker.tracker.member.service.handler.macbook + +import backend.itracker.crawl.common.ProductCategory +import backend.itracker.crawl.macbook.service.MacbookService +import backend.itracker.tracker.member.domain.Favorite +import backend.itracker.tracker.member.service.handler.FavoriteHandleable +import backend.itracker.tracker.member.service.handler.response.CommonFavoriteProductModel +import backend.itracker.tracker.member.service.handler.response.MacbookFavoriteResponse +import org.springframework.stereotype.Component + +@Component +class FavoriteMacbookHandler( + private val macbookService: MacbookService +) : FavoriteHandleable { + override fun supports(category: ProductCategory): Boolean { + return category == ProductCategory.MACBOOK_PRO || category == ProductCategory.MACBOOK_AIR + } + + override fun findAllByIds(favorites: List): List { + val macbooks = macbookService.findAllInIds(favorites.map { it.product.productId } + .toList()) + + return macbooks.map { macbook -> + val favorite = favorites.find { it.product.productId == macbook.id } + ?: throw IllegalStateException("찜한 상품을 찾을 수 없습니다.") + + MacbookFavoriteResponse.of(macbook, favorite) + } + } +} diff --git a/src/main/kotlin/backend/itracker/tracker/member/service/handler/response/AirpodsFavoriteResponse.kt b/src/main/kotlin/backend/itracker/tracker/member/service/handler/response/AirpodsFavoriteResponse.kt new file mode 100644 index 0000000..a720c6c --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/service/handler/response/AirpodsFavoriteResponse.kt @@ -0,0 +1,50 @@ +package backend.itracker.tracker.member.service.handler.response + +import backend.itracker.crawl.airpods.domain.AirPods +import backend.itracker.crawl.airpods.domain.AirPodsCategory +import backend.itracker.tracker.member.domain.Favorite +import java.math.BigDecimal +import java.time.LocalDateTime + +class AirpodsFavoriteResponse( + val id: Long, + val title: String, + val generation: Int, + val canWirelessCharging: Boolean, + val chargingType: String, + val category: String, + val color: String, + val label: Boolean, + val imageUrl: String, + val discountPercentage: Int, + val currentPrice: BigDecimal, + val isOutOfStock: Boolean, + + createdAt: LocalDateTime +): CommonFavoriteProductModel(createdAt){ + + companion object { + fun of(airPods: AirPods, favorite: Favorite): AirpodsFavoriteResponse { + val name = when (airPods.category) { + AirPodsCategory.AIRPODS -> "에어팟" + AirPodsCategory.AIRPODS_PRO -> "에어팟 프로" + AirPodsCategory.AIRPODS_MAX -> "에어팟 맥스" + } + return AirpodsFavoriteResponse( + id = airPods.id, + title = "${airPods.company} ${airPods.releaseYear} $name ${airPods.generation}세대", + generation = airPods.generation, + canWirelessCharging = airPods.canWirelessCharging, + chargingType = airPods.chargingType, + category = airPods.category.name.lowercase(), + color = airPods.color, + label = airPods.isAllTimeLowPrice(), + imageUrl = airPods.thumbnail, + discountPercentage = airPods.findDiscountPercentage(), + currentPrice = airPods.findCurrentPrice(), + isOutOfStock = airPods.isOutOfStock(), + createdAt = favorite.createdAt + ) + } + } +} diff --git a/src/main/kotlin/backend/itracker/tracker/member/service/handler/response/CommonFavoriteProductModel.kt b/src/main/kotlin/backend/itracker/tracker/member/service/handler/response/CommonFavoriteProductModel.kt new file mode 100644 index 0000000..25560ab --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/service/handler/response/CommonFavoriteProductModel.kt @@ -0,0 +1,9 @@ +package backend.itracker.tracker.member.service.handler.response + +import com.fasterxml.jackson.annotation.JsonIgnore +import java.time.LocalDateTime + +abstract class CommonFavoriteProductModel( + @JsonIgnore + val createdAt: LocalDateTime +) diff --git a/src/main/kotlin/backend/itracker/tracker/member/service/handler/response/MacbookFavoriteResponse.kt b/src/main/kotlin/backend/itracker/tracker/member/service/handler/response/MacbookFavoriteResponse.kt new file mode 100644 index 0000000..08f593a --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/service/handler/response/MacbookFavoriteResponse.kt @@ -0,0 +1,56 @@ +package backend.itracker.tracker.member.service.handler.response + +import backend.itracker.crawl.macbook.domain.Macbook +import backend.itracker.crawl.macbook.domain.MacbookCategory +import backend.itracker.tracker.member.domain.Favorite +import java.math.BigDecimal +import java.time.LocalDateTime + +class MacbookFavoriteResponse( + val id: Long, + val title: String, + val category: String, + val size: Int, + val discountPercentage: Int, + val currentPrice: BigDecimal, + val chip: String, + val cpu: String, + val gpu: String, + val storage: String, + val memory: String, + val color: String, + val label: Boolean, + val imageUrl: String, + val isOutOfStock: Boolean, + + createdAt: LocalDateTime +) : CommonFavoriteProductModel(createdAt) { + + companion object { + fun of(macbook: Macbook, favorite: Favorite): MacbookFavoriteResponse { + val koreanCategory = when (macbook.category) { + MacbookCategory.MACBOOK_AIR -> "맥북 에어" + MacbookCategory.MACBOOK_PRO -> "맥북 프로" + } + + return MacbookFavoriteResponse( + 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().setScale(0), + label = macbook.isAllTimeLowPrice(), + imageUrl = macbook.thumbnail, + isOutOfStock = macbook.isOutOfStock(), + createdAt = favorite.createdAt + ) + } + } +} diff --git a/src/main/kotlin/backend/itracker/tracker/member/service/vo/FavoriteInfo.kt b/src/main/kotlin/backend/itracker/tracker/member/service/vo/FavoriteInfo.kt new file mode 100644 index 0000000..ad201bc --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/member/service/vo/FavoriteInfo.kt @@ -0,0 +1,8 @@ +package backend.itracker.tracker.member.service.vo + +import backend.itracker.crawl.common.ProductCategory + +data class FavoriteInfo( + val productId: Long, + val category: ProductCategory +) diff --git a/src/main/kotlin/backend/itracker/tracker/oauth/client/OauthMemberClient.kt b/src/main/kotlin/backend/itracker/tracker/oauth/client/OauthMemberClient.kt index 0c20785..3531e6f 100644 --- a/src/main/kotlin/backend/itracker/tracker/oauth/client/OauthMemberClient.kt +++ b/src/main/kotlin/backend/itracker/tracker/oauth/client/OauthMemberClient.kt @@ -1,6 +1,6 @@ package backend.itracker.tracker.oauth.client -import backend.itracker.tracker.oauth.Member +import backend.itracker.tracker.member.domain.Member import backend.itracker.tracker.oauth.OauthServerType import backend.itracker.tracker.oauth.RedirectType diff --git a/src/main/kotlin/backend/itracker/tracker/oauth/repository/MemberRepository.kt b/src/main/kotlin/backend/itracker/tracker/oauth/repository/MemberRepository.kt index 5ace89b..76eb2ed 100644 --- a/src/main/kotlin/backend/itracker/tracker/oauth/repository/MemberRepository.kt +++ b/src/main/kotlin/backend/itracker/tracker/oauth/repository/MemberRepository.kt @@ -1,6 +1,6 @@ package backend.itracker.tracker.oauth.repository -import backend.itracker.tracker.oauth.Member +import backend.itracker.tracker.member.domain.Member import backend.itracker.tracker.oauth.OauthId import org.springframework.data.jpa.repository.JpaRepository import java.util.* diff --git a/src/main/kotlin/backend/itracker/tracker/oauth/service/OauthService.kt b/src/main/kotlin/backend/itracker/tracker/oauth/service/OauthService.kt index 633a07f..5fec254 100644 --- a/src/main/kotlin/backend/itracker/tracker/oauth/service/OauthService.kt +++ b/src/main/kotlin/backend/itracker/tracker/oauth/service/OauthService.kt @@ -3,8 +3,8 @@ package backend.itracker.tracker.oauth.service import backend.itracker.tracker.infra.oauth.AuthorizationHeader import backend.itracker.tracker.infra.oauth.jwt.JwtDecoder import backend.itracker.tracker.infra.oauth.jwt.JwtEncoder +import backend.itracker.tracker.member.domain.Member import backend.itracker.tracker.member.service.MemberService -import backend.itracker.tracker.oauth.Member import backend.itracker.tracker.oauth.OauthId import backend.itracker.tracker.oauth.OauthServerType import backend.itracker.tracker.oauth.RedirectType diff --git a/src/main/kotlin/backend/itracker/tracker/product/controller/ProductController.kt b/src/main/kotlin/backend/itracker/tracker/product/controller/ProductController.kt index 584ba28..23094b4 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/controller/ProductController.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/controller/ProductController.kt @@ -4,6 +4,7 @@ import backend.itracker.crawl.common.ProductCategory import backend.itracker.tracker.common.request.PageParams import backend.itracker.tracker.common.response.Pages import backend.itracker.tracker.common.response.SingleData +import backend.itracker.tracker.member.domain.Member import backend.itracker.tracker.product.handler.ProductHandlerImpl import backend.itracker.tracker.product.response.CategoryResponses import backend.itracker.tracker.product.response.filter.CommonFilterModel @@ -11,6 +12,8 @@ import backend.itracker.tracker.product.response.product.CommonProductDetailMode import backend.itracker.tracker.product.response.product.CommonProductModel import backend.itracker.tracker.product.vo.Limit import backend.itracker.tracker.product.vo.ProductFilter +import backend.itracker.tracker.product.vo.ProductInfo +import backend.itracker.tracker.resolver.AnonymousMember import org.springframework.data.domain.PageRequest import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping @@ -67,10 +70,11 @@ class ProductController( @GetMapping("/api/v1/products/{category}/{productId}") fun findFilterdProductDetail( + @AnonymousMember member: Member, @PathVariable category: ProductCategory, @PathVariable productId: Long, ): ResponseEntity { - val product = productHandler.findProductById(category, productId) + val product = productHandler.findProductById(ProductInfo(category, productId), member) return ResponseEntity.ok(product) } diff --git a/src/main/kotlin/backend/itracker/tracker/product/handler/AirPodsHandler.kt b/src/main/kotlin/backend/itracker/tracker/product/handler/AirPodsHandler.kt index bdab2c7..d1a2333 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/handler/AirPodsHandler.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/handler/AirPodsHandler.kt @@ -2,12 +2,16 @@ package backend.itracker.tracker.product.handler import backend.itracker.crawl.airpods.service.AirPodsService import backend.itracker.crawl.common.ProductCategory +import backend.itracker.tracker.member.domain.FavoriteProduct +import backend.itracker.tracker.member.domain.Member +import backend.itracker.tracker.member.domain.repository.FavoriteRepository import backend.itracker.tracker.product.response.filter.CommonFilterModel import backend.itracker.tracker.product.response.product.CommonProductDetailModel import backend.itracker.tracker.product.response.product.CommonProductModel import backend.itracker.tracker.product.response.product.airpods.AirPodsDetailResponse import backend.itracker.tracker.product.response.product.airpods.AirPodsResponse import backend.itracker.tracker.product.vo.ProductFilter +import backend.itracker.tracker.product.vo.ProductInfo import jakarta.transaction.NotSupportedException import org.springframework.data.domain.Page import org.springframework.data.domain.PageImpl @@ -17,7 +21,8 @@ import org.springframework.stereotype.Component @Component class AirPodsHandler( - private val airPodsService: AirPodsService + private val airPodsService: AirPodsService, + private val favoriteRepository: FavoriteRepository, ) : ProductHandleable { override fun supports(productCategory: ProductCategory): Boolean { return ProductCategory.AIRPODS == productCategory @@ -56,9 +61,14 @@ class AirPodsHandler( return PageImpl(contents, PageRequest.of(0, airpods.size), airpods.size.toLong()) } - override fun findProductById(productId: Long): CommonProductDetailModel { - val airPods = airPodsService.findByIdAllFetch(productId) + override fun findProductById(productInfo: ProductInfo, member: Member): CommonProductDetailModel { + val airPods = airPodsService.findByIdAllFetch(productInfo.productId) - return AirPodsDetailResponse.from(airPods) + val isFavorite = favoriteRepository.findByFavorite( + member.id, + FavoriteProduct(productInfo.productId, productInfo.productCategory) + ).isPresent + + return AirPodsDetailResponse.of(airPods, isFavorite) } } diff --git a/src/main/kotlin/backend/itracker/tracker/product/handler/MacbookHandler.kt b/src/main/kotlin/backend/itracker/tracker/product/handler/MacbookHandler.kt index f198a18..b7d03f2 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/handler/MacbookHandler.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/handler/MacbookHandler.kt @@ -4,6 +4,9 @@ import backend.itracker.crawl.common.ProductCategory import backend.itracker.crawl.macbook.domain.Macbook import backend.itracker.crawl.macbook.service.MacbookService import backend.itracker.crawl.macbook.service.dto.MacbookFilterCondition +import backend.itracker.tracker.member.domain.FavoriteProduct +import backend.itracker.tracker.member.domain.Member +import backend.itracker.tracker.member.domain.repository.FavoriteRepository import backend.itracker.tracker.product.response.filter.CommonFilterModel import backend.itracker.tracker.product.response.filter.MacbookFilterResponse import backend.itracker.tracker.product.response.product.CommonProductDetailModel @@ -11,6 +14,7 @@ import backend.itracker.tracker.product.response.product.CommonProductModel import backend.itracker.tracker.product.response.product.macbook.MacbookDetailResponse import backend.itracker.tracker.product.response.product.macbook.MacbookResponse import backend.itracker.tracker.product.vo.ProductFilter +import backend.itracker.tracker.product.vo.ProductInfo import org.springframework.data.domain.Page import org.springframework.data.domain.PageImpl import org.springframework.data.domain.Pageable @@ -20,6 +24,7 @@ import kotlin.math.min @Component class MacbookHandler( private val macbookService: MacbookService, + private val favoriteRepository: FavoriteRepository, ) : ProductHandleable { override fun supports(productCategory: ProductCategory): Boolean { @@ -71,9 +76,14 @@ class MacbookHandler( .slice(startElementNumber until lastElementNumber) } - override fun findProductById(productId: Long): CommonProductDetailModel { - val macbook = macbookService.findMacbookById(productId) + override fun findProductById(productInfo: ProductInfo, member: Member): CommonProductDetailModel { + val macbook = macbookService.findMacbookById(productInfo.productId) - return MacbookDetailResponse.from(macbook) + val isFavorite = favoriteRepository.findByFavorite( + member.id, + FavoriteProduct(productInfo.productId, productInfo.productCategory) + ).isPresent + + return MacbookDetailResponse.of(macbook, isFavorite) } } diff --git a/src/main/kotlin/backend/itracker/tracker/product/handler/ProductHandleable.kt b/src/main/kotlin/backend/itracker/tracker/product/handler/ProductHandleable.kt index 9240229..1168097 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/handler/ProductHandleable.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/handler/ProductHandleable.kt @@ -1,10 +1,12 @@ package backend.itracker.tracker.product.handler import backend.itracker.crawl.common.ProductCategory +import backend.itracker.tracker.member.domain.Member import backend.itracker.tracker.product.response.filter.CommonFilterModel import backend.itracker.tracker.product.response.product.CommonProductDetailModel import backend.itracker.tracker.product.response.product.CommonProductModel import backend.itracker.tracker.product.vo.ProductFilter +import backend.itracker.tracker.product.vo.ProductInfo import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable @@ -32,5 +34,5 @@ interface ProductHandleable { pageable: Pageable, ): Page - fun findProductById(productId: Long): CommonProductDetailModel + fun findProductById(productInfo: ProductInfo, anonymousMember: Member): CommonProductDetailModel } diff --git a/src/main/kotlin/backend/itracker/tracker/product/handler/ProductHandlerImpl.kt b/src/main/kotlin/backend/itracker/tracker/product/handler/ProductHandlerImpl.kt index 63678b4..9a81018 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/handler/ProductHandlerImpl.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/handler/ProductHandlerImpl.kt @@ -1,11 +1,13 @@ package backend.itracker.tracker.product.handler import backend.itracker.crawl.common.ProductCategory +import backend.itracker.tracker.member.domain.Member import backend.itracker.tracker.product.response.filter.CommonFilterModel import backend.itracker.tracker.product.response.product.CommonProductDetailModel import backend.itracker.tracker.product.response.product.CommonProductModel import backend.itracker.tracker.product.vo.Limit import backend.itracker.tracker.product.vo.ProductFilter +import backend.itracker.tracker.product.vo.ProductInfo import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Component @@ -47,11 +49,12 @@ class ProductHandlerImpl( } fun findProductById( - category: ProductCategory, productId: Long + productInfo: ProductInfo, + anonymousMember: Member ): CommonProductDetailModel { - val productHandler = productHandlers.find { it.supports(category) } - ?: throw IllegalArgumentException("핸들러가 지원하지 않는 카테고리 입니다. category: $category") + val productHandler = productHandlers.find { it.supports(productInfo.productCategory) } + ?: throw IllegalArgumentException("핸들러가 지원하지 않는 카테고리 입니다. category: ${productInfo.productCategory}") - return productHandler.findProductById(productId) + return productHandler.findProductById(productInfo, anonymousMember) } } diff --git a/src/main/kotlin/backend/itracker/tracker/product/response/product/CommonProductModel.kt b/src/main/kotlin/backend/itracker/tracker/product/response/product/CommonProductModel.kt index 11c985b..bb5345e 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/response/product/CommonProductModel.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/response/product/CommonProductModel.kt @@ -12,6 +12,7 @@ interface CommonProductModel { abstract class CommonProductDetailModel( + val isFavorite: Boolean, val discountPercentage: Int, val currentPrice: BigDecimal, val allTimeHighPrice: BigDecimal, diff --git a/src/main/kotlin/backend/itracker/tracker/product/response/product/airpods/AirPodsDetailResponse.kt b/src/main/kotlin/backend/itracker/tracker/product/response/product/airpods/AirPodsDetailResponse.kt index 58da8f2..ca8e84c 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/response/product/airpods/AirPodsDetailResponse.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/response/product/airpods/AirPodsDetailResponse.kt @@ -19,6 +19,7 @@ class AirPodsDetailResponse( val coupangUrl: String, val isOutOfStock: Boolean, + isFavorite: Boolean, discountPercentage: Int, currentPrice: BigDecimal, allTimeHighPrice: BigDecimal, @@ -26,6 +27,7 @@ class AirPodsDetailResponse( averagePrice: BigDecimal, priceInfos: List, ) : CommonProductDetailModel( + isFavorite, discountPercentage, currentPrice, allTimeHighPrice, @@ -34,7 +36,10 @@ class AirPodsDetailResponse( priceInfos ) { companion object { - fun from(airPods: AirPods): CommonProductDetailModel { + fun of( + airPods: AirPods, + isFavorite: Boolean = false + ): CommonProductDetailModel { val name = when (airPods.category) { AirPodsCategory.AIRPODS -> "에어팟" AirPodsCategory.AIRPODS_PRO -> "에어팟 프로" @@ -51,6 +56,7 @@ class AirPodsDetailResponse( label = airPods.isAllTimeLowPrice(), imageUrl = airPods.thumbnail, coupangUrl = airPods.partnersLink.ifBlank { airPods.productLink }, + isFavorite = isFavorite, discountPercentage = airPods.findDiscountPercentage(), currentPrice = airPods.findCurrentPrice(), isOutOfStock = airPods.isOutOfStock(), diff --git a/src/main/kotlin/backend/itracker/tracker/product/response/product/macbook/MacbookDetailResponse.kt b/src/main/kotlin/backend/itracker/tracker/product/response/product/macbook/MacbookDetailResponse.kt index f2119c9..5ef85cd 100644 --- a/src/main/kotlin/backend/itracker/tracker/product/response/product/macbook/MacbookDetailResponse.kt +++ b/src/main/kotlin/backend/itracker/tracker/product/response/product/macbook/MacbookDetailResponse.kt @@ -22,6 +22,7 @@ class MacbookDetailResponse( val coupangUrl: String, val isOutOfStock: Boolean, + isFavorite: Boolean, discountPercentage: Int, currentPrice: BigDecimal, allTimeHighPrice: BigDecimal, @@ -29,6 +30,7 @@ class MacbookDetailResponse( averagePrice: BigDecimal, priceInfos: List, ) : CommonProductDetailModel( + isFavorite, discountPercentage, currentPrice, allTimeHighPrice, @@ -38,7 +40,7 @@ class MacbookDetailResponse( ) { companion object { - fun from(macbook: Macbook): MacbookDetailResponse { + fun of(macbook: Macbook, isFavorite: Boolean = false): MacbookDetailResponse { val koreanCategory = when (macbook.category) { MacbookCategory.MACBOOK_AIR -> "맥북 에어" MacbookCategory.MACBOOK_PRO -> "맥북 프로" @@ -63,6 +65,7 @@ class MacbookDetailResponse( label = macbook.isAllTimeLowPrice(), imageUrl = macbook.thumbnail, coupangUrl = macbook.partnersLink.ifBlank { macbook.productLink }, + isFavorite = isFavorite, isOutOfStock = macbook.isOutOfStock(), priceInfos = macbook.getRecentPricesByPeriod(SIX_MONTH).macbookPrices .map { CommonPriceInfo.of(it.createdAt, it.currentPrice) } diff --git a/src/main/kotlin/backend/itracker/tracker/product/vo/ProductInfo.kt b/src/main/kotlin/backend/itracker/tracker/product/vo/ProductInfo.kt new file mode 100644 index 0000000..759897f --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/product/vo/ProductInfo.kt @@ -0,0 +1,8 @@ +package backend.itracker.tracker.product.vo + +import backend.itracker.crawl.common.ProductCategory + +data class ProductInfo( + val productCategory: ProductCategory, + val productId: Long, +) diff --git a/src/main/kotlin/backend/itracker/tracker/resolver/AnonymousMemberArgumentResolver.kt b/src/main/kotlin/backend/itracker/tracker/resolver/AnonymousMemberArgumentResolver.kt new file mode 100644 index 0000000..536bc6c --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/resolver/AnonymousMemberArgumentResolver.kt @@ -0,0 +1,33 @@ +package backend.itracker.tracker.resolver + +import backend.itracker.tracker.infra.oauth.AuthorizationHeader +import backend.itracker.tracker.member.domain.Member +import backend.itracker.tracker.oauth.service.OauthService +import org.springframework.core.MethodParameter +import org.springframework.http.HttpHeaders.AUTHORIZATION +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer + +@Component +class AnonymousMemberArgumentResolver( + private val oauthService: OauthService +) : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(AnonymousMember::class.java) + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory? + ): Member { + val authorization = webRequest.getHeader(AUTHORIZATION) ?: return Member.anonymous() + + return oauthService.findByOauthId(AuthorizationHeader(authorization)) + } +} diff --git a/src/main/kotlin/backend/itracker/tracker/resolver/LoginMember.kt b/src/main/kotlin/backend/itracker/tracker/resolver/LoginMember.kt index e0be994..0588228 100644 --- a/src/main/kotlin/backend/itracker/tracker/resolver/LoginMember.kt +++ b/src/main/kotlin/backend/itracker/tracker/resolver/LoginMember.kt @@ -3,3 +3,7 @@ package backend.itracker.tracker.resolver @Target(AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) annotation class LoginMember + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class AnonymousMember diff --git a/src/main/kotlin/backend/itracker/tracker/resolver/LoginMemberArgumentResolver.kt b/src/main/kotlin/backend/itracker/tracker/resolver/LoginMemberArgumentResolver.kt new file mode 100644 index 0000000..35566b1 --- /dev/null +++ b/src/main/kotlin/backend/itracker/tracker/resolver/LoginMemberArgumentResolver.kt @@ -0,0 +1,34 @@ +package backend.itracker.tracker.resolver + +import backend.itracker.tracker.infra.oauth.AuthorizationHeader +import backend.itracker.tracker.infra.oauth.exception.OauthRequestException +import backend.itracker.tracker.member.domain.Member +import backend.itracker.tracker.oauth.service.OauthService +import org.springframework.core.MethodParameter +import org.springframework.http.HttpHeaders.AUTHORIZATION +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer + +@Component +class LoginMemberArgumentResolver( + private val oauthService: OauthService +) : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(LoginMember::class.java) + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory? + ): Member { + val authorization = webRequest.getHeader(AUTHORIZATION) ?: throw OauthRequestException("Authorization Header가 없습니다.") + + return oauthService.findByOauthId(AuthorizationHeader(authorization)) + } +} diff --git a/src/main/kotlin/backend/itracker/tracker/resolver/OauthArgumentResolver.kt b/src/main/kotlin/backend/itracker/tracker/resolver/OauthArgumentResolver.kt index dfc3323..ffb0ca5 100644 --- a/src/main/kotlin/backend/itracker/tracker/resolver/OauthArgumentResolver.kt +++ b/src/main/kotlin/backend/itracker/tracker/resolver/OauthArgumentResolver.kt @@ -2,7 +2,7 @@ package backend.itracker.tracker.resolver import backend.itracker.tracker.infra.oauth.AuthorizationHeader import backend.itracker.tracker.infra.oauth.exception.OauthRequestException -import backend.itracker.tracker.oauth.Member +import backend.itracker.tracker.member.domain.Member import backend.itracker.tracker.oauth.service.OauthService import org.springframework.core.MethodParameter import org.springframework.http.HttpHeaders.AUTHORIZATION diff --git a/src/test/kotlin/backend/itracker/tracker/product/assured/ProductMacbookAssuredTest.kt b/src/test/kotlin/backend/itracker/tracker/product/assured/ProductMacbookAssuredTest.kt index b3e4189..45be93e 100644 --- a/src/test/kotlin/backend/itracker/tracker/product/assured/ProductMacbookAssuredTest.kt +++ b/src/test/kotlin/backend/itracker/tracker/product/assured/ProductMacbookAssuredTest.kt @@ -131,7 +131,7 @@ class ProductMacbookAssuredTest : AssuredTestConfig() { .`as`(object : TypeRef() {}) // then - assertThat(response).isEqualTo(MacbookDetailResponse.from(expected)) + assertThat(response).isEqualTo(MacbookDetailResponse.of(expected)) } @ParameterizedTest