diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacade.kt new file mode 100644 index 00000000..b0dea149 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacade.kt @@ -0,0 +1,31 @@ +package kr.co.vividnext.sodalive.v2.api.content.all.application + +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.v2.api.content.all.dto.MainContentAllTabResponse +import kr.co.vividnext.sodalive.v2.content.all.application.MainContentAllQueryService +import org.springframework.stereotype.Component + +@Component +class MainContentAllFacade( + private val queryService: MainContentAllQueryService +) { + fun getContents( + type: String?, + sort: String?, + dayOfWeek: String?, + page: Int?, + size: Int?, + member: Member? + ): MainContentAllTabResponse { + return MainContentAllTabResponse.from( + queryService.getContents( + type = type, + sort = sort, + dayOfWeek = dayOfWeek, + page = page, + size = size, + member = member + ) + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponse.kt new file mode 100644 index 00000000..e36933d8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponse.kt @@ -0,0 +1,94 @@ +package kr.co.vividnext.sodalive.v2.api.content.all.dto + +import com.fasterxml.jackson.annotation.JsonProperty +import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek +import kr.co.vividnext.sodalive.v2.common.domain.ContentSort +import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAll +import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllAudio +import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllSeries +import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllType + +data class MainContentAllTabResponse( + val type: MainContentAllType, + val totalCount: Int, + val audios: List, + val series: List, + val sort: ContentSort, + val dayOfWeek: SeriesPublishedDaysOfWeek?, + val page: Int, + val size: Int, + @JsonProperty("hasNext") + val hasNext: Boolean +) { + companion object { + fun from(tab: MainContentAll): MainContentAllTabResponse { + return MainContentAllTabResponse( + type = tab.type, + totalCount = tab.totalCount, + audios = tab.audios.map(MainContentAudioResponse::from), + series = tab.series.map(MainContentSeriesResponse::from), + sort = tab.sort, + dayOfWeek = tab.dayOfWeek, + page = tab.page.page, + size = tab.page.size, + hasNext = tab.hasNext + ) + } + } +} + +data class MainContentAudioResponse( + val audioContentId: Long, + val title: String, + val imageUrl: String?, + val price: Int, + @JsonProperty("isAdult") + val isAdult: Boolean, + @JsonProperty("isPointAvailable") + val isPointAvailable: Boolean, + @JsonProperty("isFirstContent") + val isFirstContent: Boolean, + @JsonProperty("isOriginalSeries") + val isOriginalSeries: Boolean, + val creatorNickname: String +) { + companion object { + fun from(audio: MainContentAllAudio): MainContentAudioResponse { + return MainContentAudioResponse( + audioContentId = audio.audioContentId, + title = audio.title, + imageUrl = audio.imageUrl, + price = audio.price, + isAdult = audio.isAdult, + isPointAvailable = audio.isPointAvailable, + isFirstContent = audio.isFirstContent, + isOriginalSeries = audio.isOriginalSeries, + creatorNickname = audio.creatorNickname + ) + } + } +} + +data class MainContentSeriesResponse( + val seriesId: Long, + val title: String, + val coverImageUrl: String?, + val creatorNickname: String, + @JsonProperty("isOriginal") + val isOriginal: Boolean, + @JsonProperty("isAdult") + val isAdult: Boolean +) { + companion object { + fun from(series: MainContentAllSeries): MainContentSeriesResponse { + return MainContentSeriesResponse( + seriesId = series.seriesId, + title = series.title, + coverImageUrl = series.coverImageUrl, + creatorNickname = series.creatorNickname, + isOriginal = series.isOriginal, + isAdult = series.isAdult + ) + } + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacadeTest.kt new file mode 100644 index 00000000..fa18262b --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/application/MainContentAllFacadeTest.kt @@ -0,0 +1,54 @@ +package kr.co.vividnext.sodalive.v2.api.content.all.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.content.all.application.MainContentAllQueryService +import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAll +import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllType +import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentPage +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class MainContentAllFacadeTest { + private val queryService = Mockito.mock(MainContentAllQueryService::class.java) + private val facade = MainContentAllFacade(queryService) + + @Test + @DisplayName("facade는 문자열 query parameter와 회원을 query service에 그대로 전달하고 응답 DTO로 변환한다") + fun shouldDelegateToQueryServiceAndMapResponse() { + val member = Member( + email = "viewer@test.com", + password = "password", + nickname = "viewer", + role = MemberRole.USER + ).apply { id = 10L } + Mockito.doReturn( + MainContentAll( + type = MainContentAllType.FREE, + totalCount = 0, + audios = emptyList(), + series = emptyList(), + sort = ContentSort.PRICE_LOW, + dayOfWeek = null, + page = MainContentPage(1, 30), + hasNext = false + ) + ).`when`(queryService).getContents("FREE", "PRICE_LOW", "MON", 1, 30, member) + + val response = facade.getContents( + type = "FREE", + sort = "PRICE_LOW", + dayOfWeek = "MON", + page = 1, + size = 30, + member = member + ) + + Mockito.verify(queryService).getContents("FREE", "PRICE_LOW", "MON", 1, 30, member) + assertEquals(MainContentAllType.FREE, response.type) + assertEquals(ContentSort.PRICE_LOW, response.sort) + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponseTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponseTest.kt new file mode 100644 index 00000000..4ef413df --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponseTest.kt @@ -0,0 +1,93 @@ +package kr.co.vividnext.sodalive.v2.api.content.all.dto + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek +import kr.co.vividnext.sodalive.v2.common.domain.ContentSort +import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAll +import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllAudio +import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllSeries +import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllType +import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentPage +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 + +class MainContentAllTabResponseTest { + private val objectMapper = jacksonObjectMapper() + + @Test + @DisplayName("전체 탭 도메인을 최소 공개 응답 필드로 변환한다") + fun shouldMapDomainToResponseWithMinimalFields() { + val response = MainContentAllTabResponse.from( + MainContentAll( + type = MainContentAllType.SERIES, + totalCount = 1, + audios = emptyList(), + series = listOf( + MainContentAllSeries( + seriesId = 10L, + title = "시리즈", + coverImageUrl = "https://cdn/series.jpg", + creatorNickname = "creator", + isOriginal = true, + isAdult = false + ) + ), + sort = ContentSort.LATEST, + dayOfWeek = SeriesPublishedDaysOfWeek.MON, + page = MainContentPage(0, 20), + hasNext = false + ) + ) + + assertEquals(MainContentAllType.SERIES, response.type) + assertEquals(1, response.totalCount) + assertTrue(response.audios.isEmpty()) + assertEquals("creator", response.series.first().creatorNickname) + assertEquals(SeriesPublishedDaysOfWeek.MON, response.dayOfWeek) + } + + @Test + @DisplayName("boolean 응답 필드는 is prefix를 유지하고 제외 필드는 노출하지 않는다") + fun shouldKeepBooleanJsonNamesAndHideExcludedFields() { + val response = MainContentAllTabResponse.from( + MainContentAll( + type = MainContentAllType.AUDIO, + totalCount = 1, + audios = listOf( + MainContentAllAudio( + audioContentId = 1L, + title = "audio", + imageUrl = "https://cdn/audio.jpg", + price = 100, + isAdult = false, + isPointAvailable = true, + isFirstContent = true, + isOriginalSeries = false, + creatorNickname = "creator" + ) + ), + series = emptyList(), + sort = ContentSort.LATEST, + dayOfWeek = null, + page = MainContentPage(0, 20), + hasNext = true + ) + ) + + val json = objectMapper.readTree(objectMapper.writeValueAsString(response)) + + assertEquals(false, json["audios"][0]["isAdult"].asBoolean()) + assertEquals(true, json["audios"][0]["isPointAvailable"].asBoolean()) + assertEquals(true, json["audios"][0]["isFirstContent"].asBoolean()) + assertEquals(false, json["audios"][0]["isOriginalSeries"].asBoolean()) + assertEquals(true, json["hasNext"].asBoolean()) + assertFalse(json["audios"][0].has("duration")) + assertFalse(json["series"].any { it.has("publishedDaysOfWeek") }) + assertFalse(json["series"].any { it.has("isProceeding") }) + assertFalse(json["series"].any { it.has("contentCount") }) + assertFalse(json["series"].any { it.has("paidContentCount") }) + } +}