diff --git a/core/designsystem/src/main/java/com/kikidan/designsystem/component/TodakunProgressIndicator.kt b/core/designsystem/src/main/java/com/kikidan/designsystem/component/TodakunProgressIndicator.kt index 54e23d35..4b93b907 100644 --- a/core/designsystem/src/main/java/com/kikidan/designsystem/component/TodakunProgressIndicator.kt +++ b/core/designsystem/src/main/java/com/kikidan/designsystem/component/TodakunProgressIndicator.kt @@ -1,6 +1,5 @@ package com.kikidan.designsystem.component -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -20,7 +19,6 @@ fun TodakunProgressIndicator(modifier: Modifier = Modifier) { modifier = modifier .fillMaxSize() - .background(TodakunColor.gray900.copy(alpha = 0.5f)) .clickable( indication = null, interactionSource = remember { MutableInteractionSource() }, diff --git a/core/designsystem/src/main/java/com/kikidan/designsystem/component/button/TodakunButton.kt b/core/designsystem/src/main/java/com/kikidan/designsystem/component/button/TodakunButton.kt index 352f494c..4f6c3994 100644 --- a/core/designsystem/src/main/java/com/kikidan/designsystem/component/button/TodakunButton.kt +++ b/core/designsystem/src/main/java/com/kikidan/designsystem/component/button/TodakunButton.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.Text @@ -64,6 +65,7 @@ fun PrimaryButton( size: TodakunButtonSize, modifier: Modifier = Modifier, enabled: Boolean = true, + isLoading: Boolean = false, painter: Painter? = null, contentDescription: String? = null, iconPosition: TodakunButtonIconPosition = TodakunButtonIconPosition.Start, @@ -75,6 +77,7 @@ fun PrimaryButton( onClick = onClick, modifier = modifier, enabled = enabled, + isLoading = isLoading, painter = painter, contentDescription = contentDescription, size = size, @@ -116,12 +119,14 @@ private fun TodakunButton( enabled: Boolean, size: TodakunButtonSize, modifier: Modifier = Modifier, + isLoading: Boolean = false, painter: Painter? = null, contentDescription: String? = null, iconPosition: TodakunButtonIconPosition = TodakunButtonIconPosition.Start, ) { + val isEnabled = enabled && !isLoading val textStyle = - if (enabled) TodakunTypography.body2SemiBold else TodakunTypography.body2Medium + if (isEnabled) TodakunTypography.body2SemiBold else TodakunTypography.body2Medium val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() @@ -135,7 +140,7 @@ private fun TodakunButton( Button( modifier = modifier.height(size.height), onClick = onClick, - enabled = enabled, + enabled = isEnabled, shape = RoundedCornerShape(12.dp), colors = ButtonDefaults.buttonColors( @@ -146,27 +151,35 @@ private fun TodakunButton( ), interactionSource = interactionSource, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(size.iconSpacing, Alignment.CenterHorizontally), - ) { - if (painter != null && iconPosition == TodakunButtonIconPosition.Start) { - Icon( - painter = painter, - contentDescription = contentDescription, - modifier = Modifier.size(size.iconSize), - ) - } - Text( - text = text, - style = textStyle, + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(size.iconSize), + color = TodakunColor.gray400, + strokeWidth = 2.dp, ) - if (painter != null && iconPosition == TodakunButtonIconPosition.End) { - Icon( - painter = painter, - contentDescription = contentDescription, - modifier = Modifier.size(size.iconSize), + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(size.iconSpacing, Alignment.CenterHorizontally), + ) { + if (painter != null && iconPosition == TodakunButtonIconPosition.Start) { + Icon( + painter = painter, + contentDescription = contentDescription, + modifier = Modifier.size(size.iconSize), + ) + } + Text( + text = text, + style = textStyle, ) + if (painter != null && iconPosition == TodakunButtonIconPosition.End) { + Icon( + painter = painter, + contentDescription = contentDescription, + modifier = Modifier.size(size.iconSize), + ) + } } } } diff --git a/feature/chat/src/main/java/com/kikidan/chat/ChatRoute.kt b/feature/chat/src/main/java/com/kikidan/chat/ChatRoute.kt index 93c45c5d..4b7f416d 100644 --- a/feature/chat/src/main/java/com/kikidan/chat/ChatRoute.kt +++ b/feature/chat/src/main/java/com/kikidan/chat/ChatRoute.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel +import com.kikidan.chat.model.ChatEntryState import com.kikidan.chat.model.ChatSideEffect import com.kikidan.chat.screen.ChatScreen import com.kikidan.chat.screen.ChatSplashScreen @@ -40,23 +41,28 @@ fun ChatRoute( } } - if (state.isLoading && conversationId == null && !skipSplash) { - ChatSplashScreen() - } else { - ChatScreen( - state = state, - onInputChange = viewModel::onInputChange, - onSendClick = viewModel::onSendClick, - onSuggestionClick = viewModel::onSuggestionClick, - onNewConversationClick = viewModel::startNewConversation, - onCloseClick = onCloseClick, - onHistoryClick = onNavigateToHistory, - onCalendarLaunchFail = { - scope.launch { - snackbarHostState.showSnackbar(calendarErrorMessage) - } - }, - modifier = modifier, - ) + val showSplash = state.entryState is ChatEntryState.Loading && conversationId == null && !skipSplash + when { + showSplash -> { + ChatSplashScreen() + } + + else -> { + ChatScreen( + state = state, + onInputChange = viewModel::onInputChange, + onSendClick = viewModel::onSendClick, + onSuggestionClick = viewModel::onSuggestionClick, + onNewConversationClick = viewModel::startNewConversation, + onCloseClick = onCloseClick, + onHistoryClick = onNavigateToHistory, + onCalendarLaunchFail = { + scope.launch { + snackbarHostState.showSnackbar(calendarErrorMessage) + } + }, + modifier = modifier, + ) + } } } diff --git a/feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt b/feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt index d9a2ef9d..079c0b8c 100644 --- a/feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt +++ b/feature/chat/src/main/java/com/kikidan/chat/ChatViewModel.kt @@ -2,6 +2,7 @@ package com.kikidan.chat import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import com.kikidan.chat.model.ChatEntryState import com.kikidan.chat.model.ChatSideEffect import com.kikidan.chat.model.ChatState import com.kikidan.chat.model.StreamingChatState @@ -44,13 +45,14 @@ class ChatViewModel ), ) - /** 화면 진입 시 1회. conversationId가 있으면 과거 대화를 먼저 채운다. */ fun load( conversationId: String?, skipSplash: Boolean = false, ) = intent { + if (state.entryState == ChatEntryState.Success && state.conversationId == conversationId) return@intent + savedStateHandle[KEY_CONVERSATION_ID] = conversationId - reduce { state.copy(conversationId = conversationId, isLoading = true) } + reduce { state.copy(conversationId = conversationId, entryState = ChatEntryState.Loading) } val startedAt = System.currentTimeMillis() getChatEntry() @@ -77,7 +79,7 @@ class ChatViewModel delay(MIN_LOADING_DURATION_MILLIS - elapsed) } } - reduce { state.copy(isLoading = false) } + reduce { state.copy(entryState = ChatEntryState.Success) } } fun onInputChange(value: String) = diff --git a/feature/chat/src/main/java/com/kikidan/chat/HistoryRoute.kt b/feature/chat/src/main/java/com/kikidan/chat/HistoryRoute.kt index a2d303ec..c3446d5f 100644 --- a/feature/chat/src/main/java/com/kikidan/chat/HistoryRoute.kt +++ b/feature/chat/src/main/java/com/kikidan/chat/HistoryRoute.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.kikidan.chat.model.HistorySideEffect -import com.kikidan.chat.model.toUiModel import com.kikidan.chat.screen.HistoryScreen import com.kikidan.designsystem.R import org.orbitmvi.orbit.compose.collectAsState @@ -35,7 +34,7 @@ fun HistoryRoute( } HistoryScreen( - conversations = state.conversations.map { it.toUiModel() }, + state = state, onBackClick = onBackClick, onConversationClick = onNavigateToChat, onDeleteClick = viewModel::onDeleteClick, diff --git a/feature/chat/src/main/java/com/kikidan/chat/HistoryViewModel.kt b/feature/chat/src/main/java/com/kikidan/chat/HistoryViewModel.kt index 70b0b808..28604798 100644 --- a/feature/chat/src/main/java/com/kikidan/chat/HistoryViewModel.kt +++ b/feature/chat/src/main/java/com/kikidan/chat/HistoryViewModel.kt @@ -19,30 +19,28 @@ class HistoryViewModel private val deleteConversation: DeleteConversationUseCase, ) : ViewModel(), ContainerHost { - override val container = container(HistoryState()) + override val container = container(HistoryState.Loading) fun load() = intent { - reduce { state.copy(isLoading = true) } + reduce { HistoryState.Loading } getConversations() .onSuccess { conversations -> - reduce { state.copy(conversations = conversations.toPersistentList(), isLoading = false) } + reduce { HistoryState.Success(conversations.toPersistentList()) } }.onFailure { - reduce { state.copy(isLoading = false) } + reduce { HistoryState.Failure } postSideEffect(HistorySideEffect.Error(it)) } } fun onDeleteClick(conversationId: String) = intent { + val current = state as? HistoryState.Success ?: return@intent deleteConversation(conversationId) .onSuccess { reduce { - state.copy( - conversations = - state.conversations - .filterNot { it.id == conversationId } - .toPersistentList(), + HistoryState.Success( + current.conversations.filterNot { it.id == conversationId }.toPersistentList(), ) } }.onFailure { postSideEffect(HistorySideEffect.Error(it)) } diff --git a/feature/chat/src/main/java/com/kikidan/chat/model/ChatState.kt b/feature/chat/src/main/java/com/kikidan/chat/model/ChatState.kt index ad996150..e9d523cc 100644 --- a/feature/chat/src/main/java/com/kikidan/chat/model/ChatState.kt +++ b/feature/chat/src/main/java/com/kikidan/chat/model/ChatState.kt @@ -8,7 +8,7 @@ import kotlinx.collections.immutable.persistentListOf data class ChatState( val conversationId: String? = null, - val isLoading: Boolean = true, + val entryState: ChatEntryState = ChatEntryState.Loading, val greeting: String = "", val suggestions: PersistentList = persistentListOf(), val quota: ChatQuota? = null, @@ -17,6 +17,14 @@ data class ChatState( val streamingChatState: StreamingChatState = StreamingChatState.Idle, ) +sealed interface ChatEntryState { + data object Loading : ChatEntryState + + data object Success : ChatEntryState + + data object Failure : ChatEntryState +} + sealed interface StreamingChatState { data object Idle : StreamingChatState diff --git a/feature/chat/src/main/java/com/kikidan/chat/model/HistoryState.kt b/feature/chat/src/main/java/com/kikidan/chat/model/HistoryState.kt index 61352b0d..744c5456 100644 --- a/feature/chat/src/main/java/com/kikidan/chat/model/HistoryState.kt +++ b/feature/chat/src/main/java/com/kikidan/chat/model/HistoryState.kt @@ -2,9 +2,13 @@ package com.kikidan.chat.model import com.kikidan.domain.model.chat.ConversationSummary import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf -data class HistoryState( - val isLoading: Boolean = true, - val conversations: PersistentList = persistentListOf(), -) +sealed interface HistoryState { + data object Loading : HistoryState + + data class Success( + val conversations: PersistentList, + ) : HistoryState + + data object Failure : HistoryState +} diff --git a/feature/chat/src/main/java/com/kikidan/chat/screen/HistoryScreen.kt b/feature/chat/src/main/java/com/kikidan/chat/screen/HistoryScreen.kt index 3f5e8343..6627b515 100644 --- a/feature/chat/src/main/java/com/kikidan/chat/screen/HistoryScreen.kt +++ b/feature/chat/src/main/java/com/kikidan/chat/screen/HistoryScreen.kt @@ -35,17 +35,23 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import com.kikidan.chat.model.HistoryConversationUiModel +import com.kikidan.chat.model.HistoryState +import com.kikidan.chat.model.toUiModel import com.kikidan.designsystem.R +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.chat.TodakunChatHistoryItem import com.kikidan.designsystem.component.header.TodakunSubHeader import com.kikidan.designsystem.theme.TodakunColor import com.kikidan.designsystem.theme.TodakunTheme import com.kikidan.designsystem.theme.TodakunTypography +import com.kikidan.domain.model.chat.ConversationSummary +import kotlinx.collections.immutable.persistentListOf +import java.time.Instant +import java.time.temporal.ChronoUnit @Composable internal fun HistoryScreen( - conversations: List, + state: HistoryState, onBackClick: () -> Unit, onConversationClick: (String) -> Unit, onDeleteClick: (String) -> Unit, @@ -69,24 +75,32 @@ internal fun HistoryScreen( onBackClick = onBackClick, ) - if (conversations.isEmpty()) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text( - text = stringResource(R.string.chat_history_empty), - style = TodakunTypography.body2Regular, - color = TodakunColor.gray500, - ) + when (state) { + is HistoryState.Loading, HistoryState.Failure -> { + Box(modifier = Modifier.fillMaxSize()) } - } else { - LazyColumn(modifier = Modifier.fillMaxSize()) { - items(conversations, key = { it.id }) { conversation -> - TodakunChatHistoryItem( - title = conversation.title, - relativeTime = conversation.relativeTime, - isUnread = conversation.isUnread, - onClick = { onConversationClick(conversation.id) }, - onDeleteClick = { onDeleteClick(conversation.id) }, - ) + + is HistoryState.Success -> { + if (state.conversations.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = stringResource(R.string.chat_history_empty), + style = TodakunTypography.body2Regular, + color = TodakunColor.gray500, + ) + } + } else { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(state.conversations.map { it.toUiModel() }, key = { it.id }) { conversation -> + TodakunChatHistoryItem( + title = conversation.title, + relativeTime = conversation.relativeTime, + isUnread = conversation.isUnread, + onClick = { onConversationClick(conversation.id) }, + onDeleteClick = { onDeleteClick(conversation.id) }, + ) + } + } } } } @@ -101,6 +115,11 @@ internal fun HistoryScreen( .align(Alignment.BottomEnd) .padding(end = 20.dp, bottom = 60.dp), ) + + when (state) { + is HistoryState.Loading -> TodakunProgressIndicator() + is HistoryState.Success, HistoryState.Failure -> Unit + } } } @@ -160,10 +179,25 @@ private fun NewChatButton( } private val previewConversations = - listOf( - HistoryConversationUiModel(id = "1", title = "오늘 나의 행운의 숫자는?", relativeTime = "30분 전", isUnread = true), - HistoryConversationUiModel(id = "2", title = "이직할까 말까?", relativeTime = "3시간 전", isUnread = false), - HistoryConversationUiModel(id = "3", title = "이번 달 큰 지출 해도 괜찮을까", relativeTime = "1일 전", isUnread = false), + persistentListOf( + ConversationSummary( + id = "1", + title = "오늘 나의 행운의 숫자는?", + lastMessageAt = Instant.now().minus(30, ChronoUnit.MINUTES), + unread = true, + ), + ConversationSummary( + id = "2", + title = "이직할까 말까?", + lastMessageAt = Instant.now().minus(3, ChronoUnit.HOURS), + unread = false, + ), + ConversationSummary( + id = "3", + title = "이번 달 큰 지출 해도 괜찮을까", + lastMessageAt = Instant.now().minus(1, ChronoUnit.DAYS), + unread = false, + ), ) @Preview(showBackground = true, name = "목록") @@ -171,7 +205,7 @@ private val previewConversations = private fun HistoryScreenListPreview() { TodakunTheme { HistoryScreen( - conversations = previewConversations, + state = HistoryState.Success(previewConversations), onBackClick = {}, onConversationClick = {}, onDeleteClick = {}, @@ -185,7 +219,7 @@ private fun HistoryScreenListPreview() { private fun HistoryScreenEmptyPreview() { TodakunTheme { HistoryScreen( - conversations = emptyList(), + state = HistoryState.Success(persistentListOf()), onBackClick = {}, onConversationClick = {}, onDeleteClick = {}, diff --git a/feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt b/feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt index 783021f5..1a8468c5 100644 --- a/feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt +++ b/feature/chat/src/test/java/com/kikidan/chat/ChatViewModelTest.kt @@ -1,6 +1,7 @@ package com.kikidan.chat import androidx.lifecycle.SavedStateHandle +import com.kikidan.chat.model.ChatEntryState import com.kikidan.chat.model.ChatSideEffect import com.kikidan.chat.model.ChatState import com.kikidan.chat.model.StreamingChatState @@ -20,7 +21,6 @@ import com.kikidan.domain.usecase.GetConversationDetailUseCase import com.kikidan.domain.usecase.SendChatMessageUseCase import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue @@ -38,16 +38,16 @@ class ChatViewModelTest { vm.test(this) { containerHost.load(null) - // 첫 reduce: copy(conversationId=null, isLoading=true) → 초기 상태와 동일, 미방출 + // 첫 reduce: copy(conversationId=null, entryState=Loading) → 초기 상태와 동일, 미방출 // getChatEntry 성공 → copy(greeting, suggestions, quota) 변경 val s1 = awaitState() assertEquals(defaultEntry.greeting, s1.greeting) assertEquals(defaultEntry.suggestions, s1.suggestions) assertEquals(defaultEntry.quota, s1.quota) - assertTrue(s1.isLoading) - // 최종 reduce: isLoading=false + assertTrue(s1.entryState is ChatEntryState.Loading) + // 최종 reduce: entryState=Success val s2 = awaitState() - assertFalse(s2.isLoading) + assertEquals(ChatEntryState.Success, s2.entryState) assertNull(s2.conversationId) } } @@ -64,7 +64,7 @@ class ChatViewModelTest { val se = awaitSideEffect() assertTrue(se is ChatSideEffect.Error) val s = awaitState() - assertFalse(s.isLoading) + assertEquals(ChatEntryState.Success, s.entryState) } } @@ -88,9 +88,9 @@ class ChatViewModelTest { awaitState() // 3: getConversationDetail 성공 → messages awaitState() - // 4: isLoading=false + // 4: entryState=Success val s4 = awaitState() - assertFalse(s4.isLoading) + assertEquals(ChatEntryState.Success, s4.entryState) assertEquals("c-1", s4.conversationId) assertEquals(msgs, s4.messages) } @@ -485,6 +485,25 @@ class ChatViewModelTest { } } + @Test + fun `이미 Success 상태에서 같은 conversationId로 load 재호출 시 entryState가 Loading으로 되돌아가지 않는다`() = + runTest { + // 뒤로가기로 같은 화면(nav entry)에 재진입하면 LaunchedEffect(Unit)가 다시 실행되어 + // load()가 재호출되는 상황을 재현한다. 이미 로드된 상태라면 스플래시가 재노출되면 안 된다. + val fakeRepo = FakeChatRepository().apply { chatEntryResult = Result.success(defaultEntry) } + val vm = viewModel(fakeRepo) + + vm.test(this) { + containerHost.load(null) + awaitState() // entry 로드 + val loaded = awaitState() // entryState=Success + assertEquals(ChatEntryState.Success, loaded.entryState) + + containerHost.load(null) + expectNoItems() + } + } + @Test fun `startNewConversation 호출 시 conversationId = null, messages 비워짐, suggestions 유지`() = runTest { @@ -501,7 +520,7 @@ class ChatViewModelTest { awaitState() // conversationId 설정 awaitState() // entry 로드 awaitState() // messages 로드 - val afterLoad = awaitState() // isLoading=false + val afterLoad = awaitState() // entryState=Success assertEquals("c-1", afterLoad.conversationId) assertEquals(msgs, afterLoad.messages) assertEquals(defaultEntry.suggestions, afterLoad.suggestions) diff --git a/feature/home/src/main/java/com/kikidan/home/component/FortuneDetailBottomSheet.kt b/feature/home/src/main/java/com/kikidan/home/component/FortuneDetailBottomSheet.kt index fa270a1a..00c8467f 100644 --- a/feature/home/src/main/java/com/kikidan/home/component/FortuneDetailBottomSheet.kt +++ b/feature/home/src/main/java/com/kikidan/home/component/FortuneDetailBottomSheet.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.kikidan.designsystem.R +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.button.PrimaryButton import com.kikidan.designsystem.component.button.TodakunButtonSize import com.kikidan.designsystem.theme.TodakunColor @@ -69,7 +70,9 @@ internal fun FortuneDetailBottomSheet( ) when (detail) { is DetailSheetUiState.Loading -> { - Unit + Box(modifier = Modifier.fillMaxWidth().height(240.dp)) { + TodakunProgressIndicator() + } } is DetailSheetUiState.Success -> { diff --git a/feature/home/src/main/java/com/kikidan/home/screen/FortuneReportScreen.kt b/feature/home/src/main/java/com/kikidan/home/screen/FortuneReportScreen.kt index 86a29941..5af814ba 100644 --- a/feature/home/src/main/java/com/kikidan/home/screen/FortuneReportScreen.kt +++ b/feature/home/src/main/java/com/kikidan/home/screen/FortuneReportScreen.kt @@ -61,6 +61,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.kikidan.designsystem.R +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.TodakunWhiteTooltip import com.kikidan.designsystem.theme.TodakunColor import com.kikidan.designsystem.theme.TodakunTheme @@ -194,6 +195,11 @@ internal fun FortuneReportScreen( onDismissRequest = onDetailDismiss, ) } + + when (state) { + FortuneReportState.Loading -> TodakunProgressIndicator() + is FortuneReportState.Success, FortuneReportState.Failure -> Unit + } } } diff --git a/feature/home/src/main/java/com/kikidan/home/screen/HomeScreen.kt b/feature/home/src/main/java/com/kikidan/home/screen/HomeScreen.kt index 5ca5056b..f2a930e2 100644 --- a/feature/home/src/main/java/com/kikidan/home/screen/HomeScreen.kt +++ b/feature/home/src/main/java/com/kikidan/home/screen/HomeScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.kikidan.designsystem.R +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.theme.TodakunColor import com.kikidan.designsystem.theme.TodakunTheme import com.kikidan.designsystem.theme.TodakunTypography @@ -118,6 +119,11 @@ internal fun HomeScreen( onDismissRequest = onDetailDismiss, ) } + + when (state) { + is HomeState.Loading -> TodakunProgressIndicator() + is HomeState.Success, HomeState.Failure -> Unit + } } } diff --git a/feature/luck-action/src/main/java/com/kikidan/luckaction/LuckActionViewModel.kt b/feature/luck-action/src/main/java/com/kikidan/luckaction/LuckActionViewModel.kt index 2a9fdcfa..15fec347 100644 --- a/feature/luck-action/src/main/java/com/kikidan/luckaction/LuckActionViewModel.kt +++ b/feature/luck-action/src/main/java/com/kikidan/luckaction/LuckActionViewModel.kt @@ -53,10 +53,13 @@ class LuckActionViewModel fun toggleAction(id: String) = intent { val current = state as? LuckActionUiState.Success ?: return@intent + if (current.isRefreshing) return@intent + reduce { current.copy(isRefreshing = true) } toggleLuckAction(id) .onSuccess { updated -> reduce { current.copy( + isRefreshing = false, actions = current.actions .map { if (it.id == id) it.copy(achieved = updated.achieved) else it } @@ -65,7 +68,10 @@ class LuckActionViewModel if (updated.achieved) updated.category else current.completionOverlayCategory, ) } - }.onFailure { postSideEffect(LuckActionSideEffect.Error(it)) } + }.onFailure { + reduce { current.copy(isRefreshing = false) } + postSideEffect(LuckActionSideEffect.Error(it)) + } } fun dismissCompleteOverlay() = diff --git a/feature/luck-action/src/main/java/com/kikidan/luckaction/screen/LuckActionScreen.kt b/feature/luck-action/src/main/java/com/kikidan/luckaction/screen/LuckActionScreen.kt index 575f3381..b5116527 100644 --- a/feature/luck-action/src/main/java/com/kikidan/luckaction/screen/LuckActionScreen.kt +++ b/feature/luck-action/src/main/java/com/kikidan/luckaction/screen/LuckActionScreen.kt @@ -34,6 +34,7 @@ import com.kikidan.designsystem.R import com.kikidan.designsystem.component.TodakunBadge import com.kikidan.designsystem.component.TodakunBadgeType import com.kikidan.designsystem.component.TodakunCheckbox +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.header.TodakunMainHeader import com.kikidan.designsystem.theme.TodakunColor import com.kikidan.designsystem.theme.TodakunTheme @@ -94,6 +95,12 @@ fun LuckActionScreen( ) } } + + when { + state is LuckActionUiState.Loading -> TodakunProgressIndicator() + state is LuckActionUiState.Success && state.isRefreshing -> TodakunProgressIndicator() + else -> Unit + } } } diff --git a/feature/luck-action/src/test/java/com/kikidan/luckaction/LuckActionViewModelTest.kt b/feature/luck-action/src/test/java/com/kikidan/luckaction/LuckActionViewModelTest.kt index f93c2613..ab17ec8e 100644 --- a/feature/luck-action/src/test/java/com/kikidan/luckaction/LuckActionViewModelTest.kt +++ b/feature/luck-action/src/test/java/com/kikidan/luckaction/LuckActionViewModelTest.kt @@ -266,6 +266,7 @@ class LuckActionViewModelTest { vm.test(this, initialState = initial) { containerHost.toggleAction("1") + awaitState() // isRefreshing = true val s = awaitState() as LuckActionUiState.Success assertTrue(s.actions[0].achieved) assertEquals(FortuneCategory.LOVE, s.completionOverlayCategory) diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/edit/MyPageEditViewModel.kt b/feature/mypage/src/main/java/com/kikidan/mypage/edit/MyPageEditViewModel.kt index b7885b70..49460b27 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/edit/MyPageEditViewModel.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/edit/MyPageEditViewModel.kt @@ -87,6 +87,7 @@ class MyPageEditViewModel fun save() = intent { val currentState = state as? MyPageEditUiState.Success ?: return@intent + if (currentState.isSaving) return@intent val model = currentState.model val user = User( @@ -102,9 +103,13 @@ class MyPageEditViewModel time = model.birthTime, ), ) - updateUserUseCase(user).onSuccess { - postSideEffect(MyPageEditSideEffect.NavigateBack) - } + reduce { currentState.copy(isSaving = true) } + updateUserUseCase(user) + .onSuccess { + postSideEffect(MyPageEditSideEffect.NavigateBack) + }.onFailure { + reduce { currentState.copy(isSaving = false) } + } } private fun updateModel(transform: MyPageEditUiModel.() -> MyPageEditUiModel) = diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/edit/model/MyPageEditUiState.kt b/feature/mypage/src/main/java/com/kikidan/mypage/edit/model/MyPageEditUiState.kt index d15819c1..14797cc3 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/edit/model/MyPageEditUiState.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/edit/model/MyPageEditUiState.kt @@ -5,6 +5,7 @@ sealed interface MyPageEditUiState { data class Success( val model: MyPageEditUiModel, + val isSaving: Boolean = false, ) : MyPageEditUiState data class Fail( diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/edit/ui/MyPageEditScreen.kt b/feature/mypage/src/main/java/com/kikidan/mypage/edit/ui/MyPageEditScreen.kt index 62a0b2da..54d4bfef 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/edit/ui/MyPageEditScreen.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/edit/ui/MyPageEditScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.kikidan.designsystem.R import com.kikidan.designsystem.component.TodakunCheckbox +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.TodakunSelectBox import com.kikidan.designsystem.component.TodakunSelectField import com.kikidan.designsystem.component.button.PrimaryButton @@ -78,7 +79,7 @@ fun MyPageEditScreen( when (uiState) { is MyPageEditUiState.Loading -> { - Box(modifier = Modifier.weight(1f).fillMaxSize()) + TodakunProgressIndicator(modifier = Modifier.weight(1f)) } is MyPageEditUiState.Fail -> { @@ -88,6 +89,7 @@ fun MyPageEditScreen( is MyPageEditUiState.Success -> { MyPageEditContent( model = uiState.model, + isSaving = uiState.isSaving, onGenderSelect = onGenderSelect, onDateTypeSelect = onDateTypeSelect, onBirthDateChange = onBirthDateChange, @@ -104,6 +106,7 @@ fun MyPageEditScreen( @Composable private fun MyPageEditContent( model: MyPageEditUiModel, + isSaving: Boolean, onGenderSelect: (Gender) -> Unit, onDateTypeSelect: (DateType) -> Unit, onBirthDateChange: (LocalDate) -> Unit, @@ -228,6 +231,7 @@ private fun MyPageEditContent( text = stringResource(R.string.mypage_edit_save_button), onClick = onSaveClick, size = TodakunButtonSize.Large, + isLoading = isSaving, modifier = Modifier .fillMaxWidth() diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/MyPageHomeViewModel.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/MyPageHomeViewModel.kt index ba8b74ab..bb3b59cb 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/home/MyPageHomeViewModel.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/MyPageHomeViewModel.kt @@ -48,8 +48,14 @@ class MyPageHomeViewModel fun logout() = intent { + val currentState = state as? MyPageHomeUiState.Success ?: return@intent + if (currentState.isLoggingOut) return@intent + reduce { currentState.copy(isLoggingOut = true) } logoutUseCase() .onSuccess { postSideEffect(MyPageHomeSideEffect.NavigateToLogin) } - .onFailure { postSideEffect(MyPageHomeSideEffect.ShowLogoutError) } + .onFailure { + reduce { currentState.copy(isLoggingOut = false) } + postSideEffect(MyPageHomeSideEffect.ShowLogoutError) + } } } diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeUiState.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeUiState.kt index f5f510e5..4d27ce1c 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeUiState.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeUiState.kt @@ -5,6 +5,7 @@ sealed interface MyPageHomeUiState { data class Success( val model: MyPageHomeUiModel, + val isLoggingOut: Boolean = false, ) : MyPageHomeUiState data class Fail( diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/MyPageHomeScreen.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/MyPageHomeScreen.kt index 5f9de56b..87c39df6 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/MyPageHomeScreen.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/MyPageHomeScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -24,6 +23,7 @@ import androidx.compose.ui.unit.dp import com.kikidan.designsystem.R import com.kikidan.designsystem.component.TodakunDivider import com.kikidan.designsystem.component.TodakunDividerType +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.theme.TodakunColor import com.kikidan.designsystem.theme.TodakunTheme import com.kikidan.designsystem.theme.TodakunTypography @@ -54,32 +54,46 @@ fun MyPageHomeScreen( onViewMansaeryeokClick: () -> Unit = {}, onMenuItemClick: (MyPageMenuType) -> Unit = {}, ) { - Column( + Box( modifier = modifier .fillMaxSize() .background(TodakunColor.white) .systemBarsPadding(), ) { - MyPageHomeHeader(modifier = Modifier.fillMaxWidth()) + Column(modifier = Modifier.fillMaxSize()) { + MyPageHomeHeader(modifier = Modifier.fillMaxWidth()) - when (uiState) { - is MyPageHomeUiState.Loading -> { - MyPageHomeLoading(modifier = Modifier.weight(1f)) - } + when (uiState) { + is MyPageHomeUiState.Loading -> { + MyPageHomeLoading(modifier = Modifier.weight(1f)) + } + + is MyPageHomeUiState.Fail -> { + MyPageHomeError(modifier = Modifier.weight(1f)) + } - is MyPageHomeUiState.Fail -> { - MyPageHomeError(modifier = Modifier.weight(1f)) + is MyPageHomeUiState.Success -> { + MyPageHomeContent( + model = uiState.model, + onEditClick = onEditClick, + onViewMansaeryeokClick = onViewMansaeryeokClick, + onMenuItemClick = onMenuItemClick, + modifier = Modifier.weight(1f), + ) + } } + } + when (uiState) { is MyPageHomeUiState.Success -> { - MyPageHomeContent( - model = uiState.model, - onEditClick = onEditClick, - onViewMansaeryeokClick = onViewMansaeryeokClick, - onMenuItemClick = onMenuItemClick, - modifier = Modifier.weight(1f), - ) + if (uiState.isLoggingOut) { + TodakunProgressIndicator() + } + } + + is MyPageHomeUiState.Loading, is MyPageHomeUiState.Fail -> { + Unit } } } @@ -87,9 +101,7 @@ fun MyPageHomeScreen( @Composable private fun MyPageHomeLoading(modifier: Modifier = Modifier) { - // 로딩 요구사항 없음 - Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - } + TodakunProgressIndicator(modifier = modifier) } @Composable diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/mansaeryeok/ui/MansaeryeokDetailScreen.kt b/feature/mypage/src/main/java/com/kikidan/mypage/mansaeryeok/ui/MansaeryeokDetailScreen.kt index e3149439..c05df876 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/mansaeryeok/ui/MansaeryeokDetailScreen.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/mansaeryeok/ui/MansaeryeokDetailScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.kikidan.designsystem.R +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.header.TodakunSubHeader import com.kikidan.designsystem.theme.TodakunColor import com.kikidan.designsystem.theme.TodakunTheme @@ -80,7 +81,7 @@ fun MansaeryeokDetailScreen( when (uiState) { is MansaeryeokDetailUiState.Loading -> { - Box(modifier = Modifier.weight(1f).fillMaxSize()) + TodakunProgressIndicator(modifier = Modifier.weight(1f)) } is MansaeryeokDetailUiState.Fail -> { diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/notification/ui/NotificationSettingScreen.kt b/feature/mypage/src/main/java/com/kikidan/mypage/notification/ui/NotificationSettingScreen.kt index f7a26bfa..18a1d2d8 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/notification/ui/NotificationSettingScreen.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/notification/ui/NotificationSettingScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.kikidan.designsystem.R +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.TodakunToggle import com.kikidan.designsystem.component.header.TodakunSubHeader import com.kikidan.designsystem.component.wheelpicker.TimeWheelPicker @@ -67,7 +68,7 @@ fun NotificationSettingScreen( when (uiState) { is NotificationSettingUiState.Loading -> { - Box(modifier = Modifier.weight(1f).fillMaxSize()) + TodakunProgressIndicator(modifier = Modifier.weight(1f)) } is NotificationSettingUiState.Fail -> { diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/partner/PartnerSajuManagementViewModel.kt b/feature/mypage/src/main/java/com/kikidan/mypage/partner/PartnerSajuManagementViewModel.kt index 62c87295..cda473df 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/partner/PartnerSajuManagementViewModel.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/partner/PartnerSajuManagementViewModel.kt @@ -38,10 +38,17 @@ class PartnerSajuManagementViewModel fun deletePartner(linkId: String) = intent { val currentState = state as? PartnerSajuManagementUiState.Success ?: return@intent - deletePartnerSajuUseCase(linkId).onSuccess { - val updated = currentState.model.partners.filterNot { it.linkId == linkId } - reduce { currentState.copy(model = currentState.model.copy(partners = updated)) } - } + if (currentState.isDeleting) return@intent + reduce { currentState.copy(isDeleting = true) } + deletePartnerSajuUseCase(linkId) + .onSuccess { + val updated = currentState.model.partners.filterNot { it.linkId == linkId } + reduce { + currentState.copy(isDeleting = false, model = currentState.model.copy(partners = updated)) + } + }.onFailure { + reduce { currentState.copy(isDeleting = false) } + } } fun addPartner() = diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/partner/form/PartnerSajuFormViewModel.kt b/feature/mypage/src/main/java/com/kikidan/mypage/partner/form/PartnerSajuFormViewModel.kt index d8d09cc0..a6b60a68 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/partner/form/PartnerSajuFormViewModel.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/partner/form/PartnerSajuFormViewModel.kt @@ -88,6 +88,7 @@ class PartnerSajuFormViewModel fun save() = intent { val currentState = state as? PartnerSajuFormUiState.Success ?: return@intent + if (currentState.isSaving) return@intent val model = currentState.model val birthDate = model.birthDate ?: return@intent val input = @@ -97,15 +98,19 @@ class PartnerSajuFormViewModel relationshipTypeCode = model.relationshipTypeCode, birth = Birth(dateType = model.dateType, date = birthDate, time = model.birthTime), ) + reduce { currentState.copy(isSaving = true) } val result = if (model.linkId != null) { updatePartnerSajuUseCase(model.linkId, input) } else { registerPartnerSajuUseCase(input) } - result.onSuccess { - postSideEffect(PartnerSajuFormSideEffect.NavigateBack) - } + result + .onSuccess { + postSideEffect(PartnerSajuFormSideEffect.NavigateBack) + }.onFailure { + reduce { currentState.copy(isSaving = false) } + } } private fun updateModel(transform: (PartnerSajuFormUiModel) -> PartnerSajuFormUiModel) = diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/partner/form/model/PartnerSajuFormUiState.kt b/feature/mypage/src/main/java/com/kikidan/mypage/partner/form/model/PartnerSajuFormUiState.kt index 23f5d0f5..c5e4a242 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/partner/form/model/PartnerSajuFormUiState.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/partner/form/model/PartnerSajuFormUiState.kt @@ -5,6 +5,7 @@ sealed interface PartnerSajuFormUiState { data class Success( val model: PartnerSajuFormUiModel, + val isSaving: Boolean = false, ) : PartnerSajuFormUiState data class Fail( diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/partner/form/ui/PartnerSajuFormScreen.kt b/feature/mypage/src/main/java/com/kikidan/mypage/partner/form/ui/PartnerSajuFormScreen.kt index 50325829..5de62ce5 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/partner/form/ui/PartnerSajuFormScreen.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/partner/form/ui/PartnerSajuFormScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.kikidan.designsystem.R import com.kikidan.designsystem.component.TodakunCheckbox +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.TodakunSelectBox import com.kikidan.designsystem.component.TodakunSelectField import com.kikidan.designsystem.component.TodakunTextField @@ -79,7 +80,7 @@ fun PartnerSajuFormScreen( when (uiState) { is PartnerSajuFormUiState.Loading -> { - Box(modifier = Modifier.weight(1f).fillMaxSize()) + TodakunProgressIndicator(modifier = Modifier.weight(1f)) } is PartnerSajuFormUiState.Fail -> { @@ -89,6 +90,7 @@ fun PartnerSajuFormScreen( is PartnerSajuFormUiState.Success -> { PartnerSajuFormContent( model = uiState.model, + isSaving = uiState.isSaving, onNameChange = onNameChange, onGenderSelect = onGenderSelect, onDateTypeSelect = onDateTypeSelect, @@ -106,6 +108,7 @@ fun PartnerSajuFormScreen( @Composable private fun PartnerSajuFormContent( model: PartnerSajuFormUiModel, + isSaving: Boolean, onNameChange: (String) -> Unit, onGenderSelect: (Gender) -> Unit, onDateTypeSelect: (DateType) -> Unit, @@ -255,6 +258,7 @@ private fun PartnerSajuFormContent( text = stringResource(R.string.partner_saju_form_save_button), onClick = onSaveClick, size = TodakunButtonSize.Large, + isLoading = isSaving, modifier = Modifier .align(Alignment.BottomCenter) diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/partner/model/PartnerSajuManagementUiState.kt b/feature/mypage/src/main/java/com/kikidan/mypage/partner/model/PartnerSajuManagementUiState.kt index c64ef9d3..76afa313 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/partner/model/PartnerSajuManagementUiState.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/partner/model/PartnerSajuManagementUiState.kt @@ -5,6 +5,7 @@ sealed interface PartnerSajuManagementUiState { data class Success( val model: PartnerSajuManagementUiModel, + val isDeleting: Boolean = false, ) : PartnerSajuManagementUiState data class Fail( diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/partner/ui/PartnerSajuManagementScreen.kt b/feature/mypage/src/main/java/com/kikidan/mypage/partner/ui/PartnerSajuManagementScreen.kt index af428172..9ea751d2 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/partner/ui/PartnerSajuManagementScreen.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/partner/ui/PartnerSajuManagementScreen.kt @@ -39,6 +39,7 @@ import com.kikidan.designsystem.R import com.kikidan.designsystem.component.TodakunBadge import com.kikidan.designsystem.component.TodakunBadgeType import com.kikidan.designsystem.component.TodakunPopover +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.TodakunSnackbar import com.kikidan.designsystem.component.header.TodakunSubHeader import com.kikidan.designsystem.theme.TodakunColor @@ -91,7 +92,7 @@ fun PartnerSajuManagementScreen( when (uiState) { is PartnerSajuManagementUiState.Loading -> { - Box(modifier = Modifier.weight(1f).fillMaxSize()) + TodakunProgressIndicator(modifier = Modifier.weight(1f)) } is PartnerSajuManagementUiState.Fail -> { @@ -119,6 +120,18 @@ fun PartnerSajuManagementScreen( .padding(bottom = 24.dp), ) } + + when (uiState) { + is PartnerSajuManagementUiState.Success -> { + if (uiState.isDeleting) { + TodakunProgressIndicator() + } + } + + is PartnerSajuManagementUiState.Loading, is PartnerSajuManagementUiState.Fail -> { + Unit + } + } } } diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/setting/AppSettingWithdrawalNoticeViewModel.kt b/feature/mypage/src/main/java/com/kikidan/mypage/setting/AppSettingWithdrawalNoticeViewModel.kt index 29f2b3e5..20bebe32 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/setting/AppSettingWithdrawalNoticeViewModel.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/setting/AppSettingWithdrawalNoticeViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import com.kikidan.domain.model.user.WithdrawalReason import com.kikidan.domain.usecase.user.WithdrawUserUseCase import com.kikidan.mypage.setting.model.AppSettingWithdrawalNoticeSideEffect +import com.kikidan.mypage.setting.model.AppSettingWithdrawalNoticeUiState import dagger.hilt.android.lifecycle.HiltViewModel import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost @@ -17,19 +18,23 @@ class AppSettingWithdrawalNoticeViewModel constructor( private val withdrawUserUseCase: WithdrawUserUseCase, ) : ViewModel(), - ContainerHost { - override val container: Container = container(Unit) + ContainerHost { + override val container: Container = + container(AppSettingWithdrawalNoticeUiState.Idle) fun withdraw( reason: WithdrawalReason, detail: String, ) = intent { + if (state is AppSettingWithdrawalNoticeUiState.Loading) return@intent + reduce { AppSettingWithdrawalNoticeUiState.Loading } withdrawUserUseCase(reason, detail.ifBlank { null }) .onSuccess { Log.e("moony", "Withdraw success") postSideEffect(AppSettingWithdrawalNoticeSideEffect.WithdrawalSucceeded) }.onFailure { throwable -> Log.e("moony", "Withdraw fail: $throwable") + reduce { AppSettingWithdrawalNoticeUiState.Failure } } } } diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/setting/model/AppSettingWithdrawalNoticeUiState.kt b/feature/mypage/src/main/java/com/kikidan/mypage/setting/model/AppSettingWithdrawalNoticeUiState.kt new file mode 100644 index 00000000..b0d17e67 --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/setting/model/AppSettingWithdrawalNoticeUiState.kt @@ -0,0 +1,9 @@ +package com.kikidan.mypage.setting.model + +sealed interface AppSettingWithdrawalNoticeUiState { + data object Idle : AppSettingWithdrawalNoticeUiState + + data object Loading : AppSettingWithdrawalNoticeUiState + + data object Failure : AppSettingWithdrawalNoticeUiState +} diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/setting/ui/AppSettingWithdrawalNoticeRoute.kt b/feature/mypage/src/main/java/com/kikidan/mypage/setting/ui/AppSettingWithdrawalNoticeRoute.kt index 6fff14e3..bd88086d 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/setting/ui/AppSettingWithdrawalNoticeRoute.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/setting/ui/AppSettingWithdrawalNoticeRoute.kt @@ -1,13 +1,16 @@ package com.kikidan.mypage.setting.ui import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringArrayResource import androidx.hilt.navigation.compose.hiltViewModel import com.kikidan.designsystem.R import com.kikidan.mypage.setting.AppSettingWithdrawalNoticeViewModel import com.kikidan.mypage.setting.model.AppSettingWithdrawalNoticeSideEffect +import com.kikidan.mypage.setting.model.AppSettingWithdrawalNoticeUiState import com.kikidan.mypage.setting.model.toWithdrawalReason +import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @Composable @@ -19,6 +22,7 @@ fun AppSettingWithdrawalNoticeRoute( onWithdrawalSuccess: () -> Unit = {}, viewModel: AppSettingWithdrawalNoticeViewModel = hiltViewModel(), ) { + val uiState by viewModel.collectAsState() val reasonItems = stringArrayResource(R.array.wheel_picker_withdrawal_reasons) val withdrawalReason = reasonItems.toList().toWithdrawalReason(reason) @@ -29,6 +33,7 @@ fun AppSettingWithdrawalNoticeRoute( } AppSettingWithdrawalNoticeScreen( + isWithdrawing = uiState is AppSettingWithdrawalNoticeUiState.Loading, modifier = modifier, onBackClick = onNavigateBack, onWithdrawConfirm = { diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/setting/ui/AppSettingWithdrawalNoticeScreen.kt b/feature/mypage/src/main/java/com/kikidan/mypage/setting/ui/AppSettingWithdrawalNoticeScreen.kt index aba225eb..e097188b 100644 --- a/feature/mypage/src/main/java/com/kikidan/mypage/setting/ui/AppSettingWithdrawalNoticeScreen.kt +++ b/feature/mypage/src/main/java/com/kikidan/mypage/setting/ui/AppSettingWithdrawalNoticeScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -39,6 +40,7 @@ import com.kikidan.designsystem.theme.TodakunTypography @Composable fun AppSettingWithdrawalNoticeScreen( modifier: Modifier = Modifier, + isWithdrawing: Boolean = false, onBackClick: () -> Unit = {}, onWithdrawConfirm: () -> Unit = {}, ) { @@ -49,7 +51,8 @@ fun AppSettingWithdrawalNoticeScreen( modifier = modifier .fillMaxSize() - .background(TodakunColor.white), + .background(TodakunColor.white) + .systemBarsPadding(), ) { TodakunSubHeader( title = stringResource(R.string.app_setting_withdrawal), @@ -118,7 +121,8 @@ fun AppSettingWithdrawalNoticeScreen( text = stringResource(R.string.app_setting_withdrawal_confirm_button), onClick = { showConfirmDialog = true }, size = TodakunButtonSize.Large, - enabled = agreed, + enabled = agreed && !isWithdrawing, + isLoading = isWithdrawing, modifier = Modifier .fillMaxWidth() diff --git a/feature/notification/src/main/java/com/kikidan/notification/NotificationRoute.kt b/feature/notification/src/main/java/com/kikidan/notification/NotificationRoute.kt index 4486c0c2..4dd292c2 100644 --- a/feature/notification/src/main/java/com/kikidan/notification/NotificationRoute.kt +++ b/feature/notification/src/main/java/com/kikidan/notification/NotificationRoute.kt @@ -34,7 +34,7 @@ fun NotificationRoute( } NotificationScreen( - notifications = state.notifications, + state = state, onBackClick = onBackClick, onNotificationClick = viewModel::onNotificationClick, modifier = modifier, diff --git a/feature/notification/src/main/java/com/kikidan/notification/NotificationViewModel.kt b/feature/notification/src/main/java/com/kikidan/notification/NotificationViewModel.kt index 11fa9037..908d0263 100644 --- a/feature/notification/src/main/java/com/kikidan/notification/NotificationViewModel.kt +++ b/feature/notification/src/main/java/com/kikidan/notification/NotificationViewModel.kt @@ -21,39 +21,36 @@ class NotificationViewModel private val markNotificationAsRead: MarkNotificationAsReadUseCase, ) : ViewModel(), ContainerHost { - override val container = container(NotificationState()) + override val container = container(NotificationState.Loading) // 화면 표시용 NotificationUiModel에는 deepLink가 없어 클릭 시 원본 목록에서 조회한다. private var notifications: List = emptyList() fun load() = intent { - reduce { state.copy(isLoading = true) } + reduce { NotificationState.Loading } getNotifications() .onSuccess { summary -> notifications = summary.notifications reduce { - state.copy( - isLoading = false, - notifications = summary.notifications.map { it.toUiModel() }.toPersistentList(), - ) + NotificationState.Success(summary.notifications.map { it.toUiModel() }.toPersistentList()) } }.onFailure { - reduce { state.copy(isLoading = false) } + reduce { NotificationState.Failure } postSideEffect(NotificationSideEffect.Error(it)) } } fun onNotificationClick(notificationId: String) = intent { + val current = (state as? NotificationState.Success)?.notifications ?: return@intent markNotificationAsRead(notificationId) .onSuccess { reduce { - state.copy( - notifications = - state.notifications - .map { if (it.id == notificationId) it.copy(isRead = true) else it } - .toPersistentList(), + NotificationState.Success( + current + .map { if (it.id == notificationId) it.copy(isRead = true) else it } + .toPersistentList(), ) } notifications diff --git a/feature/notification/src/main/java/com/kikidan/notification/model/NotificationState.kt b/feature/notification/src/main/java/com/kikidan/notification/model/NotificationState.kt index 5628bf68..2b99253c 100644 --- a/feature/notification/src/main/java/com/kikidan/notification/model/NotificationState.kt +++ b/feature/notification/src/main/java/com/kikidan/notification/model/NotificationState.kt @@ -1,9 +1,13 @@ package com.kikidan.notification.model import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf -data class NotificationState( - val isLoading: Boolean = true, - val notifications: PersistentList = persistentListOf(), -) +sealed interface NotificationState { + data object Loading : NotificationState + + data class Success( + val notifications: PersistentList, + ) : NotificationState + + data object Failure : NotificationState +} diff --git a/feature/notification/src/main/java/com/kikidan/notification/screen/NotificationScreen.kt b/feature/notification/src/main/java/com/kikidan/notification/screen/NotificationScreen.kt index eac146f3..2ec3ef57 100644 --- a/feature/notification/src/main/java/com/kikidan/notification/screen/NotificationScreen.kt +++ b/feature/notification/src/main/java/com/kikidan/notification/screen/NotificationScreen.kt @@ -18,19 +18,20 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.kikidan.designsystem.R +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.header.TodakunSubHeader import com.kikidan.designsystem.theme.TodakunColor import com.kikidan.designsystem.theme.TodakunTheme import com.kikidan.designsystem.theme.TodakunTypography import com.kikidan.domain.model.notification.NotificationType import com.kikidan.notification.component.NotificationItem +import com.kikidan.notification.model.NotificationState import com.kikidan.notification.model.NotificationUiModel -import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @Composable internal fun NotificationScreen( - notifications: PersistentList, + state: NotificationState, onBackClick: () -> Unit, onNotificationClick: (String) -> Unit, modifier: Modifier = Modifier, @@ -48,29 +49,42 @@ internal fun NotificationScreen( onBackClick = onBackClick, ) - if (notifications.isEmpty()) { - Box( - modifier = Modifier.fillMaxWidth().weight(1f), - contentAlignment = Alignment.Center, - ) { - Text( - text = stringResource(R.string.notification_empty), - style = TodakunTypography.body2Regular, - color = TodakunColor.gray500, - ) + when (state) { + is NotificationState.Loading, NotificationState.Failure -> { + Box(modifier = Modifier.fillMaxWidth().weight(1f)) } - } else { - Spacer(modifier = Modifier.height(16.dp)) - LazyColumn(modifier = Modifier.fillMaxWidth().weight(1f)) { - items(notifications, key = { it.id }) { notification -> - NotificationItem( - notification = notification, - onClick = { onNotificationClick(notification.id) }, - ) + + is NotificationState.Success -> { + if (state.notifications.isEmpty()) { + Box( + modifier = Modifier.fillMaxWidth().weight(1f), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(R.string.notification_empty), + style = TodakunTypography.body2Regular, + color = TodakunColor.gray500, + ) + } + } else { + Spacer(modifier = Modifier.height(16.dp)) + LazyColumn(modifier = Modifier.fillMaxWidth().weight(1f)) { + items(state.notifications, key = { it.id }) { notification -> + NotificationItem( + notification = notification, + onClick = { onNotificationClick(notification.id) }, + ) + } + } } } } } + + when (state) { + is NotificationState.Loading -> TodakunProgressIndicator() + is NotificationState.Success, NotificationState.Failure -> Unit + } } } @@ -79,11 +93,13 @@ internal fun NotificationScreen( private fun NotificationScreenPreview() { TodakunTheme { NotificationScreen( - notifications = - persistentListOf( - NotificationUiModel("n-1", NotificationType.AI_COMPLETE, "토닥이 답변이 도착했어요.", "30분 전", false), - NotificationUiModel("n-2", NotificationType.LUCKY_ACTION, "오늘 행운 액션이 열렸어요.", "3시간 전", true), - NotificationUiModel("n-3", NotificationType.FORTUNE, "토실이님과의 궁합이 도착했어요.", "2일 전", true), + state = + NotificationState.Success( + persistentListOf( + NotificationUiModel("n-1", NotificationType.AI_COMPLETE, "토닥이 답변이 도착했어요.", "30분 전", false), + NotificationUiModel("n-2", NotificationType.LUCKY_ACTION, "오늘 행운 액션이 열렸어요.", "3시간 전", true), + NotificationUiModel("n-3", NotificationType.FORTUNE, "토실이님과의 궁합이 도착했어요.", "2일 전", true), + ), ), onBackClick = {}, onNotificationClick = {}, @@ -96,7 +112,7 @@ private fun NotificationScreenPreview() { private fun NotificationScreenEmptyPreview() { TodakunTheme { NotificationScreen( - notifications = persistentListOf(), + state = NotificationState.Success(persistentListOf()), onBackClick = {}, onNotificationClick = {}, ) diff --git a/feature/notification/src/test/java/com/kikidan/notification/NotificationViewModelTest.kt b/feature/notification/src/test/java/com/kikidan/notification/NotificationViewModelTest.kt index 09c5c165..575c479e 100644 --- a/feature/notification/src/test/java/com/kikidan/notification/NotificationViewModelTest.kt +++ b/feature/notification/src/test/java/com/kikidan/notification/NotificationViewModelTest.kt @@ -47,8 +47,7 @@ class NotificationViewModelTest { vm.test(this) { containerHost.load() - val success = awaitState() - assertFalse(success.isLoading) + val success = awaitState() as NotificationState.Success assertEquals(1, success.notifications.size) assertEquals("n-1", success.notifications[0].id) assertFalse(success.notifications[0].isRead) @@ -67,7 +66,7 @@ class NotificationViewModelTest { vm.test(this) { containerHost.load() val failure = awaitState() - assertFalse(failure.isLoading) + assertEquals(NotificationState.Failure, failure) val se = awaitSideEffect() assertTrue(se is NotificationSideEffect.Error) } @@ -79,18 +78,16 @@ class NotificationViewModelTest { val fakeNotificationRepository = FakeNotificationRepository() val vm = viewModel(fakeNotificationRepository) val initial = - NotificationState( - isLoading = false, - notifications = - persistentListOf( - NotificationUiModel("n-1", NotificationType.FORTUNE, "제목1", "30분 전", false), - NotificationUiModel("n-2", NotificationType.LUCKY_ACTION, "제목2", "3시간 전", false), - ), + NotificationState.Success( + persistentListOf( + NotificationUiModel("n-1", NotificationType.FORTUNE, "제목1", "30분 전", false), + NotificationUiModel("n-2", NotificationType.LUCKY_ACTION, "제목2", "3시간 전", false), + ), ) vm.test(this, initialState = initial) { containerHost.onNotificationClick("n-1") - val settled = awaitState() + val settled = awaitState() as NotificationState.Success assertEquals("n-1", fakeNotificationRepository.lastMarkedAsReadId) assertTrue(settled.notifications.first { it.id == "n-1" }.isRead) assertFalse(settled.notifications.first { it.id == "n-2" }.isRead) diff --git a/feature/onboarding/src/main/java/com/kikidan/onboarding/OnboardingRoute.kt b/feature/onboarding/src/main/java/com/kikidan/onboarding/OnboardingRoute.kt index db38745a..ec660e4a 100644 --- a/feature/onboarding/src/main/java/com/kikidan/onboarding/OnboardingRoute.kt +++ b/feature/onboarding/src/main/java/com/kikidan/onboarding/OnboardingRoute.kt @@ -24,6 +24,7 @@ import com.kikidan.domain.model.user.BirthTime import com.kikidan.onboarding.model.OnboardingSheet import com.kikidan.onboarding.model.OnboardingSideEffect import com.kikidan.onboarding.model.OnboardingStep +import com.kikidan.onboarding.model.OnboardingSubmitState import com.kikidan.onboarding.screen.BirthInfoScreen import com.kikidan.onboarding.screen.CompleteScreen import com.kikidan.onboarding.screen.ExtraQuestionScreen @@ -125,6 +126,7 @@ fun OnboardingRoute( lifeStage = state.lifeStage, relationshipStatus = state.relationshipStatus, canProceed = state.canProceed, + isSubmitting = state.submitState is OnboardingSubmitState.Loading, onLifeStageSelect = viewModel::selectLifeStage, onRelationshipStatusSelect = viewModel::selectRelationshipStatus, onNextClick = { diff --git a/feature/onboarding/src/main/java/com/kikidan/onboarding/OnboardingViewModel.kt b/feature/onboarding/src/main/java/com/kikidan/onboarding/OnboardingViewModel.kt index 2322b041..428d3a2a 100644 --- a/feature/onboarding/src/main/java/com/kikidan/onboarding/OnboardingViewModel.kt +++ b/feature/onboarding/src/main/java/com/kikidan/onboarding/OnboardingViewModel.kt @@ -13,6 +13,7 @@ import com.kikidan.onboarding.model.OnboardingSheet import com.kikidan.onboarding.model.OnboardingSideEffect import com.kikidan.onboarding.model.OnboardingState import com.kikidan.onboarding.model.OnboardingStep +import com.kikidan.onboarding.model.OnboardingSubmitState import com.kikidan.onboarding.model.toDomain import dagger.hilt.android.lifecycle.HiltViewModel import org.orbitmvi.orbit.ContainerHost @@ -48,21 +49,21 @@ class OnboardingViewModel fun confirmComplete(onboardingToken: OnboardingToken) = intent { - if (state.isSubmitting) return@intent + if (state.submitState is OnboardingSubmitState.Loading) return@intent val signupSubmission = state.toDomain() if (signupSubmission == null) { postSideEffect(OnboardingSideEffect.InvalidInput) return@intent } - reduce { state.copy(isSubmitting = true) } + reduce { state.copy(submitState = OnboardingSubmitState.Loading) } signUpUseCase( signupSubmission = signupSubmission, onboardingToken = onboardingToken, ).onSuccess { - reduce { state.copy(isSubmitting = false, step = OnboardingStep.COMPLETE) } + reduce { state.copy(submitState = OnboardingSubmitState.Success, step = OnboardingStep.COMPLETE) } postSideEffect(OnboardingSideEffect.PermissionRequest) }.onFailure { e -> - reduce { state.copy(isSubmitting = false) } + reduce { state.copy(submitState = OnboardingSubmitState.Failure) } postSideEffect(OnboardingSideEffect.Failure(e)) } } diff --git a/feature/onboarding/src/main/java/com/kikidan/onboarding/component/OnboardingLayout.kt b/feature/onboarding/src/main/java/com/kikidan/onboarding/component/OnboardingLayout.kt index 355baa4c..da746e08 100644 --- a/feature/onboarding/src/main/java/com/kikidan/onboarding/component/OnboardingLayout.kt +++ b/feature/onboarding/src/main/java/com/kikidan/onboarding/component/OnboardingLayout.kt @@ -45,6 +45,7 @@ internal fun OnboardingLayout( onBackClick: () -> Unit, modifier: Modifier = Modifier, progressLabel: String? = null, + ctaLoading: Boolean = false, content: @Composable () -> Unit, ) { Box(modifier = modifier.background(TodakunColor.white)) { @@ -82,6 +83,7 @@ internal fun OnboardingLayout( ctaText = ctaText, onCtaClick = onCtaClick, ctaEnabled = ctaEnabled, + ctaLoading = ctaLoading, ) } } @@ -128,6 +130,7 @@ private fun NextButton( ctaEnabled: Boolean, onCtaClick: () -> Unit, modifier: Modifier = Modifier, + ctaLoading: Boolean = false, ) { Column( modifier = @@ -141,7 +144,8 @@ private fun NextButton( text = ctaText, onClick = onCtaClick, size = TodakunButtonSize.Large, - enabled = ctaEnabled, + enabled = ctaEnabled && !ctaLoading, + isLoading = ctaLoading, modifier = Modifier.fillMaxWidth(), ) } diff --git a/feature/onboarding/src/main/java/com/kikidan/onboarding/model/OnboardingState.kt b/feature/onboarding/src/main/java/com/kikidan/onboarding/model/OnboardingState.kt index 4796f37c..c7cb8544 100644 --- a/feature/onboarding/src/main/java/com/kikidan/onboarding/model/OnboardingState.kt +++ b/feature/onboarding/src/main/java/com/kikidan/onboarding/model/OnboardingState.kt @@ -17,6 +17,14 @@ enum class OnboardingSheet { BIRTH_TIME, } +sealed interface OnboardingSubmitState { + data object Loading : OnboardingSubmitState + + data object Success : OnboardingSubmitState + + data object Failure : OnboardingSubmitState +} + @Immutable data class OnboardingState( val step: OnboardingStep = OnboardingStep.NAME, @@ -28,7 +36,7 @@ data class OnboardingState( val lifeStage: Job? = null, val relationshipStatus: RelationshipStatus? = null, val sheet: OnboardingSheet? = null, - val isSubmitting: Boolean = false, + val submitState: OnboardingSubmitState? = null, ) { val isUnderAge: Boolean = birthDate?.isUnderAge() ?: false diff --git a/feature/onboarding/src/main/java/com/kikidan/onboarding/screen/ExtraQuestionScreen.kt b/feature/onboarding/src/main/java/com/kikidan/onboarding/screen/ExtraQuestionScreen.kt index caaf458d..2a9d39a4 100644 --- a/feature/onboarding/src/main/java/com/kikidan/onboarding/screen/ExtraQuestionScreen.kt +++ b/feature/onboarding/src/main/java/com/kikidan/onboarding/screen/ExtraQuestionScreen.kt @@ -35,6 +35,7 @@ internal fun ExtraQuestionScreen( onNextClick: () -> Unit, onBackClick: () -> Unit, modifier: Modifier = Modifier, + isSubmitting: Boolean = false, ) { OnboardingLayout( progress = OnboardingStep.EXTRA_QUESTION.progress, @@ -44,6 +45,7 @@ internal fun ExtraQuestionScreen( }, ctaText = stringResource(id = R.string.onboarding_start), ctaEnabled = canProceed, + ctaLoading = isSubmitting, onCtaClick = onNextClick, onBackClick = onBackClick, modifier = modifier, diff --git a/feature/onboarding/src/test/java/com/kikidan/onboarding/OnboardingViewModelTest.kt b/feature/onboarding/src/test/java/com/kikidan/onboarding/OnboardingViewModelTest.kt index 9afbe18d..c982c907 100644 --- a/feature/onboarding/src/test/java/com/kikidan/onboarding/OnboardingViewModelTest.kt +++ b/feature/onboarding/src/test/java/com/kikidan/onboarding/OnboardingViewModelTest.kt @@ -14,6 +14,7 @@ import com.kikidan.onboarding.model.OnboardingSheet import com.kikidan.onboarding.model.OnboardingSideEffect import com.kikidan.onboarding.model.OnboardingState import com.kikidan.onboarding.model.OnboardingStep +import com.kikidan.onboarding.model.OnboardingSubmitState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -149,8 +150,8 @@ class OnboardingViewModelTest { ) viewModel().test(this, initialState = initial) { containerHost.confirmComplete(onboardingToken) - expectState { copy(isSubmitting = true) } - expectState { copy(isSubmitting = false, step = OnboardingStep.COMPLETE) } + expectState { copy(submitState = OnboardingSubmitState.Loading) } + expectState { copy(submitState = OnboardingSubmitState.Success, step = OnboardingStep.COMPLETE) } expectSideEffect(OnboardingSideEffect.PermissionRequest) } } @@ -168,7 +169,7 @@ class OnboardingViewModelTest { birthTime = BirthTime.JA, lifeStage = Job.STUDENT, relationshipStatus = RelationshipStatus.SOLO, - isSubmitting = true, + submitState = OnboardingSubmitState.Loading, ) viewModel().test(this, initialState = initial) { containerHost.confirmComplete(onboardingToken) @@ -195,8 +196,8 @@ class OnboardingViewModelTest { ) viewModel().test(this, initialState = initial) { containerHost.confirmComplete(onboardingToken) - expectState { copy(isSubmitting = true) } - expectState { copy(isSubmitting = false) } + expectState { copy(submitState = OnboardingSubmitState.Loading) } + expectState { copy(submitState = OnboardingSubmitState.Failure) } expectSideEffect(OnboardingSideEffect.Failure(error)) } } diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/CompatibilityInputViewModel.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/CompatibilityInputViewModel.kt index d0712ec5..59c45722 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/CompatibilityInputViewModel.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/CompatibilityInputViewModel.kt @@ -8,6 +8,9 @@ import com.kikidan.domain.usecase.saju.GetPartnerMansaeryeokDetailUseCase import com.kikidan.domain.usecase.saju.GetPartnerSajuListUseCase import com.kikidan.sajucontents.model.CompatibilityEntrySideEffect import com.kikidan.sajucontents.model.CompatibilityInputState +import com.kikidan.sajucontents.model.CreateCompatibilityState +import com.kikidan.sajucontents.model.MyInfoLoadState +import com.kikidan.sajucontents.model.PartnerListState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentSet @@ -34,7 +37,7 @@ class CompatibilityInputViewModel fun loadMyInfo() = intent { - reduce { state.copy(isLoading = true) } + reduce { state.copy(myInfoState = MyInfoLoadState.Loading) } coroutineScope { val mansaeryeokDetailDeferred = async { getMansaeryeokDetail() } val partnerListDeferred = async { getPartnerSajuList() } @@ -49,7 +52,7 @@ class CompatibilityInputViewModel onSuccess = { (detail, list) -> reduce { state.copy( - isLoading = false, + myInfoState = MyInfoLoadState.Success, myUser = detail.user, myPillars = PillarDisplayOrder @@ -61,8 +64,7 @@ class CompatibilityInputViewModel }.toPersistentList(), partnerPicker = state.partnerPicker.copy( - isLoading = false, - partners = list.toPersistentList(), + partnersState = PartnerListState.Success(list.toPersistentList()), ), ) } @@ -73,7 +75,7 @@ class CompatibilityInputViewModel } }, onFailure = { - reduce { state.copy(isLoading = false) } + reduce { state.copy(myInfoState = MyInfoLoadState.Failure) } postSideEffect(CompatibilityEntrySideEffect.ShowLoadError) }, ) @@ -82,7 +84,9 @@ class CompatibilityInputViewModel fun openPartnerPicker() = intent { - if (state.partnerPicker.isLoading || state.isLoading) return@intent + val partnersLoading = state.partnerPicker.partnersState is PartnerListState.Loading + val myInfoLoading = state.myInfoState is MyInfoLoadState.Loading + if (partnersLoading || myInfoLoading) return@intent if (state.partnerPicker.partners.isEmpty()) { postSideEffect(CompatibilityEntrySideEffect.NavigateToPartnerForm) return@intent @@ -128,16 +132,16 @@ class CompatibilityInputViewModel fun checkCompatibility() = intent { val partnerLinkId = state.selectedPartner?.linkId ?: return@intent - if (state.isCreating) return@intent - reduce { state.copy(isCreating = true) } + if (state.createState is CreateCompatibilityState.Loading) return@intent + reduce { state.copy(createState = CreateCompatibilityState.Loading) } createCompatibility(partnerLinkId) .onSuccess { compatibility -> - reduce { state.copy(isCreating = false) } + reduce { state.copy(createState = CreateCompatibilityState.Success) } postSideEffect( CompatibilityEntrySideEffect.NavigateToResult(compatibility.id, partnerLinkId), ) }.onFailure { - reduce { state.copy(isCreating = false) } + reduce { state.copy(createState = CreateCompatibilityState.Failure) } postSideEffect(CompatibilityEntrySideEffect.ShowCreateError) } } diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/CompatibilityPartnerFormViewModel.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/CompatibilityPartnerFormViewModel.kt index 58937fc0..cad4260a 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/CompatibilityPartnerFormViewModel.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/CompatibilityPartnerFormViewModel.kt @@ -9,6 +9,7 @@ import com.kikidan.domain.model.user.Gender import com.kikidan.domain.usecase.saju.RegisterPartnerSajuUseCase import com.kikidan.sajucontents.model.CompatibilityPartnerFormSideEffect import com.kikidan.sajucontents.model.CompatibilityPartnerFormState +import com.kikidan.sajucontents.model.SavePartnerState import dagger.hilt.android.lifecycle.HiltViewModel import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost @@ -49,7 +50,7 @@ class CompatibilityPartnerFormViewModel intent { val birthDate = state.birthDate ?: return@intent if (!state.isSaveEnabled) return@intent - reduce { state.copy(isSaving = true) } + reduce { state.copy(saveState = SavePartnerState.Loading) } val input = PartnerSajuInput( @@ -60,10 +61,10 @@ class CompatibilityPartnerFormViewModel ) registerPartnerSajuUseCase(input) .onSuccess { - reduce { state.copy(isSaving = false) } + reduce { state.copy(saveState = SavePartnerState.Success) } postSideEffect(CompatibilityPartnerFormSideEffect.NavigateBack) }.onFailure { - reduce { state.copy(isSaving = false) } + reduce { state.copy(saveState = SavePartnerState.Failure) } postSideEffect(CompatibilityPartnerFormSideEffect.ShowSaveError) } } diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/DateFortuneResultViewModel.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/DateFortuneResultViewModel.kt index adea25b6..b0be9f04 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/DateFortuneResultViewModel.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/DateFortuneResultViewModel.kt @@ -2,6 +2,7 @@ package com.kikidan.sajucontents import androidx.lifecycle.ViewModel import com.kikidan.domain.usecase.GetDayFortuneUseCase +import com.kikidan.sajucontents.model.DateFortuneResultLoadState import com.kikidan.sajucontents.model.DateFortuneResultSideEffect import com.kikidan.sajucontents.model.DateFortuneResultState import dagger.hilt.android.lifecycle.HiltViewModel @@ -26,26 +27,27 @@ class DateFortuneResultViewModel fun loadResults(ids: List) = intent { - reduce { state.copy(isLoading = true) } + reduce { state.copy(resultState = DateFortuneResultLoadState.Loading) } // 탭 순서(선택한 날짜 순서)를 유지하기 위해 병렬 조회 후 요청한 id 순서 그대로 매핑한다. val results = coroutineScope { ids.map { id -> async { getDayFortune(id) } }.awaitAll() } val failure = results.firstOrNull { it.isFailure } if (failure != null) { - reduce { state.copy(isLoading = false) } + reduce { state.copy(resultState = DateFortuneResultLoadState.Failure) } postSideEffect(DateFortuneResultSideEffect.ShowError) return@intent } reduce { state.copy( - isLoading = false, - results = - results - .map { it.getOrThrow() } - .sortedByDescending { it.score } - .take(3) - .toPersistentList(), + resultState = + DateFortuneResultLoadState.Success( + results + .map { it.getOrThrow() } + .sortedByDescending { it.score } + .take(3) + .toPersistentList(), + ), selectedResultIndex = 0, ) } diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/DateFortuneViewModel.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/DateFortuneViewModel.kt index ac8f78b5..479b967c 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/DateFortuneViewModel.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/DateFortuneViewModel.kt @@ -7,6 +7,7 @@ import com.kikidan.domain.usecase.CreateDayFortunesUseCase import com.kikidan.domain.usecase.DateFortuneDefaults import com.kikidan.sajucontents.model.DateFortuneSideEffect import com.kikidan.sajucontents.model.DateFortuneState +import com.kikidan.sajucontents.model.DateFortuneSubmitState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -80,17 +81,17 @@ class DateFortuneViewModel intent { val purpose = state.selectedPurpose ?: return@intent val dates = state.selectedDates - if (dates.isEmpty() || state.isLoading) return@intent + if (dates.isEmpty() || state.submitState is DateFortuneSubmitState.Loading) return@intent - reduce { state.copy(isLoading = true) } + reduce { state.copy(submitState = DateFortuneSubmitState.Loading) } createDayFortunes(purpose, dates).fold( onSuccess = { results -> - reduce { state.copy(isLoading = false) } + reduce { state.copy(submitState = DateFortuneSubmitState.Success) } postSideEffect(DateFortuneSideEffect.NavigateToResult(results.map { it.id })) }, onFailure = { - reduce { state.copy(isLoading = false) } + reduce { state.copy(submitState = DateFortuneSubmitState.Failure) } postSideEffect(DateFortuneSideEffect.ShowError) }, ) diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/YearFortuneResultViewModel.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/YearFortuneResultViewModel.kt index b1f204c6..0bcd39e4 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/YearFortuneResultViewModel.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/YearFortuneResultViewModel.kt @@ -2,6 +2,7 @@ package com.kikidan.sajucontents import androidx.lifecycle.ViewModel import com.kikidan.domain.usecase.GetYearFortuneUseCase +import com.kikidan.sajucontents.model.YearFortuneResultLoadState import com.kikidan.sajucontents.model.YearFortuneResultSideEffect import com.kikidan.sajucontents.model.YearFortuneResultState import dagger.hilt.android.lifecycle.HiltViewModel @@ -24,15 +25,15 @@ class YearFortuneResultViewModel fun load(id: String) = intent { - if (state.isLoading) return@intent - reduce { state.copy(isLoading = true) } + if (state.resultState is YearFortuneResultLoadState.Loading) return@intent + reduce { state.copy(resultState = YearFortuneResultLoadState.Loading) } getYearFortune(id).fold( onSuccess = { result -> - reduce { state.copy(isLoading = false, fortuneResult = result) } + reduce { state.copy(resultState = YearFortuneResultLoadState.Success(result)) } }, onFailure = { - reduce { state.copy(isLoading = false) } + reduce { state.copy(resultState = YearFortuneResultLoadState.Failure) } postSideEffect(YearFortuneResultSideEffect.ShowError) }, ) diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/YearFortuneViewModel.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/YearFortuneViewModel.kt index 25b6d91f..cd243ab9 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/YearFortuneViewModel.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/YearFortuneViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import com.kikidan.domain.usecase.CreateYearFortuneUseCase import com.kikidan.sajucontents.model.YearFortuneSideEffect import com.kikidan.sajucontents.model.YearFortuneState +import com.kikidan.sajucontents.model.YearFortuneSubmitState import dagger.hilt.android.lifecycle.HiltViewModel import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost @@ -27,17 +28,17 @@ class YearFortuneViewModel fun onSubmit() = intent { - if (state.isLoading) return@intent + if (state.submitState is YearFortuneSubmitState.Loading) return@intent val year = state.selectedYear - reduce { state.copy(isLoading = true) } + reduce { state.copy(submitState = YearFortuneSubmitState.Loading) } createYearFortune(year).fold( onSuccess = { result -> - reduce { state.copy(isLoading = false) } + reduce { state.copy(submitState = YearFortuneSubmitState.Success) } postSideEffect(YearFortuneSideEffect.NavigateToResult(result.id)) }, onFailure = { - reduce { state.copy(isLoading = false) } + reduce { state.copy(submitState = YearFortuneSubmitState.Failure) } postSideEffect(YearFortuneSideEffect.ShowError) }, ) diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/CompatibilityInputState.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/CompatibilityInputState.kt index d1ddc1dc..8ef7cff7 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/CompatibilityInputState.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/CompatibilityInputState.kt @@ -7,17 +7,45 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf data class CompatibilityInputState( - val isLoading: Boolean = false, + val myInfoState: MyInfoLoadState? = null, val myUser: User? = null, val myPillars: PersistentList = persistentListOf(), val selectedPartner: PartnerSaju? = null, val selectedPartnerPillars: PersistentList = persistentListOf(), - val isCreating: Boolean = false, + val createState: CreateCompatibilityState? = null, val partnerPicker: PartnerPickerState = PartnerPickerState(), ) +sealed interface MyInfoLoadState { + data object Loading : MyInfoLoadState + + data object Success : MyInfoLoadState + + data object Failure : MyInfoLoadState +} + +sealed interface CreateCompatibilityState { + data object Loading : CreateCompatibilityState + + data object Success : CreateCompatibilityState + + data object Failure : CreateCompatibilityState +} + data class PartnerPickerState( val isVisible: Boolean = false, - val isLoading: Boolean = false, - val partners: PersistentList = persistentListOf(), -) + val partnersState: PartnerListState = PartnerListState.Loading, +) { + val partners: PersistentList + get() = (partnersState as? PartnerListState.Success)?.partners ?: persistentListOf() +} + +sealed interface PartnerListState { + data object Loading : PartnerListState + + data class Success( + val partners: PersistentList, + ) : PartnerListState + + data object Failure : PartnerListState +} diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/CompatibilityPartnerFormState.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/CompatibilityPartnerFormState.kt index 475c0a02..b42d2535 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/CompatibilityPartnerFormState.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/CompatibilityPartnerFormState.kt @@ -13,10 +13,20 @@ data class CompatibilityPartnerFormState( val birthDate: LocalDate? = null, val birthTime: BirthTime = BirthTime.UNKNOWN, val relationshipTypeCode: String = CompatibilityRelationshipType.LOVER.code, - val isSaving: Boolean = false, + val saveState: SavePartnerState? = null, ) { val isSaveEnabled: Boolean - get() = name.isNotBlank() && nameErrorMessageRes == null && birthDate != null && !isSaving + get() = + name.isNotBlank() && nameErrorMessageRes == null && birthDate != null && + saveState !is SavePartnerState.Loading +} + +sealed interface SavePartnerState { + data object Loading : SavePartnerState + + data object Success : SavePartnerState + + data object Failure : SavePartnerState } sealed interface CompatibilityPartnerFormSideEffect { diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/DateFortuneResultState.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/DateFortuneResultState.kt index 51a9c5d2..1e3d23f4 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/DateFortuneResultState.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/DateFortuneResultState.kt @@ -5,8 +5,20 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf data class DateFortuneResultState( - val isLoading: Boolean = false, - val results: ImmutableList = persistentListOf(), + val resultState: DateFortuneResultLoadState? = null, val selectedResultIndex: Int = 0, val isShareDialogVisible: Boolean = false, -) +) { + val results: ImmutableList + get() = (resultState as? DateFortuneResultLoadState.Success)?.results ?: persistentListOf() +} + +sealed interface DateFortuneResultLoadState { + data object Loading : DateFortuneResultLoadState + + data class Success( + val results: ImmutableList, + ) : DateFortuneResultLoadState + + data object Failure : DateFortuneResultLoadState +} diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/DateFortuneState.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/DateFortuneState.kt index 81bc47b6..e760dcc0 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/DateFortuneState.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/DateFortuneState.kt @@ -12,9 +12,17 @@ data class DateFortuneState( val selectedGender: Gender? = null, val selectedDates: ImmutableList = persistentListOf(), val isSheetVisible: Boolean = false, - val isLoading: Boolean = false, + val submitState: DateFortuneSubmitState? = null, ) { // selectedGender는 서버로 보내지 않는 값이라 필수 조건에서 제외한다(B-2 회귀 방지). val canSubmit: Boolean - get() = selectedPurpose != null && selectedDates.isNotEmpty() && !isLoading + get() = selectedPurpose != null && selectedDates.isNotEmpty() && submitState !is DateFortuneSubmitState.Loading +} + +sealed interface DateFortuneSubmitState { + data object Loading : DateFortuneSubmitState + + data object Success : DateFortuneSubmitState + + data object Failure : DateFortuneSubmitState } diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/YearFortuneResultState.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/YearFortuneResultState.kt index 6540ec92..f22e3f61 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/YearFortuneResultState.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/YearFortuneResultState.kt @@ -3,7 +3,19 @@ package com.kikidan.sajucontents.model import com.kikidan.domain.model.fortune.YearFortune data class YearFortuneResultState( - val fortuneResult: YearFortune? = null, - val isLoading: Boolean = false, + val resultState: YearFortuneResultLoadState? = null, val isShareDialogVisible: Boolean = false, -) +) { + val fortuneResult: YearFortune? + get() = (resultState as? YearFortuneResultLoadState.Success)?.fortune +} + +sealed interface YearFortuneResultLoadState { + data object Loading : YearFortuneResultLoadState + + data class Success( + val fortune: YearFortune, + ) : YearFortuneResultLoadState + + data object Failure : YearFortuneResultLoadState +} diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/YearFortuneState.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/YearFortuneState.kt index 6f15fadb..49269409 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/YearFortuneState.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/model/YearFortuneState.kt @@ -10,5 +10,13 @@ data class YearFortuneState( val years: ImmutableList = DEFAULT_YEARS, val selectedYear: Int = Year.now().value, val currentYear: Int = Year.now().value, - val isLoading: Boolean = false, + val submitState: YearFortuneSubmitState? = null, ) + +sealed interface YearFortuneSubmitState { + data object Loading : YearFortuneSubmitState + + data object Success : YearFortuneSubmitState + + data object Failure : YearFortuneSubmitState +} diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/CompatibilityInputScreen.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/CompatibilityInputScreen.kt index 344fa99b..a9e1bc2c 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/CompatibilityInputScreen.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/CompatibilityInputScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.button.PrimaryButton import com.kikidan.designsystem.component.button.TodakunButtonSize import com.kikidan.designsystem.component.header.TodakunSubHeader @@ -56,6 +57,9 @@ import com.kikidan.domain.model.user.User import com.kikidan.sajucontents.R import com.kikidan.sajucontents.component.CompatibilitySajuPillars import com.kikidan.sajucontents.model.CompatibilityInputState +import com.kikidan.sajucontents.model.CreateCompatibilityState +import com.kikidan.sajucontents.model.MyInfoLoadState +import com.kikidan.sajucontents.model.PartnerListState import com.kikidan.sajucontents.model.PartnerPickerState import kotlinx.collections.immutable.ImmutableList import java.time.LocalDate @@ -147,12 +151,18 @@ internal fun CompatibilityInputScreen( PrimaryButton( text = stringResource(R.string.compatibility_check_cta), onClick = onCheckCompatibilityClick, - enabled = state.selectedPartner != null && !state.isCreating, + enabled = state.selectedPartner != null && state.createState !is CreateCompatibilityState.Loading, + isLoading = state.createState is CreateCompatibilityState.Loading, size = TodakunButtonSize.Large, modifier = Modifier.fillMaxWidth(), ) } } + + when (state.myInfoState) { + is MyInfoLoadState.Loading -> TodakunProgressIndicator() + null, is MyInfoLoadState.Success, is MyInfoLoadState.Failure -> Unit + } } val selectedIndex = state.partnerPicker.partners.indexOf(state.selectedPartner) if (state.partnerPicker.isVisible) { @@ -283,20 +293,26 @@ private fun PartnerPickerSheet( if (!isOnceSelected) onSelectPartner(pickerState.partners[selectedIndex].linkId) onDismissRequest() }) { - if (!pickerState.isLoading) { - TodakunWheelPicker( - title = stringResource(R.string.compatibility_partner_picker_title), - onSaveClick = onAddNewPartner, - saveButtonLabel = stringResource(R.string.compatibility_partner_picker_add), - columns = listOf(WheelPickerColumnState(items = items, selectedIndex = selectedIndex)), - onWheelPickerColumnSelect = { _, selectedIndex -> - onSelectPartner(pickerState.partners[selectedIndex].linkId) - isOnceSelected = true - }, - directInputEnabled = false, - ) - } else { - Spacer(modifier = Modifier.height(200.dp)) + when (pickerState.partnersState) { + is PartnerListState.Loading -> { + Box(modifier = Modifier.fillMaxWidth().height(200.dp)) { + TodakunProgressIndicator() + } + } + + is PartnerListState.Success, is PartnerListState.Failure -> { + TodakunWheelPicker( + title = stringResource(R.string.compatibility_partner_picker_title), + onSaveClick = onAddNewPartner, + saveButtonLabel = stringResource(R.string.compatibility_partner_picker_add), + columns = listOf(WheelPickerColumnState(items = items, selectedIndex = selectedIndex)), + onWheelPickerColumnSelect = { _, selectedIndex -> + onSelectPartner(pickerState.partners[selectedIndex].linkId) + isOnceSelected = true + }, + directInputEnabled = false, + ) + } } } } @@ -349,6 +365,7 @@ private fun CompatibilityInputScreenPreview() { CompatibilityInputScreen( state = CompatibilityInputState( + myInfoState = MyInfoLoadState.Success, myUser = User( id = "me", diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/DateFortuneInputScreen.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/DateFortuneInputScreen.kt index 42508c00..a0ecc7eb 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/DateFortuneInputScreen.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/DateFortuneInputScreen.kt @@ -36,6 +36,7 @@ import com.kikidan.domain.model.dayfortune.DayFortunePurpose import com.kikidan.domain.usecase.DateFortuneDefaults import com.kikidan.sajucontents.component.DateSelectBottomSheet import com.kikidan.sajucontents.model.DateFortuneState +import com.kikidan.sajucontents.model.DateFortuneSubmitState import kotlinx.collections.immutable.persistentListOf import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -136,6 +137,7 @@ internal fun DateFortuneInputScreen( onClick = onSubmit, size = TodakunButtonSize.Large, enabled = state.canSubmit, + isLoading = state.submitState is DateFortuneSubmitState.Loading, ) } diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/DateFortuneResultScreen.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/DateFortuneResultScreen.kt index ec30f85f..f9c89bea 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/DateFortuneResultScreen.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/DateFortuneResultScreen.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.kikidan.designsystem.R import com.kikidan.designsystem.component.TodakunChip2 +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.TodakunTooltip import com.kikidan.designsystem.component.button.PrimaryButton import com.kikidan.designsystem.component.button.TodakunButtonSize @@ -56,6 +57,7 @@ import com.kikidan.sajucontents.BuildConfig import com.kikidan.sajucontents.component.FortuneScoreCard import com.kikidan.sajucontents.component.FortuneShareDialog import com.kikidan.sajucontents.component.ResultDateTabRow +import com.kikidan.sajucontents.model.DateFortuneResultLoadState import com.kikidan.sajucontents.model.DateFortuneResultState import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeEffect @@ -101,28 +103,40 @@ internal fun DateFortuneResultScreen( .hazeSource(state = hazeState), ) - if (selectedFortune == null) return@Box + when (state.resultState) { + null, is DateFortuneResultLoadState.Loading -> { + TodakunProgressIndicator() + } - DateFortuneResultContent( - onBackClick = onBackClick, - onExportClick = onExportClick, - onShareClick = onShareClick, - onTabSelect = onTabSelect, - onAskTodakClick = onAskTodakClick, - state = state, - selectedFortune = selectedFortune, - hazeState = hazeState, - ) + is DateFortuneResultLoadState.Failure -> { + Unit + } - if (state.isShareDialogVisible) { - FortuneShareDialog( - fortuneTitle = selectedFortune.title, - fortuneId = selectedFortune.id, - shareUrl = shareUrlFor(selectedFortune.id), - onKakaoShareFail = onKakaoShareFail, - onUrlCopy = onUrlCopy, - onDismiss = onShareDismiss, - ) + is DateFortuneResultLoadState.Success -> { + if (selectedFortune != null) { + DateFortuneResultContent( + onBackClick = onBackClick, + onExportClick = onExportClick, + onShareClick = onShareClick, + onTabSelect = onTabSelect, + onAskTodakClick = onAskTodakClick, + state = state, + selectedFortune = selectedFortune, + hazeState = hazeState, + ) + + if (state.isShareDialogVisible) { + FortuneShareDialog( + fortuneTitle = selectedFortune.title, + fortuneId = selectedFortune.id, + shareUrl = shareUrlFor(selectedFortune.id), + onKakaoShareFail = onKakaoShareFail, + onUrlCopy = onUrlCopy, + onDismiss = onShareDismiss, + ) + } + } + } } } } @@ -354,8 +368,7 @@ private fun DateFortuneResultScreenPreview() { DateFortuneResultScreen( state = DateFortuneResultState( - isLoading = false, - results = persistentListOf(sampleFortune), + resultState = DateFortuneResultLoadState.Success(persistentListOf(sampleFortune)), selectedResultIndex = 0, ), onBackClick = {}, diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/YearFortuneInputScreen.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/YearFortuneInputScreen.kt index d6500e43..8b40e1ef 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/YearFortuneInputScreen.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/YearFortuneInputScreen.kt @@ -27,6 +27,7 @@ import com.kikidan.designsystem.theme.TodakunTheme import com.kikidan.designsystem.theme.TodakunTypography import com.kikidan.sajucontents.R import com.kikidan.sajucontents.model.YearFortuneState +import com.kikidan.sajucontents.model.YearFortuneSubmitState @Composable internal fun YearFortuneInputScreen( @@ -93,7 +94,8 @@ internal fun YearFortuneInputScreen( text = stringResource(id = R.string.year_fortune_cta), onClick = onSubmit, size = TodakunButtonSize.Large, - enabled = !state.isLoading, + enabled = state.submitState !is YearFortuneSubmitState.Loading, + isLoading = state.submitState is YearFortuneSubmitState.Loading, modifier = Modifier.fillMaxWidth(), ) } diff --git a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/YearFortuneResultScreen.kt b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/YearFortuneResultScreen.kt index 4727dc4b..3146258f 100644 --- a/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/YearFortuneResultScreen.kt +++ b/feature/saju-contents/src/main/java/com/kikidan/sajucontents/screen/YearFortuneResultScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.kikidan.designsystem.component.TodakunChip2 +import com.kikidan.designsystem.component.TodakunProgressIndicator import com.kikidan.designsystem.component.button.PrimaryButton import com.kikidan.designsystem.component.button.TodakunButtonSize import com.kikidan.designsystem.theme.TodakunColor @@ -45,6 +46,7 @@ import com.kikidan.sajucontents.BuildConfig import com.kikidan.sajucontents.R import com.kikidan.sajucontents.component.FortuneScoreCard import com.kikidan.sajucontents.component.FortuneShareDialog +import com.kikidan.sajucontents.model.YearFortuneResultLoadState import com.kikidan.sajucontents.model.YearFortuneResultState import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeEffect @@ -94,6 +96,11 @@ internal fun YearFortuneResultScreen( hazeState = hazeState, ) + when (state.resultState) { + is YearFortuneResultLoadState.Loading -> TodakunProgressIndicator() + null, is YearFortuneResultLoadState.Success, is YearFortuneResultLoadState.Failure -> Unit + } + if (state.isShareDialogVisible && fortune != null) { FortuneShareDialog( fortuneTitle = fortune.title, @@ -288,7 +295,7 @@ private fun YearFortuneResultScreenPreview() { ) TodakunTheme { YearFortuneResultScreen( - state = YearFortuneResultState(fortuneResult = sample), + state = YearFortuneResultState(resultState = YearFortuneResultLoadState.Success(sample)), onBackClick = {}, onShareClick = {}, onShareDismiss = {}, diff --git a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/CompatibilityInputViewModelTest.kt b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/CompatibilityInputViewModelTest.kt index f6e56bac..90b6dc3c 100644 --- a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/CompatibilityInputViewModelTest.kt +++ b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/CompatibilityInputViewModelTest.kt @@ -21,6 +21,9 @@ import com.kikidan.sajucontents.fake.FakePartnerSajuRepository import com.kikidan.sajucontents.fake.FakeSajuRepository import com.kikidan.sajucontents.fake.FakeUserRepository import com.kikidan.sajucontents.model.CompatibilityEntrySideEffect +import com.kikidan.sajucontents.model.CreateCompatibilityState +import com.kikidan.sajucontents.model.MyInfoLoadState +import com.kikidan.sajucontents.model.PartnerListState import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -81,13 +84,14 @@ class CompatibilityInputViewModelTest { viewModel.test(this) { viewModel.loadMyInfo() - expectState { copy(isLoading = true) } + expectState { copy(myInfoState = MyInfoLoadState.Loading) } expectState { copy( - isLoading = false, + myInfoState = MyInfoLoadState.Success, myUser = fakeUserRepository.result.getOrThrow(), myPillars = persistentListOf(), - partnerPicker = partnerPicker.copy(isLoading = false, partners = persistentListOf()), + partnerPicker = + partnerPicker.copy(partnersState = PartnerListState.Success(persistentListOf())), ) } } @@ -101,16 +105,15 @@ class CompatibilityInputViewModelTest { viewModel.test(this) { viewModel.loadMyInfo() - expectState { copy(isLoading = true) } + expectState { copy(myInfoState = MyInfoLoadState.Loading) } expectState { copy( - isLoading = false, + myInfoState = MyInfoLoadState.Success, myUser = fakeUserRepository.result.getOrThrow(), myPillars = persistentListOf(), partnerPicker = partnerPicker.copy( - isLoading = false, - partners = persistentListOf(*partners.toTypedArray()), + partnersState = PartnerListState.Success(persistentListOf(*partners.toTypedArray())), ), ) } @@ -132,16 +135,15 @@ class CompatibilityInputViewModelTest { viewModel.test(this) { viewModel.loadMyInfo() - expectState { copy(isLoading = true) } + expectState { copy(myInfoState = MyInfoLoadState.Loading) } expectState { copy( - isLoading = false, + myInfoState = MyInfoLoadState.Success, myUser = fakeUserRepository.result.getOrThrow(), myPillars = persistentListOf(), partnerPicker = partnerPicker.copy( - isLoading = false, - partners = persistentListOf(*partners.toTypedArray()), + partnersState = PartnerListState.Success(persistentListOf(*partners.toTypedArray())), ), ) } @@ -167,13 +169,14 @@ class CompatibilityInputViewModelTest { viewModel.test(this) { viewModel.loadMyInfo() - expectState { copy(isLoading = true) } + expectState { copy(myInfoState = MyInfoLoadState.Loading) } expectState { copy( - isLoading = false, + myInfoState = MyInfoLoadState.Success, myUser = fakeUserRepository.result.getOrThrow(), myPillars = persistentListOf(), - partnerPicker = partnerPicker.copy(isLoading = false, partners = persistentListOf()), + partnerPicker = + partnerPicker.copy(partnersState = PartnerListState.Success(persistentListOf())), ) } @@ -202,13 +205,14 @@ class CompatibilityInputViewModelTest { viewModel.test(this) { viewModel.loadMyInfo() - expectState { copy(isLoading = true) } + expectState { copy(myInfoState = MyInfoLoadState.Loading) } expectState { copy( - isLoading = false, + myInfoState = MyInfoLoadState.Success, myUser = fakeUserRepository.result.getOrThrow(), myPillars = persistentListOf(), - partnerPicker = partnerPicker.copy(isLoading = false, partners = persistentListOf()), + partnerPicker = + partnerPicker.copy(partnersState = PartnerListState.Success(persistentListOf())), ) } @@ -222,8 +226,8 @@ class CompatibilityInputViewModelTest { } viewModel.checkCompatibility() - expectState { copy(isCreating = true) } - expectState { copy(isCreating = false) } + expectState { copy(createState = CreateCompatibilityState.Loading) } + expectState { copy(createState = CreateCompatibilityState.Success) } expectSideEffect(CompatibilityEntrySideEffect.NavigateToResult("compat-1", "partner-1")) } assertEquals("partner-1", fakeCompatibilityRepository.lastPartnerLinkId) @@ -250,13 +254,14 @@ class CompatibilityInputViewModelTest { viewModel.test(this) { viewModel.loadMyInfo() - expectState { copy(isLoading = true) } + expectState { copy(myInfoState = MyInfoLoadState.Loading) } expectState { copy( - isLoading = false, + myInfoState = MyInfoLoadState.Success, myUser = fakeUserRepository.result.getOrThrow(), myPillars = persistentListOf(), - partnerPicker = partnerPicker.copy(isLoading = false, partners = persistentListOf()), + partnerPicker = + partnerPicker.copy(partnersState = PartnerListState.Success(persistentListOf())), ) } @@ -270,8 +275,8 @@ class CompatibilityInputViewModelTest { } viewModel.checkCompatibility() - expectState { copy(isCreating = true) } - expectState { copy(isCreating = false) } + expectState { copy(createState = CreateCompatibilityState.Loading) } + expectState { copy(createState = CreateCompatibilityState.Failure) } expectSideEffect(CompatibilityEntrySideEffect.ShowCreateError) } } diff --git a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/CompatibilityPartnerFormViewModelTest.kt b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/CompatibilityPartnerFormViewModelTest.kt index eafd0eaa..bd427922 100644 --- a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/CompatibilityPartnerFormViewModelTest.kt +++ b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/CompatibilityPartnerFormViewModelTest.kt @@ -3,6 +3,7 @@ package com.kikidan.sajucontents import com.kikidan.domain.usecase.saju.RegisterPartnerSajuUseCase import com.kikidan.sajucontents.fake.FakePartnerSajuRepository import com.kikidan.sajucontents.model.CompatibilityPartnerFormSideEffect +import com.kikidan.sajucontents.model.SavePartnerState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -70,8 +71,16 @@ class CompatibilityPartnerFormViewModelTest { expectState { copy(name = "토실이", birthDate = LocalDate.of(2001, 5, 30)) } viewModel.save() - expectState { copy(name = "토실이", birthDate = LocalDate.of(2001, 5, 30), isSaving = true) } - expectState { copy(name = "토실이", birthDate = LocalDate.of(2001, 5, 30), isSaving = false) } + expectState { + copy(name = "토실이", birthDate = LocalDate.of(2001, 5, 30), saveState = SavePartnerState.Loading) + } + expectState { + copy( + name = "토실이", + birthDate = LocalDate.of(2001, 5, 30), + saveState = SavePartnerState.Success, + ) + } expectSideEffect(CompatibilityPartnerFormSideEffect.NavigateBack) } org.junit.Assert.assertEquals("토실이", fakeRepository.lastRegisterInput?.name) @@ -90,8 +99,16 @@ class CompatibilityPartnerFormViewModelTest { expectState { copy(name = "토실이", birthDate = LocalDate.of(2001, 5, 30)) } viewModel.save() - expectState { copy(name = "토실이", birthDate = LocalDate.of(2001, 5, 30), isSaving = true) } - expectState { copy(name = "토실이", birthDate = LocalDate.of(2001, 5, 30), isSaving = false) } + expectState { + copy(name = "토실이", birthDate = LocalDate.of(2001, 5, 30), saveState = SavePartnerState.Loading) + } + expectState { + copy( + name = "토실이", + birthDate = LocalDate.of(2001, 5, 30), + saveState = SavePartnerState.Failure, + ) + } expectSideEffect(CompatibilityPartnerFormSideEffect.ShowSaveError) } } diff --git a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/DateFortuneResultViewModelTest.kt b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/DateFortuneResultViewModelTest.kt index b962d5d8..db5ffaab 100644 --- a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/DateFortuneResultViewModelTest.kt +++ b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/DateFortuneResultViewModelTest.kt @@ -4,6 +4,7 @@ import com.kikidan.domain.model.dayfortune.DayFortune import com.kikidan.domain.model.dayfortune.DayFortunePurpose import com.kikidan.domain.usecase.GetDayFortuneUseCase import com.kikidan.sajucontents.fake.FakeDayFortuneRepository +import com.kikidan.sajucontents.model.DateFortuneResultLoadState import com.kikidan.sajucontents.model.DateFortuneResultSideEffect import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -60,11 +61,13 @@ class DateFortuneResultViewModelTest { viewModel.test(this) { viewModel.loadResults(listOf("id-1", "id-2")) - expectState { copy(isLoading = true) } + expectState { copy(resultState = DateFortuneResultLoadState.Loading) } expectState { copy( - isLoading = false, - results = persistentListOf(fortune("id-2", score = 95), fortune("id-1", score = 70)), + resultState = + DateFortuneResultLoadState.Success( + persistentListOf(fortune("id-2", score = 95), fortune("id-1", score = 70)), + ), selectedResultIndex = 0, ) } @@ -83,15 +86,16 @@ class DateFortuneResultViewModelTest { viewModel.test(this) { viewModel.loadResults(ids) - expectState { copy(isLoading = true) } + expectState { copy(resultState = DateFortuneResultLoadState.Loading) } expectState { copy( - isLoading = false, - results = - persistentListOf( - fortune("id-2", score = 95), - fortune("id-4", score = 80), - fortune("id-1", score = 70), + resultState = + DateFortuneResultLoadState.Success( + persistentListOf( + fortune("id-2", score = 95), + fortune("id-4", score = 80), + fortune("id-1", score = 70), + ), ), selectedResultIndex = 0, ) @@ -110,15 +114,16 @@ class DateFortuneResultViewModelTest { viewModel.test(this) { viewModel.loadResults(ids) - expectState { copy(isLoading = true) } + expectState { copy(resultState = DateFortuneResultLoadState.Loading) } expectState { copy( - isLoading = false, - results = - persistentListOf( - fortune("id-1", score = 80), - fortune("id-2", score = 80), - fortune("id-3", score = 80), + resultState = + DateFortuneResultLoadState.Success( + persistentListOf( + fortune("id-1", score = 80), + fortune("id-2", score = 80), + fortune("id-3", score = 80), + ), ), selectedResultIndex = 0, ) @@ -135,8 +140,8 @@ class DateFortuneResultViewModelTest { viewModel.test(this) { viewModel.loadResults(listOf("id-1", "id-2")) - expectState { copy(isLoading = true) } - expectState { copy(isLoading = false) } + expectState { copy(resultState = DateFortuneResultLoadState.Loading) } + expectState { copy(resultState = DateFortuneResultLoadState.Failure) } expectSideEffect(DateFortuneResultSideEffect.ShowError) } } diff --git a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/DateFortuneViewModelTest.kt b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/DateFortuneViewModelTest.kt index 15d97fd6..10369c78 100644 --- a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/DateFortuneViewModelTest.kt +++ b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/DateFortuneViewModelTest.kt @@ -8,6 +8,7 @@ import com.kikidan.domain.usecase.DateFortuneDefaults import com.kikidan.sajucontents.fake.FakeDayFortuneRepository import com.kikidan.sajucontents.model.DateFortuneSideEffect import com.kikidan.sajucontents.model.DateFortuneState +import com.kikidan.sajucontents.model.DateFortuneSubmitState import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Dispatchers @@ -125,8 +126,8 @@ class DateFortuneViewModelTest { viewModel.test(this, initialState = initial) { viewModel.submit() - expectState { copy(isLoading = true) } - expectState { copy(isLoading = false) } + expectState { copy(submitState = DateFortuneSubmitState.Loading) } + expectState { copy(submitState = DateFortuneSubmitState.Success) } expectSideEffect(DateFortuneSideEffect.NavigateToResult(emptyList())) } @@ -160,8 +161,8 @@ class DateFortuneViewModelTest { viewModel.test(this, initialState = initial) { viewModel.submit() - expectState { copy(isLoading = true) } - expectState { copy(isLoading = false) } + expectState { copy(submitState = DateFortuneSubmitState.Loading) } + expectState { copy(submitState = DateFortuneSubmitState.Success) } expectSideEffect(DateFortuneSideEffect.NavigateToResult(listOf("id-1"))) } } @@ -180,8 +181,8 @@ class DateFortuneViewModelTest { viewModel.test(this, initialState = initial) { viewModel.submit() - expectState { copy(isLoading = true) } - expectState { copy(isLoading = false) } + expectState { copy(submitState = DateFortuneSubmitState.Loading) } + expectState { copy(submitState = DateFortuneSubmitState.Failure) } expectSideEffect(DateFortuneSideEffect.ShowError) } } diff --git a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/YearFortuneResultViewModelTest.kt b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/YearFortuneResultViewModelTest.kt index 61a7dc41..73bea51d 100644 --- a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/YearFortuneResultViewModelTest.kt +++ b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/YearFortuneResultViewModelTest.kt @@ -5,6 +5,7 @@ import com.kikidan.domain.model.fortune.FortuneCategoryStar import com.kikidan.domain.model.fortune.YearFortune import com.kikidan.domain.usecase.GetYearFortuneUseCase import com.kikidan.sajucontents.fake.FakeYearFortuneRepository +import com.kikidan.sajucontents.model.YearFortuneResultLoadState import com.kikidan.sajucontents.model.YearFortuneResultSideEffect import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,8 +56,8 @@ class YearFortuneResultViewModelTest { viewModel.test(this) { viewModel.load("id-1") - expectState { copy(isLoading = true) } - expectState { copy(isLoading = false, fortuneResult = fortune) } + expectState { copy(resultState = YearFortuneResultLoadState.Loading) } + expectState { copy(resultState = YearFortuneResultLoadState.Success(fortune)) } } assertEquals("id-1", fakeRepository.lastId) @@ -70,8 +71,8 @@ class YearFortuneResultViewModelTest { viewModel.test(this) { viewModel.load("id-1") - expectState { copy(isLoading = true) } - expectState { copy(isLoading = false) } + expectState { copy(resultState = YearFortuneResultLoadState.Loading) } + expectState { copy(resultState = YearFortuneResultLoadState.Failure) } expectSideEffect(YearFortuneResultSideEffect.ShowError) } } diff --git a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/YearFortuneViewModelTest.kt b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/YearFortuneViewModelTest.kt index 481a8f68..8611ff2f 100644 --- a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/YearFortuneViewModelTest.kt +++ b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/YearFortuneViewModelTest.kt @@ -7,6 +7,7 @@ import com.kikidan.domain.usecase.CreateYearFortuneUseCase import com.kikidan.sajucontents.fake.FakeYearFortuneRepository import com.kikidan.sajucontents.model.YearFortuneSideEffect import com.kikidan.sajucontents.model.YearFortuneState +import com.kikidan.sajucontents.model.YearFortuneSubmitState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -68,8 +69,8 @@ class YearFortuneViewModelTest { viewModel.test(this, initialState = initial) { viewModel.onSubmit() - expectState { copy(isLoading = true) } - expectState { copy(isLoading = false) } + expectState { copy(submitState = YearFortuneSubmitState.Loading) } + expectState { copy(submitState = YearFortuneSubmitState.Success) } expectSideEffect(YearFortuneSideEffect.NavigateToResult("id-1")) } @@ -85,8 +86,8 @@ class YearFortuneViewModelTest { viewModel.test(this, initialState = initial) { viewModel.onSubmit() - expectState { copy(isLoading = true) } - expectState { copy(isLoading = false) } + expectState { copy(submitState = YearFortuneSubmitState.Loading) } + expectState { copy(submitState = YearFortuneSubmitState.Failure) } expectSideEffect(YearFortuneSideEffect.ShowError) } } diff --git a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/model/DateFortuneStateTest.kt b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/model/DateFortuneStateTest.kt index 060f8240..f11e75fb 100644 --- a/feature/saju-contents/src/test/java/com/kikidan/sajucontents/model/DateFortuneStateTest.kt +++ b/feature/saju-contents/src/test/java/com/kikidan/sajucontents/model/DateFortuneStateTest.kt @@ -29,7 +29,7 @@ class DateFortuneStateTest { DateFortuneState( selectedPurpose = DayFortunePurpose.TRAVEL, selectedDates = persistentListOf(LocalDate.now()), - isLoading = true, + submitState = DateFortuneSubmitState.Loading, ) assertFalse(state.canSubmit)