diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacade.kt new file mode 100644 index 00000000..177638a1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacade.kt @@ -0,0 +1,36 @@ +package kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application + +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.v2.api.creator.channel.audio.dto.CreatorChannelAudioTabResponse +import kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Service +@Transactional(readOnly = true) +class CreatorChannelAudioFacade( + private val creatorChannelAudioQueryService: CreatorChannelAudioQueryService +) { + fun getAudioTab( + creatorId: Long, + viewer: Member, + sort: String?, + themeId: Long?, + page: Int?, + size: Int?, + now: LocalDateTime = LocalDateTime.now() + ): CreatorChannelAudioTabResponse { + return CreatorChannelAudioTabResponse.from( + creatorChannelAudioQueryService.getAudioTab( + creatorId = creatorId, + viewer = viewer, + sort = sort, + themeId = themeId, + page = page, + size = size, + now = now + ) + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/dto/CreatorChannelAudioTabResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/dto/CreatorChannelAudioTabResponse.kt new file mode 100644 index 00000000..b33a20ed --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/dto/CreatorChannelAudioTabResponse.kt @@ -0,0 +1,54 @@ +package kr.co.vividnext.sodalive.v2.api.creator.channel.audio.dto + +import com.fasterxml.jackson.annotation.JsonProperty +import kr.co.vividnext.sodalive.v2.api.creator.channel.common.dto.CreatorChannelAudioContentResponse +import kr.co.vividnext.sodalive.v2.common.domain.ContentSort +import kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioTab +import kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioTheme + +data class CreatorChannelAudioTabResponse( + val audioContentCount: Int, + val paidAudioContentCount: Int, + val purchasedAudioContentCount: Int, + val purchasedAudioContentRate: Double, + val themes: List, + val audioContents: List, + val sort: ContentSort, + val themeId: Long?, + val page: Int, + val size: Int, + @JsonProperty("hasNext") + val hasNext: Boolean +) { + companion object { + fun from(tab: CreatorChannelAudioTab): CreatorChannelAudioTabResponse { + return CreatorChannelAudioTabResponse( + audioContentCount = tab.audioContentCount, + paidAudioContentCount = tab.paidAudioContentCount, + purchasedAudioContentCount = tab.purchasedAudioContentCount, + purchasedAudioContentRate = tab.purchasedAudioContentRate, + themes = tab.themes.map(CreatorChannelAudioThemeResponse::from), + audioContents = tab.audioContents.map(CreatorChannelAudioContentResponse::from), + sort = tab.sort, + themeId = tab.themeId, + page = tab.page.page, + size = tab.page.size, + hasNext = tab.hasNext + ) + } + } +} + +data class CreatorChannelAudioThemeResponse( + val themeId: Long, + val themeName: String +) { + companion object { + fun from(theme: CreatorChannelAudioTheme): CreatorChannelAudioThemeResponse { + return CreatorChannelAudioThemeResponse( + themeId = theme.themeId, + themeName = theme.themeName + ) + } + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacadeTest.kt new file mode 100644 index 00000000..42e673bb --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacadeTest.kt @@ -0,0 +1,110 @@ +package kr.co.vividnext.sodalive.v2.api.creator.channel.audio.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.common.domain.ContentSort +import kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryService +import kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioTab +import kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioTheme +import kr.co.vividnext.sodalive.v2.creator.channel.common.domain.CreatorChannelAudioContent +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 CreatorChannelAudioFacadeTest { + @Test + @DisplayName("오디오 탭 facade는 query service 결과를 공개 응답 DTO로 변환한다") + fun shouldMapAudioTabQueryResultToPublicResponse() { + val service = Mockito.mock(CreatorChannelAudioQueryService::class.java) + val facade = CreatorChannelAudioFacade(service) + val viewer = createMember(id = 10L) + val now = LocalDateTime.of(2026, 6, 19, 10, 0) + Mockito.doReturn(createTab()).`when`(service).getAudioTab( + creatorId = 1L, + viewer = viewer, + sort = "OWNED", + themeId = 10L, + page = 0, + size = 20, + now = now + ) + + val response = facade.getAudioTab( + creatorId = 1L, + viewer = viewer, + sort = "OWNED", + themeId = 10L, + page = 0, + size = 20, + now = now + ) + + assertEquals(3, response.audioContentCount) + assertEquals(2, response.paidAudioContentCount) + assertEquals(1, response.purchasedAudioContentCount) + assertEquals(50.0, response.purchasedAudioContentRate) + assertEquals(10L, response.themes.first().themeId) + assertEquals("theme", response.themes.first().themeName) + assertEquals(201L, response.audioContents.first().audioContentId) + assertTrue(response.audioContents.first().isOwned) + assertFalse(response.audioContents.first().isRented) + assertEquals(ContentSort.OWNED, response.sort) + assertEquals(10L, response.themeId) + assertEquals(0, response.page) + assertEquals(20, response.size) + assertTrue(response.hasNext) + + val json = ObjectMapper().registerModule(KotlinModule.Builder().build()).readTree( + ObjectMapper().registerModule(KotlinModule.Builder().build()).writeValueAsString(response) + ) + assertTrue(json["hasNext"].asBoolean()) + assertTrue(json["audioContents"][0]["isOwned"].asBoolean()) + assertFalse(json["audioContents"][0]["isRented"].asBoolean()) + } + + 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(): CreatorChannelAudioTab { + return CreatorChannelAudioTab( + audioContentCount = 3, + paidAudioContentCount = 2, + purchasedAudioContentCount = 1, + purchasedAudioContentRate = 50.0, + themes = listOf(CreatorChannelAudioTheme(themeId = 10L, themeName = "theme")), + audioContents = listOf( + CreatorChannelAudioContent( + audioContentId = 201L, + title = "audio", + duration = "00:10:00", + imageUrl = "audio.png", + price = 30, + isAdult = false, + isPointAvailable = true, + isFirstContent = true, + seriesName = "series", + isOriginalSeries = true, + isOwned = true, + isRented = false + ) + ), + sort = ContentSort.OWNED, + themeId = 10L, + page = CreatorChannelPage(page = 0, size = 20), + hasNext = true + ) + } +}