diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt index 0e49f283..c9c89b8e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt @@ -15,6 +15,7 @@ import kr.co.vividnext.sodalive.chat.room.QChatMessage.chatMessage import kr.co.vividnext.sodalive.chat.room.QChatParticipant.chatParticipant import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType import kr.co.vividnext.sodalive.content.main.banner.QAudioContentBanner.audioContentBanner +import kr.co.vividnext.sodalive.content.main.banner.isVisibleAt import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series import kr.co.vividnext.sodalive.event.QEvent.event @@ -49,13 +50,15 @@ import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapsho import org.springframework.stereotype.Repository import java.sql.Timestamp import java.time.LocalDateTime +import java.time.ZoneOffset import javax.persistence.EntityManager @Repository class DefaultHomeRecommendationQueryRepository( private val queryFactory: JPAQueryFactory, private val entityManager: EntityManager, - private val contentTranslationRepository: ContentTranslationRepository + private val contentTranslationRepository: ContentTranslationRepository, + private val nowProvider: () -> LocalDateTime = { LocalDateTime.now(ZoneOffset.UTC) } ) : HomeRecommendationQueryRepository { override fun findLiveRecommendations( offset: Long, @@ -132,6 +135,7 @@ class DefaultHomeRecommendationQueryRepository( audioContentBanner.isActive.isTrue, audioContentBanner.tab.isNull, audioContentBanner.lang.eq(lang), + audioContentBanner.isVisibleAt(nowProvider()), includeAdultBannerCondition(includeAdultBanners), activeBannerTargetCondition(memberId, bannerCreator, seriesOwner) ) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt index 50ec4da8..684f3ffa 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt @@ -18,7 +18,9 @@ import kr.co.vividnext.sodalive.content.AudioContent import kr.co.vividnext.sodalive.content.comment.AudioContentComment import kr.co.vividnext.sodalive.content.like.AudioContentLike import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner +import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerReservationFixtures import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType +import kr.co.vividnext.sodalive.content.main.banner.BannerReservationCase import kr.co.vividnext.sodalive.content.main.tab.AudioContentMainTab import kr.co.vividnext.sodalive.content.theme.AudioContentTheme import kr.co.vividnext.sodalive.content.translation.ContentTranslation @@ -49,6 +51,8 @@ import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomePopularCommunityR import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito @@ -71,9 +75,49 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor( queryFactory: JPAQueryFactory, contentTranslationRepository: ContentTranslationRepository ) { - private val repository = DefaultHomeRecommendationQueryRepository(queryFactory, entityManager, contentTranslationRepository) + private var bannerNow = AudioContentBannerReservationFixtures.now + private val repository = DefaultHomeRecommendationQueryRepository(queryFactory, entityManager, contentTranslationRepository) { + bannerNow + } private val scorePolicy = RecommendationScorePolicy() + @Test + @DisplayName("홈 배너는 limit 전에 미래 및 만료 예약을 제외한다") + fun shouldExcludeFutureAndExpiredReservationsBeforeLimit() { + val now = bannerNow + listOf("future", "expired", "visible-1", "visible-2").forEachIndexed { index, name -> + saveBanner(name, AudioContentBannerType.LINK, index, true).apply { + if (name == "future") startDate = now.plusYears(1).withNano(0) + if (name == "expired") endDate = now.minusYears(1).withNano(0) + } + } + flushAndClear() + + assertEquals(listOf("visible-1", "visible-2"), repository.findHomeBanners(limit = 2).map { it.thumbnailImage }) + } + + @ParameterizedTest(name = "{0}") + @MethodSource("kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerReservationFixtures#cases") + @DisplayName("홈 배너는 UTC 초 정밀도의 예약 경계를 포함한다") + fun shouldApplyReservationBoundaries(case: BannerReservationCase) { + bannerNow = case.now + entityManager.createNativeQuery("alter table content_banner alter column start_date timestamp(0)").executeUpdate() + entityManager.createNativeQuery("alter table content_banner alter column end_date timestamp(0)").executeUpdate() + val banner = saveBanner("candidate", AudioContentBannerType.LINK, 1, true).apply { + startDate = case.startDate + endDate = case.endDate + } + flushAndClear() + val stored = entityManager.find(AudioContentBanner::class.java, banner.id) + assertEquals(case.startDate, stored.startDate) + assertEquals(case.endDate, stored.endDate) + + assertEquals( + if (case.visible) listOf("candidate") else emptyList(), + repository.findHomeBanners(limit = 2).map { it.thumbnailImage } + ) + } + @Test @DisplayName("라이브 추천은 활성 크리에이터의 진행 라이브를 최신순 최대 20개 조회한다") fun shouldFindLatestLiveRecommendationsWithLimit() {