diff --git a/src/main/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityApi.kt b/src/main/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityApi.kt index 061eac03..1631633a 100644 --- a/src/main/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityApi.kt +++ b/src/main/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityApi.kt @@ -2,8 +2,8 @@ package com.celuveat.celeb.adapter.`in`.rest import com.celuveat.auth.adaptor.`in`.rest.Auth import com.celuveat.auth.adaptor.`in`.rest.AuthContext +import com.celuveat.celeb.adapter.`in`.rest.response.BestCelebrityResponse import com.celuveat.celeb.adapter.`in`.rest.response.CelebrityResponse -import com.celuveat.celeb.adapter.`in`.rest.response.SimpleCelebrityResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.enums.ParameterIn @@ -53,5 +53,7 @@ interface CelebrityApi { @Operation(summary = "인기 셀럽 조회") @GetMapping("/interested") - fun readBestCelebrities(): List + fun readBestCelebrities( + @Auth auth: AuthContext, + ): List } diff --git a/src/main/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityController.kt b/src/main/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityController.kt index 128e2981..5fc8b31d 100644 --- a/src/main/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityController.kt +++ b/src/main/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityController.kt @@ -2,8 +2,8 @@ package com.celuveat.celeb.adapter.`in`.rest import com.celuveat.auth.adaptor.`in`.rest.Auth import com.celuveat.auth.adaptor.`in`.rest.AuthContext +import com.celuveat.celeb.adapter.`in`.rest.response.BestCelebrityResponse import com.celuveat.celeb.adapter.`in`.rest.response.CelebrityResponse -import com.celuveat.celeb.adapter.`in`.rest.response.SimpleCelebrityResponse import com.celuveat.celeb.application.port.`in`.AddInterestedCelebrityUseCase import com.celuveat.celeb.application.port.`in`.DeleteInterestedCelebrityUseCase import com.celuveat.celeb.application.port.`in`.ReadBestCelebritiesUseCase @@ -55,7 +55,11 @@ class CelebrityController( } @GetMapping("/best") - override fun readBestCelebrities(): List { - return readBestCelebritiesUseCase.readBestCelebrities().map { SimpleCelebrityResponse.from(it) } + override fun readBestCelebrities( + @Auth auth: AuthContext, + ): List { + val optionalMemberId = auth.optionalMemberId() + val celebritiesResults = readBestCelebritiesUseCase.readBestCelebrities(optionalMemberId) + return celebritiesResults.map { BestCelebrityResponse.from(it) } } } diff --git a/src/main/kotlin/com/celuveat/celeb/adapter/in/rest/response/CelebrityResponse.kt b/src/main/kotlin/com/celuveat/celeb/adapter/in/rest/response/CelebrityResponse.kt index f444f909..2fc01638 100644 --- a/src/main/kotlin/com/celuveat/celeb/adapter/in/rest/response/CelebrityResponse.kt +++ b/src/main/kotlin/com/celuveat/celeb/adapter/in/rest/response/CelebrityResponse.kt @@ -1,8 +1,10 @@ package com.celuveat.celeb.adapter.`in`.rest.response +import com.celuveat.celeb.application.port.`in`.result.BestCelebrityResult import com.celuveat.celeb.application.port.`in`.result.CelebrityResult import com.celuveat.celeb.application.port.`in`.result.SimpleCelebrityResult import com.celuveat.celeb.application.port.`in`.result.YoutubeContentResult +import com.celuveat.restaurant.adapter.`in`.rest.response.RestaurantPreviewResponse import io.swagger.v3.oas.annotations.media.Schema data class CelebrityResponse( @@ -131,3 +133,23 @@ data class SimpleCelebrityResponse( } } } + +data class BestCelebrityResponse( + @Schema( + description = "연예인 정보", + ) + val celebrity: SimpleCelebrityResponse, + @Schema( + description = "식당 정보", + ) + val restaurants: List, +) { + companion object { + fun from(result: BestCelebrityResult): BestCelebrityResponse { + return BestCelebrityResponse( + celebrity = SimpleCelebrityResponse.from(result.celebrity), + restaurants = result.restaurants.map { RestaurantPreviewResponse.from(it) }, + ) + } + } +} diff --git a/src/main/kotlin/com/celuveat/celeb/adapter/out/persistence/CelebrityPersistenceAdapter.kt b/src/main/kotlin/com/celuveat/celeb/adapter/out/persistence/CelebrityPersistenceAdapter.kt index 81ca29f7..f2e4a110 100644 --- a/src/main/kotlin/com/celuveat/celeb/adapter/out/persistence/CelebrityPersistenceAdapter.kt +++ b/src/main/kotlin/com/celuveat/celeb/adapter/out/persistence/CelebrityPersistenceAdapter.kt @@ -37,7 +37,7 @@ class CelebrityPersistenceAdapter( .mapValues { (_, celebrityYoutubeContents) -> celebrityYoutubeContents.map { it.youtubeContent } } override fun findBestCelebrities(): List { - return celebrityJpaRepository.findAllBySubscriberCountDescTop15().map { + return celebrityJpaRepository.findAllBySubscriberCountDescTop10().map { celebrityPersistenceMapper.toDomainWithoutYoutubeContent(it) } } diff --git a/src/main/kotlin/com/celuveat/celeb/adapter/out/persistence/entity/CelebrityJpaRepository.kt b/src/main/kotlin/com/celuveat/celeb/adapter/out/persistence/entity/CelebrityJpaRepository.kt index ab27dc1d..e336843a 100644 --- a/src/main/kotlin/com/celuveat/celeb/adapter/out/persistence/entity/CelebrityJpaRepository.kt +++ b/src/main/kotlin/com/celuveat/celeb/adapter/out/persistence/entity/CelebrityJpaRepository.kt @@ -16,8 +16,8 @@ interface CelebrityJpaRepository : JpaRepository { FROM CelebrityYoutubeContentJpaEntity cyc JOIN cyc.youtubeContent yc JOIN cyc.celebrity c - ORDER BY yc.subscriberCount DESC LIMIT 15 + ORDER BY yc.subscriberCount DESC LIMIT 10 """, ) - fun findAllBySubscriberCountDescTop15(): Set + fun findAllBySubscriberCountDescTop10(): Set } diff --git a/src/main/kotlin/com/celuveat/celeb/application/CelebrityQueryService.kt b/src/main/kotlin/com/celuveat/celeb/application/CelebrityQueryService.kt index 107e1ccf..0a53237f 100644 --- a/src/main/kotlin/com/celuveat/celeb/application/CelebrityQueryService.kt +++ b/src/main/kotlin/com/celuveat/celeb/application/CelebrityQueryService.kt @@ -1,16 +1,66 @@ package com.celuveat.celeb.application +import com.celuveat.celeb.application.port.`in`.ReadBestCelebritiesUseCase import com.celuveat.celeb.application.port.`in`.ReadInterestedCelebritiesUseCase +import com.celuveat.celeb.application.port.`in`.result.BestCelebrityResult import com.celuveat.celeb.application.port.`in`.result.CelebrityResult +import com.celuveat.celeb.application.port.`in`.result.SimpleCelebrityResult +import com.celuveat.celeb.application.port.out.ReadCelebritiesPort import com.celuveat.celeb.application.port.out.ReadInterestedCelebritiesPort +import com.celuveat.restaurant.application.port.`in`.result.RestaurantPreviewResult +import com.celuveat.restaurant.application.port.out.ReadInterestedRestaurantPort +import com.celuveat.restaurant.application.port.out.ReadRestaurantPort +import com.celuveat.restaurant.domain.InterestedRestaurant +import com.celuveat.restaurant.domain.Restaurant import org.springframework.stereotype.Service @Service class CelebrityQueryService( + private val readCelebritiesPort: ReadCelebritiesPort, + private val readRestaurantPort: ReadRestaurantPort, private val readInterestedCelebritiesPort: ReadInterestedCelebritiesPort, -) : ReadInterestedCelebritiesUseCase { + private val readInterestedRestaurantPort: ReadInterestedRestaurantPort, +) : ReadInterestedCelebritiesUseCase, ReadBestCelebritiesUseCase { override fun getInterestedCelebrities(memberId: Long): List { val celebrities = readInterestedCelebritiesPort.findInterestedCelebrities(memberId) return celebrities.map { CelebrityResult.from(it.celebrity) } } + + override fun readBestCelebrities(memberId: Long?): List { + val bestCelebrities = readCelebritiesPort.findBestCelebrities() + val restaurantsByCelebrity = bestCelebrities.associate { + it.id to readRestaurantPort.findVisitedRestaurantByCelebrity( + celebrityId = it.id, + page = 0, + size = 3 + ).contents + } + val interestedRestaurants = readInterestedRestaurants(memberId, restaurantsByCelebrity) + + return bestCelebrities.map { celebrity -> + BestCelebrityResult( + celebrity = SimpleCelebrityResult.from(celebrity), + restaurants = restaurantsByCelebrity[celebrity.id]!!.map { + RestaurantPreviewResult.of( + restaurant = it, + liked = interestedRestaurants[it.id]?.let { true } ?: false + ) + } + ) + } + } + + private fun readInterestedRestaurants( + memberId: Long?, + restaurantsByCelebrity: Map> + ): Map { + val interestedRestaurants = memberId?.let { + val restaurantIds = restaurantsByCelebrity.values.flatten().map { it.id } + readInterestedRestaurantPort.findInterestedRestaurantsByIds( + memberId = it, + restaurantIds = restaurantIds + ).associateBy { interested -> interested.restaurant.id } + } ?: emptyMap() + return interestedRestaurants + } } diff --git a/src/main/kotlin/com/celuveat/celeb/application/CelebrityService.kt b/src/main/kotlin/com/celuveat/celeb/application/CelebrityService.kt index 6121d853..b4b4faa7 100644 --- a/src/main/kotlin/com/celuveat/celeb/application/CelebrityService.kt +++ b/src/main/kotlin/com/celuveat/celeb/application/CelebrityService.kt @@ -2,12 +2,9 @@ package com.celuveat.celeb.application import com.celuveat.celeb.application.port.`in`.AddInterestedCelebrityUseCase import com.celuveat.celeb.application.port.`in`.DeleteInterestedCelebrityUseCase -import com.celuveat.celeb.application.port.`in`.ReadBestCelebritiesUseCase import com.celuveat.celeb.application.port.`in`.command.AddInterestedCelebrityCommand import com.celuveat.celeb.application.port.`in`.command.DeleteInterestedCelebrityCommand -import com.celuveat.celeb.application.port.`in`.result.SimpleCelebrityResult import com.celuveat.celeb.application.port.out.DeleteInterestedCelebrityPort -import com.celuveat.celeb.application.port.out.ReadCelebritiesPort import com.celuveat.celeb.application.port.out.ReadInterestedCelebritiesPort import com.celuveat.celeb.application.port.out.SaveInterestedCelebrityPort import com.celuveat.celeb.exceptions.AlreadyInterestedCelebrityException @@ -16,11 +13,10 @@ import org.springframework.stereotype.Service @Service class CelebrityService( - private val readCelebritiesPort: ReadCelebritiesPort, private val readInterestedCelebritiesPort: ReadInterestedCelebritiesPort, private val saveInterestedCelebrityPort: SaveInterestedCelebrityPort, private val deleteInterestedCelebrityPort: DeleteInterestedCelebrityPort, -) : ReadBestCelebritiesUseCase, AddInterestedCelebrityUseCase, DeleteInterestedCelebrityUseCase { +) : AddInterestedCelebrityUseCase, DeleteInterestedCelebrityUseCase { override fun addInterestedCelebrity(command: AddInterestedCelebrityCommand) { throwWhen( readInterestedCelebritiesPort.existsInterestedCelebrity(command.celebrityId, command.memberId), @@ -31,9 +27,4 @@ class CelebrityService( override fun deleteInterestedCelebrity(command: DeleteInterestedCelebrityCommand) { deleteInterestedCelebrityPort.deleteInterestedCelebrity(command.celebrityId, command.memberId) } - - override fun readBestCelebrities(): List { - val bestCelebrities = readCelebritiesPort.findBestCelebrities() - return bestCelebrities.map { SimpleCelebrityResult.from(it) } - } } diff --git a/src/main/kotlin/com/celuveat/celeb/application/port/in/ReadBestCelebritiesUseCase.kt b/src/main/kotlin/com/celuveat/celeb/application/port/in/ReadBestCelebritiesUseCase.kt index ea422e47..6413e397 100644 --- a/src/main/kotlin/com/celuveat/celeb/application/port/in/ReadBestCelebritiesUseCase.kt +++ b/src/main/kotlin/com/celuveat/celeb/application/port/in/ReadBestCelebritiesUseCase.kt @@ -1,7 +1,7 @@ package com.celuveat.celeb.application.port.`in` -import com.celuveat.celeb.application.port.`in`.result.SimpleCelebrityResult +import com.celuveat.celeb.application.port.`in`.result.BestCelebrityResult interface ReadBestCelebritiesUseCase { - fun readBestCelebrities(): List + fun readBestCelebrities(memberId: Long?): List } diff --git a/src/main/kotlin/com/celuveat/celeb/application/port/in/result/CelebrityResult.kt b/src/main/kotlin/com/celuveat/celeb/application/port/in/result/CelebrityResult.kt index 9e25ed39..0eec247e 100644 --- a/src/main/kotlin/com/celuveat/celeb/application/port/in/result/CelebrityResult.kt +++ b/src/main/kotlin/com/celuveat/celeb/application/port/in/result/CelebrityResult.kt @@ -1,6 +1,7 @@ package com.celuveat.celeb.application.port.`in`.result import com.celuveat.celeb.domain.Celebrity +import com.celuveat.restaurant.application.port.`in`.result.RestaurantPreviewResult data class CelebrityResult( val id: Long, @@ -37,3 +38,8 @@ data class SimpleCelebrityResult( } } } + +data class BestCelebrityResult( + val celebrity: SimpleCelebrityResult, + val restaurants: List, +) diff --git a/src/test/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityControllerTest.kt b/src/test/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityControllerTest.kt index 77b36521..ee2e1aba 100644 --- a/src/test/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityControllerTest.kt +++ b/src/test/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityControllerTest.kt @@ -1,15 +1,15 @@ package com.celuveat.celeb.adapter.`in`.rest import com.celuveat.auth.application.port.`in`.ExtractMemberIdUseCase -import com.celuveat.celeb.adapter.`in`.rest.response.SimpleCelebrityResponse +import com.celuveat.celeb.adapter.`in`.rest.response.BestCelebrityResponse import com.celuveat.celeb.application.port.`in`.AddInterestedCelebrityUseCase import com.celuveat.celeb.application.port.`in`.DeleteInterestedCelebrityUseCase import com.celuveat.celeb.application.port.`in`.ReadBestCelebritiesUseCase import com.celuveat.celeb.application.port.`in`.ReadInterestedCelebritiesUseCase import com.celuveat.celeb.application.port.`in`.command.AddInterestedCelebrityCommand import com.celuveat.celeb.application.port.`in`.command.DeleteInterestedCelebrityCommand +import com.celuveat.celeb.application.port.`in`.result.BestCelebrityResult import com.celuveat.celeb.application.port.`in`.result.CelebrityResult -import com.celuveat.celeb.application.port.`in`.result.SimpleCelebrityResult import com.celuveat.support.sut import com.fasterxml.jackson.databind.ObjectMapper import com.navercorp.fixturemonkey.kotlin.giveMeBuilder @@ -96,11 +96,11 @@ class CelebrityControllerTest( } context("인기 셀럽을 조회 한다") { - val results = sut.giveMeBuilder() + val results = sut.giveMeBuilder() .sampleList(3) - val response = results.map { SimpleCelebrityResponse.from(it) } + val response = results.map { BestCelebrityResponse.from(it) } test("조회 성공") { - every { readBestCelebritiesUseCase.readBestCelebrities() } returns results + every { readBestCelebritiesUseCase.readBestCelebrities(null) } returns results mockMvc.get("/celebrities/best").andExpect { status { isOk() }