fix(creator): 오디오 탭 theme 선택 정규화를 보정한다
This commit is contained in:
@@ -10,7 +10,13 @@ import kr.co.vividnext.sodalive.common.ApiResponse
|
|||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.v2.common.data.ContentSort
|
import kr.co.vividnext.sodalive.v2.common.data.ContentSort
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.audio.data.CreatorChannelAudioTabResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.data.CreatorChannelAudioTabResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.CreatorChannelAudioContentUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.CreatorChannelAudioRateUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.CreatorChannelAudioThemeUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.effectiveSelectedThemeId
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.toAudioContentUiModels
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.toRateUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.toThemeUiModels
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
||||||
|
|
||||||
class CreatorChannelAudioViewModel(
|
class CreatorChannelAudioViewModel(
|
||||||
@@ -74,7 +80,7 @@ class CreatorChannelAudioViewModel(
|
|||||||
val current = _audioStateLiveData.value as? CreatorChannelAudioUiState.Content ?: content
|
val current = _audioStateLiveData.value as? CreatorChannelAudioUiState.Content ?: content
|
||||||
if (response.success && data != null) {
|
if (response.success && data != null) {
|
||||||
_audioStateLiveData.value = current.copy(
|
_audioStateLiveData.value = current.copy(
|
||||||
audioContents = current.audioContents + data.displayableAudioContents(),
|
audioContents = current.audioContents + data.audioContents.toAudioContentUiModels(),
|
||||||
page = data.page,
|
page = data.page,
|
||||||
size = data.size,
|
size = data.size,
|
||||||
hasNext = data.hasNext,
|
hasNext = data.hasNext,
|
||||||
@@ -102,7 +108,8 @@ class CreatorChannelAudioViewModel(
|
|||||||
requestAudio(page = FIRST_PAGE, sort = sort, themeId = themeId, generation = generation) { response ->
|
requestAudio(page = FIRST_PAGE, sort = sort, themeId = themeId, generation = generation) { response ->
|
||||||
val data = response.data
|
val data = response.data
|
||||||
if (response.success && data != null) {
|
if (response.success && data != null) {
|
||||||
val audioContents = data.displayableAudioContents()
|
selectedThemeId = data.effectiveSelectedThemeId()
|
||||||
|
val audioContents = data.audioContents.toAudioContentUiModels()
|
||||||
_audioStateLiveData.value = if (audioContents.isEmpty() || data.audioContentCount == 0) {
|
_audioStateLiveData.value = if (audioContents.isEmpty() || data.audioContentCount == 0) {
|
||||||
CreatorChannelAudioUiState.Empty
|
CreatorChannelAudioUiState.Empty
|
||||||
} else {
|
} else {
|
||||||
@@ -153,18 +160,15 @@ class CreatorChannelAudioViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CreatorChannelAudioTabResponse.displayableAudioContents(): List<CreatorChannelAudioContentResponse> =
|
|
||||||
audioContents.filter { it.duration != null }
|
|
||||||
|
|
||||||
private fun CreatorChannelAudioTabResponse.toContentState(
|
private fun CreatorChannelAudioTabResponse.toContentState(
|
||||||
audioContents: List<CreatorChannelAudioContentResponse>,
|
audioContents: List<CreatorChannelAudioContentUiModel>,
|
||||||
isLoadingMore: Boolean = false
|
isLoadingMore: Boolean = false
|
||||||
) = CreatorChannelAudioUiState.Content(
|
) = CreatorChannelAudioUiState.Content(
|
||||||
audioContentCount = audioContentCount,
|
audioContentCount = audioContentCount,
|
||||||
themes = toThemeUiModels(),
|
themes = toThemeUiModels(),
|
||||||
selectedSort = sort,
|
selectedSort = sort,
|
||||||
selectedThemeId = themeId,
|
selectedThemeId = effectiveSelectedThemeId(),
|
||||||
rate = toRateUiModel(),
|
rate = toRateUiModel(isOwner),
|
||||||
audioContents = audioContents,
|
audioContents = audioContents,
|
||||||
page = page,
|
page = page,
|
||||||
size = size,
|
size = size,
|
||||||
@@ -172,33 +176,11 @@ class CreatorChannelAudioViewModel(
|
|||||||
isLoadingMore = isLoadingMore
|
isLoadingMore = isLoadingMore
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun CreatorChannelAudioTabResponse.toThemeUiModels(): List<CreatorChannelAudioThemeUiModel> =
|
|
||||||
listOf(CreatorChannelAudioThemeUiModel(themeId = null, title = ALL_THEME_TITLE, isSelected = themeId == null)) +
|
|
||||||
themes.map { theme ->
|
|
||||||
CreatorChannelAudioThemeUiModel(
|
|
||||||
themeId = theme.themeId,
|
|
||||||
title = theme.themeName,
|
|
||||||
isSelected = theme.themeId == themeId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CreatorChannelAudioTabResponse.toRateUiModel(): CreatorChannelAudioRateUiModel? =
|
|
||||||
if (!isOwner && themeId == null) {
|
|
||||||
CreatorChannelAudioRateUiModel(
|
|
||||||
ratePercent = purchasedAudioContentRate,
|
|
||||||
purchasedCount = purchasedAudioContentCount,
|
|
||||||
paidCount = paidAudioContentCount
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
|
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val DEFAULT_PAGE_SIZE = 20
|
val DEFAULT_PAGE_SIZE = 20
|
||||||
private const val FIRST_PAGE = 0
|
private const val FIRST_PAGE = 0
|
||||||
private const val ALL_THEME_TITLE = "전체"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +194,7 @@ sealed interface CreatorChannelAudioUiState {
|
|||||||
val selectedSort: ContentSort,
|
val selectedSort: ContentSort,
|
||||||
val selectedThemeId: Long?,
|
val selectedThemeId: Long?,
|
||||||
val rate: CreatorChannelAudioRateUiModel?,
|
val rate: CreatorChannelAudioRateUiModel?,
|
||||||
val audioContents: List<CreatorChannelAudioContentResponse>,
|
val audioContents: List<CreatorChannelAudioContentUiModel>,
|
||||||
val page: Int,
|
val page: Int,
|
||||||
val size: Int,
|
val size: Int,
|
||||||
val hasNext: Boolean,
|
val hasNext: Boolean,
|
||||||
@@ -220,15 +202,3 @@ sealed interface CreatorChannelAudioUiState {
|
|||||||
val paginationErrorMessage: String? = null
|
val paginationErrorMessage: String? = null
|
||||||
) : CreatorChannelAudioUiState
|
) : CreatorChannelAudioUiState
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CreatorChannelAudioThemeUiModel(
|
|
||||||
val themeId: Long?,
|
|
||||||
val title: String,
|
|
||||||
val isSelected: Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
data class CreatorChannelAudioRateUiModel(
|
|
||||||
val ratePercent: Double,
|
|
||||||
val purchasedCount: Int,
|
|
||||||
val paidCount: Int
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
|||||||
import kr.co.vividnext.sodalive.v2.common.data.ContentSort
|
import kr.co.vividnext.sodalive.v2.common.data.ContentSort
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.audio.data.CreatorChannelAudioTabResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.data.CreatorChannelAudioTabResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.audio.data.CreatorChannelAudioThemeResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.data.CreatorChannelAudioThemeResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.audio.model.CreatorChannelAudioRateUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@@ -151,6 +152,64 @@ class CreatorChannelAudioViewModelTest {
|
|||||||
verifyGetAudio(sort = ContentSort.POPULAR, themeId = 10L)
|
verifyGetAudio(sort = ContentSort.POPULAR, themeId = 10L)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `응답 themeId가 서버 themes에 없으면 selectedThemeId를 null로 정규화하고 후속 정렬 요청도 null로 보낸다`() {
|
||||||
|
stubGetAudio(response = Single.just(ApiResponse(true, audioResponse(themeId = 99L), null)))
|
||||||
|
stubGetAudio(
|
||||||
|
sort = ContentSort.POPULAR,
|
||||||
|
themeId = null,
|
||||||
|
response = Single.just(ApiResponse(true, audioResponse(sort = ContentSort.POPULAR, themeId = null), null))
|
||||||
|
)
|
||||||
|
viewModel.loadAudio(100L, isOwner = false)
|
||||||
|
|
||||||
|
val initialState = viewModel.audioStateLiveData.requireValue() as CreatorChannelAudioUiState.Content
|
||||||
|
assertNull(initialState.selectedThemeId)
|
||||||
|
assertEquals(CreatorChannelAudioRateUiModel(75.0, 3, 4), initialState.rate)
|
||||||
|
|
||||||
|
viewModel.changeSort(ContentSort.POPULAR)
|
||||||
|
|
||||||
|
val sortedState = viewModel.audioStateLiveData.requireValue() as CreatorChannelAudioUiState.Content
|
||||||
|
assertNull(sortedState.selectedThemeId)
|
||||||
|
verifyGetAudio(sort = ContentSort.POPULAR, themeId = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `선택 테마 응답 themeId가 서버 themes에 없으면 내부 selectedThemeId도 null로 정규화한다`() {
|
||||||
|
stubGetAudio(response = Single.just(ApiResponse(true, audioResponse(themeId = null), null)))
|
||||||
|
stubGetAudio(
|
||||||
|
themeId = 10L,
|
||||||
|
response = Single.just(ApiResponse(true, audioResponse(themeId = 99L), null))
|
||||||
|
)
|
||||||
|
stubGetAudio(
|
||||||
|
sort = ContentSort.POPULAR,
|
||||||
|
themeId = null,
|
||||||
|
response = Single.just(ApiResponse(true, audioResponse(sort = ContentSort.POPULAR, themeId = null), null))
|
||||||
|
)
|
||||||
|
whenever(
|
||||||
|
repository.getAudio(
|
||||||
|
100L,
|
||||||
|
0,
|
||||||
|
CreatorChannelAudioViewModel.DEFAULT_PAGE_SIZE,
|
||||||
|
ContentSort.POPULAR,
|
||||||
|
10L,
|
||||||
|
"Bearer test-token"
|
||||||
|
)
|
||||||
|
).thenThrow(AssertionError("정규화 후 정렬 요청은 이전 선택 themeId를 사용하면 안 된다"))
|
||||||
|
viewModel.loadAudio(100L, isOwner = false)
|
||||||
|
|
||||||
|
viewModel.changeTheme(10L)
|
||||||
|
|
||||||
|
val themeState = viewModel.audioStateLiveData.requireValue() as CreatorChannelAudioUiState.Content
|
||||||
|
assertNull(themeState.selectedThemeId)
|
||||||
|
|
||||||
|
viewModel.changeSort(ContentSort.POPULAR)
|
||||||
|
|
||||||
|
val sortedState = viewModel.audioStateLiveData.requireValue() as CreatorChannelAudioUiState.Content
|
||||||
|
assertNull(sortedState.selectedThemeId)
|
||||||
|
verifyGetAudio(themeId = 10L)
|
||||||
|
verifyGetAudio(sort = ContentSort.POPULAR, themeId = null)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `같은 정렬 또는 같은 테마를 다시 선택하면 API를 재호출하지 않는다`() {
|
fun `같은 정렬 또는 같은 테마를 다시 선택하면 API를 재호출하지 않는다`() {
|
||||||
stubGetAudio(response = Single.just(ApiResponse(true, audioResponse(), null)))
|
stubGetAudio(response = Single.just(ApiResponse(true, audioResponse(), null)))
|
||||||
|
|||||||
Reference in New Issue
Block a user