Skip to content

Commit

Permalink
feat: 이번주 업데이트된 음식점 조회 api (#46)
Browse files Browse the repository at this point in the history
* feat: 최근 리뷰 조회 서비스 작성

* fix: 최근 업데이트된 음식점 조회를 이번주 업데이트된 음식점 조회로 변경

* feat: 이번주 업데이트된 음식점 조회 API 작성

* chore: apply ktlint

* chore: createdDate -> createdAt
  • Loading branch information
shin-mallang authored Aug 20, 2024
1 parent ecc16de commit fd6c9f4
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,11 @@ interface RestaurantApi {
@RequestParam category: String?,
@PageableDefault(size = 10, page = 0) pageable: Pageable,
): SliceResponse<RestaurantPreviewResponse>

@Operation(summary = "이번주 업데이트된 음식점 조회")
@GetMapping("/weekly")
fun readWeeklyUpdatedRestaurants(
@Auth auth: AuthContext,
@PageableDefault(size = 10, page = 0) pageable: Pageable,
): SliceResponse<RestaurantPreviewResponse>
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import com.celuveat.restaurant.application.port.`in`.ReadCelebrityRecommendResta
import com.celuveat.restaurant.application.port.`in`.ReadCelebrityVisitedRestaurantUseCase
import com.celuveat.restaurant.application.port.`in`.ReadInterestedRestaurantsUseCase
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.ReadCelebrityRecommendRestaurantsQuery
import com.celuveat.restaurant.application.port.`in`.query.ReadCelebrityVisitedRestaurantQuery
import com.celuveat.restaurant.application.port.`in`.query.ReadInterestedRestaurantsQuery
import com.celuveat.restaurant.application.port.`in`.query.ReadRestaurantsQuery
import com.celuveat.restaurant.application.port.`in`.query.ReadWeeklyUpdateRestaurantsQuery
import org.springframework.data.domain.Pageable
import org.springframework.data.web.PageableDefault
import org.springframework.web.bind.annotation.DeleteMapping
Expand All @@ -35,6 +37,7 @@ class RestaurantController(
private val readCelebrityVisitedRestaurantUseCase: ReadCelebrityVisitedRestaurantUseCase,
private val readCelebrityRecommendRestaurantsUseCase: ReadCelebrityRecommendRestaurantsUseCase,
private val readRestaurantsUseCase: ReadRestaurantsUseCase,
private val readWeeklyUpdateRestaurantsUseCase: ReadWeeklyUpdateRestaurantsUseCase,
) : RestaurantApi {
@GetMapping("/interested")
override fun getInterestedRestaurants(
Expand Down Expand Up @@ -130,4 +133,22 @@ class RestaurantController(
converter = RestaurantPreviewResponse::from,
)
}

@GetMapping("/weekly")
override fun readWeeklyUpdatedRestaurants(
auth: AuthContext,
pageable: Pageable,
): SliceResponse<RestaurantPreviewResponse> {
val result = readWeeklyUpdateRestaurantsUseCase.readWeeklyUpdateRestaurants(
ReadWeeklyUpdateRestaurantsQuery(
auth.optionalMemberId(),
page = pageable.pageNumber,
size = pageable.pageSize,
),
)
return SliceResponse.from(
sliceResult = result,
converter = RestaurantPreviewResponse::from,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import com.celuveat.restaurant.application.port.out.ReadRestaurantPort
import com.celuveat.restaurant.domain.Restaurant
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import java.time.LocalDate
import java.time.LocalTime

@Adapter
class RestaurantPersistenceAdapter(
Expand Down Expand Up @@ -80,6 +82,32 @@ class RestaurantPersistenceAdapter(
)
}

override fun readByCreatedAtBetween(
startOfWeek: LocalDate,
endOfWeek: LocalDate,
page: Int,
size: Int,
): SliceResult<Restaurant> {
val pageRequest = PageRequest.of(page, size, LATEST_SORTER)
val restaurants = restaurantJpaRepository.findByCreatedAtBetween(
startOfWeek.atStartOfDay(),
endOfWeek.atTime(LocalTime.MAX),
pageRequest,
)
val imagesByRestaurants = restaurantImageJpaRepository.findByRestaurantIn(restaurants.content)
.groupBy { it.restaurant.id }
return SliceResult.of(
contents = restaurants.content.map {
restaurantPersistenceMapper.toDomain(
it,
imagesByRestaurants[it.id]!!,
)
},
currentPage = page,
hasNext = restaurants.hasNext(),
)
}

companion object {
val LATEST_SORTER = Sort.by("createdAt").descending()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@ package com.celuveat.restaurant.adapter.out.persistence.entity

import com.celuveat.common.utils.findByIdOrThrow
import com.celuveat.restaurant.exception.NotFoundRestaurantException
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Slice
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import java.time.LocalDateTime

interface RestaurantJpaRepository : JpaRepository<RestaurantJpaEntity, Long>, CustomRestaurantRepository {
override fun getById(id: Long): RestaurantJpaEntity {
return findByIdOrThrow(id) { NotFoundRestaurantException }
}

@Query("SELECT r FROM RestaurantJpaEntity r WHERE r.createdAt >= :startOfWeek AND r.createdAt <= :endOfWeek ORDER BY r.createdAt DESC")
fun findByCreatedAtBetween(
startOfWeek: LocalDateTime,
endOfWeek: LocalDateTime,
pageable: Pageable,
): Slice<RestaurantJpaEntity>
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@ import com.celuveat.restaurant.application.port.`in`.ReadCelebrityRecommendResta
import com.celuveat.restaurant.application.port.`in`.ReadCelebrityVisitedRestaurantUseCase
import com.celuveat.restaurant.application.port.`in`.ReadInterestedRestaurantsUseCase
import com.celuveat.restaurant.application.port.`in`.ReadRestaurantsUseCase
import com.celuveat.restaurant.application.port.`in`.ReadWeeklyUpdateRestaurantsUseCase
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
import com.celuveat.restaurant.application.port.`in`.query.ReadRestaurantsQuery
import com.celuveat.restaurant.application.port.`in`.query.ReadWeeklyUpdateRestaurantsQuery
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.Restaurant
import org.springframework.stereotype.Service
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.temporal.TemporalAdjusters

@Service
class RestaurantQueryService(
Expand All @@ -24,7 +29,8 @@ class RestaurantQueryService(
) : ReadInterestedRestaurantsUseCase,
ReadCelebrityVisitedRestaurantUseCase,
ReadCelebrityRecommendRestaurantsUseCase,
ReadRestaurantsUseCase {
ReadRestaurantsUseCase,
ReadWeeklyUpdateRestaurantsUseCase {
override fun readInterestedRestaurant(query: ReadInterestedRestaurantsQuery): SliceResult<RestaurantPreviewResult> {
val interestedRestaurants = readInterestedRestaurantPort.readInterestedRestaurants(
query.memberId,
Expand Down Expand Up @@ -73,16 +79,6 @@ class RestaurantQueryService(
}
}

private fun readInterestedRestaurants(
memberId: Long?,
restaurantIds: List<Long>,
): Set<Restaurant> {
return memberId?.let {
readInterestedRestaurantPort.readInterestedRestaurantsByIds(it, restaurantIds)
.map { interested -> interested.restaurant }.toSet()
} ?: emptySet()
}

override fun readRestaurants(query: ReadRestaurantsQuery): SliceResult<RestaurantPreviewResult> {
val restaurants = readRestaurantPort.readRestaurantsByCondition(
category = query.category,
Expand All @@ -101,4 +97,30 @@ class RestaurantQueryService(
)
}
}

override fun readWeeklyUpdateRestaurants(query: ReadWeeklyUpdateRestaurantsQuery): SliceResult<RestaurantPreviewResult> {
val startOfWeek: LocalDate = query.baseDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
val endOfWeek: LocalDate = query.baseDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY))
val restaurants = readRestaurantPort.readByCreatedAtBetween(startOfWeek, endOfWeek, query.page, query.size)
val restaurantIds = restaurants.contents.map { it.id }
val celebritiesByRestaurants = readCelebritiesPort.readVisitedCelebritiesByRestaurants(restaurantIds)
val interestedRestaurants = readInterestedRestaurants(query.memberId, restaurantIds)
return restaurants.convertContent {
RestaurantPreviewResult.of(
restaurant = it,
liked = interestedRestaurants.contains(it),
visitedCelebrities = celebritiesByRestaurants[it.id]!!,
)
}
}

private fun readInterestedRestaurants(
memberId: Long?,
restaurantIds: List<Long>,
): Set<Restaurant> {
return memberId?.let {
readInterestedRestaurantPort.readInterestedRestaurantsByIds(it, restaurantIds)
.map { interested -> interested.restaurant }.toSet()
} ?: emptySet()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.celuveat.restaurant.application.port.`in`

import com.celuveat.common.application.port.`in`.result.SliceResult
import com.celuveat.restaurant.application.port.`in`.query.ReadWeeklyUpdateRestaurantsQuery
import com.celuveat.restaurant.application.port.`in`.result.RestaurantPreviewResult

/**
* 최근 업데이트된 맛집
*/
interface ReadWeeklyUpdateRestaurantsUseCase {
fun readWeeklyUpdateRestaurants(query: ReadWeeklyUpdateRestaurantsQuery): SliceResult<RestaurantPreviewResult>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.celuveat.restaurant.application.port.`in`.query

import java.time.LocalDate

private const val DEFAULT_LATEST_UPDATED_RESTAURANTS_SIZE = 10

data class ReadWeeklyUpdateRestaurantsQuery(
val memberId: Long?,
val page: Int = 0,
val size: Int = DEFAULT_LATEST_UPDATED_RESTAURANTS_SIZE,
val baseDate: LocalDate = LocalDate.now(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.celuveat.restaurant.application.port.out

import com.celuveat.common.application.port.`in`.result.SliceResult
import com.celuveat.restaurant.domain.Restaurant
import java.time.LocalDate

interface ReadRestaurantPort {
fun readVisitedRestaurantByCelebrity(
Expand All @@ -20,4 +21,11 @@ interface ReadRestaurantPort {
page: Int,
size: Int,
): SliceResult<Restaurant>

fun readByCreatedAtBetween(
startOfWeek: LocalDate,
endOfWeek: LocalDate,
page: Int,
size: Int,
): SliceResult<Restaurant>
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import com.navercorp.fixturemonkey.kotlin.setExp
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContainInOrder
import io.kotest.matchers.shouldBe
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.temporal.TemporalAdjusters

@PersistenceAdapterTest
class RestaurantPersistenceAdapterTest(
Expand Down Expand Up @@ -162,4 +165,57 @@ class RestaurantPersistenceAdapterTest(
restaurants.contents.map { it.roadAddress } shouldContainInOrder listOf("서울", "서울")
restaurants.hasNext shouldBe false
}

test("최근 업데이트된 음식점을 조회한다.") {
// given
val savedRestaurants = restaurantJpaRepository.saveAll(
listOf(
sut.giveMeBuilder<RestaurantJpaEntity>()
.set(RestaurantJpaEntity::name, "1 음식점")
.sample(),
sut.giveMeBuilder<RestaurantJpaEntity>()
.set(RestaurantJpaEntity::name, "2 음식점")
.sample(),
sut.giveMeBuilder<RestaurantJpaEntity>()
.set(RestaurantJpaEntity::name, "3 음식점")
.sample(),
),
)
val savedCelebrity = celebrityJpaRepository.save(sut.giveMeOne())
celebrityRestaurantJpaRepository.saveAll(
savedRestaurants.map {
CelebrityRestaurantJpaEntity(
celebrity = savedCelebrity,
restaurant = it,
)
},
)
val baseDate = LocalDate.now()
val startOfWeek: LocalDate = baseDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
val endOfWeek: LocalDate = baseDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY))

restaurantImageJpaRepository.saveAll(
savedRestaurants.map {
sut.giveMeBuilder<RestaurantImageJpaEntity>()
.set(RestaurantImageJpaEntity::id, 0)
.set(RestaurantImageJpaEntity::restaurant, it)
.set(RestaurantImageJpaEntity::isThumbnail, true, 1)
.sampleList(3)
}.flatten(),
)

// when
val weeklyUpdatedRestaurants = restaurantPersistenceAdapter.readByCreatedAtBetween(
startOfWeek,
endOfWeek,
0,
3,
)

// then
weeklyUpdatedRestaurants.size shouldBe 3
weeklyUpdatedRestaurants.contents.map { it.name } shouldContainInOrder listOf(
"3 음식점", "2 음식점", "1 음식점",
)
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.celuveat.restaurant.application.port.`in`.query.ReadCelebrityRecommen
import com.celuveat.restaurant.application.port.`in`.query.ReadCelebrityVisitedRestaurantQuery
import com.celuveat.restaurant.application.port.`in`.query.ReadInterestedRestaurantsQuery
import com.celuveat.restaurant.application.port.`in`.query.ReadRestaurantsQuery
import com.celuveat.restaurant.application.port.`in`.query.ReadWeeklyUpdateRestaurantsQuery
import com.celuveat.restaurant.application.port.out.ReadInterestedRestaurantPort
import com.celuveat.restaurant.application.port.out.ReadRestaurantPort
import com.celuveat.restaurant.domain.InterestedRestaurant
Expand Down Expand Up @@ -295,6 +296,65 @@ class RestaurantQueryServiceTest : BehaviorSpec({
}
}
}

Given("최근 업데이트된 음식점 조회 시") {
val restaurants = SliceResult.of(
contents = sut.giveMeBuilder<Restaurant>().sampleList(2),
currentPage = 0,
hasNext = false,
)
val restaurantIds = restaurants.contents.map { it.id }
val celebritiesByRestaurants = mapOf(
restaurantIds[0] to sut.giveMeBuilder<Celebrity>()
.setExp(Celebrity::youtubeContents, generateYoutubeContents(size = 2))
.sampleList(2),
restaurantIds[1] to sut.giveMeBuilder<Celebrity>()
.setExp(Celebrity::youtubeContents, generateYoutubeContents(size = 1))
.sampleList(1),
)
When("회원이 최근 업데이트된 음식점 조회하면") {
val memberId = 1L
every { readRestaurantPort.readByCreatedAtBetween(any(), any(), any(), any()) } returns restaurants
every { readCelebritiesPort.readVisitedCelebritiesByRestaurants(restaurantIds) } returns celebritiesByRestaurants
every {
readInterestedRestaurantPort.readInterestedRestaurantsByIds(
memberId,
restaurantIds,
)
} returns listOf(
sut.giveMeBuilder<InterestedRestaurant>()
.setExp(InterestedRestaurant::restaurant, restaurants.contents[0])
.sample(),
) // 첫 번째 음식점만 관심 등록

val latestRestaurants = restaurantQueryService.readWeeklyUpdateRestaurants(
ReadWeeklyUpdateRestaurantsQuery(memberId, 0, 10),
).contents

Then("관심 등록 여부가 포함되어 응답한다") {
latestRestaurants.size shouldBe 2
latestRestaurants[0].liked shouldBe true
latestRestaurants[0].visitedCelebrities.size shouldBe 2
latestRestaurants[1].liked shouldBe false
latestRestaurants[1].visitedCelebrities.size shouldBe 1
}
}

When("비회원이 최근 업데이트된 음식점 조회하면") {
every { readRestaurantPort.readByCreatedAtBetween(any(), any(), any(), any()) } returns restaurants
every { readCelebritiesPort.readVisitedCelebritiesByRestaurants(restaurantIds) } returns celebritiesByRestaurants

val latestRestaurants = restaurantQueryService.readWeeklyUpdateRestaurants(
ReadWeeklyUpdateRestaurantsQuery(memberId = null, page = 0, size = 10),
).contents

Then("관심 등록 여부는 false로 응답한다") {
latestRestaurants.size shouldBe 2
latestRestaurants.map { it.liked } shouldBe listOf(false, false)
verify { readInterestedRestaurantPort wasNot Called }
}
}
}
}) {
override suspend fun afterEach(
testCase: TestCase,
Expand Down

0 comments on commit fd6c9f4

Please sign in to comment.