diff --git a/profile/presentation/src/main/kotlin/com/ruleup/profile/presentation/edit/viewmodel/ProfileEditViewModel.kt b/profile/presentation/src/main/kotlin/com/ruleup/profile/presentation/edit/viewmodel/ProfileEditViewModel.kt index d152d49e8..ce0ff12ab 100644 --- a/profile/presentation/src/main/kotlin/com/ruleup/profile/presentation/edit/viewmodel/ProfileEditViewModel.kt +++ b/profile/presentation/src/main/kotlin/com/ruleup/profile/presentation/edit/viewmodel/ProfileEditViewModel.kt @@ -17,6 +17,7 @@ import com.ruleup.ui.error.userFacingMessage import com.ruleup.ui.mvi.MviViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import java.time.LocalDate import java.time.OffsetDateTime @@ -113,10 +114,13 @@ class ProfileEditViewModel dispatch(ProfileEditReducerEvent.Loading) viewModelScope.launch { runCatching { - val profileDeferred = async { profileRepository.getProfile() } - // 마스터 조회 실패는 기본 상한(6)으로 흡수한다. - val catalogDeferred = async { runCatching { profileRepository.getCategories() }.getOrNull() } - profileDeferred.await() to catalogDeferred.await() + // coroutineScope 없이 launch 에 바로 async 를 걸면 getProfile 실패가 launch 까지 무너뜨려 크래시한다. + coroutineScope { + val profileDeferred = async { profileRepository.getProfile() } + // 마스터 조회 실패는 기본 상한(6)으로 흡수한다. + val catalogDeferred = async { runCatching { profileRepository.getCategories() }.getOrNull() } + profileDeferred.await() to catalogDeferred.await() + } }.onSuccess { (profile, catalog) -> dispatch( ProfileEditReducerEvent.Loaded( diff --git a/profile/presentation/src/main/kotlin/com/ruleup/profile/presentation/settings/viewmodel/SettingsViewModel.kt b/profile/presentation/src/main/kotlin/com/ruleup/profile/presentation/settings/viewmodel/SettingsViewModel.kt index 9565a24e9..b71c0104e 100644 --- a/profile/presentation/src/main/kotlin/com/ruleup/profile/presentation/settings/viewmodel/SettingsViewModel.kt +++ b/profile/presentation/src/main/kotlin/com/ruleup/profile/presentation/settings/viewmodel/SettingsViewModel.kt @@ -93,20 +93,20 @@ class SettingsViewModel private fun load() { viewModelScope.launch { - // 넷은 서로 독립이라 함께 던진다. - val loaded = + // 다섯은 서로 독립이라 함께 던진다. + val (loaded, seenAnswers) = coroutineScope { val a = async { runCatching { accountRepository.getAgreements() }.getOrNull() } val s = async { runCatching { accountRepository.getSanctions() }.getOrNull() } val p = async { runCatching { profileRepository.getMyProfile() }.getOrNull() } val i = async { runCatching { inquiryRepository.getInquiries() }.getOrNull() } - SettingsLoad(a.await(), s.await(), p.await(), i.await()) + val seen = async { runCatching { inquiryReadStore.seenAnswers() }.getOrDefault(emptyMap()) } + SettingsLoad(a.await(), s.await(), p.await(), i.await()) to seen.await() } if (loaded.isEmpty) { dispatch(SettingsReducerEvent.LoadFinished) return@launch } - val seenAnswers = runCatching { inquiryReadStore.seenAnswers() }.getOrDefault(emptyMap()) dispatch( SettingsReducerEvent.Loaded( provider = diff --git a/profile/presentation/src/main/kotlin/com/ruleup/profile/presentation/tier/viewmodel/MyTierHistoryViewModel.kt b/profile/presentation/src/main/kotlin/com/ruleup/profile/presentation/tier/viewmodel/MyTierHistoryViewModel.kt index 6784d615e..9a1d5bb90 100644 --- a/profile/presentation/src/main/kotlin/com/ruleup/profile/presentation/tier/viewmodel/MyTierHistoryViewModel.kt +++ b/profile/presentation/src/main/kotlin/com/ruleup/profile/presentation/tier/viewmodel/MyTierHistoryViewModel.kt @@ -8,6 +8,8 @@ import com.ruleup.profile.domain.repository.MyPageRepository import com.ruleup.ui.mvi.MviViewModel import com.ruleup.ui.mvi.NoEffect import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -66,8 +68,12 @@ class MyTierHistoryViewModel if (currentState.history != null || currentState.changes.isNotEmpty()) return dispatch(MyTierHistoryReducerEvent.Loading) viewModelScope.launch { - val history = runCatching { myPageRepository.getTierHistory() }.getOrNull() - val changes = runCatching { myPageRepository.getScoreChanges() } + val (history, changes) = + coroutineScope { + val history = async { runCatching { myPageRepository.getTierHistory() }.getOrNull() } + val changes = async { runCatching { myPageRepository.getScoreChanges() } } + history.await() to changes.await() + } changes .onSuccess { dispatch(loaded(history, it)) } diff --git a/profile/presentation/src/test/kotlin/com/ruleup/profile/presentation/edit/viewmodel/ProfileEditViewModelTest.kt b/profile/presentation/src/test/kotlin/com/ruleup/profile/presentation/edit/viewmodel/ProfileEditViewModelTest.kt index d36424da7..23f43ffc1 100644 --- a/profile/presentation/src/test/kotlin/com/ruleup/profile/presentation/edit/viewmodel/ProfileEditViewModelTest.kt +++ b/profile/presentation/src/test/kotlin/com/ruleup/profile/presentation/edit/viewmodel/ProfileEditViewModelTest.kt @@ -22,6 +22,8 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertTrue /** 프로필 편집. */ @@ -54,6 +56,17 @@ class ProfileEditViewModelTest { assertTrue(viewModel.uiState.value.maxSelectable > 0) } + @Test + fun `프로필 조회가 실패하면 크래시 없이 오류를 보여준다`() = + runTest { + val viewModel = viewModel(repo(loadProfile = { throw IllegalStateException("서버 오류") })) + + viewModel.onIntent(ProfileEditIntent.Load) + + assertNotNull(viewModel.uiState.value.errorMessage) + assertFalse(viewModel.uiState.value.isLoading) + } + @Test fun `바꾼 게 없으면 저장하지 않는다`() = runTest { @@ -350,6 +363,7 @@ class ProfileEditViewModelTest { } private fun repo( + loadProfile: (() -> Profile)? = null, categories: (() -> com.ruleup.profile.domain.entity.CategoryCatalog)? = { catalog() }, checkNickname: ((String) -> NicknameCheck)? = { NicknameCheck(valid = true, available = true, reason = null) }, updateProfile: (() -> Profile)? = { profile() }, @@ -357,7 +371,7 @@ class ProfileEditViewModelTest { uploadImage: ((String) -> String)? = { "https://example.com/new.jpg" }, deleteImage: () -> Unit = {}, ) = FakeProfileRepository( - profile = { profile().let { it.copy(user = it.user.copy(profileImageUrl = imageUrl)) } }, + profile = loadProfile ?: { profile().let { it.copy(user = it.user.copy(profileImageUrl = imageUrl)) } }, categories = categories, checkNickname = checkNickname, updateProfile = updateProfile, diff --git a/support/presentation/src/main/kotlin/com/ruleup/support/presentation/list/viewmodel/InquiryListViewModel.kt b/support/presentation/src/main/kotlin/com/ruleup/support/presentation/list/viewmodel/InquiryListViewModel.kt index 068bab37f..5ae72e2cc 100644 --- a/support/presentation/src/main/kotlin/com/ruleup/support/presentation/list/viewmodel/InquiryListViewModel.kt +++ b/support/presentation/src/main/kotlin/com/ruleup/support/presentation/list/viewmodel/InquiryListViewModel.kt @@ -11,6 +11,8 @@ import com.ruleup.support.domain.repository.InquiryRepository import com.ruleup.ui.mvi.MviViewModel import com.ruleup.ui.mvi.NoEffect import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -51,12 +53,15 @@ class InquiryListViewModel private fun load() { dispatch(InquiryListReducerEvent.Loading) viewModelScope.launch { - runCatching { inquiryRepository.getInquiries() } - .onSuccess { items -> + runCatching { + coroutineScope { // 기록을 못 읽으면 전부 새 답변으로 보인다. 목록을 막을 일은 아니다 - val seen = runCatching { inquiryReadStore.seenAnswers() }.getOrDefault(emptyMap()) - dispatch(InquiryListReducerEvent.Loaded(items, seen)) - }.onFailure { dispatch(InquiryListReducerEvent.Failed(it.userMessage())) } + val seen = async { runCatching { inquiryReadStore.seenAnswers() }.getOrDefault(emptyMap()) } + inquiryRepository.getInquiries() to seen.await() + } + }.onSuccess { (items, seen) -> + dispatch(InquiryListReducerEvent.Loaded(items, seen)) + }.onFailure { dispatch(InquiryListReducerEvent.Failed(it.userMessage())) } } } }