feat(audio-recommendation): 실시간 추천 조회 repository를 추가한다
This commit is contained in:
+258
@@ -0,0 +1,258 @@
|
||||
package kr.co.vividnext.sodalive.v2.audio.recommendation.adapter.out.persistence
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
|
||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner
|
||||
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.context.annotation.Import
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@DataJpaTest(
|
||||
properties = [
|
||||
"spring.cache.type=none",
|
||||
"spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;NON_KEYWORDS=VALUE"
|
||||
]
|
||||
)
|
||||
@Import(QueryDslConfig::class)
|
||||
class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
private val entityManager: EntityManager,
|
||||
queryFactory: JPAQueryFactory
|
||||
) {
|
||||
private val repository = DefaultAudioRecommendationQueryRepository(queryFactory, "https://cdn.test")
|
||||
|
||||
@Test
|
||||
@DisplayName("배너는 홈 추천 배너와 같은 활성/탭/차단 정책과 CDN URL을 적용한다")
|
||||
fun shouldFindBannersWithHomeBannerPolicy() {
|
||||
val viewer = saveMember("viewer", MemberRole.USER)
|
||||
val visibleCreator = saveMember("visible-creator", MemberRole.CREATOR)
|
||||
val blockedCreator = saveMember("blocked-creator", MemberRole.CREATOR)
|
||||
val visibleBanner = saveBanner("visible.png", AudioContentBannerType.CREATOR, 1, creator = visibleCreator)
|
||||
val adultBanner = saveBanner("adult.png", AudioContentBannerType.LINK, 2, isAdult = true, link = "https://adult.test")
|
||||
saveBanner("inactive.png", AudioContentBannerType.LINK, 2, isActive = false, link = "https://inactive.test")
|
||||
saveBanner("blocked.png", AudioContentBannerType.CREATOR, 3, creator = blockedCreator)
|
||||
saveBlock(viewer, blockedCreator)
|
||||
flushAndClear()
|
||||
|
||||
val banners = repository.findBanners(limit = 20, memberId = viewer.id, canViewAdultContent = false)
|
||||
|
||||
assertEquals(
|
||||
listOf("https://cdn.test/${visibleBanner.thumbnailImage}", "https://cdn.test/${adultBanner.thumbnailImage}"),
|
||||
banners.map { it.imageUrl }
|
||||
)
|
||||
assertEquals(visibleCreator.id, banners.first().creatorId)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("오리지널 시리즈는 활성 원본 시리즈를 최신순으로 반환하고 성인/차단 조건을 적용한다")
|
||||
fun shouldFindOriginalSeriesWithVisibilityConditions() {
|
||||
val viewer = saveMember("series-viewer", MemberRole.USER)
|
||||
val visibleCreator = saveMember("series-visible", MemberRole.CREATOR)
|
||||
val blockedCreator = saveMember("series-blocked", MemberRole.CREATOR)
|
||||
val visibleSeries = (1..13).map { index ->
|
||||
saveSeries("visible-series-$index", visibleCreator, isOriginal = true, coverImage = "series-$index.png")
|
||||
}
|
||||
saveSeries("normal-series", visibleCreator, isOriginal = false)
|
||||
saveSeries("adult-series", visibleCreator, isOriginal = true, isAdult = true)
|
||||
saveSeries("blocked-series", blockedCreator, isOriginal = true)
|
||||
saveBlock(viewer, blockedCreator)
|
||||
flushAndClear()
|
||||
|
||||
val series = repository.findOriginalSeries(12, viewer.id, canViewAdultContent = false, now = LocalDateTime.now())
|
||||
|
||||
assertEquals(12, series.size)
|
||||
assertEquals(visibleSeries.map { it.id }.asReversed().take(12), series.map { it.seriesId })
|
||||
assertEquals("https://cdn.test/series-13.png", series.first().coverImageUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최신/무료/포인트 오디오는 공개 조건과 공통 AudioCard enrichment를 적용한다")
|
||||
fun shouldFindRealtimeAudioCardsWithCommonEnrichment() {
|
||||
val now = LocalDateTime.of(2026, 6, 23, 12, 0)
|
||||
val creator = saveMember("audio-creator", MemberRole.CREATOR)
|
||||
val theme = saveTheme()
|
||||
val first = saveAudio(
|
||||
creator = creator,
|
||||
theme = theme,
|
||||
title = "first",
|
||||
releaseDate = now.minusDays(3),
|
||||
price = 0,
|
||||
isPointAvailable = true,
|
||||
coverImage = "first.png"
|
||||
)
|
||||
val latest = saveAudio(
|
||||
creator = creator,
|
||||
theme = theme,
|
||||
title = "latest",
|
||||
releaseDate = now.minusDays(1),
|
||||
price = 10,
|
||||
isPointAvailable = false,
|
||||
coverImage = "latest.png"
|
||||
)
|
||||
saveAudio(creator, theme, "adult", now.minusHours(1), isAdult = true)
|
||||
saveAudio(creator, theme, "future", now.plusDays(1))
|
||||
saveAudio(creator, theme, "inactive", now.minusHours(2)).isActive = false
|
||||
saveAudio(creator, theme, "no-duration", now.minusHours(3)).duration = null
|
||||
saveAudio(creator, theme, "no-release-date", now.minusHours(4)).releaseDate = null
|
||||
val inactiveCreator = saveMember("inactive-audio-creator", MemberRole.CREATOR, isActive = false)
|
||||
saveAudio(inactiveCreator, theme, "inactive-creator", now.minusHours(5))
|
||||
val viewer = saveMember("blocked-audio-viewer", MemberRole.USER)
|
||||
val blockedCreator = saveMember("blocked-audio-creator", MemberRole.CREATOR)
|
||||
saveAudio(blockedCreator, theme, "blocked", now.minusHours(6))
|
||||
saveBlock(viewer, blockedCreator)
|
||||
val originalSeries = saveSeries("original", creator, isOriginal = true)
|
||||
saveSeriesContent(originalSeries, latest)
|
||||
val limitCreator = saveMember("limit-audio-creator", MemberRole.CREATOR)
|
||||
repeat(11) { index ->
|
||||
saveAudio(
|
||||
creator = limitCreator,
|
||||
theme = theme,
|
||||
title = "free-point-$index",
|
||||
releaseDate = now.minusDays(10).minusMinutes(index.toLong()),
|
||||
price = 0,
|
||||
isPointAvailable = true
|
||||
)
|
||||
}
|
||||
flushAndClear()
|
||||
|
||||
val latestAudios = repository.findLatestAudios(12, viewer.id, canViewAdultContent = false, now = now)
|
||||
val freeAudios = repository.findFreeAudios(10, viewer.id, canViewAdultContent = false, now = now)
|
||||
val pointAudios = repository.findPointAudios(10, viewer.id, canViewAdultContent = false, now = now)
|
||||
|
||||
assertEquals(12, latestAudios.size)
|
||||
assertEquals(listOf(latest.id, first.id), latestAudios.take(2).map { it.audioContentId })
|
||||
assertEquals(10, freeAudios.size)
|
||||
assertEquals(true, freeAudios.all { it.price == 0 })
|
||||
assertEquals(10, pointAudios.size)
|
||||
assertEquals(true, pointAudios.all { it.isPointAvailable })
|
||||
val latestCard = latestAudios.first()
|
||||
assertEquals("latest", latestCard.title)
|
||||
assertEquals("00:01", latestCard.duration)
|
||||
assertEquals("https://cdn.test/latest.png", latestCard.imageUrl)
|
||||
assertEquals(10, latestCard.price)
|
||||
assertEquals(false, latestCard.isAdult)
|
||||
assertEquals(false, latestCard.isPointAvailable)
|
||||
assertEquals(false, latestCard.isFirstContent)
|
||||
assertEquals(true, latestCard.isOriginalSeries)
|
||||
assertEquals(creator.nickname, latestCard.creatorNickname)
|
||||
assertEquals(true, latestAudios[1].isFirstContent)
|
||||
assertEquals(false, latestAudios[1].isOriginalSeries)
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole, isActive: Boolean = true): Member {
|
||||
val member = Member(
|
||||
email = "$nickname@test.com",
|
||||
password = "password",
|
||||
nickname = nickname,
|
||||
role = role,
|
||||
isActive = isActive
|
||||
)
|
||||
entityManager.persist(member)
|
||||
return member
|
||||
}
|
||||
|
||||
private fun saveTheme(): AudioContentTheme {
|
||||
val theme = AudioContentTheme(theme = "theme", image = "theme.png", isActive = true)
|
||||
entityManager.persist(theme)
|
||||
return theme
|
||||
}
|
||||
|
||||
private fun saveBanner(
|
||||
thumbnailImage: String,
|
||||
type: AudioContentBannerType,
|
||||
orders: Int,
|
||||
isActive: Boolean = true,
|
||||
isAdult: Boolean = false,
|
||||
creator: Member? = null,
|
||||
link: String? = null
|
||||
): AudioContentBanner {
|
||||
val banner = AudioContentBanner(
|
||||
thumbnailImage = thumbnailImage,
|
||||
type = type,
|
||||
isAdult = isAdult,
|
||||
isActive = isActive,
|
||||
orders = orders
|
||||
)
|
||||
banner.creator = creator
|
||||
banner.link = link
|
||||
entityManager.persist(banner)
|
||||
return banner
|
||||
}
|
||||
|
||||
private fun saveSeries(
|
||||
title: String,
|
||||
creator: Member,
|
||||
isOriginal: Boolean,
|
||||
isAdult: Boolean = false,
|
||||
coverImage: String? = null
|
||||
): Series {
|
||||
val genre = SeriesGenre("genre-$title")
|
||||
entityManager.persist(genre)
|
||||
val series = Series(title = title, introduction = "intro", isOriginal = isOriginal, isAdult = isAdult, isActive = true)
|
||||
series.genre = genre
|
||||
series.member = creator
|
||||
series.coverImage = coverImage
|
||||
entityManager.persist(series)
|
||||
return series
|
||||
}
|
||||
|
||||
private fun saveAudio(
|
||||
creator: Member,
|
||||
theme: AudioContentTheme,
|
||||
title: String,
|
||||
releaseDate: LocalDateTime,
|
||||
price: Int = 0,
|
||||
isAdult: Boolean = false,
|
||||
isPointAvailable: Boolean = false,
|
||||
coverImage: String? = null
|
||||
): AudioContent {
|
||||
val audio = AudioContent(
|
||||
title = title,
|
||||
detail = "detail",
|
||||
languageCode = "ko",
|
||||
price = price,
|
||||
releaseDate = releaseDate,
|
||||
isAdult = isAdult,
|
||||
isPointAvailable = isPointAvailable
|
||||
)
|
||||
audio.isActive = true
|
||||
audio.duration = "00:01"
|
||||
audio.coverImage = coverImage
|
||||
audio.member = creator
|
||||
audio.theme = theme
|
||||
entityManager.persist(audio)
|
||||
return audio
|
||||
}
|
||||
|
||||
private fun saveSeriesContent(series: Series, audio: AudioContent) {
|
||||
val seriesContent = SeriesContent()
|
||||
seriesContent.series = series
|
||||
seriesContent.content = audio
|
||||
entityManager.persist(seriesContent)
|
||||
}
|
||||
|
||||
private fun saveBlock(member: Member, blockedMember: Member) {
|
||||
val block = BlockMember(isActive = true)
|
||||
block.member = member
|
||||
block.blockedMember = blockedMember
|
||||
entityManager.persist(block)
|
||||
}
|
||||
|
||||
private fun flushAndClear() {
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user