feat(content-all): 전체 탭 API 응답 조립을 추가한다

This commit is contained in:
2026-06-25 11:26:56 +09:00
parent 24556c1987
commit 9bd0ce712e
4 changed files with 272 additions and 0 deletions

View File

@@ -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
)
)
}
}

View File

@@ -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<MainContentAudioResponse>,
val series: List<MainContentSeriesResponse>,
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
)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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") })
}
}