feat(creator-channel): 시리즈 탭 응답 변환을 추가한다

This commit is contained in:
2026-06-20 04:35:55 +09:00
parent e8b8287968
commit dd68e64628
3 changed files with 231 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.series.application
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.series.dto.CreatorChannelSeriesTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.series.application.CreatorChannelSeriesQueryService
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
@Service
@Transactional(readOnly = true)
class CreatorChannelSeriesFacade(
private val creatorChannelSeriesQueryService: CreatorChannelSeriesQueryService
) {
fun getSeriesTab(
creatorId: Long,
viewer: Member,
sort: String?,
page: Int?,
size: Int?,
now: LocalDateTime = LocalDateTime.now()
): CreatorChannelSeriesTabResponse {
return CreatorChannelSeriesTabResponse.from(
creatorChannelSeriesQueryService.getSeriesTab(
creatorId = creatorId,
viewer = viewer,
sort = sort,
page = page,
size = size,
now = now
)
)
}
}

View File

@@ -0,0 +1,64 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.series.dto
import com.fasterxml.jackson.annotation.JsonProperty
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import kr.co.vividnext.sodalive.v2.creator.channel.series.domain.CreatorChannelSeries
import kr.co.vividnext.sodalive.v2.creator.channel.series.domain.CreatorChannelSeriesTab
data class CreatorChannelSeriesTabResponse(
val seriesCount: Int,
val series: List<CreatorChannelSeriesResponse>,
val sort: ContentSort,
val page: Int,
val size: Int,
@JsonProperty("hasNext")
val hasNext: Boolean
) {
companion object {
fun from(tab: CreatorChannelSeriesTab): CreatorChannelSeriesTabResponse {
return CreatorChannelSeriesTabResponse(
seriesCount = tab.seriesCount,
series = tab.series.map(CreatorChannelSeriesResponse::from),
sort = tab.sort,
page = tab.page.page,
size = tab.page.size,
hasNext = tab.hasNext
)
}
}
}
data class CreatorChannelSeriesResponse(
val seriesId: Long,
val title: String,
val coverImageUrl: String?,
val publishedDaysOfWeek: String,
@JsonProperty("isOriginal")
val isOriginal: Boolean,
@JsonProperty("isAdult")
val isAdult: Boolean,
@JsonProperty("isProceeding")
val isProceeding: Boolean,
val contentCount: Int,
val purchasedContentCount: Int?,
val paidContentCount: Int?,
val purchasedPaidContentRate: Int?
) {
companion object {
fun from(series: CreatorChannelSeries): CreatorChannelSeriesResponse {
return CreatorChannelSeriesResponse(
seriesId = series.seriesId,
title = series.title,
coverImageUrl = series.coverImageUrl,
publishedDaysOfWeek = series.publishedDaysOfWeek,
isOriginal = series.isOriginal,
isAdult = series.isAdult,
isProceeding = series.isProceeding,
contentCount = series.contentCount,
purchasedContentCount = series.purchasedContentCount,
paidContentCount = series.paidContentCount,
purchasedPaidContentRate = series.purchasedPaidContentRate
)
}
}
}

View File

@@ -0,0 +1,133 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.series.application
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.api.creator.channel.series.dto.CreatorChannelSeriesTabResponse
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelPage
import kr.co.vividnext.sodalive.v2.creator.channel.series.application.CreatorChannelSeriesQueryService
import kr.co.vividnext.sodalive.v2.creator.channel.series.domain.CreatorChannelSeries
import kr.co.vividnext.sodalive.v2.creator.channel.series.domain.CreatorChannelSeriesTab
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
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 CreatorChannelSeriesFacadeTest {
@Test
@DisplayName("시리즈 탭 응답 DTO는 domain tab 값을 공개 응답 필드로 그대로 매핑한다")
fun shouldMapSeriesTabDomainToPublicResponse() {
val response = CreatorChannelSeriesTabResponse.from(createTab())
assertEquals(2, response.seriesCount)
assertEquals(101L, response.series.first().seriesId)
assertEquals("series", response.series.first().title)
assertEquals("https://cdn.test/cover.png", response.series.first().coverImageUrl)
assertEquals("Every Mon, Thu", response.series.first().publishedDaysOfWeek)
assertTrue(response.series.first().isOriginal)
assertFalse(response.series.first().isAdult)
assertTrue(response.series.first().isProceeding)
assertEquals(5, response.series.first().contentCount)
assertEquals(3, response.series.first().purchasedContentCount)
assertEquals(4, response.series.first().paidContentCount)
assertEquals(75, response.series.first().purchasedPaidContentRate)
assertEquals(ContentSort.OWNED, response.sort)
assertEquals(1, response.page)
assertEquals(20, response.size)
assertTrue(response.hasNext)
val mapper = ObjectMapper().registerModule(KotlinModule.Builder().build())
val json = mapper.readTree(mapper.writeValueAsString(response))
assertTrue(json["hasNext"].asBoolean())
assertTrue(json["series"][0]["isOriginal"].asBoolean())
assertFalse(json["series"][0]["isAdult"].asBoolean())
assertTrue(json["series"][0]["isProceeding"].asBoolean())
}
@Test
@DisplayName("시리즈 탭 facade는 query service 결과를 공개 응답 DTO로 변환한다")
fun shouldMapSeriesTabQueryResultToPublicResponse() {
val service = Mockito.mock(CreatorChannelSeriesQueryService::class.java)
val facade = CreatorChannelSeriesFacade(service)
val viewer = createMember(id = 10L)
val now = LocalDateTime.of(2026, 6, 20, 10, 0)
Mockito.doReturn(createTab()).`when`(service).getSeriesTab(
creatorId = 1L,
viewer = viewer,
sort = "OWNED",
page = 1,
size = 20,
now = now
)
val response = facade.getSeriesTab(
creatorId = 1L,
viewer = viewer,
sort = "OWNED",
page = 1,
size = 20,
now = now
)
assertEquals(2, response.seriesCount)
assertEquals(101L, response.series.first().seriesId)
assertEquals(75, response.series.first().purchasedPaidContentRate)
assertEquals(ContentSort.OWNED, response.sort)
assertEquals(1, response.page)
assertEquals(20, response.size)
assertTrue(response.hasNext)
assertNull(response.series.last().purchasedPaidContentRate)
}
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(): CreatorChannelSeriesTab {
return CreatorChannelSeriesTab(
seriesCount = 2,
series = listOf(
CreatorChannelSeries(
seriesId = 101L,
title = "series",
coverImageUrl = "https://cdn.test/cover.png",
publishedDaysOfWeek = "Every Mon, Thu",
isOriginal = true,
isAdult = false,
isProceeding = true,
contentCount = 5,
purchasedContentCount = 3,
paidContentCount = 4,
purchasedPaidContentRate = 75
),
CreatorChannelSeries(
seriesId = 102L,
title = "creator series",
coverImageUrl = null,
publishedDaysOfWeek = "Random",
isOriginal = false,
isAdult = false,
isProceeding = false,
contentCount = 1,
purchasedContentCount = null,
paidContentCount = null,
purchasedPaidContentRate = null
)
),
sort = ContentSort.OWNED,
page = CreatorChannelPage(page = 1, size = 20),
hasNext = true
)
}
}