diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModel.kt index 8d93dd0d..5875b75f 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModel.kt @@ -10,7 +10,13 @@ import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SharedPreferenceManager 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.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 class CreatorChannelAudioViewModel( @@ -74,7 +80,7 @@ class CreatorChannelAudioViewModel( val current = _audioStateLiveData.value as? CreatorChannelAudioUiState.Content ?: content if (response.success && data != null) { _audioStateLiveData.value = current.copy( - audioContents = current.audioContents + data.displayableAudioContents(), + audioContents = current.audioContents + data.audioContents.toAudioContentUiModels(), page = data.page, size = data.size, hasNext = data.hasNext, @@ -102,7 +108,8 @@ class CreatorChannelAudioViewModel( requestAudio(page = FIRST_PAGE, sort = sort, themeId = themeId, generation = generation) { response -> val data = response.data 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) { CreatorChannelAudioUiState.Empty } else { @@ -153,18 +160,15 @@ class CreatorChannelAudioViewModel( ) } - private fun CreatorChannelAudioTabResponse.displayableAudioContents(): List = - audioContents.filter { it.duration != null } - private fun CreatorChannelAudioTabResponse.toContentState( - audioContents: List, + audioContents: List, isLoadingMore: Boolean = false ) = CreatorChannelAudioUiState.Content( audioContentCount = audioContentCount, themes = toThemeUiModels(), selectedSort = sort, - selectedThemeId = themeId, - rate = toRateUiModel(), + selectedThemeId = effectiveSelectedThemeId(), + rate = toRateUiModel(isOwner), audioContents = audioContents, page = page, size = size, @@ -172,33 +176,11 @@ class CreatorChannelAudioViewModel( isLoadingMore = isLoadingMore ) - private fun CreatorChannelAudioTabResponse.toThemeUiModels(): List = - 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}" companion object { val DEFAULT_PAGE_SIZE = 20 private const val FIRST_PAGE = 0 - private const val ALL_THEME_TITLE = "전체" } } @@ -212,7 +194,7 @@ sealed interface CreatorChannelAudioUiState { val selectedSort: ContentSort, val selectedThemeId: Long?, val rate: CreatorChannelAudioRateUiModel?, - val audioContents: List, + val audioContents: List, val page: Int, val size: Int, val hasNext: Boolean, @@ -220,15 +202,3 @@ sealed interface CreatorChannelAudioUiState { val paginationErrorMessage: String? = null ) : 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 -) diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModelTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModelTest.kt index 77b85388..67c80f87 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModelTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/audio/CreatorChannelAudioViewModelTest.kt @@ -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.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.model.CreatorChannelAudioRateUiModel import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository import org.junit.After @@ -151,6 +152,64 @@ class CreatorChannelAudioViewModelTest { 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 fun `같은 정렬 또는 같은 테마를 다시 선택하면 API를 재호출하지 않는다`() { stubGetAudio(response = Single.just(ApiResponse(true, audioResponse(), null)))