diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt index 7dbebbc7..781f64e0 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt @@ -4,6 +4,7 @@ import com.threegap.bitnagil.domain.emotion.model.DailyEmotion import com.threegap.bitnagil.domain.emotion.model.EmotionMarbleType import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import java.time.LocalDate @Serializable data class DailyEmotionResponse( @@ -17,10 +18,11 @@ data class DailyEmotionResponse( val emotionMarbleHomeMessage: String?, ) -fun DailyEmotionResponse.toDomain(): DailyEmotion = +fun DailyEmotionResponse.toDomain(fetchedDate: LocalDate): DailyEmotion = DailyEmotion( type = emotionMarbleType, name = emotionMarbleName, imageUrl = imageUrl, homeMessage = emotionMarbleHomeMessage, + fetchedDate = fetchedDate, ) 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 811209dc..6bb5aa9b 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 @@ -4,17 +4,26 @@ import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource import com.threegap.bitnagil.data.emotion.model.response.toDomain import com.threegap.bitnagil.domain.emotion.model.DailyEmotion import com.threegap.bitnagil.domain.emotion.model.Emotion -import com.threegap.bitnagil.domain.emotion.model.EmotionChangeEvent import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.onSubscription +import java.time.LocalDate +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject class EmotionRepositoryImpl @Inject constructor( private val emotionDataSource: EmotionDataSource, ) : EmotionRepository { + + private val isFetching = AtomicBoolean(false) + private val _dailyEmotionFlow = MutableStateFlow(DailyEmotion.INIT) + override val dailyEmotionFlow: Flow = _dailyEmotionFlow + .onSubscription { + if (_dailyEmotionFlow.value.isStale(LocalDate.now())) fetchDailyEmotion() + } + override suspend fun getEmotions(): Result> { return emotionDataSource.getEmotions().map { response -> response.map { it.toDomain() } @@ -23,20 +32,23 @@ class EmotionRepositoryImpl @Inject constructor( override suspend fun registerEmotion(emotionMarbleType: String): Result> { return emotionDataSource.registerEmotion(emotionMarbleType).map { - it.recommendedRoutines.map { - emotionRecommendedRoutineDto -> + it.recommendedRoutines.map { emotionRecommendedRoutineDto -> emotionRecommendedRoutineDto.toEmotionRecommendRoutine() } }.also { - if (it.isSuccess) { - _emotionChangeEventFlow.emit(EmotionChangeEvent.ChangeEmotion(emotionMarbleType)) - } + if (it.isSuccess) fetchDailyEmotion() } } - override suspend fun fetchDailyEmotion(currentDate: String): Result = - emotionDataSource.fetchDailyEmotion(currentDate).map { it.toDomain() } - - private val _emotionChangeEventFlow = MutableSharedFlow() - override suspend fun getEmotionChangeEventFlow(): Flow = _emotionChangeEventFlow.asSharedFlow() + override suspend fun fetchDailyEmotion(): Result { + if (!isFetching.compareAndSet(false, true)) return Result.success(Unit) + return try { + val today = LocalDate.now() + emotionDataSource.fetchDailyEmotion(today.toString()).map { + _dailyEmotionFlow.value = it.toDomain(today) + } + } finally { + isFetching.set(false) + } + } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.kt index ab27dd52..e582467e 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.kt @@ -1,8 +1,23 @@ package com.threegap.bitnagil.domain.emotion.model +import java.time.LocalDate + data class DailyEmotion( val type: EmotionMarbleType?, val name: String?, val imageUrl: String?, val homeMessage: String?, -) + val fetchedDate: LocalDate = LocalDate.MIN, +) { + fun isStale(today: LocalDate): Boolean = fetchedDate != today + + companion object { + val INIT = DailyEmotion( + type = null, + name = null, + imageUrl = null, + homeMessage = null, + fetchedDate = LocalDate.MIN, + ) + } +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt deleted file mode 100644 index f848a647..00000000 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.threegap.bitnagil.domain.emotion.model - -sealed interface EmotionChangeEvent { - data class ChangeEmotion(val emotionType: String) : EmotionChangeEvent -} 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 d2a1df80..51fd97ca 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 @@ -2,13 +2,12 @@ package com.threegap.bitnagil.domain.emotion.repository import com.threegap.bitnagil.domain.emotion.model.DailyEmotion import com.threegap.bitnagil.domain.emotion.model.Emotion -import com.threegap.bitnagil.domain.emotion.model.EmotionChangeEvent import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import kotlinx.coroutines.flow.Flow interface EmotionRepository { + val dailyEmotionFlow: Flow suspend fun getEmotions(): Result> suspend fun registerEmotion(emotionMarbleType: String): Result> - suspend fun fetchDailyEmotion(currentDate: String): Result - suspend fun getEmotionChangeEventFlow(): Flow + suspend fun fetchDailyEmotion(): Result } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt deleted file mode 100644 index d30147c4..00000000 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.threegap.bitnagil.domain.emotion.usecase - -import com.threegap.bitnagil.domain.emotion.model.EmotionChangeEvent -import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository -import kotlinx.coroutines.flow.Flow -import javax.inject.Inject - -class GetEmotionChangeEventFlowUseCase @Inject constructor( - private val repository: EmotionRepository, -) { - suspend operator fun invoke(): Flow = repository.getEmotionChangeEventFlow() -} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchDailyEmotionUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt similarity index 51% rename from domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchDailyEmotionUseCase.kt rename to domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt index 0b5d5bdb..aa064f8d 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchDailyEmotionUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt @@ -2,14 +2,11 @@ package com.threegap.bitnagil.domain.emotion.usecase import com.threegap.bitnagil.domain.emotion.model.DailyEmotion import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository -import java.time.LocalDate +import kotlinx.coroutines.flow.Flow import javax.inject.Inject -class FetchDailyEmotionUseCase @Inject constructor( +class ObserveDailyEmotionUseCase @Inject constructor( private val emotionRepository: EmotionRepository, ) { - suspend operator fun invoke(): Result { - val currentDate = LocalDate.now().toString() - return emotionRepository.fetchDailyEmotion(currentDate) - } + operator fun invoke(): Flow = emotionRepository.dailyEmotionFlow } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt index 810c9a1f..0e199238 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt @@ -2,8 +2,7 @@ package com.threegap.bitnagil.presentation.screen.home import android.util.Log import androidx.lifecycle.ViewModel -import com.threegap.bitnagil.domain.emotion.usecase.FetchDailyEmotionUseCase -import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionChangeEventFlowUseCase +import com.threegap.bitnagil.domain.emotion.usecase.ObserveDailyEmotionUseCase import com.threegap.bitnagil.domain.onboarding.usecase.GetOnBoardingRecommendRoutineEventFlowUseCase import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfo import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos @@ -34,23 +33,37 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( + private val observeDailyEmotionUseCase: ObserveDailyEmotionUseCase, private val fetchWeeklyRoutinesUseCase: FetchWeeklyRoutinesUseCase, private val fetchUserProfileUseCase: FetchUserProfileUseCase, - private val fetchDailyEmotionUseCase: FetchDailyEmotionUseCase, private val routineCompletionUseCase: RoutineCompletionUseCase, private val getWriteRoutineEventFlowUseCase: GetWriteRoutineEventFlowUseCase, - private val getEmotionChangeEventFlowUseCase: GetEmotionChangeEventFlowUseCase, private val getOnBoardingRecommendRoutineEventFlowUseCase: GetOnBoardingRecommendRoutineEventFlowUseCase, private val toggleRoutineUseCase: ToggleRoutineUseCase, ) : ContainerHost, ViewModel() { - override val container: Container = container(initialState = HomeState.INIT) + override val container: Container = + container( + initialState = HomeState.INIT, + buildSettings = { repeatOnSubscribedStopTimeout = 5_000L }, + ) private val pendingChangesByDate = mutableMapOf>() private val routineSyncTrigger = MutableSharedFlow(extraBufferCapacity = 64) init { initialize() + observeDailyEmotion() + } + + private fun observeDailyEmotion() { + intent { + repeatOnSubscription { + observeDailyEmotionUseCase() + .map { it.toUiModel() } + .collect { reduce { state.copy(dailyEmotion = it) } } + } + } } fun selectDate(data: LocalDate) { @@ -186,10 +199,8 @@ class HomeViewModel @Inject constructor( intent { coroutineScope { launch { fetchUserProfile() } - launch { fetchDailyEmotion() } launch { fetchWeeklyRoutines(state.currentWeeks) } launch { observeWriteRoutineEvent() } - launch { observeEmotionChangeEvent() } launch { observeRecommendRoutineEvent() } launch { observeWeekChanges() } launch { observeRoutineUpdates() } @@ -205,14 +216,6 @@ class HomeViewModel @Inject constructor( } } - private suspend fun observeEmotionChangeEvent() { - subIntent { - getEmotionChangeEventFlowUseCase().collect { - fetchDailyEmotion() - } - } - } - private suspend fun observeRecommendRoutineEvent() { subIntent { getOnBoardingRecommendRoutineEventFlowUseCase().collect { @@ -278,21 +281,6 @@ class HomeViewModel @Inject constructor( } } - private suspend fun fetchDailyEmotion() { - subIntent { - reduce { state.copy(loadingCount = state.loadingCount + 1) } - fetchDailyEmotionUseCase().fold( - onSuccess = { - reduce { state.copy(dailyEmotion = it.toUiModel(), loadingCount = state.loadingCount - 1) } - }, - onFailure = { - Log.e("HomeViewModel", "나의 감정 실패: ${it.message}") - reduce { state.copy(loadingCount = state.loadingCount - 1) } - }, - ) - } - } - private fun syncRoutineChangesForDate(dateKey: String) { intent { val dateChanges = pendingChangesByDate.remove(dateKey) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt index c9381687..9ff91736 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt @@ -1,8 +1,7 @@ package com.threegap.bitnagil.presentation.screen.recommendroutine import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionChangeEventFlowUseCase +import com.threegap.bitnagil.domain.emotion.usecase.ObserveDailyEmotionUseCase import com.threegap.bitnagil.domain.recommendroutine.model.RecommendCategory import com.threegap.bitnagil.domain.recommendroutine.model.RecommendLevel import com.threegap.bitnagil.domain.recommendroutine.usecase.FetchRecommendRoutinesUseCase @@ -12,7 +11,8 @@ import com.threegap.bitnagil.presentation.screen.recommendroutine.model.Recommen import com.threegap.bitnagil.presentation.screen.recommendroutine.model.RecommendRoutinesUiModel import com.threegap.bitnagil.presentation.screen.recommendroutine.model.toUiModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container @@ -20,17 +20,23 @@ import javax.inject.Inject @HiltViewModel class RecommendRoutineViewModel @Inject constructor( + private val observeDailyEmotionUseCase: ObserveDailyEmotionUseCase, private val fetchRecommendRoutinesUseCase: FetchRecommendRoutinesUseCase, - private val getEmotionChangeEventFlowUseCase: GetEmotionChangeEventFlowUseCase, ) : ContainerHost, ViewModel() { override val container: Container = - container(initialState = RecommendRoutineState.INIT) - - init { - loadRecommendRoutines() - observeEmotionChangeEvent() - } + container( + initialState = RecommendRoutineState.INIT, + buildSettings = { repeatOnSubscribedStopTimeout = 5_000L }, + onCreate = { + repeatOnSubscription { + observeDailyEmotionUseCase() + .map { it.type } + .distinctUntilChanged() + .collect { loadRecommendRoutines() } + } + }, + ) private var recommendRoutines: RecommendRoutinesUiModel = RecommendRoutinesUiModel.INIT @@ -80,14 +86,6 @@ class RecommendRoutineViewModel @Inject constructor( } } - private fun observeEmotionChangeEvent() { - viewModelScope.launch { - getEmotionChangeEventFlowUseCase().collect { - loadRecommendRoutines() - } - } - } - private fun loadRecommendRoutines() { intent { reduce { state.copy(isLoading = true) }