feat(creator): 채널 라이브 다시듣기 저장소를 추가한다
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.live.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.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.live.room.GenderRestriction
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoom
|
||||
import kr.co.vividnext.sodalive.member.Gender
|
||||
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.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 DefaultCreatorChannelLiveQueryRepositoryTest @Autowired constructor(
|
||||
private val entityManager: EntityManager,
|
||||
queryFactory: JPAQueryFactory
|
||||
) {
|
||||
private val repository = DefaultCreatorChannelLiveQueryRepository(queryFactory)
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 다시듣기 count는 공개 다시듣기 콘텐츠와 성인 노출 정책만 반영한다")
|
||||
fun shouldCountPublicLiveReplayAudioContentsOnly() {
|
||||
val now = LocalDateTime.of(2026, 6, 17, 12, 0)
|
||||
val creator = saveMember("count-creator", MemberRole.CREATOR)
|
||||
val liveReplayTheme = saveTheme("다시듣기")
|
||||
saveAudioContent(creator, now.minusDays(2), isAdult = false, theme = liveReplayTheme)
|
||||
saveAudioContent(creator, now.minusDays(1), isAdult = false, theme = liveReplayTheme)
|
||||
saveAudioContent(creator, now.minusHours(1), isAdult = true, theme = liveReplayTheme)
|
||||
saveAudioContent(creator, now.minusHours(2), isAdult = false, theme = saveTheme("수면"))
|
||||
saveAudioContent(creator, now.plusHours(1), isAdult = false, theme = liveReplayTheme)
|
||||
saveAudioContent(creator, now.minusHours(3), isAdult = false, theme = liveReplayTheme).isActive = false
|
||||
saveAudioContent(creator, now.minusHours(4), isAdult = false, theme = saveTheme("inactive", isActive = false))
|
||||
saveAudioContent(creator, now.minusHours(5), isAdult = false, theme = liveReplayTheme).duration = null
|
||||
saveAudioContent(creator, now.minusHours(6), isAdult = false, theme = liveReplayTheme).releaseDate = null
|
||||
flushAndClear()
|
||||
|
||||
val hiddenAdultCount = repository.countLiveReplayAudioContents(creator.id!!, now, canViewAdultContent = false)
|
||||
val visibleAdultCount = repository.countLiveReplayAudioContents(creator.id!!, now, canViewAdultContent = true)
|
||||
|
||||
assertEquals(2, hiddenAdultCount)
|
||||
assertEquals(3, visibleAdultCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 다시듣기 목록은 page 인자와 기본 정렬을 DB에서 적용하고 series/firstContent를 채운다")
|
||||
fun shouldFindLiveReplayAudioContentsWithPaginationAndLatestSort() {
|
||||
val now = LocalDateTime.of(2026, 6, 17, 12, 0)
|
||||
val creator = saveMember("list-creator", MemberRole.CREATOR)
|
||||
val theme = saveTheme("다시듣기")
|
||||
val oldFirst = saveAudioContent(creator, now.minusDays(30), isAdult = false, theme = theme, price = 100)
|
||||
repeat(20) { index ->
|
||||
saveAudioContent(creator, now.minusDays(29L - index), isAdult = false, theme = theme, price = 100 + index)
|
||||
}
|
||||
val sameDateLowPrice = saveAudioContent(creator, now.minusHours(1), isAdult = false, theme = theme, price = 100)
|
||||
val sameDateHighPrice = saveAudioContent(creator, now.minusHours(1), isAdult = false, theme = theme, price = 300)
|
||||
val series = saveSeries("live-replay-series", creator, isOriginal = true)
|
||||
saveSeriesContent(series, sameDateHighPrice)
|
||||
flushAndClear()
|
||||
|
||||
val firstPage = repository.findLiveReplayAudioContents(
|
||||
creatorId = creator.id!!,
|
||||
viewerId = null,
|
||||
now = now,
|
||||
canViewAdultContent = false,
|
||||
sort = ContentSort.LATEST,
|
||||
offset = 0,
|
||||
limit = 21
|
||||
)
|
||||
val secondPage = repository.findLiveReplayAudioContents(
|
||||
creatorId = creator.id!!,
|
||||
viewerId = null,
|
||||
now = now,
|
||||
canViewAdultContent = false,
|
||||
sort = ContentSort.LATEST,
|
||||
offset = 20,
|
||||
limit = 21
|
||||
)
|
||||
|
||||
assertEquals(21, firstPage.size)
|
||||
assertEquals(listOf(sameDateHighPrice.id, sameDateLowPrice.id), firstPage.take(2).map { it.audioContentId })
|
||||
assertEquals(3, secondPage.size)
|
||||
assertEquals(firstPage[20].audioContentId, secondPage.first().audioContentId)
|
||||
assertEquals(oldFirst.id, secondPage.last().audioContentId)
|
||||
assertEquals("live-replay-series", firstPage.first().seriesName)
|
||||
assertEquals(true, firstPage.first().isOriginalSeries)
|
||||
assertTrue(secondPage.last().isFirstContent)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 다시듣기 목록은 가격 정렬을 적용한다")
|
||||
fun shouldSortLiveReplayAudioContentsByPrice() {
|
||||
val now = LocalDateTime.of(2026, 6, 17, 12, 0)
|
||||
val creator = saveMember("price-creator", MemberRole.CREATOR)
|
||||
val theme = saveTheme("다시듣기")
|
||||
val low = saveAudioContent(creator, now.minusDays(1), isAdult = false, theme = theme, price = 100)
|
||||
val high = saveAudioContent(creator, now.minusDays(2), isAdult = false, theme = theme, price = 300)
|
||||
flushAndClear()
|
||||
|
||||
val highRecords = repository.findLiveReplayAudioContents(creator.id!!, null, now, false, ContentSort.PRICE_HIGH, 0, 20)
|
||||
val lowRecords = repository.findLiveReplayAudioContents(creator.id!!, null, now, false, ContentSort.PRICE_LOW, 0, 20)
|
||||
|
||||
assertEquals(listOf(high.id, low.id), highRecords.map { it.audioContentId })
|
||||
assertEquals(listOf(low.id, high.id), lowRecords.map { it.audioContentId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("인기순은 활성 주문 can 합계를 기준으로 정렬하고 point와 비활성 주문을 제외한다")
|
||||
fun shouldSortLiveReplayAudioContentsByPopularCanRevenue() {
|
||||
val now = LocalDateTime.of(2026, 6, 17, 12, 0)
|
||||
val viewer = saveMember("popular-viewer", MemberRole.USER)
|
||||
val creator = saveMember("popular-creator", MemberRole.CREATOR)
|
||||
val theme = saveTheme("다시듣기")
|
||||
val olderHighRevenue = saveAudioContent(creator, now.minusDays(3), isAdult = false, theme = theme, price = 100)
|
||||
val newerLowRevenue = saveAudioContent(creator, now.minusDays(1), isAdult = false, theme = theme, price = 100)
|
||||
val inactiveRevenue = saveAudioContent(creator, now.minusDays(2), isAdult = false, theme = theme, price = 100)
|
||||
saveOrder(viewer, creator, olderHighRevenue, OrderType.KEEP, can = 500, point = 900)
|
||||
saveOrder(viewer, creator, newerLowRevenue, OrderType.KEEP, can = 100, point = 9000)
|
||||
saveOrder(viewer, creator, inactiveRevenue, OrderType.KEEP, isActive = false, can = 1000)
|
||||
flushAndClear()
|
||||
|
||||
val records = repository.findLiveReplayAudioContents(creator.id!!, viewer.id!!, now, false, ContentSort.POPULAR, 0, 20)
|
||||
|
||||
assertEquals(listOf(olderHighRevenue.id, newerLowRevenue.id, inactiveRevenue.id), records.map { it.audioContentId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("소장순은 조회자 KEEP 콘텐츠를 먼저 정렬하고 소장/대여 상태를 함께 반환한다")
|
||||
fun shouldSortLiveReplayAudioContentsByOwnedAndReturnOrderStates() {
|
||||
val now = LocalDateTime.of(2026, 6, 17, 12, 0)
|
||||
val viewer = saveMember("owned-viewer", MemberRole.USER)
|
||||
val creator = saveMember("owned-creator", MemberRole.CREATOR)
|
||||
val theme = saveTheme("다시듣기")
|
||||
val keepAndRental = saveAudioContent(creator, now.minusDays(4), isAdult = false, theme = theme)
|
||||
val expiredRental = saveAudioContent(creator, now.minusDays(3), isAdult = false, theme = theme)
|
||||
val rentalOnly = saveAudioContent(creator, now.minusDays(2), isAdult = false, theme = theme)
|
||||
val keepOnly = saveAudioContent(creator, now.minusDays(1), isAdult = false, theme = theme)
|
||||
saveOrder(viewer, creator, keepOnly, OrderType.KEEP)
|
||||
saveOrder(viewer, creator, rentalOnly, OrderType.RENTAL, endDate = now.plusDays(1))
|
||||
saveOrder(viewer, creator, expiredRental, OrderType.RENTAL, endDate = now.minusDays(1))
|
||||
saveOrder(viewer, creator, keepAndRental, OrderType.KEEP)
|
||||
saveOrder(viewer, creator, keepAndRental, OrderType.RENTAL, endDate = now.plusDays(1))
|
||||
flushAndClear()
|
||||
|
||||
val records = repository.findLiveReplayAudioContents(creator.id!!, viewer.id!!, now, false, ContentSort.OWNED, 0, 20)
|
||||
|
||||
assertEquals(listOf(keepOnly.id, keepAndRental.id, rentalOnly.id, expiredRental.id), records.map { it.audioContentId })
|
||||
assertEquals(listOf(true, true, false, false), records.map { it.isOwned })
|
||||
assertEquals(listOf(false, true, true, false), records.map { it.isRented })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("현재 라이브 조회는 홈 API와 같은 성인/성별/크리에이터 입장 정책을 적용한다")
|
||||
fun shouldFindCurrentLiveWithHomePolicy() {
|
||||
val now = LocalDateTime.of(2026, 6, 17, 12, 0)
|
||||
val creator = saveMember("current-live-creator", MemberRole.CREATOR)
|
||||
val viewerCreator = saveMember("current-live-viewer", MemberRole.CREATOR)
|
||||
saveLiveRoom(creator, now.minusMinutes(3), channelName = "adult", isAdult = true)
|
||||
saveLiveRoom(
|
||||
creator,
|
||||
now.minusMinutes(4),
|
||||
channelName = "male-only",
|
||||
isAdult = false,
|
||||
genderRestriction = GenderRestriction.MALE_ONLY
|
||||
)
|
||||
saveLiveRoom(
|
||||
creator,
|
||||
now.minusMinutes(5),
|
||||
channelName = "creator-hidden",
|
||||
isAdult = false,
|
||||
isAvailableJoinCreator = false
|
||||
)
|
||||
val visible = saveLiveRoom(creator, now.minusMinutes(6), channelName = "visible", isAdult = false)
|
||||
flushAndClear()
|
||||
|
||||
val live = repository.findCurrentLive(
|
||||
creatorId = creator.id!!,
|
||||
now = now,
|
||||
canViewAdultContent = false,
|
||||
viewerId = viewerCreator.id!!,
|
||||
isViewerCreator = true,
|
||||
effectiveViewerGender = Gender.FEMALE
|
||||
)
|
||||
|
||||
assertEquals(visible.id, live!!.liveId)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("크리에이터 조회와 차단 관계 조회는 live service port 계약을 만족한다")
|
||||
fun shouldFindCreatorAndBlockedRelationship() {
|
||||
val viewer = saveMember("blocked-viewer", MemberRole.USER)
|
||||
val creator = saveMember("blocked-creator", MemberRole.CREATOR)
|
||||
saveBlock(creator, viewer)
|
||||
flushAndClear()
|
||||
|
||||
val record = repository.findCreator(creator.id!!, viewer.id!!)
|
||||
|
||||
assertEquals(creator.id, record!!.creatorId)
|
||||
assertEquals(MemberRole.CREATOR, record.role)
|
||||
assertTrue(repository.existsBlockedBetween(viewer.id!!, creator.id!!))
|
||||
}
|
||||
|
||||
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 saveLiveRoom(
|
||||
creator: Member,
|
||||
beginDateTime: LocalDateTime,
|
||||
channelName: String?,
|
||||
isAdult: Boolean,
|
||||
isActive: Boolean = true,
|
||||
genderRestriction: GenderRestriction = GenderRestriction.ALL,
|
||||
isAvailableJoinCreator: Boolean = true
|
||||
): LiveRoom {
|
||||
val liveRoom = LiveRoom(
|
||||
title = "live-${creator.nickname}-$beginDateTime",
|
||||
notice = "notice",
|
||||
beginDateTime = beginDateTime,
|
||||
numberOfPeople = 0,
|
||||
coverImage = "live.png",
|
||||
isAdult = isAdult,
|
||||
price = 50,
|
||||
isAvailableJoinCreator = isAvailableJoinCreator,
|
||||
genderRestriction = genderRestriction
|
||||
)
|
||||
liveRoom.member = creator
|
||||
liveRoom.channelName = channelName
|
||||
liveRoom.isActive = isActive
|
||||
entityManager.persist(liveRoom)
|
||||
return liveRoom
|
||||
}
|
||||
|
||||
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 saveTheme(name: String, isActive: Boolean = true): AudioContentTheme {
|
||||
val theme = AudioContentTheme(theme = name, image = "$name.png", isActive = isActive)
|
||||
entityManager.persist(theme)
|
||||
return theme
|
||||
}
|
||||
|
||||
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 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