feat(creator-channel): 오디오 탭 repository를 추가한다
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.audio.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.order.Order
|
||||
import kr.co.vividnext.sodalive.content.order.OrderType
|
||||
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslation
|
||||
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationPayload
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslation
|
||||
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 kr.co.vividnext.sodalive.v2.common.domain.ContentSort
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
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 DefaultCreatorChannelAudioQueryRepositoryTest @Autowired constructor(
|
||||
private val entityManager: EntityManager,
|
||||
queryFactory: JPAQueryFactory
|
||||
) {
|
||||
private val repository = DefaultCreatorChannelAudioQueryRepository(queryFactory)
|
||||
|
||||
@Test
|
||||
@DisplayName("크리에이터, 차단 관계, 활성 테마, 테마 번역 fallback을 조회한다")
|
||||
fun shouldFindCreatorBlockAndThemesWithTranslationFallback() {
|
||||
val viewer = saveMember("audio-viewer", MemberRole.USER)
|
||||
val creator = saveMember("audio-creator", MemberRole.CREATOR)
|
||||
val translatedTheme = saveTheme("수면", orders = 2)
|
||||
val blankTranslatedTheme = saveTheme("집중", orders = 1)
|
||||
val inactiveTheme = saveTheme("비활성", isActive = false)
|
||||
saveThemeTranslation(translatedTheme, "en", "Sleep")
|
||||
saveThemeTranslation(blankTranslatedTheme, "en", " ")
|
||||
saveThemeTranslation(inactiveTheme, "en", "Inactive")
|
||||
saveBlock(creator, viewer)
|
||||
flushAndClear()
|
||||
|
||||
val record = repository.findCreator(creator.id!!, viewer.id!!)
|
||||
val themes = repository.findAudioThemes("en")
|
||||
|
||||
assertEquals(creator.id, record!!.creatorId)
|
||||
assertEquals(MemberRole.CREATOR, record.role)
|
||||
assertTrue(repository.existsBlockedBetween(viewer.id!!, creator.id!!))
|
||||
assertEquals(translatedTheme.id, repository.findActiveThemeId(translatedTheme.id!!))
|
||||
assertEquals(null, repository.findActiveThemeId(inactiveTheme.id!!))
|
||||
assertEquals(listOf(blankTranslatedTheme.id, translatedTheme.id), themes.map { it.themeId })
|
||||
assertEquals(listOf("집중", "Sleep"), themes.map { it.themeName })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("오디오 콘텐츠 count는 공개 조건, 성인 노출 정책, 활성 themeId 필터를 공유한다")
|
||||
fun shouldCountPublicAudioContentsWithFilters() {
|
||||
val now = LocalDateTime.of(2026, 6, 19, 12, 0)
|
||||
val creator = saveMember("count-creator", MemberRole.CREATOR)
|
||||
val theme = saveTheme("수면")
|
||||
val otherTheme = saveTheme("집중")
|
||||
val inactiveTheme = saveTheme("비활성", isActive = false)
|
||||
saveAudioContent(creator, now.minusDays(2), false, theme, price = 0)
|
||||
saveAudioContent(creator, now.minusDays(1), false, theme, price = 100)
|
||||
saveAudioContent(creator, now.minusHours(1), true, theme, price = 200)
|
||||
saveAudioContent(creator, now.minusHours(2), false, otherTheme, price = 0)
|
||||
saveAudioContent(creator, now.plusHours(1), false, theme, price = 100)
|
||||
saveAudioContent(creator, now.minusHours(3), false, theme, price = 100).isActive = false
|
||||
saveAudioContent(creator, now.minusHours(4), false, inactiveTheme, price = 100)
|
||||
saveAudioContent(creator, now.minusHours(5), false, theme, price = 100).duration = null
|
||||
saveAudioContent(creator, now.minusHours(6), false, theme, price = 100).releaseDate = null
|
||||
flushAndClear()
|
||||
|
||||
assertEquals(3, repository.countAudioContents(creator.id!!, null, now, canViewAdultContent = false))
|
||||
assertEquals(4, repository.countAudioContents(creator.id!!, null, now, canViewAdultContent = true))
|
||||
assertEquals(2, repository.countAudioContents(creator.id!!, theme.id, now, canViewAdultContent = false))
|
||||
assertEquals(1, repository.countPaidAudioContents(creator.id!!, null, now, canViewAdultContent = false))
|
||||
assertEquals(2, repository.countPaidAudioContents(creator.id!!, theme.id, now, canViewAdultContent = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("구매 count는 유료 콘텐츠의 활성 KEEP 또는 유효 RENTAL 주문을 distinct로 계산한다")
|
||||
fun shouldCountPurchasedPaidAudioContentsOnly() {
|
||||
val now = LocalDateTime.of(2026, 6, 19, 12, 0)
|
||||
val viewer = saveMember("purchase-viewer", MemberRole.USER)
|
||||
val creator = saveMember("purchase-creator", MemberRole.CREATOR)
|
||||
val theme = saveTheme("수면")
|
||||
val keep = saveAudioContent(creator, now.minusDays(6), false, theme, price = 100)
|
||||
val rental = saveAudioContent(creator, now.minusDays(5), false, theme, price = 100)
|
||||
val duplicate = saveAudioContent(creator, now.minusDays(4), false, theme, price = 100)
|
||||
val expiredRental = saveAudioContent(creator, now.minusDays(3), false, theme, price = 100)
|
||||
val inactiveOrder = saveAudioContent(creator, now.minusDays(2), false, theme, price = 100)
|
||||
val free = saveAudioContent(creator, now.minusDays(1), false, theme, price = 0)
|
||||
saveOrder(viewer, creator, keep, OrderType.KEEP)
|
||||
saveOrder(viewer, creator, rental, OrderType.RENTAL, endDate = now.plusDays(1))
|
||||
saveOrder(viewer, creator, duplicate, OrderType.KEEP)
|
||||
saveOrder(viewer, creator, duplicate, OrderType.RENTAL, endDate = now.plusDays(1))
|
||||
saveOrder(viewer, creator, expiredRental, OrderType.RENTAL, endDate = now.minusDays(1))
|
||||
saveOrder(viewer, creator, inactiveOrder, OrderType.KEEP, isActive = false)
|
||||
saveOrder(viewer, creator, free, OrderType.KEEP)
|
||||
flushAndClear()
|
||||
|
||||
val count = repository.countPurchasedAudioContents(creator.id!!, viewer.id!!, null, now, false)
|
||||
|
||||
assertEquals(3, count)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("목록은 limit 그대로 조회하고 최신순, 시리즈 번역, 주문 상태, 전체 첫 콘텐츠를 반환한다")
|
||||
fun shouldFindAudioContentsWithLatestSortAndEnrichedFields() {
|
||||
val now = LocalDateTime.of(2026, 6, 19, 12, 0)
|
||||
val viewer = saveMember("latest-viewer", MemberRole.USER)
|
||||
val creator = saveMember("latest-creator", MemberRole.CREATOR)
|
||||
val theme = saveTheme("수면")
|
||||
val otherTheme = saveTheme("집중")
|
||||
val firstContent = saveAudioContent(creator, now.minusDays(30), false, otherTheme, price = 0)
|
||||
val oldSelected = saveAudioContent(creator, now.minusDays(3), false, theme, price = 100)
|
||||
val sameDateLowPrice = saveAudioContent(creator, now.minusDays(1), false, theme, price = 100)
|
||||
val sameDateHighPrice = saveAudioContent(creator, now.minusDays(1), false, theme, price = 300, isPointAvailable = true)
|
||||
val series = saveSeries("original-series", creator, isOriginal = true)
|
||||
saveSeriesContent(series, sameDateHighPrice)
|
||||
saveSeriesTranslation(series, "en", "Translated Series")
|
||||
saveOrder(viewer, creator, sameDateHighPrice, OrderType.KEEP)
|
||||
saveOrder(viewer, creator, sameDateHighPrice, OrderType.RENTAL, endDate = now.plusDays(1))
|
||||
flushAndClear()
|
||||
|
||||
val firstPage = repository.findAudioContents(
|
||||
creator.id!!,
|
||||
viewer.id!!,
|
||||
theme.id,
|
||||
now,
|
||||
false,
|
||||
ContentSort.LATEST,
|
||||
"en",
|
||||
offset = 0,
|
||||
limit = 2
|
||||
)
|
||||
val allThemes = repository.findAudioContents(
|
||||
creator.id!!,
|
||||
viewer.id!!,
|
||||
null,
|
||||
now,
|
||||
false,
|
||||
ContentSort.LATEST,
|
||||
"en",
|
||||
offset = 0,
|
||||
limit = 10
|
||||
)
|
||||
|
||||
assertEquals(2, firstPage.size)
|
||||
assertEquals(listOf(sameDateHighPrice.id, sameDateLowPrice.id), firstPage.map { it.audioContentId })
|
||||
assertEquals("Translated Series", firstPage.first().seriesName)
|
||||
assertEquals(true, firstPage.first().isOriginalSeries)
|
||||
assertTrue(firstPage.first().isOwned)
|
||||
assertTrue(firstPage.first().isRented)
|
||||
assertTrue(firstPage.first().isPointAvailable)
|
||||
assertEquals(firstContent.id, allThemes.last().audioContentId)
|
||||
assertTrue(allThemes.last().isFirstContent)
|
||||
assertFalse(firstPage.any { it.audioContentId == oldSelected.id && it.isFirstContent })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("목록은 가격순과 인기순 can 매출 정렬을 적용한다")
|
||||
fun shouldSortAudioContentsByPriceAndPopularRevenue() {
|
||||
val now = LocalDateTime.of(2026, 6, 19, 12, 0)
|
||||
val viewer = saveMember("sort-viewer", MemberRole.USER)
|
||||
val creator = saveMember("sort-creator", MemberRole.CREATOR)
|
||||
val theme = saveTheme("수면")
|
||||
val low = saveAudioContent(creator, now.minusDays(3), false, theme, price = 100)
|
||||
val high = saveAudioContent(creator, now.minusDays(2), false, theme, price = 300)
|
||||
val noRevenue = saveAudioContent(creator, now.minusDays(1), false, theme, price = 200)
|
||||
saveOrder(viewer, creator, low, OrderType.KEEP, can = 500, point = 9000)
|
||||
saveOrder(viewer, creator, high, OrderType.KEEP, can = 100, point = 9999)
|
||||
saveOrder(viewer, creator, noRevenue, OrderType.KEEP, isActive = false, can = 1000)
|
||||
flushAndClear()
|
||||
|
||||
val highRecords = repository.findAudioContents(
|
||||
creator.id!!,
|
||||
viewer.id!!,
|
||||
null,
|
||||
now,
|
||||
false,
|
||||
ContentSort.PRICE_HIGH,
|
||||
"ko",
|
||||
0,
|
||||
20
|
||||
)
|
||||
val lowRecords = repository.findAudioContents(
|
||||
creator.id!!,
|
||||
viewer.id!!,
|
||||
null,
|
||||
now,
|
||||
false,
|
||||
ContentSort.PRICE_LOW,
|
||||
"ko",
|
||||
0,
|
||||
20
|
||||
)
|
||||
val popularRecords = repository.findAudioContents(
|
||||
creator.id!!,
|
||||
viewer.id!!,
|
||||
null,
|
||||
now,
|
||||
false,
|
||||
ContentSort.POPULAR,
|
||||
"ko",
|
||||
0,
|
||||
20
|
||||
)
|
||||
|
||||
assertEquals(listOf(high.id, noRevenue.id, low.id), highRecords.map { it.audioContentId })
|
||||
assertEquals(listOf(low.id, noRevenue.id, high.id), lowRecords.map { it.audioContentId })
|
||||
assertEquals(listOf(low.id, high.id, noRevenue.id), popularRecords.map { it.audioContentId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("소장순은 KEEP 또는 유효 RENTAL 콘텐츠를 먼저 노출하고 시리즈명 blank 번역은 원문 fallback한다")
|
||||
fun shouldSortAudioContentsByOwnedAndReturnOrderStatesWithSeriesFallback() {
|
||||
val now = LocalDateTime.of(2026, 6, 19, 12, 0)
|
||||
val viewer = saveMember("owned-viewer", MemberRole.USER)
|
||||
val creator = saveMember("owned-creator", MemberRole.CREATOR)
|
||||
val theme = saveTheme("수면")
|
||||
val noOrder = saveAudioContent(creator, now.minusDays(5), false, theme, price = 100)
|
||||
val expiredRental = saveAudioContent(creator, now.minusDays(4), false, theme, price = 100)
|
||||
val keepAndRental = saveAudioContent(creator, now.minusDays(3), false, theme, price = 100)
|
||||
val rentalOnly = saveAudioContent(creator, now.minusDays(2), false, theme, price = 100)
|
||||
val keepOnly = saveAudioContent(creator, now.minusDays(1), false, theme, price = 100)
|
||||
val series = saveSeries("fallback-series", creator, isOriginal = false)
|
||||
saveSeriesContent(series, keepOnly)
|
||||
saveSeriesTranslation(series, "en", " ")
|
||||
saveOrder(viewer, creator, keepOnly, OrderType.KEEP)
|
||||
saveOrder(viewer, creator, rentalOnly, OrderType.RENTAL, endDate = now.plusDays(1))
|
||||
saveOrder(viewer, creator, keepAndRental, OrderType.KEEP)
|
||||
saveOrder(viewer, creator, keepAndRental, OrderType.RENTAL, endDate = now.plusDays(1))
|
||||
saveOrder(viewer, creator, expiredRental, OrderType.RENTAL, endDate = now.minusDays(1))
|
||||
flushAndClear()
|
||||
|
||||
val records = repository.findAudioContents(creator.id!!, viewer.id!!, null, now, false, ContentSort.OWNED, "en", 0, 20)
|
||||
|
||||
assertEquals(
|
||||
listOf(keepOnly.id, rentalOnly.id, keepAndRental.id, expiredRental.id, noOrder.id),
|
||||
records.map { it.audioContentId }
|
||||
)
|
||||
assertEquals(listOf(true, false, true, false, false), records.map { it.isOwned })
|
||||
assertEquals(listOf(false, true, true, false, false), records.map { it.isRented })
|
||||
assertEquals("fallback-series", records.first().seriesName)
|
||||
assertEquals(false, records.first().isOriginalSeries)
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole, isActive: Boolean = true): Member {
|
||||
val member = Member(
|
||||
email = "$nickname@test.com",
|
||||
password = "password",
|
||||
nickname = nickname,
|
||||
profileImage = "$nickname.png",
|
||||
role = role,
|
||||
isActive = isActive
|
||||
)
|
||||
entityManager.persist(member)
|
||||
return member
|
||||
}
|
||||
|
||||
private fun saveBlock(member: Member, blockedMember: Member): BlockMember {
|
||||
val block = BlockMember(isActive = true)
|
||||
block.member = member
|
||||
block.blockedMember = blockedMember
|
||||
entityManager.persist(block)
|
||||
return block
|
||||
}
|
||||
|
||||
private fun saveTheme(name: String, isActive: Boolean = true, orders: Int = 1): AudioContentTheme {
|
||||
val theme = AudioContentTheme(theme = name, image = "$name.png", isActive = isActive, orders = orders)
|
||||
entityManager.persist(theme)
|
||||
return theme
|
||||
}
|
||||
|
||||
private fun saveThemeTranslation(theme: AudioContentTheme, locale: String, translatedTheme: String): ContentThemeTranslation {
|
||||
val translation = ContentThemeTranslation(theme.id!!, locale, translatedTheme)
|
||||
entityManager.persist(translation)
|
||||
return translation
|
||||
}
|
||||
|
||||
private fun saveAudioContent(
|
||||
creator: Member,
|
||||
releaseDate: LocalDateTime,
|
||||
isAdult: Boolean,
|
||||
theme: AudioContentTheme,
|
||||
price: Int = 0,
|
||||
isPointAvailable: Boolean = false
|
||||
): AudioContent {
|
||||
val content = AudioContent(
|
||||
title = "audio-${creator.nickname}-$releaseDate",
|
||||
detail = "detail",
|
||||
languageCode = "ko",
|
||||
releaseDate = releaseDate,
|
||||
isAdult = isAdult,
|
||||
price = price,
|
||||
isPointAvailable = isPointAvailable
|
||||
)
|
||||
content.member = creator
|
||||
content.theme = theme
|
||||
content.isActive = true
|
||||
content.coverImage = "audio.png"
|
||||
content.duration = "00:10:00"
|
||||
entityManager.persist(content)
|
||||
return content
|
||||
}
|
||||
|
||||
private fun saveSeries(title: String, creator: Member, isOriginal: Boolean = false): Series {
|
||||
val series = Series(title = title, introduction = "introduction", languageCode = "ko", isOriginal = isOriginal)
|
||||
series.member = creator
|
||||
series.genre = saveSeriesGenre(title)
|
||||
series.coverImage = "$title.png"
|
||||
entityManager.persist(series)
|
||||
return series
|
||||
}
|
||||
|
||||
private fun saveSeriesGenre(name: String): SeriesGenre {
|
||||
val genre = SeriesGenre(genre = "genre-$name", isAdult = false, isActive = true)
|
||||
entityManager.persist(genre)
|
||||
return genre
|
||||
}
|
||||
|
||||
private fun saveSeriesContent(series: Series, content: AudioContent): SeriesContent {
|
||||
val seriesContent = SeriesContent()
|
||||
seriesContent.series = series
|
||||
seriesContent.content = content
|
||||
entityManager.persist(seriesContent)
|
||||
return seriesContent
|
||||
}
|
||||
|
||||
private fun saveSeriesTranslation(series: Series, locale: String, title: String): SeriesTranslation {
|
||||
val translation = SeriesTranslation(
|
||||
seriesId = series.id!!,
|
||||
locale = locale,
|
||||
renderedPayload = SeriesTranslationPayload(title = title, introduction = "", keywords = emptyList())
|
||||
)
|
||||
entityManager.persist(translation)
|
||||
entityManager.flush()
|
||||
val payload = "{\"title\":\"$title\",\"introduction\":\"\",\"keywords\":[]}"
|
||||
entityManager.createNativeQuery(
|
||||
"update series_translation set rendered_payload = '$payload' format json where id = :id"
|
||||
)
|
||||
.setParameter("id", translation.id)
|
||||
.executeUpdate()
|
||||
return translation
|
||||
}
|
||||
|
||||
private fun saveOrder(
|
||||
member: Member,
|
||||
creator: Member,
|
||||
content: AudioContent,
|
||||
type: OrderType,
|
||||
isActive: Boolean = true,
|
||||
endDate: LocalDateTime? = null,
|
||||
can: Int? = null,
|
||||
point: Int = 0
|
||||
): Order {
|
||||
val order = Order(type = type, isActive = isActive)
|
||||
order.member = member
|
||||
order.creator = creator
|
||||
order.audioContent = content
|
||||
can?.let { order.can = it }
|
||||
order.point = point
|
||||
entityManager.persist(order)
|
||||
if (endDate != null) {
|
||||
entityManager.flush()
|
||||
order.endDate = endDate
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
private fun flushAndClear() {
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user