feat(creator): 채널 라이브 탭 응답 조립을 추가한다

This commit is contained in:
2026-06-17 20:19:38 +09:00
parent 90c0af0c8b
commit f78772b613
3 changed files with 237 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.live.application
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.live.dto.CreatorChannelLiveTabResponse
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryService
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
@Service
@Transactional(readOnly = true)
class CreatorChannelLiveFacade(
private val creatorChannelLiveQueryService: CreatorChannelLiveQueryService
) {
fun getLiveTab(
creatorId: Long,
viewer: Member,
sort: ContentSort,
page: Int,
size: Int,
now: LocalDateTime = LocalDateTime.now()
): CreatorChannelLiveTabResponse {
return CreatorChannelLiveTabResponse.from(
creatorChannelLiveQueryService.getLiveTab(
creatorId = creatorId,
viewer = viewer,
sort = sort,
page = page,
size = size,
now = now
)
)
}
}

View File

@@ -0,0 +1,101 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.live.dto
import com.fasterxml.jackson.annotation.JsonProperty
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.CreatorChannelLiveTab
import java.time.LocalDateTime
import java.time.ZoneOffset
data class CreatorChannelLiveTabResponse(
val liveReplayContentCount: Int,
val currentLive: CreatorChannelLiveResponse?,
val liveReplayContents: List<CreatorChannelAudioContentResponse>,
val sort: ContentSort,
val page: Int,
val size: Int,
@JsonProperty("hasNext")
val hasNext: Boolean
) {
companion object {
fun from(tab: CreatorChannelLiveTab): CreatorChannelLiveTabResponse {
return CreatorChannelLiveTabResponse(
liveReplayContentCount = tab.liveReplayContentCount,
currentLive = tab.currentLive?.let(CreatorChannelLiveResponse::from),
liveReplayContents = tab.liveReplayContents.map(CreatorChannelAudioContentResponse::from),
sort = tab.sort,
page = tab.page.page,
size = tab.page.size,
hasNext = tab.hasNext
)
}
}
}
data class CreatorChannelAudioContentResponse(
val audioContentId: Long,
val title: String,
val duration: String?,
val imageUrl: String?,
val price: Int,
@JsonProperty("isAdult")
val isAdult: Boolean,
@JsonProperty("isPointAvailable")
val isPointAvailable: Boolean,
@JsonProperty("isFirstContent")
val isFirstContent: Boolean,
val seriesName: String?,
@JsonProperty("isOriginalSeries")
val isOriginalSeries: Boolean?,
@JsonProperty("isOwned")
val isOwned: Boolean,
@JsonProperty("isRented")
val isRented: Boolean
) {
companion object {
fun from(content: CreatorChannelAudioContent): CreatorChannelAudioContentResponse {
return CreatorChannelAudioContentResponse(
audioContentId = content.audioContentId,
title = content.title,
duration = content.duration,
imageUrl = content.imageUrl,
price = content.price,
isAdult = content.isAdult,
isPointAvailable = content.isPointAvailable,
isFirstContent = content.isFirstContent,
seriesName = content.seriesName,
isOriginalSeries = content.isOriginalSeries,
isOwned = content.isOwned,
isRented = content.isRented
)
}
}
}
data class CreatorChannelLiveResponse(
val liveId: Long,
val title: String,
val coverImageUrl: String?,
val beginDateTimeUtc: String,
val price: Int,
@JsonProperty("isAdult")
val isAdult: Boolean
) {
companion object {
fun from(live: CreatorChannelLive): CreatorChannelLiveResponse {
return CreatorChannelLiveResponse(
liveId = live.liveId,
title = live.title,
coverImageUrl = live.coverImageUrl,
beginDateTimeUtc = live.beginDateTime.toUtcIso(),
price = live.price,
isAdult = live.isAdult
)
}
}
}
private fun LocalDateTime.toUtcIso(): String {
return atOffset(ZoneOffset.UTC).toInstant().toString()
}

View File

@@ -0,0 +1,101 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.live.application
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryService
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.CreatorChannelLiveTab
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelPage
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.mockito.Mockito
import java.time.LocalDateTime
class CreatorChannelLiveFacadeTest {
@Test
@DisplayName("라이브 탭 facade는 query service 결과를 공개 응답 DTO로 변환한다")
fun shouldMapLiveTabQueryResultToPublicResponse() {
val service = Mockito.mock(CreatorChannelLiveQueryService::class.java)
val facade = CreatorChannelLiveFacade(service)
val viewer = createMember(id = 10L)
val now = LocalDateTime.of(2026, 6, 17, 10, 0)
Mockito.doReturn(createTab()).`when`(service).getLiveTab(
creatorId = 1L,
viewer = viewer,
sort = ContentSort.LATEST,
page = 0,
size = 20,
now = now
)
val response = facade.getLiveTab(
creatorId = 1L,
viewer = viewer,
sort = ContentSort.LATEST,
page = 0,
size = 20,
now = now
)
assertEquals(1, response.liveReplayContentCount)
assertEquals(101L, response.currentLive?.liveId)
assertEquals("2026-06-17T01:00:00Z", response.currentLive?.beginDateTimeUtc)
assertEquals(201L, response.liveReplayContents.first().audioContentId)
assertTrue(response.liveReplayContents.first().isOwned)
assertFalse(response.liveReplayContents.first().isRented)
assertEquals(ContentSort.LATEST, response.sort)
assertEquals(0, response.page)
assertEquals(20, response.size)
assertFalse(response.hasNext)
}
private fun createMember(id: Long): Member {
return Member(
email = "viewer$id@test.com",
password = "password",
nickname = "viewer$id",
role = MemberRole.USER
).apply {
this.id = id
}
}
private fun createTab(): CreatorChannelLiveTab {
return CreatorChannelLiveTab(
liveReplayContentCount = 1,
currentLive = CreatorChannelLive(
liveId = 101L,
title = "live",
coverImageUrl = "live.png",
beginDateTime = LocalDateTime.of(2026, 6, 17, 1, 0),
price = 20,
isAdult = true
),
liveReplayContents = listOf(
CreatorChannelAudioContent(
audioContentId = 201L,
title = "audio",
duration = "00:10:00",
imageUrl = "audio.png",
price = 30,
isAdult = false,
isPointAvailable = true,
isFirstContent = true,
publishedAt = LocalDateTime.of(2026, 6, 16, 1, 0),
seriesName = "series",
isOriginalSeries = true,
isOwned = true,
isRented = false
)
),
sort = ContentSort.LATEST,
page = CreatorChannelPage(page = 0, size = 20),
hasNext = false
)
}
}