feat(creator): 채널 라이브 탭 조회 서비스를 추가한다

This commit is contained in:
2026-06-17 18:20:52 +09:00
parent 6a3ca5f44f
commit 3e3642bb7f
3 changed files with 602 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
package kr.co.vividnext.sodalive.v2.creator.channel.live.application
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
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.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.member.contentpreference.isAdultVisibleByPolicy
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelAudioContent
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLive
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicy
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveTab
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelAudioContentRecord
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelCreatorRecord
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelLiveQueryPort
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelLiveRecord
import org.springframework.beans.factory.ObjectProvider
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
@Service
@Transactional(readOnly = true)
class CreatorChannelLiveQueryService(
private val queryPortProvider: ObjectProvider<CreatorChannelLiveQueryPort>,
private val queryPolicy: CreatorChannelLiveReplayQueryPolicy,
private val memberContentPreferenceService: MemberContentPreferenceService,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun getLiveTab(
creatorId: Long,
viewer: Member,
sort: ContentSort,
page: Int,
size: Int,
now: LocalDateTime = LocalDateTime.now()
): CreatorChannelLiveTab {
val livePage = queryPolicy.createPage(page, size)
val queryPort = queryPortProvider.getObject()
val viewerId = viewer.id!!
val creator = queryPort.findCreator(creatorId, viewerId)
?: throw SodaException(messageKey = "member.validation.user_not_found")
if (queryPort.existsBlockedBetween(viewerId, creatorId)) {
val messageTemplate = messageSource
.getMessage("explorer.creator.blocked_access", langContext.lang)
.orEmpty()
throw SodaException(message = String.format(messageTemplate, creator.nickname))
}
validateCreatorRole(creator)
val preference = memberContentPreferenceService.getStoredPreference(viewer)
val canViewAdultContent = isAdultVisibleByPolicy(viewer, preference.isAdultContentVisible)
val isViewerCreator = viewerId == creatorId
val effectiveViewerGender = viewer.effectiveGender()
val fetchedContents = queryPort.findLiveReplayAudioContents(
creatorId = creatorId,
viewerId = viewerId,
now = now,
canViewAdultContent = canViewAdultContent,
sort = sort,
offset = livePage.offset,
limit = livePage.fetchLimit
)
return CreatorChannelLiveTab(
liveReplayContentCount = queryPort.countLiveReplayAudioContents(
creatorId = creatorId,
now = now,
canViewAdultContent = canViewAdultContent
),
currentLive = queryPort.findCurrentLive(
creatorId = creatorId,
now = now,
canViewAdultContent = canViewAdultContent,
viewerId = viewerId,
isViewerCreator = isViewerCreator,
effectiveViewerGender = effectiveViewerGender
)?.toDomain(),
liveReplayContents = queryPolicy.limitItems(fetchedContents, livePage).map { it.toDomain() },
sort = sort,
page = livePage,
hasNext = queryPolicy.hasNext(fetchedContents, livePage)
)
}
private fun validateCreatorRole(creator: CreatorChannelCreatorRecord) {
when (creator.role) {
MemberRole.CREATOR -> return
else -> throw SodaException(messageKey = "member.validation.creator_not_found")
}
}
private fun Member.effectiveGender(): Gender {
auth?.let { return if (it.gender == 1) Gender.MALE else Gender.FEMALE }
return gender
}
private fun CreatorChannelLiveRecord.toDomain() = CreatorChannelLive(
liveId = liveId,
title = title,
coverImageUrl = coverImagePath.toCdnUrl(),
beginDateTime = beginDateTime,
price = price,
isAdult = isAdult
)
private fun CreatorChannelAudioContentRecord.toDomain() = CreatorChannelAudioContent(
audioContentId = audioContentId,
title = title,
duration = duration,
imageUrl = imagePath.toCdnUrl(),
price = price,
isAdult = isAdult,
isPointAvailable = isPointAvailable,
isFirstContent = isFirstContent,
publishedAt = publishedAt,
seriesName = seriesName,
isOriginalSeries = isOriginalSeries,
isOwned = isOwned,
isRented = isRented
)
private fun String?.toCdnUrl(): String? {
if (isNullOrBlank()) return null
if (startsWith("https://") || startsWith("http://")) return this
return "$cloudFrontHost/$this"
}
}

View File

@@ -0,0 +1,68 @@
package kr.co.vividnext.sodalive.v2.creator.channel.live.port.out
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import java.time.LocalDateTime
interface CreatorChannelLiveQueryPort {
fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord?
fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
fun findCurrentLive(
creatorId: Long,
now: LocalDateTime,
canViewAdultContent: Boolean,
viewerId: Long?,
isViewerCreator: Boolean,
effectiveViewerGender: Gender?
): CreatorChannelLiveRecord?
fun countLiveReplayAudioContents(
creatorId: Long,
now: LocalDateTime,
canViewAdultContent: Boolean
): Int
fun findLiveReplayAudioContents(
creatorId: Long,
viewerId: Long?,
now: LocalDateTime,
canViewAdultContent: Boolean,
sort: ContentSort,
offset: Long,
limit: Int
): List<CreatorChannelAudioContentRecord>
}
data class CreatorChannelCreatorRecord(
val creatorId: Long,
val role: MemberRole,
val nickname: String
)
data class CreatorChannelLiveRecord(
val liveId: Long,
val title: String,
val coverImagePath: String?,
val beginDateTime: LocalDateTime,
val price: Int,
val isAdult: Boolean
)
data class CreatorChannelAudioContentRecord(
val audioContentId: Long,
val title: String,
val duration: String?,
val imagePath: String?,
val price: Int,
val isAdult: Boolean,
val isPointAvailable: Boolean,
val isFirstContent: Boolean,
val publishedAt: LocalDateTime,
val seriesName: String?,
val isOriginalSeries: Boolean?,
val isOwned: Boolean,
val isRented: Boolean
)