diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/DayRoutinesDto.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/DayRoutinesDto.kt index 6ceff985..28bbdfcb 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/DayRoutinesDto.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/DayRoutinesDto.kt @@ -1,6 +1,6 @@ package com.threegap.bitnagil.data.routine.model.response -import com.threegap.bitnagil.domain.routine.model.DayRoutines +import com.threegap.bitnagil.domain.routine.model.DailyRoutines import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -12,8 +12,8 @@ data class DayRoutinesDto( val allCompleted: Boolean, ) -fun DayRoutinesDto.toDomain(): DayRoutines = - DayRoutines( - routineList = routineList.map { it.toDomain() }, - allCompleted = allCompleted, +fun DayRoutinesDto.toDomain(): DailyRoutines = + DailyRoutines( + routines = routineList.map { it.toDomain() }, + isAllCompleted = allCompleted, ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutineDto.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutineDto.kt index d78c6a20..e63d5756 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutineDto.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutineDto.kt @@ -36,16 +36,16 @@ data class RoutineDto( fun RoutineDto.toDomain(): Routine = Routine( - routineId = this.routineId, - routineName = this.routineName, - repeatDay = this.repeatDay.map { DayOfWeek.fromString(it) }, + id = this.routineId, + name = this.routineName, + repeatDays = this.repeatDay.map { DayOfWeek.fromString(it) }, executionTime = this.executionTime, routineDate = this.routineDate, - routineCompleteYn = this.routineCompleteYn, + isCompleted = this.routineCompleteYn, subRoutineNames = this.subRoutineNames, - subRoutineCompleteYn = this.subRoutineCompleteYn, + subRoutineCompletionStates = this.subRoutineCompleteYn, recommendedRoutineType = RecommendedRoutineType.fromString(this.recommendedRoutineType), - routineDeletedYn = routineDeletedYn, + isDeleted = routineDeletedYn, startDate = this.routineStartDate, endDate = this.routineEndDate, ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutinesResponseDto.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutinesResponseDto.kt index bbf5f92f..5f9fd500 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutinesResponseDto.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutinesResponseDto.kt @@ -1,6 +1,6 @@ package com.threegap.bitnagil.data.routine.model.response -import com.threegap.bitnagil.domain.routine.model.Routines +import com.threegap.bitnagil.domain.routine.model.RoutineSchedule import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -11,8 +11,8 @@ data class RoutinesResponseDto( ) fun RoutinesResponseDto.toDomain() = - Routines( - routines = this.routines.mapValues { (_, dayRoutinesDto) -> + RoutineSchedule( + dailyRoutines = this.routines.mapValues { (_, dayRoutinesDto) -> dayRoutinesDto.toDomain() }, ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/repositoryImpl/RoutineRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/repositoryImpl/RoutineRepositoryImpl.kt index 2e1542b0..2c76fe7a 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/routine/repositoryImpl/RoutineRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/routine/repositoryImpl/RoutineRepositoryImpl.kt @@ -5,14 +5,14 @@ import com.threegap.bitnagil.data.routine.model.request.toDto import com.threegap.bitnagil.data.routine.model.response.toDomain import com.threegap.bitnagil.domain.routine.model.Routine import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos -import com.threegap.bitnagil.domain.routine.model.Routines +import com.threegap.bitnagil.domain.routine.model.RoutineSchedule import com.threegap.bitnagil.domain.routine.repository.RoutineRepository import javax.inject.Inject class RoutineRepositoryImpl @Inject constructor( private val routineRemoteDataSource: RoutineRemoteDataSource, ) : RoutineRepository { - override suspend fun fetchWeeklyRoutines(startDate: String, endDate: String): Result = + override suspend fun fetchWeeklyRoutines(startDate: String, endDate: String): Result = routineRemoteDataSource.fetchWeeklyRoutines(startDate, endDate) .map { it.toDomain() } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchTodayEmotionUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchTodayEmotionUseCase.kt index 35750da6..b98453e4 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchTodayEmotionUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchTodayEmotionUseCase.kt @@ -2,11 +2,14 @@ package com.threegap.bitnagil.domain.emotion.usecase import com.threegap.bitnagil.domain.emotion.model.TodayEmotion import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository +import java.time.LocalDate import javax.inject.Inject class FetchTodayEmotionUseCase @Inject constructor( private val emotionRepository: EmotionRepository, ) { - suspend operator fun invoke(currentDate: String): Result = - emotionRepository.fetchTodayEmotion(currentDate) + suspend operator fun invoke(): Result { + val currentDate = LocalDate.now().toString() + return emotionRepository.fetchTodayEmotion(currentDate) + } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/DailyRoutines.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/DailyRoutines.kt new file mode 100644 index 00000000..894c2c92 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/DailyRoutines.kt @@ -0,0 +1,13 @@ +package com.threegap.bitnagil.domain.routine.model + +/** + * 특정 하루("일간")의 루틴 정보. + * [RoutineSchedule] Map의 '값(value)'으로 사용됩니다. + * + * @property routines 해당 날짜에 포함된 [Routine](개별 루틴)의 목록. + * @property isAllCompleted 해당 날짜의 모든 루틴이 완료되었는지 여부. + */ +data class DailyRoutines( + val routines: List, + val isAllCompleted: Boolean, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/DayRoutines.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/DayRoutines.kt deleted file mode 100644 index 2fb1abdd..00000000 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/DayRoutines.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.threegap.bitnagil.domain.routine.model - -data class DayRoutines( - val routineList: List, - val allCompleted: Boolean, -) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routine.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routine.kt index 3f9eee0d..d275d11f 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routine.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routine.kt @@ -1,16 +1,16 @@ package com.threegap.bitnagil.domain.routine.model data class Routine( - val routineId: String, - val routineName: String, - val repeatDay: List, + val id: String, + val name: String, + val repeatDays: List, val executionTime: String, val startDate: String, val endDate: String, val routineDate: String, - val routineCompleteYn: Boolean, - val routineDeletedYn: Boolean, + val isCompleted: Boolean, + val isDeleted: Boolean, val subRoutineNames: List, - val subRoutineCompleteYn: List, + val subRoutineCompletionStates: List, val recommendedRoutineType: RecommendedRoutineType?, ) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineSchedule.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineSchedule.kt new file mode 100644 index 00000000..99736fab --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineSchedule.kt @@ -0,0 +1,11 @@ +package com.threegap.bitnagil.domain.routine.model + +/** + * 특정 기간(예: 주간, 월간) 동안의 루틴 일정표. + * + * @property dailyRoutines 날짜(String, "YYYY-MM-DD")를 key로, + * 해당 날짜의 루틴 정보([DailyRoutines])를 value로 가지는 Map. + */ +data class RoutineSchedule( + val dailyRoutines: Map, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineToggleState.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineToggleState.kt new file mode 100644 index 00000000..0b3a5e79 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineToggleState.kt @@ -0,0 +1,6 @@ +package com.threegap.bitnagil.domain.routine.model + +data class RoutineToggleState( + val isCompleted: Boolean, + val subRoutinesIsCompleted: List, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routines.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routines.kt deleted file mode 100644 index 93a895b8..00000000 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routines.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.threegap.bitnagil.domain.routine.model - -data class Routines( - val routines: Map, -) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/repository/RoutineRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/repository/RoutineRepository.kt index d978d967..cc2625d3 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/repository/RoutineRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/repository/RoutineRepository.kt @@ -2,10 +2,10 @@ package com.threegap.bitnagil.domain.routine.repository import com.threegap.bitnagil.domain.routine.model.Routine import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos -import com.threegap.bitnagil.domain.routine.model.Routines +import com.threegap.bitnagil.domain.routine.model.RoutineSchedule interface RoutineRepository { - suspend fun fetchWeeklyRoutines(startDate: String, endDate: String): Result + suspend fun fetchWeeklyRoutines(startDate: String, endDate: String): Result suspend fun syncRoutineCompletion(routineCompletionInfos: RoutineCompletionInfos): Result suspend fun getRoutine(routineId: String): Result suspend fun deleteRoutine(routineId: String): Result diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/FetchWeeklyRoutinesUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/FetchWeeklyRoutinesUseCase.kt index 0876e3fe..084b81e5 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/FetchWeeklyRoutinesUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/FetchWeeklyRoutinesUseCase.kt @@ -1,12 +1,13 @@ package com.threegap.bitnagil.domain.routine.usecase -import com.threegap.bitnagil.domain.routine.model.Routines +import com.threegap.bitnagil.domain.routine.model.RoutineSchedule import com.threegap.bitnagil.domain.routine.repository.RoutineRepository import javax.inject.Inject class FetchWeeklyRoutinesUseCase @Inject constructor( private val routineRepository: RoutineRepository, ) { - suspend operator fun invoke(startDate: String, endDate: String): Result = - routineRepository.fetchWeeklyRoutines(startDate, endDate) + suspend operator fun invoke(startDate: String, endDate: String): Result { + return routineRepository.fetchWeeklyRoutines(startDate, endDate) + } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/ToggleRoutineUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/ToggleRoutineUseCase.kt new file mode 100644 index 00000000..a0a68ebb --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/ToggleRoutineUseCase.kt @@ -0,0 +1,55 @@ +package com.threegap.bitnagil.domain.routine.usecase + +import com.threegap.bitnagil.domain.routine.model.RoutineToggleState +import javax.inject.Inject + +class ToggleRoutineUseCase @Inject constructor() { + + /** + * 메인 루틴을 토글합니다. + * 메인 루틴이 토글되면 모든 서브 루틴도 같은 상태로 변경됩니다. + * + * @param isCompleted 현재 메인 루틴 완료 상태 + * @param subRoutineStates 현재 서브 루틴 완료 상태 리스트 + * @return 토글된 루틴 상태 + */ + fun toggleMainRoutine( + isCompleted: Boolean, + subRoutineStates: List, + ): RoutineToggleState { + val newIsCompleted = !isCompleted + val newSubRoutineStates = subRoutineStates.map { newIsCompleted } + + return RoutineToggleState( + isCompleted = newIsCompleted, + subRoutinesIsCompleted = newSubRoutineStates, + ) + } + + /** + * 특정 서브 루틴을 토글합니다. + * 모든 서브 루틴이 완료되면 메인 루틴도 자동으로 완료됩니다. + * + * @param index 토글할 서브 루틴의 인덱스 + * @param subRoutineStates 현재 서브 루틴 완료 상태 리스트 + * @return 토글된 루틴 상태, 잘못된 인덱스인 경우 null + */ + fun toggleSubRoutine( + index: Int, + subRoutineStates: List, + ): RoutineToggleState? { + if (index !in subRoutineStates.indices) return null + + val newState = !subRoutineStates[index] + val newSubRoutineStates = subRoutineStates.toMutableList().apply { + this[index] = newState + } + + val allCompleted = newSubRoutineStates.all { it } + + return RoutineToggleState( + isCompleted = allCompleted, + subRoutinesIsCompleted = newSubRoutineStates, + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt index 3ac06c15..8a8639b9 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt @@ -22,18 +22,17 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple -import com.threegap.bitnagil.presentation.common.flow.collectAsEffect import com.threegap.bitnagil.presentation.home.component.template.CollapsibleHomeHeader import com.threegap.bitnagil.presentation.home.component.template.EmptyRoutineView import com.threegap.bitnagil.presentation.home.component.template.RoutineSection import com.threegap.bitnagil.presentation.home.component.template.WeeklyDatePicker -import com.threegap.bitnagil.presentation.home.model.HomeIntent import com.threegap.bitnagil.presentation.home.model.HomeSideEffect import com.threegap.bitnagil.presentation.home.model.HomeState import com.threegap.bitnagil.presentation.home.util.rememberCollapsibleHeaderState +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect import java.time.LocalDate @Composable @@ -44,57 +43,28 @@ fun HomeScreenContainer( navigateToRoutineList: (String) -> Unit, viewModel: HomeViewModel = hiltViewModel(), ) { - val uiState by viewModel.stateFlow.collectAsStateWithLifecycle() + val uiState by viewModel.collectAsState() - viewModel.sideEffectFlow.collectAsEffect { sideEffect -> + viewModel.collectSideEffect { sideEffect -> when (sideEffect) { - is HomeSideEffect.NavigateToGuide -> { - navigateToGuide() - } - - is HomeSideEffect.NavigateToRegisterRoutine -> { - navigateToRegisterRoutine() - } - - is HomeSideEffect.NavigateToEmotion -> { - navigateToEmotion() - } - - is HomeSideEffect.NavigateToRoutineList -> { - navigateToRoutineList(sideEffect.selectedDate) - } + is HomeSideEffect.NavigateToGuide -> navigateToGuide() + is HomeSideEffect.NavigateToRegisterRoutine -> navigateToRegisterRoutine() + is HomeSideEffect.NavigateToEmotion -> navigateToEmotion() + is HomeSideEffect.NavigateToRoutineList -> navigateToRoutineList(sideEffect.selectedDate) } } HomeScreen( uiState = uiState, - onDateSelect = { date -> - viewModel.sendIntent(HomeIntent.OnDateSelect(date)) - }, - onPreviousWeekClick = { - viewModel.sendIntent(HomeIntent.OnPreviousWeekClick) - }, - onNextWeekClick = { - viewModel.sendIntent(HomeIntent.OnNextWeekClick) - }, - onRoutineCompletionToggle = { routineId, isCompleted -> - viewModel.toggleRoutineCompletion(routineId, isCompleted) - }, - onSubRoutineCompletionToggle = { routineId, subRoutineIndex, isCompleted -> - viewModel.toggleSubRoutineCompletion(routineId, subRoutineIndex, isCompleted) - }, - onHelpClick = { - viewModel.sendIntent(HomeIntent.OnHelpClick) - }, - onRegisterRoutineClick = { - viewModel.sendIntent(HomeIntent.OnRegisterRoutineClick) - }, - onRegisterEmotionClick = { - viewModel.sendIntent(HomeIntent.OnRegisterEmotionClick) - }, - onShowMoreRoutinesClick = { - viewModel.sendIntent((HomeIntent.OnShowMoreRoutinesClick)) - }, + onDateSelect = viewModel::selectDate, + onPreviousWeekClick = viewModel::getPreviousWeek, + onNextWeekClick = viewModel::getNextWeek, + onRoutineToggle = viewModel::toggleRoutine, + onSubRoutineToggle = viewModel::toggleSubRoutine, + onHelpClick = viewModel::navigateToGuide, + onRegisterRoutineClick = viewModel::navigateToRegisterRoutine, + onRegisterEmotionClick = viewModel::navigateToEmotion, + onShowMoreRoutinesClick = viewModel::navigateToRoutineList, ) } @@ -104,8 +74,8 @@ private fun HomeScreen( onDateSelect: (LocalDate) -> Unit, onPreviousWeekClick: () -> Unit, onNextWeekClick: () -> Unit, - onRoutineCompletionToggle: (String, Boolean) -> Unit, - onSubRoutineCompletionToggle: (String, Int, Boolean) -> Unit, + onRoutineToggle: (String) -> Unit, + onSubRoutineToggle: (String, Int) -> Unit, onHelpClick: () -> Unit, onRegisterRoutineClick: () -> Unit, onRegisterEmotionClick: () -> Unit, @@ -125,7 +95,7 @@ private fun HomeScreen( WeeklyDatePicker( selectedDate = uiState.selectedDate, weeklyDates = uiState.currentWeeks, - routines = uiState.routines, + routines = uiState.routineSchedule, onDateSelect = onDateSelect, onPreviousWeekClick = onPreviousWeekClick, onNextWeekClick = onNextWeekClick, @@ -183,15 +153,13 @@ private fun HomeScreen( ) { items( items = uiState.selectedDateRoutines, - key = { routine -> "${routine.routineId}_${uiState.selectedDate}" }, + key = { routine -> "${routine.id}_${uiState.selectedDate}" }, ) { routine -> RoutineSection( routine = routine, - onRoutineToggle = { isCompleted -> - onRoutineCompletionToggle(routine.routineId, isCompleted) - }, - onSubRoutineToggle = { subRoutineIndex, isCompleted -> - onSubRoutineCompletionToggle(routine.routineId, subRoutineIndex, isCompleted) + onRoutineToggle = { onRoutineToggle(routine.id) }, + onSubRoutineToggle = { subRoutineIndex -> + onSubRoutineToggle(routine.id, subRoutineIndex) }, ) } @@ -213,12 +181,12 @@ private fun HomeScreen( @Composable private fun HomeScreenPreview() { HomeScreen( - uiState = HomeState(), + uiState = HomeState.INIT, onDateSelect = {}, onPreviousWeekClick = {}, onNextWeekClick = {}, - onRoutineCompletionToggle = { _, _ -> }, - onSubRoutineCompletionToggle = { _, _, _ -> }, + onRoutineToggle = { _ -> }, + onSubRoutineToggle = { _, _ -> }, onHelpClick = {}, onRegisterRoutineClick = {}, onRegisterEmotionClick = {}, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeViewModel.kt index 6439515b..ea9a12f8 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeViewModel.kt @@ -1,8 +1,7 @@ package com.threegap.bitnagil.presentation.home import android.util.Log -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.ViewModel import com.threegap.bitnagil.domain.emotion.usecase.FetchTodayEmotionUseCase import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionChangeEventFlowUseCase import com.threegap.bitnagil.domain.onboarding.usecase.GetOnBoardingRecommendRoutineEventFlowUseCase @@ -10,31 +9,31 @@ import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfo import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos import com.threegap.bitnagil.domain.routine.usecase.FetchWeeklyRoutinesUseCase import com.threegap.bitnagil.domain.routine.usecase.RoutineCompletionUseCase +import com.threegap.bitnagil.domain.routine.usecase.ToggleRoutineUseCase import com.threegap.bitnagil.domain.user.usecase.FetchUserProfileUseCase import com.threegap.bitnagil.domain.writeroutine.usecase.GetWriteRoutineEventFlowUseCase -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel -import com.threegap.bitnagil.presentation.home.model.HomeIntent import com.threegap.bitnagil.presentation.home.model.HomeSideEffect import com.threegap.bitnagil.presentation.home.model.HomeState -import com.threegap.bitnagil.presentation.home.model.RoutineUiModel -import com.threegap.bitnagil.presentation.home.model.RoutinesUiModel +import com.threegap.bitnagil.presentation.home.model.ToggleStrategy import com.threegap.bitnagil.presentation.home.model.toUiModel import com.threegap.bitnagil.presentation.home.util.getCurrentWeekDays import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import org.orbitmvi.orbit.syntax.Syntax +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container import java.time.LocalDate import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, private val fetchWeeklyRoutinesUseCase: FetchWeeklyRoutinesUseCase, private val fetchUserProfileUseCase: FetchUserProfileUseCase, private val fetchTodayEmotionUseCase: FetchTodayEmotionUseCase, @@ -42,130 +41,189 @@ class HomeViewModel @Inject constructor( private val getWriteRoutineEventFlowUseCase: GetWriteRoutineEventFlowUseCase, private val getEmotionChangeEventFlowUseCase: GetEmotionChangeEventFlowUseCase, private val getOnBoardingRecommendRoutineEventFlowUseCase: GetOnBoardingRecommendRoutineEventFlowUseCase, -) : MviViewModel( - initState = HomeState(), - savedStateHandle = savedStateHandle, -) { - private val pendingChangesByDate = mutableMapOf>() - private val backupStatesByDate = mutableMapOf() - private val routineSyncTrigger = MutableSharedFlow() + private val toggleRoutineUseCase: ToggleRoutineUseCase, +) : ContainerHost, ViewModel() { + + override val container: Container = container(initialState = HomeState.INIT) + + private val pendingChangesByDate = mutableMapOf>() + private val routineSyncTrigger = MutableSharedFlow(extraBufferCapacity = 64) init { - observeWriteRoutineEvent() - observeEmotionChangeEvent() - observeRecommendRoutineEvent() - observeWeekChanges() - observeRoutineUpdates() - fetchWeeklyRoutines(stateFlow.value.currentWeeks) - fetchUserProfile() - fetchTodayEmotion(LocalDate.now()) + initialize() } - override suspend fun Syntax.reduceState( - intent: HomeIntent, - state: HomeState, - ): HomeState? { - val newState = when (intent) { - is HomeIntent.UpdateLoading -> { - state.copy(isLoading = intent.isLoading) + fun selectDate(data: LocalDate) { + intent { + val previousDateKey = state.selectedDate.toString() + if (pendingChangesByDate.containsKey(previousDateKey)) { + syncRoutineChangesForDate(previousDateKey) } - is HomeIntent.LoadUserProfile -> { - state.copy(userNickname = intent.nickname) - } + reduce { state.copy(selectedDate = data) } + } + } - is HomeIntent.LoadWeeklyRoutines -> { - state.copy(routines = intent.routines) + fun getNextWeek() { + intent { + val currentDateKey = state.selectedDate.toString() + if (pendingChangesByDate.containsKey(currentDateKey)) { + syncRoutineChangesForDate(currentDateKey) } - is HomeIntent.OnDateSelect -> { - state.copy(selectedDate = intent.date) - } + val newWeek = state.selectedDate.plusWeeks(1).getCurrentWeekDays() + reduce { state.copy(currentWeeks = newWeek, selectedDate = newWeek.first()) } + } + } - is HomeIntent.OnNextWeekClick -> { - val newWeek = state.selectedDate.plusWeeks(1).getCurrentWeekDays() - state.copy( - currentWeeks = newWeek, - selectedDate = newWeek.first(), - ) + fun getPreviousWeek() { + intent { + val currentDateKey = state.selectedDate.toString() + if (pendingChangesByDate.containsKey(currentDateKey)) { + syncRoutineChangesForDate(currentDateKey) } - is HomeIntent.OnPreviousWeekClick -> { - val newWeek = state.selectedDate.minusWeeks(1).getCurrentWeekDays() - state.copy( - currentWeeks = newWeek, - selectedDate = newWeek.first(), - ) - } + val newWeek = state.selectedDate.minusWeeks(1).getCurrentWeekDays() + reduce { state.copy(currentWeeks = newWeek, selectedDate = newWeek.first()) } + } + } - is HomeIntent.OnRoutineCompletionToggle -> { - updateMainRoutine(state, intent.routineId, intent.isCompleted) - } + fun toggleRoutine(routineId: String) { + intent { + updateRoutineState(routineId, ToggleStrategy.Main) + } + } - is HomeIntent.OnSubRoutineCompletionToggle -> { - updateSubRoutine(state, intent.routineId, intent.subRoutineIndex, intent.isCompleted) - } + fun toggleSubRoutine(routineId: String, subRoutineIndex: Int) { + intent { + updateRoutineState(routineId, ToggleStrategy.Sub(subRoutineIndex)) + } + } - is HomeIntent.LoadTodayEmotion -> { - state.copy(todayEmotion = intent.emotion) - } + private suspend fun updateRoutineState(routineId: String, strategy: ToggleStrategy) { + subIntent { + val dateKey = state.selectedDate.toString() + val dailySchedule = state.routineSchedule.dailyRoutines[dateKey] ?: return@subIntent + val routine = dailySchedule.routines.find { it.id == routineId } ?: return@subIntent + + val toggledState = when (strategy) { + is ToggleStrategy.Main -> { + toggleRoutineUseCase.toggleMainRoutine( + isCompleted = routine.isCompleted, + subRoutineStates = routine.subRoutineCompletionStates, + ) + } - is HomeIntent.OnHelpClick -> { - sendSideEffect(HomeSideEffect.NavigateToGuide) - null - } + is ToggleStrategy.Sub -> { + toggleRoutineUseCase.toggleSubRoutine( + index = strategy.index, + subRoutineStates = routine.subRoutineCompletionStates, + ) + } + } ?: return@subIntent - is HomeIntent.OnRegisterEmotionClick -> { - sendSideEffect(HomeSideEffect.NavigateToEmotion) - null + val updatedRoutines = dailySchedule.routines.map { routine -> + if (routine.id == routineId) { + routine.copy( + isCompleted = toggledState.isCompleted, + subRoutineCompletionStates = toggledState.subRoutinesIsCompleted, + ) + } else { + routine + } } - is HomeIntent.OnRegisterRoutineClick -> { - sendSideEffect(HomeSideEffect.NavigateToRegisterRoutine) - null - } + val updatedDailySchedule = dailySchedule.copy( + routines = updatedRoutines, + isAllCompleted = updatedRoutines.all { it.isCompleted }, + ) - is HomeIntent.OnShowMoreRoutinesClick -> { - val selectedDate = stateFlow.value.selectedDate.toString() - sendSideEffect(HomeSideEffect.NavigateToRoutineList(selectedDate)) - null - } + val newSchedule = state.routineSchedule.copy( + dailyRoutines = state.routineSchedule.dailyRoutines + (dateKey to updatedDailySchedule), + ) + + reduce { state.copy(routineSchedule = newSchedule) } + + val change = RoutineCompletionInfo( + routineId = routineId, + routineCompleteYn = toggledState.isCompleted, + subRoutineCompleteYn = toggledState.subRoutinesIsCompleted, + ) + + val dateChanges = pendingChangesByDate.getOrPut(dateKey) { mutableMapOf() } + dateChanges[routineId] = change + + routineSyncTrigger.emit(dateKey) + } + } + + fun navigateToGuide() { + intent { + postSideEffect(HomeSideEffect.NavigateToGuide) + } + } + + fun navigateToEmotion() { + intent { + postSideEffect(HomeSideEffect.NavigateToEmotion) + } + } + + fun navigateToRegisterRoutine() { + intent { + postSideEffect(HomeSideEffect.NavigateToRegisterRoutine) + } + } + + fun navigateToRoutineList() { + intent { + val selectedDate = state.selectedDate.toString() + postSideEffect(HomeSideEffect.NavigateToRoutineList(selectedDate)) + } + } - is HomeIntent.RoutineToggleCompletionFailure -> { - null + private fun initialize() { + intent { + coroutineScope { + launch { fetchUserProfile() } + launch { fetchTodayEmotion() } + launch { fetchWeeklyRoutines(state.currentWeeks) } + launch { observeWriteRoutineEvent() } + launch { observeEmotionChangeEvent() } + launch { observeRecommendRoutineEvent() } + launch { observeWeekChanges() } + launch { observeRoutineUpdates() } } } - return newState } - private fun observeWriteRoutineEvent() { - viewModelScope.launch { + private suspend fun observeWriteRoutineEvent() { + subIntent { getWriteRoutineEventFlowUseCase().collect { - fetchWeeklyRoutines(stateFlow.value.currentWeeks) + fetchWeeklyRoutines(state.currentWeeks) } } } - private fun observeEmotionChangeEvent() { - viewModelScope.launch { + private suspend fun observeEmotionChangeEvent() { + subIntent { getEmotionChangeEventFlowUseCase().collect { - val currentDate = LocalDate.now() - fetchTodayEmotion(currentDate) + fetchTodayEmotion() } } } - private fun observeRecommendRoutineEvent() { - viewModelScope.launch { + private suspend fun observeRecommendRoutineEvent() { + subIntent { getOnBoardingRecommendRoutineEventFlowUseCase().collect { - fetchWeeklyRoutines(stateFlow.value.currentWeeks) + fetchWeeklyRoutines(state.currentWeeks) } } } @OptIn(FlowPreview::class) - private fun observeWeekChanges() { - viewModelScope.launch { + private suspend fun observeWeekChanges() { + subIntent { container.stateFlow .map { it.currentWeeks } .distinctUntilChanged() @@ -178,232 +236,77 @@ class HomeViewModel @Inject constructor( } @OptIn(FlowPreview::class) - private fun observeRoutineUpdates() { - viewModelScope.launch { + private suspend fun observeRoutineUpdates() { + subIntent { routineSyncTrigger - .debounce(2000L) - .collect { date -> - syncRoutineChangesForDate(date) + .debounce(500L) + .collect { dateKey -> + syncRoutineChangesForDate(dateKey) } } } - private fun fetchUserProfile() { - sendIntent(HomeIntent.UpdateLoading(true)) - viewModelScope.launch { + private suspend fun fetchUserProfile() { + subIntent { + reduce { state.copy(loadingCount = state.loadingCount + 1) } fetchUserProfileUseCase().fold( onSuccess = { - sendIntent(HomeIntent.LoadUserProfile(it.nickname)) - sendIntent(HomeIntent.UpdateLoading(false)) + reduce { state.copy(userNickname = it.nickname, loadingCount = state.loadingCount - 1) } }, - onFailure = { error -> - Log.e("HomeViewModel", "유저 정보 가져오기 실패: ${error.message}") - sendIntent(HomeIntent.UpdateLoading(false)) + onFailure = { + Log.e("HomeViewModel", "유저 정보 가져오기 실패: ${it.message}") + reduce { state.copy(loadingCount = state.loadingCount - 1) } }, ) } } - private fun fetchWeeklyRoutines(currentWeeks: List) { - sendIntent(HomeIntent.UpdateLoading(true)) - val startDate = currentWeeks.first().toString() - val endDate = currentWeeks.last().toString() - viewModelScope.launch { + private suspend fun fetchWeeklyRoutines(currentWeeks: List) { + subIntent { + reduce { state.copy(loadingCount = state.loadingCount + 1) } + val startDate = currentWeeks.first().toString() + val endDate = currentWeeks.last().toString() fetchWeeklyRoutinesUseCase(startDate, endDate).fold( - onSuccess = { routines -> - sendIntent(HomeIntent.LoadWeeklyRoutines(routines.toUiModel())) - sendIntent(HomeIntent.UpdateLoading(false)) + onSuccess = { + reduce { state.copy(routineSchedule = it.toUiModel(), loadingCount = state.loadingCount - 1) } }, - onFailure = { error -> - Log.e("HomeViewModel", "루틴 가져오기 실패: ${error.message}") - sendIntent(HomeIntent.UpdateLoading(false)) + onFailure = { + Log.e("HomeViewModel", "루틴 가져오기 실패: ${it.message}") + reduce { state.copy(loadingCount = state.loadingCount - 1) } }, ) } } - private fun fetchTodayEmotion(currentDate: LocalDate) { - sendIntent(HomeIntent.UpdateLoading(true)) - viewModelScope.launch { - fetchTodayEmotionUseCase(currentDate.toString()).fold( - onSuccess = { todayEmotion -> - sendIntent(HomeIntent.LoadTodayEmotion(todayEmotion?.toUiModel())) - sendIntent(HomeIntent.UpdateLoading(false)) + private suspend fun fetchTodayEmotion() { + subIntent { + reduce { state.copy(loadingCount = state.loadingCount + 1) } + fetchTodayEmotionUseCase().fold( + onSuccess = { + reduce { state.copy(todayEmotion = it?.toUiModel(), loadingCount = state.loadingCount - 1) } }, - onFailure = { error -> - Log.e("HomeViewModel", "나의 감정 실패: ${error.message}") - sendIntent(HomeIntent.UpdateLoading(false)) + onFailure = { + Log.e("HomeViewModel", "나의 감정 실패: ${it.message}") + reduce { state.copy(loadingCount = state.loadingCount - 1) } }, ) } } - fun toggleRoutineCompletion(routineId: String, isCompleted: Boolean) { - val originalState = container.stateFlow.value - sendIntent(HomeIntent.OnRoutineCompletionToggle(routineId, isCompleted)) - - val predictedUpdatedState = updateMainRoutine(originalState, routineId, isCompleted) - processRoutineToggleChanges(originalState, predictedUpdatedState) - } - - fun toggleSubRoutineCompletion(routineId: String, subRoutineIndex: Int, isCompleted: Boolean) { - val originalState = container.stateFlow.value - sendIntent(HomeIntent.OnSubRoutineCompletionToggle(routineId, subRoutineIndex, isCompleted)) - - val predictedUpdatedState = updateSubRoutine(originalState, routineId, subRoutineIndex, isCompleted) - processRoutineToggleChanges(originalState, predictedUpdatedState) - } - - private fun processRoutineToggleChanges( - originalState: HomeState, - updatedState: HomeState, - ) { - val selectedDate = originalState.selectedDate - val dateKey = selectedDate.toString() - - if (!backupStatesByDate.containsKey(dateKey)) { - backupStatesByDate[dateKey] = originalState.routines - } - - val originalRoutines = backupStatesByDate[dateKey] ?: originalState.routines - val changes = calculateStateChanges(originalRoutines, updatedState.routines, selectedDate) - - if (changes.isNotEmpty()) { - pendingChangesByDate[dateKey] = changes.toMutableList() - viewModelScope.launch { - routineSyncTrigger.emit(selectedDate) - } - } else { - pendingChangesByDate.remove(dateKey) - } - } - - private fun updateMainRoutine( - state: HomeState, - routineId: String, - isCompleted: Boolean, - ): HomeState { - return updateRoutinesForDate(state) { routinesForDate -> - val routineIndex = routinesForDate.indexOfFirst { it.routineId == routineId } - if (routineIndex == -1) return@updateRoutinesForDate false - - val routine = routinesForDate[routineIndex] - val updatedRoutine = routine.copy( - routineCompleteYn = isCompleted, - subRoutineCompleteYn = routine.subRoutineCompleteYn.map { isCompleted }, - ) - - routinesForDate[routineIndex] = updatedRoutine - true - } - } + private fun syncRoutineChangesForDate(dateKey: String) { + intent { + val dateChanges = pendingChangesByDate.remove(dateKey) + if (dateChanges.isNullOrEmpty()) return@intent - private fun updateSubRoutine( - state: HomeState, - routineId: String, - subRoutineIndex: Int, - isCompleted: Boolean, - ): HomeState { - return updateRoutinesForDate(state) { routinesForDate -> - val routineIndex = routinesForDate.indexOfFirst { it.routineId == routineId } - if (routineIndex == -1) return@updateRoutinesForDate false + val changesToSync = dateChanges.values.toList() + val syncRequest = RoutineCompletionInfos(routineCompletionInfos = changesToSync) - val routine = routinesForDate[routineIndex] - - if (subRoutineIndex !in routine.subRoutineCompleteYn.indices) { - return@updateRoutinesForDate false - } - - val updatedSubRoutineCompleteYn = routine.subRoutineCompleteYn.toMutableList().also { - it[subRoutineIndex] = isCompleted - } - - val routineCompleted = updatedSubRoutineCompleteYn.all { it } - - val updatedRoutine = routine.copy( - subRoutineCompleteYn = updatedSubRoutineCompleteYn, - routineCompleteYn = routineCompleted, + routineCompletionUseCase(syncRequest).fold( + onSuccess = {}, + onFailure = { error -> + fetchWeeklyRoutines(state.currentWeeks) + }, ) - - routinesForDate[routineIndex] = updatedRoutine - true } } - - private fun updateRoutinesForDate( - state: HomeState, - updateLogic: (MutableList) -> Boolean, - ): HomeState { - val dateKey = state.selectedDate.toString() - val dayRoutines = state.routines.routines[dateKey] ?: return state - val routinesForDate = dayRoutines.routineList.toMutableList() - - if (!updateLogic(routinesForDate)) return state - - val allCompleted = routinesForDate.all { it.routineCompleteYn } - - val updatedRoutinesByDate = state.routines.routines.toMutableMap() - updatedRoutinesByDate[dateKey] = dayRoutines.copy( - routineList = routinesForDate, - allCompleted = allCompleted, - ) - - return state.copy(routines = RoutinesUiModel(updatedRoutinesByDate)) - } - - private fun calculateStateChanges( - originalRoutines: RoutinesUiModel, - updatedRoutines: RoutinesUiModel, - date: LocalDate, - ): List { - val dateKey = date.toString() - val originalRoutineList = originalRoutines.routines[dateKey]?.routineList ?: emptyList() - val updatedRoutineList = updatedRoutines.routines[dateKey]?.routineList ?: emptyList() - - return buildList { - updatedRoutineList.forEach { updatedRoutine -> - val originalRoutine = originalRoutineList.find { it.routineId == updatedRoutine.routineId } - - val hasMainRoutineChanged = originalRoutine?.routineCompleteYn != updatedRoutine.routineCompleteYn - val hasSubRoutinesChanged = originalRoutine?.subRoutineCompleteYn != updatedRoutine.subRoutineCompleteYn - - if (hasMainRoutineChanged || hasSubRoutinesChanged) { - add( - RoutineCompletionInfo( - routineId = updatedRoutine.routineId, - routineCompleteYn = updatedRoutine.routineCompleteYn, - subRoutineCompleteYn = updatedRoutine.subRoutineCompleteYn, - ), - ) - } - } - } - } - - private suspend fun syncRoutineChangesForDate(date: LocalDate) { - val dateKey = date.toString() - val unsyncedChanges = pendingChangesByDate[dateKey] ?: return - - if (unsyncedChanges.isEmpty()) return - - val syncRequest = RoutineCompletionInfos( - routineCompletionInfos = unsyncedChanges.toList(), - ) - - routineCompletionUseCase(syncRequest).fold( - onSuccess = { - pendingChangesByDate.remove(dateKey) - backupStatesByDate.remove(dateKey) - }, - onFailure = { error -> - Log.e("HomeViewModel", "루틴 동기화 실패: ${error.message}") - val backupState = backupStatesByDate[dateKey] ?: return - sendIntent(HomeIntent.RoutineToggleCompletionFailure) - sendIntent(HomeIntent.LoadWeeklyRoutines(backupState)) - pendingChangesByDate.remove(dateKey) - backupStatesByDate.remove(dateKey) - sendIntent(HomeIntent.UpdateLoading(false)) - }, - ) - } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/RoutineItem.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/RoutineItem.kt index acadbc38..7ecb619d 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/RoutineItem.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/RoutineItem.kt @@ -19,13 +19,15 @@ import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple -import com.threegap.bitnagil.presentation.home.model.RoutineUiModel @Composable fun RoutineItem( - routine: RoutineUiModel, - onRoutineToggle: (Boolean) -> Unit, - onSubRoutineToggle: (Int, Boolean) -> Unit, + name: String, + isCompleted: Boolean, + subRoutineNames: List, + subRoutineIsCompleted: List, + onRoutineToggle: () -> Unit, + onSubRoutineToggle: (Int) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -43,19 +45,19 @@ fun RoutineItem( Row( modifier = Modifier .fillMaxWidth() - .clickableWithoutRipple { onRoutineToggle(!routine.routineCompleteYn) }, + .clickableWithoutRipple { onRoutineToggle() }, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Text( - text = routine.routineName, + text = name, style = BitnagilTheme.typography.body1SemiBold, color = BitnagilTheme.colors.coolGray10, modifier = Modifier.weight(1f), ) BitnagilIcon( - id = if (routine.routineCompleteYn) R.drawable.ic_check_circle else R.drawable.ic_check_default, + id = if (isCompleted) R.drawable.ic_check_circle else R.drawable.ic_check_default, tint = null, modifier = Modifier .padding(start = 10.dp) @@ -63,7 +65,7 @@ fun RoutineItem( ) } - if (routine.subRoutineNames.isNotEmpty()) { + if (subRoutineNames.isNotEmpty()) { HorizontalDivider( thickness = 1.dp, color = BitnagilTheme.colors.coolGray97, @@ -71,11 +73,9 @@ fun RoutineItem( ) SubRoutinesItem( - subRoutineNames = routine.subRoutineNames, - subRoutineCompleteYn = routine.subRoutineCompleteYn, - onSubRoutineToggle = { index, isCompleted -> - onSubRoutineToggle(index, isCompleted) - }, + subRoutineNames = subRoutineNames, + subRoutineCompleteYn = subRoutineIsCompleted, + onSubRoutineToggle = { index -> onSubRoutineToggle(index) }, ) } } @@ -85,19 +85,12 @@ fun RoutineItem( @Composable private fun RoutineItemPreview() { RoutineItem( - routine = RoutineUiModel( - routineId = "uuid1", - routineName = "개운하게 일어나기", - repeatDay = emptyList(), - executionTime = "20:30:00", - routineDate = "2025-08-15", - routineCompleteYn = false, - subRoutineNames = listOf("물 마시기", "스트레칭하기", "심호흡하기"), - subRoutineCompleteYn = listOf(true, false, true), - recommendedRoutineType = null, - ), + name = "개운하게 일어나기", + isCompleted = false, + subRoutineNames = listOf("물 마시기", "스트레칭하기", "심호흡하기"), + subRoutineIsCompleted = listOf(true, false, true), onRoutineToggle = { }, - onSubRoutineToggle = { _, _ -> }, + onSubRoutineToggle = { _ -> }, ) } @@ -105,18 +98,11 @@ private fun RoutineItemPreview() { @Composable private fun NoneSubRoutineRoutineItemPreview() { RoutineItem( - routine = RoutineUiModel( - routineId = "uuid1", - routineName = "개운하게 일어나기", - repeatDay = emptyList(), - executionTime = "20:30:00", - routineDate = "2025-08-15", - routineCompleteYn = false, - subRoutineNames = emptyList(), - subRoutineCompleteYn = emptyList(), - recommendedRoutineType = null, - ), + name = "개운하게 일어나기", + isCompleted = false, + subRoutineNames = emptyList(), + subRoutineIsCompleted = emptyList(), onRoutineToggle = {}, - onSubRoutineToggle = { _, _ -> }, + onSubRoutineToggle = { _ -> }, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SubRoutinesItem.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SubRoutinesItem.kt index 01651f28..b713b466 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SubRoutinesItem.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SubRoutinesItem.kt @@ -20,7 +20,7 @@ import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple fun SubRoutinesItem( subRoutineNames: List, subRoutineCompleteYn: List, - onSubRoutineToggle: (Int, Boolean) -> Unit, + onSubRoutineToggle: (Int) -> Unit, modifier: Modifier = Modifier, ) { val minSize = minOf(subRoutineNames.size, subRoutineCompleteYn.size) @@ -38,7 +38,7 @@ fun SubRoutinesItem( horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier .fillMaxWidth() - .clickableWithoutRipple { onSubRoutineToggle(index, !isCompleted) }, + .clickableWithoutRipple { onSubRoutineToggle(index) }, ) { BitnagilIcon( id = if (isCompleted) R.drawable.ic_check_circle else R.drawable.ic_check_default, @@ -63,6 +63,6 @@ private fun SubRoutinesItemPreview() { SubRoutinesItem( subRoutineNames = listOf("물 마시기", "스트레칭하기", "심호흡하기"), subRoutineCompleteYn = listOf(true, false, true), - onSubRoutineToggle = { _, _ -> }, + onSubRoutineToggle = { _ -> }, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSection.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSection.kt index 86c8c2f4..5256e69b 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSection.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSection.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.domain.routine.model.DayOfWeek import com.threegap.bitnagil.presentation.home.component.block.RoutineItem import com.threegap.bitnagil.presentation.home.model.RoutineUiModel import com.threegap.bitnagil.presentation.home.util.formatExecutionTime24Hour @@ -17,8 +18,8 @@ import com.threegap.bitnagil.presentation.home.util.formatExecutionTime24Hour @Composable fun RoutineSection( routine: RoutineUiModel, - onRoutineToggle: (Boolean) -> Unit, - onSubRoutineToggle: (Int, Boolean) -> Unit, + onRoutineToggle: () -> Unit, + onSubRoutineToggle: (Int) -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -33,7 +34,10 @@ fun RoutineSection( ) RoutineItem( - routine = routine, + name = routine.name, + isCompleted = routine.isCompleted, + subRoutineNames = routine.subRoutineNames, + subRoutineIsCompleted = routine.subRoutineCompletionStates, onRoutineToggle = onRoutineToggle, onSubRoutineToggle = onSubRoutineToggle, modifier = Modifier.fillMaxWidth(), @@ -44,51 +48,19 @@ fun RoutineSection( @Preview(showBackground = true) @Composable private fun RoutineSectionPreview() { -// RoutineSection( -// routine = RoutineUiModel( -// routineId = "uuid1", -// routineName = "개운하게 일어나기", -// executionTime = "20:30:00", -// routineCompletionId = 1, -// isCompleted = false, -// isModified = false, -// subRoutines = listOf( -// SubRoutineUiModel( -// subRoutineId = "uuid1", -// historySeq = 1, -// subRoutineName = "물 마시기", -// sortOrder = 1, -// routineCompletionId = 1, -// isCompleted = false, -// isModified = false, -// routineType = RoutineType.SUB_ROUTINE, -// ), -// SubRoutineUiModel( -// subRoutineId = "uuid2", -// historySeq = 1, -// subRoutineName = "스트레칭하기", -// sortOrder = 1, -// routineCompletionId = 1, -// isCompleted = false, -// isModified = false, -// routineType = RoutineType.SUB_ROUTINE, -// ), -// SubRoutineUiModel( -// subRoutineId = "uuid3", -// historySeq = 1, -// subRoutineName = "심호흡하기", -// sortOrder = 1, -// routineCompletionId = 1, -// isCompleted = false, -// isModified = false, -// routineType = RoutineType.SUB_ROUTINE, -// ), -// ), -// historySeq = 1, -// repeatDay = listOf(), -// routineType = RoutineType.ROUTINE, -// ), -// onRoutineToggle = {}, -// onSubRoutineToggle = { _, _ -> }, -// ) + RoutineSection( + routine = RoutineUiModel( + id = "1", + name = "개운하게 일어나기", + repeatDays = listOf(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY), + executionTime = "08:00", + routineDate = "2023-10-27", + isCompleted = false, + subRoutineNames = listOf("Make bed", "Brush teeth", "Meditate"), + subRoutineCompletionStates = listOf(true, false, false), + recommendedRoutineType = null, + ), + onRoutineToggle = {}, + onSubRoutineToggle = {}, + ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/WeeklyDatePicker.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/WeeklyDatePicker.kt index 45eed46a..0ee23625 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/WeeklyDatePicker.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/WeeklyDatePicker.kt @@ -37,7 +37,7 @@ import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon import com.threegap.bitnagil.designsystem.component.atom.BitnagilIconButton import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple -import com.threegap.bitnagil.presentation.home.model.RoutinesUiModel +import com.threegap.bitnagil.presentation.home.model.RoutineScheduleUiModel import com.threegap.bitnagil.presentation.home.util.formatDayOfMonth import com.threegap.bitnagil.presentation.home.util.formatDayOfWeekShort import com.threegap.bitnagil.presentation.home.util.formatMonthYear @@ -48,7 +48,7 @@ import java.time.LocalDate fun WeeklyDatePicker( selectedDate: LocalDate, weeklyDates: List, - routines: RoutinesUiModel, + routines: RoutineScheduleUiModel, onDateSelect: (LocalDate) -> Unit, onPreviousWeekClick: () -> Unit, onNextWeekClick: () -> Unit, @@ -57,7 +57,7 @@ fun WeeklyDatePicker( val today = remember { LocalDate.now() } val completionStates = remember(weeklyDates, routines) { weeklyDates.associateWith { date -> - routines.routines[date.toString()]?.allCompleted ?: false + routines.dailyRoutines[date.toString()]?.isAllCompleted ?: false } } @@ -197,7 +197,7 @@ private fun WeeklyDatePickerPreview() { WeeklyDatePicker( selectedDate = selectedDate, weeklyDates = selectedDate.getCurrentWeekDays(), - routines = RoutinesUiModel(), + routines = RoutineScheduleUiModel(), onDateSelect = { selectedDate = it }, onPreviousWeekClick = { selectedDate = selectedDate.minusWeeks(1) }, onNextWeekClick = { selectedDate = selectedDate.plusWeeks(1) }, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/DailyRoutinesUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/DailyRoutinesUiModel.kt new file mode 100644 index 00000000..295415b8 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/DailyRoutinesUiModel.kt @@ -0,0 +1,17 @@ +package com.threegap.bitnagil.presentation.home.model + +import android.os.Parcelable +import com.threegap.bitnagil.domain.routine.model.DailyRoutines +import kotlinx.parcelize.Parcelize + +@Parcelize +data class DailyRoutinesUiModel( + val routines: List = emptyList(), + val isAllCompleted: Boolean = false, +) : Parcelable + +fun DailyRoutines.toUiModel(): DailyRoutinesUiModel = + DailyRoutinesUiModel( + routines = routines.map { it.toUiModel() }, + isAllCompleted = isAllCompleted, + ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/DayRoutinesUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/DayRoutinesUiModel.kt deleted file mode 100644 index a17726fa..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/DayRoutinesUiModel.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.threegap.bitnagil.presentation.home.model - -import android.os.Parcelable -import com.threegap.bitnagil.domain.routine.model.DayRoutines -import kotlinx.parcelize.Parcelize - -@Parcelize -data class DayRoutinesUiModel( - val routineList: List = emptyList(), - val allCompleted: Boolean = false, -) : Parcelable - -fun DayRoutines.toUiModel(): DayRoutinesUiModel = - DayRoutinesUiModel( - routineList = routineList.map { it.toUiModel() }, - allCompleted = allCompleted, - ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeIntent.kt deleted file mode 100644 index 4d4a6e68..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeIntent.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.threegap.bitnagil.presentation.home.model - -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent -import java.time.LocalDate - -sealed class HomeIntent : MviIntent { - data class UpdateLoading(val isLoading: Boolean) : HomeIntent() - data class LoadUserProfile(val nickname: String) : HomeIntent() - data class LoadTodayEmotion(val emotion: TodayEmotionUiModel?) : HomeIntent() - data class LoadWeeklyRoutines(val routines: RoutinesUiModel) : HomeIntent() - data class OnDateSelect(val date: LocalDate) : HomeIntent() - data class OnRoutineCompletionToggle(val routineId: String, val isCompleted: Boolean) : HomeIntent() - data class OnSubRoutineCompletionToggle(val routineId: String, val subRoutineIndex: Int, val isCompleted: Boolean) : HomeIntent() - data object RoutineToggleCompletionFailure : HomeIntent() - data object OnHelpClick : HomeIntent() - data object OnRegisterEmotionClick : HomeIntent() - data object OnRegisterRoutineClick : HomeIntent() - data object OnPreviousWeekClick : HomeIntent() - data object OnNextWeekClick : HomeIntent() - data object OnShowMoreRoutinesClick : HomeIntent() -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeSideEffect.kt index 6e727ef5..9f55ba02 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeSideEffect.kt @@ -1,8 +1,6 @@ package com.threegap.bitnagil.presentation.home.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect - -sealed interface HomeSideEffect : MviSideEffect { +sealed interface HomeSideEffect { data object NavigateToGuide : HomeSideEffect data object NavigateToRegisterRoutine : HomeSideEffect data object NavigateToEmotion : HomeSideEffect diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeState.kt index 63b7fc66..a54c49a4 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeState.kt @@ -1,19 +1,30 @@ package com.threegap.bitnagil.presentation.home.model -import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState import com.threegap.bitnagil.presentation.home.util.getCurrentWeekDays -import kotlinx.parcelize.Parcelize import java.time.LocalDate -@Parcelize data class HomeState( - val isLoading: Boolean = false, - val userNickname: String = "", - val todayEmotion: TodayEmotionUiModel? = null, - val selectedDate: LocalDate = LocalDate.now(), - val currentWeeks: List = LocalDate.now().getCurrentWeekDays(), - val routines: RoutinesUiModel = RoutinesUiModel(), -) : MviState { + val loadingCount: Int, + val userNickname: String, + val todayEmotion: TodayEmotionUiModel?, + val selectedDate: LocalDate, + val currentWeeks: List, + val routineSchedule: RoutineScheduleUiModel, +) { + val isLoading: Boolean + get() = loadingCount > 0 + val selectedDateRoutines: List - get() = routines.routines[selectedDate.toString()]?.routineList ?: emptyList() + get() = routineSchedule.dailyRoutines[selectedDate.toString()]?.routines ?: emptyList() + + companion object { + val INIT = HomeState( + loadingCount = 0, + userNickname = "", + todayEmotion = null, + selectedDate = LocalDate.now(), + currentWeeks = LocalDate.now().getCurrentWeekDays(), + routineSchedule = RoutineScheduleUiModel(), + ) + } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineScheduleUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineScheduleUiModel.kt new file mode 100644 index 00000000..3304cfda --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineScheduleUiModel.kt @@ -0,0 +1,17 @@ +package com.threegap.bitnagil.presentation.home.model + +import android.os.Parcelable +import com.threegap.bitnagil.domain.routine.model.RoutineSchedule +import kotlinx.parcelize.Parcelize + +@Parcelize +data class RoutineScheduleUiModel( + val dailyRoutines: Map = emptyMap(), +) : Parcelable + +fun RoutineSchedule.toUiModel(): RoutineScheduleUiModel = + RoutineScheduleUiModel( + dailyRoutines = this.dailyRoutines.mapValues { (_, dayRoutines) -> + dayRoutines.toUiModel() + }, + ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineUiModel.kt index b1059f56..6b8e73c4 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineUiModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineUiModel.kt @@ -8,26 +8,26 @@ import kotlinx.parcelize.Parcelize @Parcelize data class RoutineUiModel( - val routineId: String, - val routineName: String, - val repeatDay: List, + val id: String, + val name: String, + val repeatDays: List, val executionTime: String, val routineDate: String, - val routineCompleteYn: Boolean, + val isCompleted: Boolean, val subRoutineNames: List, - val subRoutineCompleteYn: List, + val subRoutineCompletionStates: List, val recommendedRoutineType: RecommendedRoutineType?, ) : Parcelable fun Routine.toUiModel(): RoutineUiModel = RoutineUiModel( - routineId = this.routineId, - routineName = this.routineName, - repeatDay = this.repeatDay, + id = this.id, + name = this.name, + repeatDays = this.repeatDays, executionTime = this.executionTime, routineDate = this.routineDate, - routineCompleteYn = this.routineCompleteYn, + isCompleted = this.isCompleted, subRoutineNames = this.subRoutineNames, - subRoutineCompleteYn = this.subRoutineCompleteYn, + subRoutineCompletionStates = this.subRoutineCompletionStates, recommendedRoutineType = this.recommendedRoutineType, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutinesUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutinesUiModel.kt deleted file mode 100644 index 928c7f63..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutinesUiModel.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.threegap.bitnagil.presentation.home.model - -import android.os.Parcelable -import com.threegap.bitnagil.domain.routine.model.Routines -import kotlinx.parcelize.Parcelize - -@Parcelize -data class RoutinesUiModel( - val routines: Map = emptyMap(), -) : Parcelable - -fun Routines.toUiModel(): RoutinesUiModel = - RoutinesUiModel( - routines = this.routines.mapValues { (_, dayRoutines) -> - dayRoutines.toUiModel() - }, - ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/ToggleStrategy.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/ToggleStrategy.kt new file mode 100644 index 00000000..29dbb0d8 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/ToggleStrategy.kt @@ -0,0 +1,6 @@ +package com.threegap.bitnagil.presentation.home.model + +sealed interface ToggleStrategy { + data object Main : ToggleStrategy + data class Sub(val index: Int) : ToggleStrategy +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/DayRoutinesUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/DayRoutinesUiModel.kt index b3dc638f..7516e137 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/DayRoutinesUiModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/DayRoutinesUiModel.kt @@ -1,7 +1,7 @@ package com.threegap.bitnagil.presentation.routinelist.model import android.os.Parcelable -import com.threegap.bitnagil.domain.routine.model.DayRoutines +import com.threegap.bitnagil.domain.routine.model.DailyRoutines import kotlinx.parcelize.Parcelize @Parcelize @@ -9,7 +9,7 @@ data class DayRoutinesUiModel( val routineList: List = emptyList(), ) : Parcelable -fun DayRoutines.toUiModel(): DayRoutinesUiModel = +fun DailyRoutines.toUiModel(): DayRoutinesUiModel = DayRoutinesUiModel( - routineList = routineList.map { it.toUiModel() }, + routineList = routines.map { it.toUiModel() }, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineUiModel.kt index 0796f915..41a1b9eb 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineUiModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineUiModel.kt @@ -31,14 +31,14 @@ data class RoutineUiModel( fun Routine.toUiModel(): RoutineUiModel = RoutineUiModel( - routineId = this.routineId, - routineName = this.routineName, - repeatDay = this.repeatDay, + routineId = this.id, + routineName = this.name, + repeatDay = this.repeatDays, executionTime = this.executionTime.formatExecutionTime12Hour(), routineDate = this.routineDate, startDate = this.startDate, endDate = this.endDate, - routineDeletedYn = this.routineDeletedYn, + routineDeletedYn = this.isDeleted, subRoutineNames = this.subRoutineNames, recommendedRoutineType = this.recommendedRoutineType, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutinesUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutinesUiModel.kt index e9d5ffb4..21fd81c7 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutinesUiModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutinesUiModel.kt @@ -1,7 +1,7 @@ package com.threegap.bitnagil.presentation.routinelist.model import android.os.Parcelable -import com.threegap.bitnagil.domain.routine.model.Routines +import com.threegap.bitnagil.domain.routine.model.RoutineSchedule import kotlinx.parcelize.Parcelize @Parcelize @@ -9,9 +9,9 @@ data class RoutinesUiModel( val routines: Map = emptyMap(), ) : Parcelable -fun Routines.toUiModel(): RoutinesUiModel = +fun RoutineSchedule.toUiModel(): RoutinesUiModel = RoutinesUiModel( - routines = this.routines.mapValues { (_, dayRoutines) -> + routines = this.dailyRoutines.mapValues { (_, dayRoutines) -> dayRoutines.toUiModel() }, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt index 5f8f869e..ad4227ba 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt @@ -83,8 +83,8 @@ class WriteRoutineViewModel @AssistedInject constructor( onSuccess = { routine -> sendIntent( WriteRoutineIntent.SetRoutine( - name = routine.routineName, - repeatDays = routine.repeatDay.map { Day.fromDayOfWeek(it) }, + name = routine.name, + repeatDays = routine.repeatDays.map { Day.fromDayOfWeek(it) }, startTime = Time.fromDomainTimeString(routine.executionTime), subRoutines = listOf( routine.subRoutineNames.getOrNull(0) ?: "",