feat(creator): 채널 홈 오디오 주문 상태를 조회한다
This commit is contained in:
@@ -9,6 +9,8 @@ import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
|
|||||||
import kr.co.vividnext.sodalive.chat.character.QChatCharacter.chatCharacter
|
import kr.co.vividnext.sodalive.chat.character.QChatCharacter.chatCharacter
|
||||||
import kr.co.vividnext.sodalive.content.ContentType
|
import kr.co.vividnext.sodalive.content.ContentType
|
||||||
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
||||||
|
import kr.co.vividnext.sodalive.content.order.OrderType
|
||||||
|
import kr.co.vividnext.sodalive.content.order.QOrder.order
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
|
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent
|
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers
|
import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers
|
||||||
@@ -159,9 +161,11 @@ class DefaultCreatorChannelHomeQueryRepository(
|
|||||||
viewerId: Long?
|
viewerId: Long?
|
||||||
): CreatorChannelAudioContentRecord? {
|
): CreatorChannelAudioContentRecord? {
|
||||||
val row = findAudioContentRows(creatorId, now, null, canViewAdultContent, 1).firstOrNull() ?: return null
|
val row = findAudioContentRows(creatorId, now, null, canViewAdultContent, 1).firstOrNull() ?: return null
|
||||||
|
val contentId = itAudioId(row)
|
||||||
return row.toAudioRecord(
|
return row.toAudioRecord(
|
||||||
firstContentId = firstAudioContentId(creatorId, now, canViewAdultContent),
|
firstContentId = firstAudioContentId(creatorId, now, canViewAdultContent),
|
||||||
seriesByContentId = audioSeriesByContentIds(listOf(itAudioId(row)))
|
seriesByContentId = audioSeriesByContentIds(listOf(contentId)),
|
||||||
|
orderStatesByContentId = orderStatesByContentIds(viewerId, listOf(contentId), now)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,9 +360,11 @@ class DefaultCreatorChannelHomeQueryRepository(
|
|||||||
limit: Int
|
limit: Int
|
||||||
): List<CreatorChannelAudioContentRecord> {
|
): List<CreatorChannelAudioContentRecord> {
|
||||||
val rows = findAudioContentRows(creatorId, now, latestAudioContentId, canViewAdultContent, limit)
|
val rows = findAudioContentRows(creatorId, now, latestAudioContentId, canViewAdultContent, limit)
|
||||||
|
val contentIds = rows.map { itAudioId(it) }
|
||||||
val firstContentId = firstAudioContentId(creatorId, now, canViewAdultContent)
|
val firstContentId = firstAudioContentId(creatorId, now, canViewAdultContent)
|
||||||
val seriesByContentId = audioSeriesByContentIds(rows.map { itAudioId(it) })
|
val seriesByContentId = audioSeriesByContentIds(contentIds)
|
||||||
return rows.map { it.toAudioRecord(firstContentId, seriesByContentId) }
|
val orderStatesByContentId = orderStatesByContentIds(viewerId, contentIds, now)
|
||||||
|
return rows.map { it.toAudioRecord(firstContentId, seriesByContentId, orderStatesByContentId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findSeries(
|
override fun findSeries(
|
||||||
@@ -551,10 +557,12 @@ class DefaultCreatorChannelHomeQueryRepository(
|
|||||||
|
|
||||||
private fun com.querydsl.core.Tuple.toAudioRecord(
|
private fun com.querydsl.core.Tuple.toAudioRecord(
|
||||||
firstContentId: Long?,
|
firstContentId: Long?,
|
||||||
seriesByContentId: Map<Long, AudioSeriesSummary>
|
seriesByContentId: Map<Long, AudioSeriesSummary>,
|
||||||
|
orderStatesByContentId: Map<Long, AudioOrderState>
|
||||||
): CreatorChannelAudioContentRecord {
|
): CreatorChannelAudioContentRecord {
|
||||||
val audioContentId = get(audioContent.id)!!
|
val audioContentId = get(audioContent.id)!!
|
||||||
val seriesSummary = seriesByContentId[audioContentId]
|
val seriesSummary = seriesByContentId[audioContentId]
|
||||||
|
val orderState = orderStatesByContentId[audioContentId]
|
||||||
return CreatorChannelAudioContentRecord(
|
return CreatorChannelAudioContentRecord(
|
||||||
audioContentId = audioContentId,
|
audioContentId = audioContentId,
|
||||||
title = get(audioContent.title)!!,
|
title = get(audioContent.title)!!,
|
||||||
@@ -567,11 +575,38 @@ class DefaultCreatorChannelHomeQueryRepository(
|
|||||||
publishedAt = get(audioContent.releaseDate)!!,
|
publishedAt = get(audioContent.releaseDate)!!,
|
||||||
seriesName = seriesSummary?.title,
|
seriesName = seriesSummary?.title,
|
||||||
isOriginalSeries = seriesSummary?.isOriginal,
|
isOriginalSeries = seriesSummary?.isOriginal,
|
||||||
isOwned = false,
|
isOwned = orderState?.isOwned ?: false,
|
||||||
isRented = false
|
isRented = orderState?.isRented ?: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun orderStatesByContentIds(
|
||||||
|
viewerId: Long?,
|
||||||
|
contentIds: List<Long>,
|
||||||
|
now: LocalDateTime
|
||||||
|
): Map<Long, AudioOrderState> {
|
||||||
|
if (viewerId == null || contentIds.isEmpty()) return emptyMap()
|
||||||
|
return queryFactory
|
||||||
|
.select(order.audioContent.id, order.type)
|
||||||
|
.from(order)
|
||||||
|
.where(
|
||||||
|
order.member.id.eq(viewerId),
|
||||||
|
order.audioContent.id.`in`(contentIds),
|
||||||
|
order.isActive.isTrue,
|
||||||
|
order.type.eq(OrderType.KEEP)
|
||||||
|
.or(order.type.eq(OrderType.RENTAL).and(order.endDate.after(now)))
|
||||||
|
)
|
||||||
|
.fetch()
|
||||||
|
.groupBy { it.get(order.audioContent.id)!! }
|
||||||
|
.mapValues { (_, rows) ->
|
||||||
|
val types = rows.map { it.get(order.type)!! }.toSet()
|
||||||
|
AudioOrderState(
|
||||||
|
isOwned = OrderType.KEEP in types,
|
||||||
|
isRented = OrderType.RENTAL in types
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun audioSeriesByContentIds(contentIds: List<Long>): Map<Long, AudioSeriesSummary> {
|
private fun audioSeriesByContentIds(contentIds: List<Long>): Map<Long, AudioSeriesSummary> {
|
||||||
if (contentIds.isEmpty()) return emptyMap()
|
if (contentIds.isEmpty()) return emptyMap()
|
||||||
return queryFactory
|
return queryFactory
|
||||||
@@ -912,6 +947,11 @@ class DefaultCreatorChannelHomeQueryRepository(
|
|||||||
val isOriginal: Boolean
|
val isOriginal: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private data class AudioOrderState(
|
||||||
|
val isOwned: Boolean,
|
||||||
|
val isRented: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
private data class SeriesContentStats(
|
private data class SeriesContentStats(
|
||||||
val contentCount: Int,
|
val contentCount: Int,
|
||||||
val latestPublishedAt: LocalDateTime
|
val latestPublishedAt: LocalDateTime
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
|||||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||||
import kr.co.vividnext.sodalive.content.AudioContent
|
import kr.co.vividnext.sodalive.content.AudioContent
|
||||||
import kr.co.vividnext.sodalive.content.ContentType
|
import kr.co.vividnext.sodalive.content.ContentType
|
||||||
|
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.content.theme.AudioContentTheme
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
|
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.creator.admin.content.series.SeriesContent
|
||||||
@@ -526,6 +528,44 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
|
|||||||
assertTrue(records.last().isPointAvailable)
|
assertTrue(records.last().isPointAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("최신 오디오와 오디오 목록은 조회자의 유효한 소장/대여 주문 상태를 함께 반환한다")
|
||||||
|
fun shouldFindAudioContentOwnershipFlagsByViewerOrders() {
|
||||||
|
val now = LocalDateTime.of(2026, 6, 12, 12, 0)
|
||||||
|
val viewer = saveMember("audio-order-viewer", MemberRole.USER)
|
||||||
|
val creator = saveMember("audio-order-creator", MemberRole.CREATOR)
|
||||||
|
val keepAndRental = saveAudioContent(creator, now.minusDays(3), isAdult = false)
|
||||||
|
val rentalOnly = saveAudioContent(creator, now.minusDays(2), isAdult = false)
|
||||||
|
val keepOnly = saveAudioContent(creator, now.minusDays(1), isAdult = false)
|
||||||
|
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))
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val latestRecord = repository.findLatestAudioContent(
|
||||||
|
creator.id!!,
|
||||||
|
now,
|
||||||
|
canViewAdultContent = false,
|
||||||
|
viewerId = viewer.id!!
|
||||||
|
)
|
||||||
|
val records = repository.findAudioContents(
|
||||||
|
creator.id!!,
|
||||||
|
now,
|
||||||
|
latestAudioContentId = latestRecord!!.audioContentId,
|
||||||
|
canViewAdultContent = false,
|
||||||
|
viewerId = viewer.id!!,
|
||||||
|
limit = 9
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(keepOnly.id, latestRecord.audioContentId)
|
||||||
|
assertTrue(latestRecord.isOwned)
|
||||||
|
assertFalse(latestRecord.isRented)
|
||||||
|
assertEquals(listOf(rentalOnly.id, keepAndRental.id), records.map { it.audioContentId })
|
||||||
|
assertEquals(listOf(false, true), records.map { it.isOwned })
|
||||||
|
assertEquals(listOf(true, true), records.map { it.isRented })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("오디오 목록은 releaseDate가 null인 콘텐츠를 제외한다")
|
@DisplayName("오디오 목록은 releaseDate가 null인 콘텐츠를 제외한다")
|
||||||
fun shouldExcludeNullReleaseDateAudioContent() {
|
fun shouldExcludeNullReleaseDateAudioContent() {
|
||||||
@@ -1441,6 +1481,23 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
|
|||||||
return useCan
|
return useCan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveOrder(
|
||||||
|
member: Member,
|
||||||
|
creator: Member,
|
||||||
|
content: AudioContent,
|
||||||
|
type: OrderType,
|
||||||
|
isActive: Boolean = true,
|
||||||
|
endDate: LocalDateTime? = null
|
||||||
|
): Order {
|
||||||
|
val order = Order(type = type, isActive = isActive)
|
||||||
|
order.member = member
|
||||||
|
order.creator = creator
|
||||||
|
order.audioContent = content
|
||||||
|
endDate?.let { order.endDate = it }
|
||||||
|
entityManager.persist(order)
|
||||||
|
return order
|
||||||
|
}
|
||||||
|
|
||||||
private fun saveCheers(
|
private fun saveCheers(
|
||||||
member: Member,
|
member: Member,
|
||||||
creator: Member,
|
creator: Member,
|
||||||
|
|||||||
Reference in New Issue
Block a user