diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index 6e1fdc00..64162060 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -11,6 +11,8 @@ import com.threegap.bitnagil.presentation.emotion.EmotionScreenContainer import com.threegap.bitnagil.presentation.intro.IntroScreenContainer import com.threegap.bitnagil.presentation.login.LoginScreenContainer import com.threegap.bitnagil.presentation.onboarding.OnBoardingScreenContainer +import com.threegap.bitnagil.presentation.onboarding.OnBoardingViewModel +import com.threegap.bitnagil.presentation.onboarding.model.navarg.OnBoardingScreenArg import com.threegap.bitnagil.presentation.setting.SettingScreenContainer import com.threegap.bitnagil.presentation.splash.SplashScreenContainer import com.threegap.bitnagil.presentation.terms.TermsAgreementScreenContainer @@ -80,7 +82,7 @@ fun MainNavHost( ) }, navigateToOnBoarding = { - navigator.navController.navigate(Route.OnBoarding) + navigator.navController.navigate(Route.OnBoarding()) }, navigateToBack = { navigator.navController.popBackStack() }, ) @@ -92,7 +94,7 @@ fun MainNavHost( navigator.navController.navigate(Route.Setting) }, navigateToOnBoarding = { - navigator.navController.navigate(Route.OnBoarding) + navigator.navController.navigate(Route.OnBoarding(isNew = false)) }, navigateToNotice = { }, @@ -150,8 +152,20 @@ fun MainNavHost( ) } - composable { + composable { navBackStackEntry -> + val arg = navBackStackEntry.toRoute() + val onBoardingScreenArg = if (arg.isNew) { + OnBoardingScreenArg.NEW + } else { + OnBoardingScreenArg.RESET + } + + val viewModel = hiltViewModel { factory -> + factory.create(onBoardingScreenArg) + } + OnBoardingScreenContainer( + onBoardingViewModel = viewModel, navigateToHome = { navigator.navController.navigate(Route.Home) { popUpTo(navigator.navController.graph.startDestinationId) { diff --git a/app/src/main/java/com/threegap/bitnagil/Route.kt b/app/src/main/java/com/threegap/bitnagil/Route.kt index c94979a3..dc540dd8 100644 --- a/app/src/main/java/com/threegap/bitnagil/Route.kt +++ b/app/src/main/java/com/threegap/bitnagil/Route.kt @@ -29,7 +29,9 @@ sealed interface Route { data object Setting : Route @Serializable - data object OnBoarding : Route + data class OnBoarding( + val isNew: Boolean = true, + ) : Route @Serializable data class WriteRoutine( diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilSelectButton.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilSelectButton.kt index 85167f5a..dabf3fad 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilSelectButton.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilSelectButton.kt @@ -30,7 +30,7 @@ import com.threegap.bitnagil.designsystem.BitnagilTheme @Composable fun BitnagilSelectButton( title: String, - onClick: () -> Unit, + onClick: (() -> Unit)?, modifier: Modifier = Modifier, description: String? = null, selected: Boolean = false, @@ -57,11 +57,17 @@ fun BitnagilSelectButton( .fillMaxWidth() .clip(shape) .background(backgroundColor) - .clickable( - interactionSource = interactionSource, - indication = null, - onClick = onClick, - ) + .let { + if (onClick != null) { + it.clickable( + interactionSource = interactionSource, + indication = null, + onClick = onClick, + ) + } else { + it + } + } .padding(horizontal = 20.dp, vertical = 16.dp) .semantics { role = Role.Button diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/model/dto/EmotionRecommendedRoutineDto.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/model/dto/EmotionRecommendedRoutineDto.kt index 497acdb3..5ac14441 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/model/dto/EmotionRecommendedRoutineDto.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/model/dto/EmotionRecommendedRoutineDto.kt @@ -1,5 +1,6 @@ package com.threegap.bitnagil.data.emotion.model.dto +import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -13,4 +14,12 @@ data class EmotionRecommendedRoutineDto( val recommendedRoutineDescription: String, @SerialName("recommendedSubRoutineSearchResult") val recommendedSubRoutineSearchResult: List, -) +) { + fun toEmotionRecommendRoutine(): EmotionRecommendRoutine { + return EmotionRecommendRoutine( + routineId = recommendedRoutineId.toString(), + routineName = recommendedRoutineName, + routineDescription = recommendedRoutineDescription, + ) + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt index 55ceb838..6c08bf65 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt @@ -3,6 +3,7 @@ package com.threegap.bitnagil.data.emotion.repositoryImpl import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource import com.threegap.bitnagil.data.emotion.model.response.toDomain import com.threegap.bitnagil.domain.emotion.model.Emotion +import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import com.threegap.bitnagil.domain.emotion.model.MyEmotion import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import javax.inject.Inject @@ -26,7 +27,7 @@ class EmotionRepositoryImpl @Inject constructor( } } - override suspend fun registerEmotion(emotion: Emotion): Result { + override suspend fun registerEmotion(emotion: Emotion): Result> { val selectedEmotion = when (emotion) { Emotion.CALM -> "CALM" Emotion.VITALITY -> "VITALITY" @@ -36,7 +37,12 @@ class EmotionRepositoryImpl @Inject constructor( Emotion.FATIGUE -> "FATIGUE" } - return emotionDataSource.registerEmotion(selectedEmotion).map { _ -> } + return emotionDataSource.registerEmotion(selectedEmotion).map { + it.recommendedRoutines.map { + emotionRecommendedRoutineDto -> + emotionRecommendedRoutineDto.toEmotionRecommendRoutine() + } + } } override suspend fun getMyEmotionMarble(currentDate: String): Result = diff --git a/data/src/main/java/com/threegap/bitnagil/data/onboarding/model/dto/OnBoardingItemDto.kt b/data/src/main/java/com/threegap/bitnagil/data/onboarding/model/dto/OnBoardingItemDto.kt index 6146011b..308b9e28 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/onboarding/model/dto/OnBoardingItemDto.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/onboarding/model/dto/OnBoardingItemDto.kt @@ -35,25 +35,25 @@ data class OnBoardingItemDto( ) val RealOutingZeroPerWeek = OnBoardingItemDto( - id = "ZERO_PER_WEEK", + id = "NEVER", title = "밖에 나가지 않고 집에서만 지냈어요", description = null, ) val RealOutingOneToTwoPerWeek = OnBoardingItemDto( - id = "ONE_TO_TWO_PER_WEEK", + id = "SHORT", title = "잠깐 외출했어요", description = null, ) val RealOutingThreeToFourPerWeek = OnBoardingItemDto( - id = "THREE_TO_FOUR_PER_WEEK", + id = "SOMETIMES", title = "가끔 나가요", description = null, ) val RealOutingMoreThanFivePerWeek = OnBoardingItemDto( - id = "MORE_THAN_FIVE_PER_WEEK", + id = "OFTEN", title = "자주 외출해요", description = null, ) @@ -83,19 +83,19 @@ data class OnBoardingItemDto( ) val TargetOutingOneToTwoPerWeek = OnBoardingItemDto( - id = "ONE_TO_TWO_PER_WEEK", + id = "ONE_PER_WEEK", title = "시작이 더 중요해요", description = "일주일에 1회", ) val TargetOutingThreeToFourPerWeek = OnBoardingItemDto( - id = "THREE_TO_FOUR_PER_WEEK", + id = "TWO_TO_THREE_PER_WEEK", title = "너무 무리하지 않아도 괜찮아요", description = "일주일에 2~3회", ) val TargetOutingMoreThenFivePerWeek = OnBoardingItemDto( - id = "MORE_THAN_FIVE_PER_WEEK", + id = "MORE_THAN_FOUR_PER_WEEK", title = "이 정도면 충분히 활력 있는 한 주가 될거에요", description = "일주일에 4회 이상", ) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionRecommendRoutine.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionRecommendRoutine.kt new file mode 100644 index 00000000..a351fad5 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionRecommendRoutine.kt @@ -0,0 +1,7 @@ +package com.threegap.bitnagil.domain.emotion.model + +data class EmotionRecommendRoutine( + val routineId: String, + val routineName: String, + val routineDescription: String, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt index a47ca36b..1cb68de7 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt @@ -1,10 +1,11 @@ package com.threegap.bitnagil.domain.emotion.repository import com.threegap.bitnagil.domain.emotion.model.Emotion +import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import com.threegap.bitnagil.domain.emotion.model.MyEmotion interface EmotionRepository { suspend fun getEmotions(): Result> - suspend fun registerEmotion(emotion: Emotion): Result + suspend fun registerEmotion(emotion: Emotion): Result> suspend fun getMyEmotionMarble(currentDate: String): Result } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/RegisterEmotionUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/RegisterEmotionUseCase.kt index bad0b24e..6c695312 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/RegisterEmotionUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/RegisterEmotionUseCase.kt @@ -1,13 +1,14 @@ package com.threegap.bitnagil.domain.emotion.usecase import com.threegap.bitnagil.domain.emotion.model.Emotion +import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import javax.inject.Inject class RegisterEmotionUseCase @Inject constructor( private val emotionRepository: EmotionRepository, ) { - suspend operator fun invoke(emotion: Emotion): Result { + suspend operator fun invoke(emotion: Emotion): Result> { return emotionRepository.registerEmotion(emotion) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionScreen.kt index fe071e53..20fc5846 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionScreen.kt @@ -1,5 +1,6 @@ package com.threegap.bitnagil.presentation.emotion +import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -11,22 +12,32 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.component.atom.BitnagilSelectButton +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButtonColor +import com.threegap.bitnagil.designsystem.component.block.BitnagilProgressTopBar import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple import com.threegap.bitnagil.presentation.common.flow.collectAsEffect import com.threegap.bitnagil.presentation.emotion.model.Emotion +import com.threegap.bitnagil.presentation.emotion.model.EmotionRecommendRoutineUiModel +import com.threegap.bitnagil.presentation.emotion.model.EmotionScreenStep import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionSideEffect import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionState @@ -37,17 +48,30 @@ fun EmotionScreenContainer( ) { val state by viewModel.stateFlow.collectAsState() + BackHandler { + viewModel.moveToPrev() + } + viewModel.sideEffectFlow.collectAsEffect { sideEffect -> when (sideEffect) { EmotionSideEffect.NavigateToBack -> navigateToBack() } } - EmotionScreen( - state = state, - onClickPreviousButton = navigateToBack, - onClickEmotion = viewModel::selectEmotion, - ) + when (state.step) { + EmotionScreenStep.Emotion -> EmotionScreen( + state = state, + onClickPreviousButton = navigateToBack, + onClickEmotion = viewModel::selectEmotion, + ) + EmotionScreenStep.RecommendRoutines -> EmotionRecommendRoutineScreen( + state = state, + onClickPreviousButton = viewModel::moveToPrev, + onClickRoutine = viewModel::selectRecommendRoutine, + onClickRegisterRecommendRoutines = viewModel::registerRecommendRoutines, + onClickSkip = navigateToBack, + ) + } } @Composable @@ -111,17 +135,126 @@ private fun EmotionScreen( } } +@Composable +private fun EmotionRecommendRoutineScreen( + state: EmotionState, + onClickPreviousButton: () -> Unit, + onClickRoutine: (String) -> Unit, + onClickRegisterRecommendRoutines: () -> Unit, + onClickSkip: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(color = BitnagilTheme.colors.coolGray99), + ) { + BitnagilProgressTopBar( + onBackClick = onClickPreviousButton, + progress = 1f, + ) + + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp, end = 16.dp, bottom = 20.dp, top = 32.dp), + ) { + Text( + text = "오늘 감정에 따른\n루틴을 추천드릴께요!", + color = BitnagilTheme.colors.navy500, + style = BitnagilTheme.typography.title2Bold, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = "오늘 당신의 감정 상태에 맞춰 구성된 맞춤 루틴이에요.\n원하는 루틴을 선택해서 가볍게 시작해보세요.", + color = BitnagilTheme.colors.coolGray50, + style = BitnagilTheme.typography.body2Medium, + ) + + Spacer(modifier = Modifier.height(28.dp)) + + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(state = scrollState), + ) { + for (recommendRoutine in state.recommendRoutines) { + BitnagilSelectButton( + title = recommendRoutine.name, + description = recommendRoutine.description, + onClick = { onClickRoutine(recommendRoutine.id) }, + selected = recommendRoutine.selected, + modifier = Modifier.padding(bottom = 12.dp), + ) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + BitnagilTextButton( + text = "변경하기", + onClick = onClickRegisterRecommendRoutines, + enabled = state.registerRecommendRoutinesButtonEnabled, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + BitnagilTextButton( + text = "건너뛰기", + onClick = onClickSkip, + colors = BitnagilTextButtonColor.skip().copy( + defaultBackgroundColor = Color.Transparent, + pressedBackgroundColor = Color.Transparent, + disabledBackgroundColor = Color.Transparent, + ), + textStyle = BitnagilTheme.typography.body2Regular, + textDecoration = TextDecoration.Underline, + ) + } + } +} + @Preview @Composable -fun MyPageScreenPreview() { +private fun EmotionScreenPreview() { BitnagilTheme { EmotionScreen( state = EmotionState( emotions = Emotion.entries, isLoading = false, + step = EmotionScreenStep.Emotion, + recommendRoutines = listOf(), ), onClickEmotion = { _ -> }, onClickPreviousButton = {}, ) } } + +@Preview +@Composable +private fun EmotionRecommendRoutineScreenPreview() { + BitnagilTheme { + EmotionRecommendRoutineScreen( + state = EmotionState( + emotions = Emotion.entries, + isLoading = false, + step = EmotionScreenStep.RecommendRoutines, + recommendRoutines = listOf( + EmotionRecommendRoutineUiModel( + id = "1", + name = "루틴 이름", + description = "루틴 설명", + selected = true, + ), + ), + ), + onClickPreviousButton = {}, + onClickRoutine = {}, + onClickRegisterRecommendRoutines = {}, + onClickSkip = {}, + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt index db699073..9729b1ec 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/EmotionViewModel.kt @@ -4,15 +4,17 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionsUseCase import com.threegap.bitnagil.domain.emotion.usecase.RegisterEmotionUseCase +import com.threegap.bitnagil.domain.onboarding.usecase.RegisterRecommendOnBoardingRoutinesUseCase import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel import com.threegap.bitnagil.presentation.emotion.model.Emotion +import com.threegap.bitnagil.presentation.emotion.model.EmotionRecommendRoutineUiModel +import com.threegap.bitnagil.presentation.emotion.model.EmotionScreenStep import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionIntent import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionSideEffect import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import org.orbitmvi.orbit.syntax.simple.SimpleSyntax -import org.orbitmvi.orbit.syntax.simple.postSideEffect import javax.inject.Inject @HiltViewModel @@ -20,6 +22,7 @@ class EmotionViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val getEmotionsUseCase: GetEmotionsUseCase, private val registerEmotionUseCase: RegisterEmotionUseCase, + private val registerRecommendOnBoardingRoutinesUseCase: RegisterRecommendOnBoardingRoutinesUseCase, ) : MviViewModel( savedStateHandle = savedStateHandle, initState = EmotionState.Init, @@ -51,15 +54,55 @@ class EmotionViewModel @Inject constructor( isLoading = false, ) } - EmotionIntent.RegisterEmotionSuccess -> { - postSideEffect(EmotionSideEffect.NavigateToBack) - return null + is EmotionIntent.RegisterEmotionSuccess -> { + return state.copy( + recommendRoutines = intent.recommendRoutines, + step = EmotionScreenStep.RecommendRoutines, + isLoading = false, + ) } EmotionIntent.RegisterEmotionLoading -> { return state.copy( isLoading = true, ) } + EmotionIntent.RegisterRecommendRoutinesLoading -> { + return state.copy( + isLoading = true, + ) + } + EmotionIntent.RegisterRecommendRoutinesFailure -> { + return state.copy( + isLoading = false, + ) + } + EmotionIntent.RegisterRecommendRoutinesSuccess -> { + sendSideEffect(EmotionSideEffect.NavigateToBack) + return null + } + EmotionIntent.BackToSelectEmotionStep -> { + return state.copy( + recommendRoutines = listOf(), + step = EmotionScreenStep.Emotion, + isLoading = false, + ) + } + + is EmotionIntent.SelectRecommendRoutine -> { + val selectChangedRecommendRoutines = state.recommendRoutines.map { + if (it.id == intent.recommendRoutineId) { + it.copy(selected = !it.selected) + } else { + it + } + } + return state.copy(recommendRoutines = selectChangedRecommendRoutines) + } + + EmotionIntent.NavigateToBack -> { + sendSideEffect(EmotionSideEffect.NavigateToBack) + return null + } } } @@ -70,8 +113,9 @@ class EmotionViewModel @Inject constructor( viewModelScope.launch { sendIntent(EmotionIntent.RegisterEmotionLoading) registerEmotionUseCase(emotion = emotion.toDomain()).fold( - onSuccess = { - sendIntent(EmotionIntent.RegisterEmotionSuccess) + onSuccess = { emotionRecommendRoutines -> + val recommendRoutines = emotionRecommendRoutines.map { EmotionRecommendRoutineUiModel.fromEmotionRecommendRoutine(it) } + sendIntent(EmotionIntent.RegisterEmotionSuccess(recommendRoutines)) }, onFailure = { // todo 실패 케이스 정의되면 처리 @@ -79,4 +123,41 @@ class EmotionViewModel @Inject constructor( ) } } + + fun selectRecommendRoutine(recommendRoutineId: String) { + viewModelScope.launch { + sendIntent(EmotionIntent.SelectRecommendRoutine(recommendRoutineId)) + } + } + + fun moveToPrev() { + viewModelScope.launch { + val currentState = stateFlow.value + + when (currentState.step) { + EmotionScreenStep.Emotion -> sendIntent(EmotionIntent.NavigateToBack) + EmotionScreenStep.RecommendRoutines -> sendIntent(EmotionIntent.BackToSelectEmotionStep) + } + } + } + + fun registerRecommendRoutines() { + val isLoading = stateFlow.value.isLoading + if (isLoading) return + + viewModelScope.launch { + sendIntent(EmotionIntent.RegisterRecommendRoutinesLoading) + + val currentState = stateFlow.value + val selectedRecommendRoutineIds = currentState.recommendRoutines.filter { it.selected }.map { it.id } + registerRecommendOnBoardingRoutinesUseCase(selectedRecommendRoutineIds).fold( + onSuccess = { + sendIntent(EmotionIntent.RegisterRecommendRoutinesSuccess) + }, + onFailure = { + sendIntent(EmotionIntent.RegisterRecommendRoutinesFailure) + }, + ) + } + } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/EmotionRecommendRoutineUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/EmotionRecommendRoutineUiModel.kt new file mode 100644 index 00000000..a93a3784 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/EmotionRecommendRoutineUiModel.kt @@ -0,0 +1,26 @@ +package com.threegap.bitnagil.presentation.emotion.model + +import android.os.Parcelable +import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine +import kotlinx.parcelize.Parcelize + +@Parcelize +data class EmotionRecommendRoutineUiModel( + val id: String, + val name: String, + val description: String, + val selected: Boolean, +) : Parcelable { + companion object { + fun fromEmotionRecommendRoutine( + emotionRecommendRoutine: EmotionRecommendRoutine, + ): EmotionRecommendRoutineUiModel { + return EmotionRecommendRoutineUiModel( + id = emotionRecommendRoutine.routineId, + name = emotionRecommendRoutine.routineName, + description = emotionRecommendRoutine.routineDescription, + selected = false, + ) + } + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/EmotionScreenStep.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/EmotionScreenStep.kt new file mode 100644 index 00000000..434c06ce --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/EmotionScreenStep.kt @@ -0,0 +1,6 @@ +package com.threegap.bitnagil.presentation.emotion.model + +enum class EmotionScreenStep { + Emotion, RecommendRoutines, + ; +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionIntent.kt index 2778c0c6..33e2b74f 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionIntent.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionIntent.kt @@ -2,9 +2,16 @@ package com.threegap.bitnagil.presentation.emotion.model.mvi import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent import com.threegap.bitnagil.presentation.emotion.model.Emotion +import com.threegap.bitnagil.presentation.emotion.model.EmotionRecommendRoutineUiModel sealed class EmotionIntent : MviIntent { data class EmotionListLoadSuccess(val emotions: List) : EmotionIntent() - data object RegisterEmotionSuccess : EmotionIntent() + data class RegisterEmotionSuccess(val recommendRoutines: List) : EmotionIntent() data object RegisterEmotionLoading : EmotionIntent() + data object RegisterRecommendRoutinesLoading : EmotionIntent() + data object RegisterRecommendRoutinesSuccess : EmotionIntent() + data object RegisterRecommendRoutinesFailure : EmotionIntent() + data object BackToSelectEmotionStep : EmotionIntent() + data class SelectRecommendRoutine(val recommendRoutineId: String) : EmotionIntent() + data object NavigateToBack : EmotionIntent() } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionState.kt index c43268a6..511c1b76 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/emotion/model/mvi/EmotionState.kt @@ -3,6 +3,8 @@ package com.threegap.bitnagil.presentation.emotion.model.mvi import androidx.compose.runtime.Immutable import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState import com.threegap.bitnagil.presentation.emotion.model.Emotion +import com.threegap.bitnagil.presentation.emotion.model.EmotionRecommendRoutineUiModel +import com.threegap.bitnagil.presentation.emotion.model.EmotionScreenStep import kotlinx.parcelize.Parcelize @Parcelize @@ -10,11 +12,18 @@ import kotlinx.parcelize.Parcelize data class EmotionState( val emotions: List, val isLoading: Boolean, + val recommendRoutines: List, + val step: EmotionScreenStep, ) : MviState { companion object { val Init = EmotionState( emotions = emptyList(), isLoading = true, + recommendRoutines = emptyList(), + step = EmotionScreenStep.Emotion, ) } + + val registerRecommendRoutinesButtonEnabled: Boolean + get() = recommendRoutines.any { it.selected } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingScreen.kt index 69c623ef..e0c07696 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingScreen.kt @@ -14,7 +14,9 @@ import com.threegap.bitnagil.designsystem.component.block.BitnagilProgressTopBar import com.threegap.bitnagil.presentation.common.flow.collectAsEffect import com.threegap.bitnagil.presentation.onboarding.component.template.OnBoardingAbstractTemplate import com.threegap.bitnagil.presentation.onboarding.component.template.OnBoardingSelectTemplate +import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingItem import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingPageInfo +import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingSetType import com.threegap.bitnagil.presentation.onboarding.model.mvi.OnBoardingSideEffect import com.threegap.bitnagil.presentation.onboarding.model.mvi.OnBoardingState @@ -87,12 +89,20 @@ private fun OnBoardingScreen( OnBoardingSelectTemplate( modifier = Modifier.weight(1f), title = "당신만의 추천 루틴이\n생성되었어요!", - subText = "당신의 생활 패턴과 목표에 맞춰 구성된 맞춤 루틴이에요.\n지금부터 가볍게 시작해보세요.", + subText = state.onBoardingSetType.subText, items = currentOnBoardingPageInfo.routines, nextButtonEnable = state.nextButtonEnable, onClickNextButton = onClickRegister, - onClickItem = onClickRoutine, - onClickSkip = onClickSkip, + onClickItem = if (state.onBoardingSetType.canSelectRoutine) { + onClickRoutine + } else { + null + }, + onClickSkip = if (state.onBoardingSetType.canSkip) { + onClickSkip + } else { + null + }, ) } is OnBoardingPageInfo.SelectOnBoarding -> { @@ -121,9 +131,14 @@ fun OnBoardingScreenPreview() { OnBoardingScreen( state = OnBoardingState.Idle( nextButtonEnable = false, - currentOnBoardingPageInfo = OnBoardingPageInfo.SelectOnBoarding(id = "id", title = "title", description = "description"), + currentOnBoardingPageInfo = OnBoardingPageInfo.RecommendRoutines( + listOf( + OnBoardingItem("1", "루틴명", "세부 루틴 한 줄 설명", null), + ), + ), totalStep = 5, currentStep = 1, + onBoardingSetType = OnBoardingSetType.RESET, ), onClickNext = {}, onClickPreviousInSelectOnBoarding = {}, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt index 3b409dd6..07f96d4c 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/OnBoardingViewModel.kt @@ -10,9 +10,14 @@ import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingAbstractTextItem import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingItem import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingPageInfo +import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingSetType import com.threegap.bitnagil.presentation.onboarding.model.mvi.OnBoardingIntent import com.threegap.bitnagil.presentation.onboarding.model.mvi.OnBoardingSideEffect import com.threegap.bitnagil.presentation.onboarding.model.mvi.OnBoardingState +import com.threegap.bitnagil.presentation.onboarding.model.navarg.OnBoardingScreenArg +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.async @@ -20,19 +25,23 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.orbitmvi.orbit.syntax.simple.SimpleSyntax -import javax.inject.Inject -@HiltViewModel -class OnBoardingViewModel @Inject constructor( +@HiltViewModel(assistedFactory = OnBoardingViewModel.Factory::class) +class OnBoardingViewModel @AssistedInject constructor( savedStateHandle: SavedStateHandle, private val getOnBoardingsUseCase: GetOnBoardingsUseCase, private val getRecommendOnBoardingRoutinesUseCase: GetRecommendOnBoardingRoutinesUseCase, private val getOnBoardingAbstractUseCase: GetOnBoardingAbstractUseCase, private val registerRecommendOnBoardingRoutinesUseCase: RegisterRecommendOnBoardingRoutinesUseCase, + @Assisted private val onBoardingArg: OnBoardingScreenArg, ) : MviViewModel( initState = OnBoardingState.Loading, savedStateHandle = savedStateHandle, ) { + @AssistedFactory interface Factory { + fun create(onBoardingArg: OnBoardingScreenArg): OnBoardingViewModel + } + // 내부에 전체 온보딩 항목 저장 private val onBoardingPageInfos = mutableListOf() @@ -68,6 +77,7 @@ class OnBoardingViewModel @Inject constructor( currentOnBoardingPageInfo = onBoardingPageInfos.first(), totalStep = onBoardingPageInfos.size + 2, currentStep = 1, + onBoardingSetType = OnBoardingSetType.fromOnBoardingScreenArg(onBoardingArg), ) } @@ -137,7 +147,7 @@ class OnBoardingViewModel @Inject constructor( return currentState.copy( currentOnBoardingPageInfo = recommendRoutinePageInfo, currentStep = currentState.currentStep + 1, - nextButtonEnable = false, + nextButtonEnable = !currentState.onBoardingSetType.canSelectRoutine, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/component/template/OnBoardingSelectTemplate.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/component/template/OnBoardingSelectTemplate.kt index 634dbf5c..9c84e45d 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/component/template/OnBoardingSelectTemplate.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/component/template/OnBoardingSelectTemplate.kt @@ -25,7 +25,7 @@ fun OnBoardingSelectTemplate( items: List, nextButtonEnable: Boolean = false, onClickNextButton: () -> Unit, - onClickItem: (String) -> Unit, + onClickItem: ((String) -> Unit)?, onClickSkip: (() -> Unit)? = null, ) { Column( @@ -59,8 +59,10 @@ fun OnBoardingSelectTemplate( BitnagilSelectButton( title = item.title, description = item.description, - onClick = { - onClickItem(item.id) + onClick = if (onClickItem != null) { + { onClickItem(item.id) } + } else { + null }, selected = item.selected, modifier = Modifier.padding(bottom = 12.dp), diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/OnBoardingSetType.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/OnBoardingSetType.kt new file mode 100644 index 00000000..d76da889 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/OnBoardingSetType.kt @@ -0,0 +1,32 @@ +package com.threegap.bitnagil.presentation.onboarding.model + +import com.threegap.bitnagil.presentation.onboarding.model.navarg.OnBoardingScreenArg + +enum class OnBoardingSetType( + val subText: String, + val canSkip: Boolean, + val canSelectRoutine: Boolean, +) { + NEW( + subText = "당신의 생활 패턴과 목표에 맞춰 구성된 맞춤 루틴이에요.\n지금부터 가볍게 시작해보세요.", + canSkip = true, + canSelectRoutine = true, + ), + RESET( + subText = "생활 패턴과 목표에 맞춰 다시 구성된 맞춤 루틴이에요.\n원하는 루틴을 선택해서 가볍게 시작해보세요.", + canSkip = false, + canSelectRoutine = false, + ), + ; + + companion object { + fun fromOnBoardingScreenArg( + onBoardingScreenArg: OnBoardingScreenArg, + ): OnBoardingSetType { + return when (onBoardingScreenArg) { + OnBoardingScreenArg.NEW -> NEW + OnBoardingScreenArg.RESET -> RESET + } + } + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingState.kt index 5baecc4c..75910ffe 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/mvi/OnBoardingState.kt @@ -3,6 +3,7 @@ package com.threegap.bitnagil.presentation.onboarding.model.mvi import android.os.Parcelable import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingPageInfo +import com.threegap.bitnagil.presentation.onboarding.model.OnBoardingSetType import kotlinx.parcelize.Parcelize @Parcelize @@ -17,5 +18,6 @@ sealed class OnBoardingState(val progress: Float) : Parcelable, MviState { val currentOnBoardingPageInfo: OnBoardingPageInfo, val totalStep: Int, val currentStep: Int, + val onBoardingSetType: OnBoardingSetType, ) : OnBoardingState(progress = currentStep.toFloat() / totalStep.toFloat()) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/navarg/OnBoardingScreenArg.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/navarg/OnBoardingScreenArg.kt new file mode 100644 index 00000000..6d485070 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/onboarding/model/navarg/OnBoardingScreenArg.kt @@ -0,0 +1,6 @@ +package com.threegap.bitnagil.presentation.onboarding.model.navarg + +enum class OnBoardingScreenArg { + NEW, RESET, + ; +}