feat(banner): v2 홈 추천에 예약 기간 필터를 적용한다

This commit is contained in:
2026-10-27 20:48:54 +09:00
parent da73b98374
commit 0a46d8a123
2 changed files with 50 additions and 2 deletions
@@ -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)
)
@@ -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() {