diff --git a/src/main/kotlin/com/celuveat/celeb/adapter/out/persistence/entity/CelebrityRestaurantJpaRepository.kt b/src/main/kotlin/com/celuveat/celeb/adapter/out/persistence/entity/CelebrityRestaurantJpaRepository.kt index 88bb595e..d4872f49 100644 --- a/src/main/kotlin/com/celuveat/celeb/adapter/out/persistence/entity/CelebrityRestaurantJpaRepository.kt +++ b/src/main/kotlin/com/celuveat/celeb/adapter/out/persistence/entity/CelebrityRestaurantJpaRepository.kt @@ -19,6 +19,15 @@ interface CelebrityRestaurantJpaRepository : JpaRepository + @Query( + """ + SELECT COUNT(cr) + FROM CelebrityRestaurantJpaEntity cr + WHERE cr.celebrity.id = :celebrityId + """, + ) + fun countRestaurantsByCelebrityId(celebrityId: Long): Long + @Query( """ SELECT cr.restaurant diff --git a/src/main/kotlin/com/celuveat/common/adapter/out/persistence/JdslConfig.kt b/src/main/kotlin/com/celuveat/common/adapter/out/persistence/JdslConfig.kt new file mode 100644 index 00000000..c75e7064 --- /dev/null +++ b/src/main/kotlin/com/celuveat/common/adapter/out/persistence/JdslConfig.kt @@ -0,0 +1,13 @@ +package com.celuveat.common.adapter.out.persistence + +import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class JdslConfig { + @Bean + fun jpqlRenderer(): JpqlRenderer { + return JpqlRenderer() + } +} diff --git a/src/main/kotlin/com/celuveat/restaurant/adapter/in/rest/RestaurantApi.kt b/src/main/kotlin/com/celuveat/restaurant/adapter/in/rest/RestaurantApi.kt index 18d2f2b1..cc7356d4 100644 --- a/src/main/kotlin/com/celuveat/restaurant/adapter/in/rest/RestaurantApi.kt +++ b/src/main/kotlin/com/celuveat/restaurant/adapter/in/rest/RestaurantApi.kt @@ -29,6 +29,13 @@ interface RestaurantApi { @PageableDefault(size = 10, page = 0) pageable: Pageable, ): SliceResponse + @SecurityRequirement(name = "JWT") + @Operation(summary = "관심 음식점 개수 조회") + @GetMapping("/interested/count") + fun getAmountOfInterestedRestaurants( + @Auth auth: AuthContext, + ): Int + @SecurityRequirement(name = "JWT") @Operation(summary = "관심 음식점 추가") @PostMapping("/interested/{restaurantId}") @@ -71,6 +78,18 @@ interface RestaurantApi { @PageableDefault(size = 10, page = 0) pageable: Pageable, ): SliceResponse + @Operation(summary = "셀럽이 다녀간 음식점 개수 조회") + @GetMapping("/celebrity/{celebrityId}/count") + fun readAmountOfRestaurantsByCelebrity( + @Parameter( + `in` = ParameterIn.PATH, + description = "셀럽 ID", + example = "1", + required = true, + ) + @PathVariable celebrityId: Long, + ): Int + @Operation(summary = "셀럽 추천 음식점 조회") @GetMapping("/celebrity/recommend") fun readCelebrityRecommendRestaurants( @@ -85,6 +104,12 @@ interface RestaurantApi { @PageableDefault(size = 10, page = 0) pageable: Pageable, ): SliceResponse + @Operation(summary = "음식점 개수 조회") + @GetMapping("/count") + fun readAmountOfRestaurants( + @ModelAttribute request: ReadRestaurantsRequest, + ): Int + @Operation(summary = "이번주 업데이트된 음식점 조회") @GetMapping("/weekly") fun readWeeklyUpdatedRestaurants( @@ -92,6 +117,12 @@ interface RestaurantApi { @PageableDefault(size = 10, page = 0) pageable: Pageable, ): SliceResponse + @Operation(summary = "이번주 업데이트된 음식점 개수 조회") + @GetMapping("/weekly/count") + fun readAmountOfWeeklyUpdatedRestaurants( + @Auth auth: AuthContext, + ): Int + @Operation(summary = "주변 음식점 조회") @GetMapping("/nearby/{restaurantId}") fun readNearByRestaurants( diff --git a/src/main/kotlin/com/celuveat/restaurant/adapter/in/rest/RestaurantController.kt b/src/main/kotlin/com/celuveat/restaurant/adapter/in/rest/RestaurantController.kt index e4341a4c..abc5bf24 100644 --- a/src/main/kotlin/com/celuveat/restaurant/adapter/in/rest/RestaurantController.kt +++ b/src/main/kotlin/com/celuveat/restaurant/adapter/in/rest/RestaurantController.kt @@ -3,11 +3,16 @@ package com.celuveat.restaurant.adapter.`in`.rest import com.celuveat.auth.adapter.`in`.rest.Auth import com.celuveat.auth.adapter.`in`.rest.AuthContext import com.celuveat.common.adapter.`in`.rest.response.SliceResponse +import com.celuveat.common.utils.geometry.SquarePolygon import com.celuveat.restaurant.adapter.`in`.rest.request.ReadRestaurantsRequest import com.celuveat.restaurant.adapter.`in`.rest.response.RestaurantDetailResponse import com.celuveat.restaurant.adapter.`in`.rest.response.RestaurantPreviewResponse import com.celuveat.restaurant.application.port.`in`.AddInterestedRestaurantsUseCase import com.celuveat.restaurant.application.port.`in`.DeleteInterestedRestaurantsUseCase +import com.celuveat.restaurant.application.port.`in`.ReadAmountOfInterestedRestaurantUseCase +import com.celuveat.restaurant.application.port.`in`.ReadAmountOfRestaurantByCelebrityUseCase +import com.celuveat.restaurant.application.port.`in`.ReadAmountOfRestaurantsUseCase +import com.celuveat.restaurant.application.port.`in`.ReadAmountOfWeeklyUpdateRestaurantsUseCase import com.celuveat.restaurant.application.port.`in`.ReadCelebrityRecommendRestaurantsUseCase import com.celuveat.restaurant.application.port.`in`.ReadCelebrityVisitedRestaurantUseCase import com.celuveat.restaurant.application.port.`in`.ReadInterestedRestaurantsUseCase @@ -18,6 +23,7 @@ import com.celuveat.restaurant.application.port.`in`.ReadRestaurantsUseCase import com.celuveat.restaurant.application.port.`in`.ReadWeeklyUpdateRestaurantsUseCase import com.celuveat.restaurant.application.port.`in`.command.AddInterestedRestaurantCommand import com.celuveat.restaurant.application.port.`in`.command.DeleteInterestedRestaurantCommand +import com.celuveat.restaurant.application.port.`in`.query.CountRestaurantsQuery import com.celuveat.restaurant.application.port.`in`.query.ReadCelebrityRecommendRestaurantsQuery import com.celuveat.restaurant.application.port.`in`.query.ReadCelebrityVisitedRestaurantQuery import com.celuveat.restaurant.application.port.`in`.query.ReadInterestedRestaurantsQuery @@ -39,15 +45,19 @@ import org.springframework.web.bind.annotation.RestController @RestController class RestaurantController( private val readInterestedRestaurantsUseCase: ReadInterestedRestaurantsUseCase, + private val readAmountOfInterestedRestaurantUseCase: ReadAmountOfInterestedRestaurantUseCase, private val addInterestedRestaurantsUseCase: AddInterestedRestaurantsUseCase, private val deleteInterestedRestaurantsUseCase: DeleteInterestedRestaurantsUseCase, private val readCelebrityVisitedRestaurantUseCase: ReadCelebrityVisitedRestaurantUseCase, private val readCelebrityRecommendRestaurantsUseCase: ReadCelebrityRecommendRestaurantsUseCase, private val readRestaurantsUseCase: ReadRestaurantsUseCase, + private val readAmountOfRestaurantsUseCase: ReadAmountOfRestaurantsUseCase, private val readWeeklyUpdateRestaurantsUseCase: ReadWeeklyUpdateRestaurantsUseCase, + private val readAmountOfWeeklyUpdateRestaurantsUseCase: ReadAmountOfWeeklyUpdateRestaurantsUseCase, private val readNearbyRestaurantsUseCase: ReadNearbyRestaurantsUseCase, private val readRestaurantDetailUseCase: ReadRestaurantDetailUseCase, private val readPopularRestaurantsUseCase: ReadPopularRestaurantsUseCase, + private val readAmountOfRestaurantByCelebrityUseCase: ReadAmountOfRestaurantByCelebrityUseCase, ) : RestaurantApi { @GetMapping("/interested") override fun getInterestedRestaurants( @@ -67,6 +77,14 @@ class RestaurantController( ) } + @GetMapping("/interested/count") + override fun getAmountOfInterestedRestaurants( + @Auth auth: AuthContext, + ): Int { + val memberId = auth.memberId() + return readAmountOfInterestedRestaurantUseCase.readAmountOfInterestedRestaurant(memberId) + } + @PostMapping("/interested/{restaurantId}") override fun addInterestedRestaurant( @Auth auth: AuthContext, @@ -113,6 +131,13 @@ class RestaurantController( ) } + @GetMapping("/celebrity/{celebrityId}/count") + override fun readAmountOfRestaurantsByCelebrity( + @PathVariable celebrityId: Long, + ): Int { + return readAmountOfRestaurantByCelebrityUseCase.readAmountOfRestaurantByCelebrity(celebrityId) + } + @GetMapping("/celebrity/recommend") override fun readCelebrityRecommendRestaurants( @Auth auth: AuthContext, @@ -141,6 +166,23 @@ class RestaurantController( ) } + @GetMapping("/count") + override fun readAmountOfRestaurants( + @ModelAttribute request: ReadRestaurantsRequest, + ): Int { + val query = CountRestaurantsQuery( + category = request.category, + region = request.region, + searchArea = SquarePolygon.ofNullable( + lowLongitude = request.lowLongitude, + highLongitude = request.highLongitude, + lowLatitude = request.lowLatitude, + highLatitude = request.highLatitude, + ), + ) + return readAmountOfRestaurantsUseCase.readAmountOfRestaurants(query) + } + @GetMapping("/weekly") override fun readWeeklyUpdatedRestaurants( @Auth auth: AuthContext, @@ -159,6 +201,10 @@ class RestaurantController( ) } + override fun readAmountOfWeeklyUpdatedRestaurants(auth: AuthContext): Int { + return readAmountOfWeeklyUpdateRestaurantsUseCase.readAmountOfWeeklyUpdateRestaurants() + } + @GetMapping("/nearby/{restaurantId}") override fun readNearByRestaurants( @Auth auth: AuthContext, diff --git a/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/RestaurantPersistenceAdapter.kt b/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/RestaurantPersistenceAdapter.kt index d19a7767..7064f3fb 100644 --- a/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/RestaurantPersistenceAdapter.kt +++ b/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/RestaurantPersistenceAdapter.kt @@ -45,6 +45,10 @@ class RestaurantPersistenceAdapter( ) } + override fun countRestaurantByCelebrity(celebrityId: Long): Int { + return celebrityRestaurantJpaRepository.countRestaurantsByCelebrityId(celebrityId).toInt() + } + override fun readById(id: Long): Restaurant { val restaurant = restaurantJpaRepository.getById(id) val images = restaurantImageJpaRepository.findByRestaurant(restaurant) @@ -88,6 +92,14 @@ class RestaurantPersistenceAdapter( ) } + override fun countRestaurantsByCondition( + category: String?, + region: String?, + searchArea: SquarePolygon?, + ): Int { + return restaurantJpaRepository.countAllByFilter(RestaurantFilter(category, region, searchArea)).toInt() + } + override fun readByCreatedAtBetween( startOfWeek: LocalDate, endOfWeek: LocalDate, @@ -114,6 +126,16 @@ class RestaurantPersistenceAdapter( ) } + override fun countByCreatedAtBetween( + startOfWeek: LocalDate, + endOfWeek: LocalDate, + ): Int { + return restaurantJpaRepository.countByCreatedAtBetween( + startOfWeek.atStartOfDay(), + endOfWeek.atTime(LocalTime.MAX), + ).toInt() + } + override fun readNearby(id: Long): List { val centralRestaurant = restaurantJpaRepository.getById(id) val restaurants = restaurantJpaRepository.findTop5NearestInDistance( diff --git a/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/entity/CustomRestaurantRepository.kt b/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/entity/CustomRestaurantRepository.kt index d23ee131..02d428b4 100644 --- a/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/entity/CustomRestaurantRepository.kt +++ b/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/entity/CustomRestaurantRepository.kt @@ -9,6 +9,8 @@ interface CustomRestaurantRepository { filter: RestaurantFilter, pageable: Pageable, ): Slice + + fun countAllByFilter(filter: RestaurantFilter): Long } data class RestaurantFilter( diff --git a/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/entity/CustomRestaurantRepositoryImpl.kt b/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/entity/CustomRestaurantRepositoryImpl.kt index 5cee6fb0..9d77a4d7 100644 --- a/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/entity/CustomRestaurantRepositoryImpl.kt +++ b/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/entity/CustomRestaurantRepositoryImpl.kt @@ -1,6 +1,10 @@ package com.celuveat.restaurant.adapter.out.persistence.entity +import com.linecorp.kotlinjdsl.dsl.jpql.jpql +import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderContext +import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderer import com.linecorp.kotlinjdsl.support.spring.data.jpa.repository.KotlinJdslJpqlExecutor +import jakarta.persistence.EntityManager import org.springframework.data.domain.Pageable import org.springframework.data.domain.Slice import org.springframework.data.domain.SliceImpl @@ -9,6 +13,9 @@ import org.springframework.stereotype.Repository @Repository class CustomRestaurantRepositoryImpl( private val executor: KotlinJdslJpqlExecutor, + private val entityManager: EntityManager, + private val context: JpqlRenderContext, + private val renderer: JpqlRenderer, ) : CustomRestaurantRepository { override fun findAllByFilter( filter: RestaurantFilter, @@ -25,7 +32,7 @@ class CustomRestaurantRepositoryImpl( filter.searchArea?.let { path(RestaurantJpaEntity::longitude).between( it.lowLongitude, - it.highLongitude + it.highLongitude, ) }, filter.searchArea?.let { path(RestaurantJpaEntity::latitude).between(it.lowLatitude, it.highLatitude) }, @@ -38,4 +45,29 @@ class CustomRestaurantRepositoryImpl( findSlice.hasNext(), ) } + + override fun countAllByFilter(filter: RestaurantFilter): Long { + val query = jpql { + select( + count(entity(RestaurantJpaEntity::class)), + ).from( + entity(RestaurantJpaEntity::class), + ).whereAnd( + filter.category?.let { path(RestaurantJpaEntity::category).eq(it) }, + filter.region?.let { path(RestaurantJpaEntity::roadAddress).like("%$it%") }, + filter.searchArea?.let { + path(RestaurantJpaEntity::longitude).between( + it.lowLongitude, + it.highLongitude, + ) + }, + filter.searchArea?.let { path(RestaurantJpaEntity::latitude).between(it.lowLatitude, it.highLatitude) }, + ) + } + + val rendered = renderer.render(query, context) + return entityManager.createQuery(rendered.query, Long::class.java) + .apply { rendered.params.forEach { (name, value) -> setParameter(name, value) } } + .singleResult + } } diff --git a/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/entity/RestaurantJpaRepository.kt b/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/entity/RestaurantJpaRepository.kt index 81d5f52a..5c4b7b09 100644 --- a/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/entity/RestaurantJpaRepository.kt +++ b/src/main/kotlin/com/celuveat/restaurant/adapter/out/persistence/entity/RestaurantJpaRepository.kt @@ -20,6 +20,11 @@ interface RestaurantJpaRepository : JpaRepository, Cu pageable: Pageable, ): Slice + fun countByCreatedAtBetween( + startOfWeek: LocalDateTime, + endOfWeek: LocalDateTime, + ): Long + @Query( """ SELECT r.* diff --git a/src/main/kotlin/com/celuveat/restaurant/application/RestaurantQueryService.kt b/src/main/kotlin/com/celuveat/restaurant/application/RestaurantQueryService.kt index 6434e1aa..618e9071 100644 --- a/src/main/kotlin/com/celuveat/restaurant/application/RestaurantQueryService.kt +++ b/src/main/kotlin/com/celuveat/restaurant/application/RestaurantQueryService.kt @@ -2,6 +2,10 @@ package com.celuveat.restaurant.application import com.celuveat.celeb.application.port.out.ReadCelebritiesPort import com.celuveat.common.application.port.`in`.result.SliceResult +import com.celuveat.restaurant.application.port.`in`.ReadAmountOfInterestedRestaurantUseCase +import com.celuveat.restaurant.application.port.`in`.ReadAmountOfRestaurantByCelebrityUseCase +import com.celuveat.restaurant.application.port.`in`.ReadAmountOfRestaurantsUseCase +import com.celuveat.restaurant.application.port.`in`.ReadAmountOfWeeklyUpdateRestaurantsUseCase import com.celuveat.restaurant.application.port.`in`.ReadCelebrityRecommendRestaurantsUseCase import com.celuveat.restaurant.application.port.`in`.ReadCelebrityVisitedRestaurantUseCase import com.celuveat.restaurant.application.port.`in`.ReadInterestedRestaurantsUseCase @@ -10,6 +14,7 @@ import com.celuveat.restaurant.application.port.`in`.ReadPopularRestaurantsUseCa import com.celuveat.restaurant.application.port.`in`.ReadRestaurantDetailUseCase import com.celuveat.restaurant.application.port.`in`.ReadRestaurantsUseCase import com.celuveat.restaurant.application.port.`in`.ReadWeeklyUpdateRestaurantsUseCase +import com.celuveat.restaurant.application.port.`in`.query.CountRestaurantsQuery import com.celuveat.restaurant.application.port.`in`.query.ReadCelebrityRecommendRestaurantsQuery import com.celuveat.restaurant.application.port.`in`.query.ReadCelebrityVisitedRestaurantQuery import com.celuveat.restaurant.application.port.`in`.query.ReadInterestedRestaurantsQuery @@ -24,6 +29,7 @@ import com.celuveat.restaurant.application.port.out.ReadInterestedRestaurantPort import com.celuveat.restaurant.application.port.out.ReadRestaurantPort import org.springframework.stereotype.Service import java.time.DayOfWeek +import java.time.LocalDate import java.time.temporal.TemporalAdjusters @Service @@ -32,13 +38,17 @@ class RestaurantQueryService( private val readCelebritiesPort: ReadCelebritiesPort, private val readInterestedRestaurantPort: ReadInterestedRestaurantPort, ) : ReadInterestedRestaurantsUseCase, + ReadAmountOfInterestedRestaurantUseCase, ReadCelebrityVisitedRestaurantUseCase, ReadCelebrityRecommendRestaurantsUseCase, ReadRestaurantsUseCase, ReadRestaurantDetailUseCase, ReadWeeklyUpdateRestaurantsUseCase, ReadNearbyRestaurantsUseCase, - ReadPopularRestaurantsUseCase { + ReadPopularRestaurantsUseCase, + ReadAmountOfRestaurantByCelebrityUseCase, + ReadAmountOfWeeklyUpdateRestaurantsUseCase, + ReadAmountOfRestaurantsUseCase { override fun readInterestedRestaurant(query: ReadInterestedRestaurantsQuery): SliceResult { val interestedRestaurants = readInterestedRestaurantPort.readInterestedRestaurants( query.memberId, @@ -57,6 +67,10 @@ class RestaurantQueryService( } } + override fun readAmountOfInterestedRestaurant(memberId: Long): Int { + return readInterestedRestaurantPort.countByMemberId(memberId) + } + override fun readCelebrityVisitedRestaurant(query: ReadCelebrityVisitedRestaurantQuery): SliceResult { val visitedRestaurants = readRestaurantPort.readVisitedRestaurantByCelebrity( query.celebrityId, @@ -73,6 +87,10 @@ class RestaurantQueryService( } } + override fun readAmountOfRestaurantByCelebrity(celebrityId: Long): Int { + return readRestaurantPort.countRestaurantByCelebrity(celebrityId) + } + override fun readCelebrityRecommendRestaurants(query: ReadCelebrityRecommendRestaurantsQuery): List { val restaurants = readRestaurantPort.readCelebrityRecommendRestaurant() val restaurantIds = restaurants.map { it.id } @@ -107,6 +125,14 @@ class RestaurantQueryService( } } + override fun readAmountOfRestaurants(query: CountRestaurantsQuery): Int { + return readRestaurantPort.countRestaurantsByCondition( + category = query.category, + region = query.region, + searchArea = query.searchArea, + ) + } + override fun readWeeklyUpdateRestaurants(query: ReadWeeklyUpdateRestaurantsQuery): SliceResult { val startOfWeek = query.baseDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) val endOfWeek = query.baseDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)) @@ -123,6 +149,12 @@ class RestaurantQueryService( } } + override fun readAmountOfWeeklyUpdateRestaurants(baseDate: LocalDate): Int { + val startOfWeek = baseDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) + val endOfWeek = baseDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)) + return readRestaurantPort.countByCreatedAtBetween(startOfWeek = startOfWeek, endOfWeek = endOfWeek) + } + override fun readNearbyRestaurants(query: ReadNearbyRestaurantsQuery): List { val restaurants = readRestaurantPort.readNearby(query.restaurantId) val restaurantIds = restaurants.map { it.id } diff --git a/src/main/kotlin/com/celuveat/restaurant/application/port/in/ReadAmountOfInterestedRestaurantUseCase.kt b/src/main/kotlin/com/celuveat/restaurant/application/port/in/ReadAmountOfInterestedRestaurantUseCase.kt new file mode 100644 index 00000000..b2bee7fd --- /dev/null +++ b/src/main/kotlin/com/celuveat/restaurant/application/port/in/ReadAmountOfInterestedRestaurantUseCase.kt @@ -0,0 +1,5 @@ +package com.celuveat.restaurant.application.port.`in` + +interface ReadAmountOfInterestedRestaurantUseCase { + fun readAmountOfInterestedRestaurant(memberId: Long): Int +} diff --git a/src/main/kotlin/com/celuveat/restaurant/application/port/in/ReadAmountOfRestaurantByCelebrityUseCase.kt b/src/main/kotlin/com/celuveat/restaurant/application/port/in/ReadAmountOfRestaurantByCelebrityUseCase.kt new file mode 100644 index 00000000..af49f8f8 --- /dev/null +++ b/src/main/kotlin/com/celuveat/restaurant/application/port/in/ReadAmountOfRestaurantByCelebrityUseCase.kt @@ -0,0 +1,5 @@ +package com.celuveat.restaurant.application.port.`in` + +interface ReadAmountOfRestaurantByCelebrityUseCase { + fun readAmountOfRestaurantByCelebrity(celebrityId: Long): Int +} diff --git a/src/main/kotlin/com/celuveat/restaurant/application/port/in/ReadAmountOfRestaurantsUseCase.kt b/src/main/kotlin/com/celuveat/restaurant/application/port/in/ReadAmountOfRestaurantsUseCase.kt new file mode 100644 index 00000000..b4d85723 --- /dev/null +++ b/src/main/kotlin/com/celuveat/restaurant/application/port/in/ReadAmountOfRestaurantsUseCase.kt @@ -0,0 +1,7 @@ +package com.celuveat.restaurant.application.port.`in` + +import com.celuveat.restaurant.application.port.`in`.query.CountRestaurantsQuery + +interface ReadAmountOfRestaurantsUseCase { + fun readAmountOfRestaurants(query: CountRestaurantsQuery): Int +} diff --git a/src/main/kotlin/com/celuveat/restaurant/application/port/in/ReadAmountOfWeeklyUpdateRestaurantsUseCase.kt b/src/main/kotlin/com/celuveat/restaurant/application/port/in/ReadAmountOfWeeklyUpdateRestaurantsUseCase.kt new file mode 100644 index 00000000..8748433f --- /dev/null +++ b/src/main/kotlin/com/celuveat/restaurant/application/port/in/ReadAmountOfWeeklyUpdateRestaurantsUseCase.kt @@ -0,0 +1,7 @@ +package com.celuveat.restaurant.application.port.`in` + +import java.time.LocalDate + +interface ReadAmountOfWeeklyUpdateRestaurantsUseCase { + fun readAmountOfWeeklyUpdateRestaurants(baseDate: LocalDate = LocalDate.now()): Int +} diff --git a/src/main/kotlin/com/celuveat/restaurant/application/port/in/query/ReadRestaurantsQuery.kt b/src/main/kotlin/com/celuveat/restaurant/application/port/in/query/ReadRestaurantsQuery.kt index 11beb5db..7dad3a23 100644 --- a/src/main/kotlin/com/celuveat/restaurant/application/port/in/query/ReadRestaurantsQuery.kt +++ b/src/main/kotlin/com/celuveat/restaurant/application/port/in/query/ReadRestaurantsQuery.kt @@ -12,3 +12,9 @@ data class ReadRestaurantsQuery( val page: Int = 0, val size: Int = DEFAULT_RESTAURANTS_SIZE, ) + +data class CountRestaurantsQuery( + val category: String?, + val region: String?, + val searchArea: SquarePolygon?, +) diff --git a/src/main/kotlin/com/celuveat/restaurant/application/port/out/ReadRestaurantPort.kt b/src/main/kotlin/com/celuveat/restaurant/application/port/out/ReadRestaurantPort.kt index ea96b367..532b037a 100644 --- a/src/main/kotlin/com/celuveat/restaurant/application/port/out/ReadRestaurantPort.kt +++ b/src/main/kotlin/com/celuveat/restaurant/application/port/out/ReadRestaurantPort.kt @@ -24,6 +24,12 @@ interface ReadRestaurantPort { size: Int, ): SliceResult + fun countRestaurantsByCondition( + category: String?, + region: String?, + searchArea: SquarePolygon?, + ): Int + fun readByCreatedAtBetween( startOfWeek: LocalDate, endOfWeek: LocalDate, @@ -31,6 +37,11 @@ interface ReadRestaurantPort { size: Int, ): SliceResult + fun countByCreatedAtBetween( + startOfWeek: LocalDate, + endOfWeek: LocalDate, + ): Int + fun readNearby(id: Long): List fun readTop10InterestedRestaurantsInDate( @@ -39,4 +50,6 @@ interface ReadRestaurantPort { ): List fun readByName(name: String): List + + fun countRestaurantByCelebrity(celebrityId: Long): Int } diff --git a/src/main/kotlin/com/celuveat/review/adapter/in/rest/ReviewApi.kt b/src/main/kotlin/com/celuveat/review/adapter/in/rest/ReviewApi.kt index 40c2ba3c..775c932f 100644 --- a/src/main/kotlin/com/celuveat/review/adapter/in/rest/ReviewApi.kt +++ b/src/main/kotlin/com/celuveat/review/adapter/in/rest/ReviewApi.kt @@ -102,6 +102,18 @@ interface ReviewApi { @PageableDefault(size = 10, page = 0) pageable: Pageable, ): SliceResponse + @Operation(summary = "특정 음식점의 리뷰 개수 조회") + @GetMapping("/restaurants/{restaurantId}/count") + fun readAmountOfRestaurantsReviews( + @Parameter( + `in` = ParameterIn.PATH, + description = "음식점 ID", + example = "1", + required = true, + ) + @PathVariable restaurantId: Long, + ): Int + @Operation(summary = "리뷰 상세조회") @GetMapping("/{reviewId}") fun readReview( diff --git a/src/main/kotlin/com/celuveat/review/adapter/in/rest/ReviewController.kt b/src/main/kotlin/com/celuveat/review/adapter/in/rest/ReviewController.kt index 2945c65f..64764c1a 100644 --- a/src/main/kotlin/com/celuveat/review/adapter/in/rest/ReviewController.kt +++ b/src/main/kotlin/com/celuveat/review/adapter/in/rest/ReviewController.kt @@ -10,6 +10,7 @@ import com.celuveat.review.adapter.`in`.rest.response.SingleReviewResponse import com.celuveat.review.application.port.`in`.ClickHelpfulReviewUseCase import com.celuveat.review.application.port.`in`.DeleteHelpfulReviewUseCase import com.celuveat.review.application.port.`in`.DeleteReviewUseCase +import com.celuveat.review.application.port.`in`.ReadAmountOfRestaurantReviewsUseCase import com.celuveat.review.application.port.`in`.ReadRestaurantReviewsUseCase import com.celuveat.review.application.port.`in`.ReadSingleReviewUseCase import com.celuveat.review.application.port.`in`.UpdateReviewUseCase @@ -36,6 +37,7 @@ class ReviewController( private val clickHelpfulReviewUseCase: ClickHelpfulReviewUseCase, private val deleteHelpfulReviewUseCase: DeleteHelpfulReviewUseCase, private val readRestaurantReviewsUseCase: ReadRestaurantReviewsUseCase, + private val readAmountOfRestaurantReviewsUseCase: ReadAmountOfRestaurantReviewsUseCase, private val readSingleReviewUseCase: ReadSingleReviewUseCase, ) : ReviewApi { @PostMapping @@ -102,6 +104,13 @@ class ReviewController( ) } + @GetMapping("/restaurants/{restaurantId}/count") + override fun readAmountOfRestaurantsReviews( + @PathVariable restaurantId: Long, + ): Int { + return readAmountOfRestaurantReviewsUseCase.readAmountOfRestaurantReviews(restaurantId) + } + @GetMapping("/{reviewId}") override fun readReview( @Auth auth: AuthContext, diff --git a/src/main/kotlin/com/celuveat/review/adapter/out/persistence/ReviewPersistenceAdapter.kt b/src/main/kotlin/com/celuveat/review/adapter/out/persistence/ReviewPersistenceAdapter.kt index 1f4046f1..efece018 100644 --- a/src/main/kotlin/com/celuveat/review/adapter/out/persistence/ReviewPersistenceAdapter.kt +++ b/src/main/kotlin/com/celuveat/review/adapter/out/persistence/ReviewPersistenceAdapter.kt @@ -66,6 +66,10 @@ class ReviewPersistenceAdapter( return reviewJpaRepository.countByWriterId(memberId).toInt() } + override fun countByRestaurantId(restaurantId: Long): Int { + return reviewJpaRepository.countByRestaurantId(restaurantId).toInt() + } + companion object { val LATEST_SORTER = Sort.by("createdAt").descending() } diff --git a/src/main/kotlin/com/celuveat/review/adapter/out/persistence/entity/ReviewJpaRepository.kt b/src/main/kotlin/com/celuveat/review/adapter/out/persistence/entity/ReviewJpaRepository.kt index 31d52778..222130fd 100644 --- a/src/main/kotlin/com/celuveat/review/adapter/out/persistence/entity/ReviewJpaRepository.kt +++ b/src/main/kotlin/com/celuveat/review/adapter/out/persistence/entity/ReviewJpaRepository.kt @@ -17,4 +17,6 @@ interface ReviewJpaRepository : JpaRepository { ): Slice fun countByWriterId(writerId: Long): Long + + fun countByRestaurantId(restaurantId: Long): Long } diff --git a/src/main/kotlin/com/celuveat/review/application/port/ReviewQueryService.kt b/src/main/kotlin/com/celuveat/review/application/port/ReviewQueryService.kt index 6c289f57..4acc845b 100644 --- a/src/main/kotlin/com/celuveat/review/application/port/ReviewQueryService.kt +++ b/src/main/kotlin/com/celuveat/review/application/port/ReviewQueryService.kt @@ -1,6 +1,7 @@ package com.celuveat.review.application.port import com.celuveat.common.application.port.`in`.result.SliceResult +import com.celuveat.review.application.port.`in`.ReadAmountOfRestaurantReviewsUseCase import com.celuveat.review.application.port.`in`.ReadRestaurantReviewsUseCase import com.celuveat.review.application.port.`in`.ReadSingleReviewUseCase import com.celuveat.review.application.port.`in`.result.ReviewPreviewResult @@ -15,7 +16,7 @@ class ReviewQueryService( private val readReviewPort: ReadReviewPort, private val readHelpfulReviewPort: ReadHelpfulReviewPort, private val saveReviewPort: SaveReviewPort, -) : ReadRestaurantReviewsUseCase, ReadSingleReviewUseCase { +) : ReadRestaurantReviewsUseCase, ReadSingleReviewUseCase, ReadAmountOfRestaurantReviewsUseCase { override fun readAll( memberId: Long?, restaurantId: Long, @@ -30,6 +31,10 @@ class ReviewQueryService( return reviewResults.convertContent { ReviewPreviewResult.of(it, reviewHelpfulReviewMapping.contains(it)) } } + override fun readAmountOfRestaurantReviews(restaurantId: Long): Int { + return readReviewPort.countByRestaurantId(restaurantId) + } + override fun read( memberId: Long?, reviewId: Long, diff --git a/src/main/kotlin/com/celuveat/review/application/port/in/ReadAmountOfRestaurantReviewsUseCase.kt b/src/main/kotlin/com/celuveat/review/application/port/in/ReadAmountOfRestaurantReviewsUseCase.kt new file mode 100644 index 00000000..8c976d68 --- /dev/null +++ b/src/main/kotlin/com/celuveat/review/application/port/in/ReadAmountOfRestaurantReviewsUseCase.kt @@ -0,0 +1,5 @@ +package com.celuveat.review.application.port.`in` + +interface ReadAmountOfRestaurantReviewsUseCase { + fun readAmountOfRestaurantReviews(restaurantId: Long): Int +} diff --git a/src/main/kotlin/com/celuveat/review/application/port/out/ReadReviewPort.kt b/src/main/kotlin/com/celuveat/review/application/port/out/ReadReviewPort.kt index 4718cdf1..e1c73f6b 100644 --- a/src/main/kotlin/com/celuveat/review/application/port/out/ReadReviewPort.kt +++ b/src/main/kotlin/com/celuveat/review/application/port/out/ReadReviewPort.kt @@ -13,4 +13,6 @@ interface ReadReviewPort { ): SliceResult fun countByWriterId(memberId: Long): Int + + fun countByRestaurantId(restaurantId: Long): Int } diff --git a/src/test/kotlin/com/celuveat/restaurant/adapter/in/rest/RestaurantControllerTest.kt b/src/test/kotlin/com/celuveat/restaurant/adapter/in/rest/RestaurantControllerTest.kt index fa8a7445..1c8b1584 100644 --- a/src/test/kotlin/com/celuveat/restaurant/adapter/in/rest/RestaurantControllerTest.kt +++ b/src/test/kotlin/com/celuveat/restaurant/adapter/in/rest/RestaurantControllerTest.kt @@ -8,6 +8,10 @@ import com.celuveat.restaurant.adapter.`in`.rest.response.RestaurantDetailRespon import com.celuveat.restaurant.adapter.`in`.rest.response.RestaurantPreviewResponse import com.celuveat.restaurant.application.port.`in`.AddInterestedRestaurantsUseCase import com.celuveat.restaurant.application.port.`in`.DeleteInterestedRestaurantsUseCase +import com.celuveat.restaurant.application.port.`in`.ReadAmountOfInterestedRestaurantUseCase +import com.celuveat.restaurant.application.port.`in`.ReadAmountOfRestaurantByCelebrityUseCase +import com.celuveat.restaurant.application.port.`in`.ReadAmountOfRestaurantsUseCase +import com.celuveat.restaurant.application.port.`in`.ReadAmountOfWeeklyUpdateRestaurantsUseCase import com.celuveat.restaurant.application.port.`in`.ReadCelebrityRecommendRestaurantsUseCase import com.celuveat.restaurant.application.port.`in`.ReadCelebrityVisitedRestaurantUseCase import com.celuveat.restaurant.application.port.`in`.ReadInterestedRestaurantsUseCase @@ -59,6 +63,10 @@ class RestaurantControllerTest( @MockkBean val readNearbyRestaurantsUseCase: ReadNearbyRestaurantsUseCase, @MockkBean val readRestaurantDetailUseCase: ReadRestaurantDetailUseCase, @MockkBean val readPopularRestaurantsUseCase: ReadPopularRestaurantsUseCase, + @MockkBean val readAmountOfInterestedRestaurantUseCase: ReadAmountOfInterestedRestaurantUseCase, + @MockkBean val readAmountOfRestaurantByCelebrityUseCase: ReadAmountOfRestaurantByCelebrityUseCase, + @MockkBean val readAmountOfWeeklyUpdateRestaurantsUseCase: ReadAmountOfWeeklyUpdateRestaurantsUseCase, + @MockkBean val readAmountOfRestaurantsUseCase: ReadAmountOfRestaurantsUseCase, // for AuthMemberArgumentResolver @MockkBean val extractMemberIdUseCase: ExtractMemberIdUseCase, ) : FunSpec({ diff --git a/src/test/kotlin/com/celuveat/restaurant/adapter/out/persistence/RestaurantPersistenceAdapterTest.kt b/src/test/kotlin/com/celuveat/restaurant/adapter/out/persistence/RestaurantPersistenceAdapterTest.kt index 1ee5813c..2ad57061 100644 --- a/src/test/kotlin/com/celuveat/restaurant/adapter/out/persistence/RestaurantPersistenceAdapterTest.kt +++ b/src/test/kotlin/com/celuveat/restaurant/adapter/out/persistence/RestaurantPersistenceAdapterTest.kt @@ -176,6 +176,26 @@ class RestaurantPersistenceAdapterTest( restaurants.hasNext shouldBe false } + test("조건에 맞는 음식점의 개수를 조회 한다.") { + // given + restaurantJpaRepository.saveAll( + sut.giveMeBuilder() + .setExp(RestaurantJpaEntity::category, "한식", 2) + .setExp(RestaurantJpaEntity::roadAddress, "서울", 1) + .sampleList(5), + ) + + // when + val count = restaurantPersistenceAdapter.countRestaurantsByCondition( + category = "한식", + region = "서울", + searchArea = null, + ) + + // then + count shouldBe 1 + } + test("최근 업데이트된 음식점을 조회한다.") { // given val savedRestaurants = restaurantJpaRepository.saveAll( diff --git a/src/test/kotlin/com/celuveat/support/PersistenceAdapterTest.kt b/src/test/kotlin/com/celuveat/support/PersistenceAdapterTest.kt index d3093e5b..d1c3e784 100644 --- a/src/test/kotlin/com/celuveat/support/PersistenceAdapterTest.kt +++ b/src/test/kotlin/com/celuveat/support/PersistenceAdapterTest.kt @@ -1,6 +1,7 @@ package com.celuveat.support import com.celuveat.auth.adapter.out.TokenAdapter +import com.celuveat.common.adapter.out.persistence.JdslConfig import com.celuveat.common.adapter.out.persistence.JpaConfig import com.celuveat.common.annotation.Adapter import com.celuveat.common.annotation.Mapper @@ -18,6 +19,6 @@ import org.springframework.context.annotation.Import includeFilters = [ComponentScan.Filter(type = FilterType.ANNOTATION, classes = [Adapter::class, Mapper::class])], excludeFilters = [ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = [TokenAdapter::class])], ) -@Import(JpaConfig::class, KotlinJdslAutoConfiguration::class) +@Import(JpaConfig::class, KotlinJdslAutoConfiguration::class, JdslConfig::class) @DataJpaTest annotation class PersistenceAdapterTest