From e96a6e766332a3e9552a3834cd5783aabc1c4223 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 8 Mar 2026 01:20:55 +0900 Subject: [PATCH 01/20] =?UTF-8?q?Refactor:=20DailyEmotion=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=ED=9D=90=EB=A6=84=EC=9D=84=20Flow=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EA=B4=80=EC=B0=B0=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/response/DailyEmotionResponse.kt | 2 + .../repositoryImpl/EmotionRepositoryImpl.kt | 31 ++++++++------ .../domain/emotion/model/DailyEmotion.kt | 18 +++++++- .../emotion/model/EmotionChangeEvent.kt | 5 --- .../emotion/repository/EmotionRepository.kt | 5 +-- .../GetEmotionChangeEventFlowUseCase.kt | 12 ------ ...eCase.kt => ObserveDailyEmotionUseCase.kt} | 9 ++-- .../presentation/screen/home/HomeViewModel.kt | 42 ++++++------------- .../RecommendRoutineViewModel.kt | 34 +++++++-------- 9 files changed, 71 insertions(+), 87 deletions(-) delete mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt delete mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt rename domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/{FetchDailyEmotionUseCase.kt => ObserveDailyEmotionUseCase.kt} (51%) 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..f2bfd6fd 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( @@ -23,4 +24,5 @@ fun DailyEmotionResponse.toDomain(): DailyEmotion = name = emotionMarbleName, imageUrl = imageUrl, homeMessage = emotionMarbleHomeMessage, + fetchedDate = LocalDate.now(), ) 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..d1ac495f 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,24 @@ 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 javax.inject.Inject class EmotionRepositoryImpl @Inject constructor( private val emotionDataSource: EmotionDataSource, ) : EmotionRepository { + + private val _dailyEmotionFlow = MutableStateFlow(DailyEmotion.INIT) + override val dailyEmotionFlow: Flow = _dailyEmotionFlow + .onSubscription { + if (_dailyEmotionFlow.value.isStale) fetchDailyEmotion() + } + override suspend fun getEmotions(): Result> { return emotionDataSource.getEmotions().map { response -> response.map { it.toDomain() } @@ -23,20 +30,18 @@ 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 { + val currentDate = LocalDate.now().toString() + return emotionDataSource.fetchDailyEmotion(currentDate).map { + _dailyEmotionFlow.value = it.toDomain() + } + } } 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..c27f4266 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,24 @@ 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, +) { + val isStale: Boolean + get() = fetchedDate != LocalDate.now() + + 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..b36624c8 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,12 +33,11 @@ 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() { @@ -51,6 +49,17 @@ class HomeViewModel @Inject constructor( init { initialize() + observeDailyEmotion() + } + + private fun observeDailyEmotion() { + intent { + repeatOnSubscription { + observeDailyEmotionUseCase() + .map { it.toUiModel() } + .collect { reduce { state.copy(dailyEmotion = it) } } + } + } } fun selectDate(data: LocalDate) { @@ -186,10 +195,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 +212,6 @@ class HomeViewModel @Inject constructor( } } - private suspend fun observeEmotionChangeEvent() { - subIntent { - getEmotionChangeEventFlowUseCase().collect { - fetchDailyEmotion() - } - } - } - private suspend fun observeRecommendRoutineEvent() { subIntent { getOnBoardingRecommendRoutineEventFlowUseCase().collect { @@ -278,21 +277,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) } From ead94622468d3505abe60b0ffa5b5ce86e70a147 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 9 Mar 2026 01:56:05 +0900 Subject: [PATCH 02/20] =?UTF-8?q?Fix:=20EmotionRepository=20=EB=82=B4=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=ED=98=B8=EC=B6=9C=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?=EB=B0=8F=20HomeViewModel=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../emotion/repositoryImpl/EmotionRepositoryImpl.kt | 13 ++++++++++--- .../presentation/screen/home/HomeViewModel.kt | 6 +++++- 2 files changed, 15 insertions(+), 4 deletions(-) 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 d1ac495f..1cb7b3c4 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 @@ -10,12 +10,14 @@ import kotlinx.coroutines.flow.Flow 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 { @@ -39,9 +41,14 @@ class EmotionRepositoryImpl @Inject constructor( } override suspend fun fetchDailyEmotion(): Result { - val currentDate = LocalDate.now().toString() - return emotionDataSource.fetchDailyEmotion(currentDate).map { - _dailyEmotionFlow.value = it.toDomain() + if (!isFetching.compareAndSet(false, true)) return Result.success(Unit) + return try { + val currentDate = LocalDate.now().toString() + emotionDataSource.fetchDailyEmotion(currentDate).map { + _dailyEmotionFlow.value = it.toDomain() + } + } finally { + isFetching.set(false) } } } 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 b36624c8..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 @@ -42,7 +42,11 @@ class HomeViewModel @Inject constructor( 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) From 3edbfd1589caf240bae3f5e1011108e27075134e Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 9 Mar 2026 23:17:24 +0900 Subject: [PATCH 03/20] =?UTF-8?q?Refactor:=20DailyEmotion=20=EB=82=B4=20is?= =?UTF-8?q?Stale=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20L?= =?UTF-8?q?ocalDate=20=EC=A3=BC=EC=9E=85=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/emotion/model/response/DailyEmotionResponse.kt | 4 ++-- .../data/emotion/repositoryImpl/EmotionRepositoryImpl.kt | 8 ++++---- .../bitnagil/domain/emotion/model/DailyEmotion.kt | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) 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 f2bfd6fd..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 @@ -18,11 +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 = LocalDate.now(), + 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 1cb7b3c4..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 @@ -21,7 +21,7 @@ class EmotionRepositoryImpl @Inject constructor( private val _dailyEmotionFlow = MutableStateFlow(DailyEmotion.INIT) override val dailyEmotionFlow: Flow = _dailyEmotionFlow .onSubscription { - if (_dailyEmotionFlow.value.isStale) fetchDailyEmotion() + if (_dailyEmotionFlow.value.isStale(LocalDate.now())) fetchDailyEmotion() } override suspend fun getEmotions(): Result> { @@ -43,9 +43,9 @@ class EmotionRepositoryImpl @Inject constructor( override suspend fun fetchDailyEmotion(): Result { if (!isFetching.compareAndSet(false, true)) return Result.success(Unit) return try { - val currentDate = LocalDate.now().toString() - emotionDataSource.fetchDailyEmotion(currentDate).map { - _dailyEmotionFlow.value = it.toDomain() + 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 c27f4266..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 @@ -9,8 +9,7 @@ data class DailyEmotion( val homeMessage: String?, val fetchedDate: LocalDate = LocalDate.MIN, ) { - val isStale: Boolean - get() = fetchedDate != LocalDate.now() + fun isStale(today: LocalDate): Boolean = fetchedDate != today companion object { val INIT = DailyEmotion( From 46ea1946be0b13a75ca109f6302c32d7548a3b2b Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 11 Mar 2026 02:50:10 +0900 Subject: [PATCH 04/20] =?UTF-8?q?Refactor:=20UserDataSource=EB=A5=BC=20Use?= =?UTF-8?q?rRemoteDataSource=EC=99=80=20UserLocalDataSource=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/di/data/DataSourceModule.kt | 12 +++++++--- .../user/datasource/UserLocalDataSource.kt | 10 ++++++++ ...rDataSource.kt => UserRemoteDataSource.kt} | 2 +- .../datasourceImpl/UserLocalDataSourceImpl.kt | 24 +++++++++++++++++++ ...rceImpl.kt => UserRemoteDataSourceImpl.kt} | 10 ++++---- 5 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt rename data/src/main/java/com/threegap/bitnagil/data/user/datasource/{UserDataSource.kt => UserRemoteDataSource.kt} (85%) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt rename data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/{UserDataSourceImpl.kt => UserRemoteDataSourceImpl.kt} (65%) diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt index 6f59ec1a..4c3fab8f 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt @@ -20,8 +20,10 @@ import com.threegap.bitnagil.data.report.datasource.ReportDataSource import com.threegap.bitnagil.data.report.datasourceImpl.ReportDataSourceImpl import com.threegap.bitnagil.data.routine.datasource.RoutineRemoteDataSource import com.threegap.bitnagil.data.routine.datasourceImpl.RoutineRemoteDataSourceImpl -import com.threegap.bitnagil.data.user.datasource.UserDataSource -import com.threegap.bitnagil.data.user.datasourceImpl.UserDataSourceImpl +import com.threegap.bitnagil.data.user.datasource.UserLocalDataSource +import com.threegap.bitnagil.data.user.datasource.UserRemoteDataSource +import com.threegap.bitnagil.data.user.datasourceImpl.UserLocalDataSourceImpl +import com.threegap.bitnagil.data.user.datasourceImpl.UserRemoteDataSourceImpl import com.threegap.bitnagil.data.version.datasource.VersionDataSource import com.threegap.bitnagil.data.version.datasourceImpl.VersionDataSourceImpl import dagger.Binds @@ -56,7 +58,11 @@ abstract class DataSourceModule { @Binds @Singleton - abstract fun bindUserDataSource(userDataSourceImpl: UserDataSourceImpl): UserDataSource + abstract fun bindUserLocalDataSource(userLocalDataSourceImpl: UserLocalDataSourceImpl): UserLocalDataSource + + @Binds + @Singleton + abstract fun bindUserRemoteDataSource(userRemoteDataSourceImpl: UserRemoteDataSourceImpl): UserRemoteDataSource @Binds @Singleton diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt new file mode 100644 index 00000000..2edeebcf --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt @@ -0,0 +1,10 @@ +package com.threegap.bitnagil.data.user.datasource + +import com.threegap.bitnagil.domain.user.model.UserProfile +import kotlinx.coroutines.flow.StateFlow + +interface UserLocalDataSource { + val userProfile: StateFlow + suspend fun saveUserProfile(userProfile: UserProfile) + fun clearCache() +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserRemoteDataSource.kt similarity index 85% rename from data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserDataSource.kt rename to data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserRemoteDataSource.kt index c492b8e0..dc194abc 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserRemoteDataSource.kt @@ -2,6 +2,6 @@ package com.threegap.bitnagil.data.user.datasource import com.threegap.bitnagil.data.user.model.response.UserProfileResponse -interface UserDataSource { +interface UserRemoteDataSource { suspend fun fetchUserProfile(): Result } diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt new file mode 100644 index 00000000..6d12a9a1 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt @@ -0,0 +1,24 @@ +package com.threegap.bitnagil.data.user.datasourceImpl + +import com.threegap.bitnagil.data.user.datasource.UserLocalDataSource +import com.threegap.bitnagil.domain.user.model.UserProfile +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UserLocalDataSourceImpl @Inject constructor() : UserLocalDataSource { + private val _userProfile = MutableStateFlow(null) + override val userProfile: StateFlow = _userProfile.asStateFlow() + + override suspend fun saveUserProfile(userProfile: UserProfile) { + _userProfile.update { userProfile } + } + + override fun clearCache() { + _userProfile.update { null } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserRemoteDataSourceImpl.kt similarity index 65% rename from data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserDataSourceImpl.kt rename to data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserRemoteDataSourceImpl.kt index 9b7f9190..550725c8 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserRemoteDataSourceImpl.kt @@ -1,16 +1,14 @@ package com.threegap.bitnagil.data.user.datasourceImpl import com.threegap.bitnagil.data.common.safeApiCall -import com.threegap.bitnagil.data.user.datasource.UserDataSource +import com.threegap.bitnagil.data.user.datasource.UserRemoteDataSource import com.threegap.bitnagil.data.user.model.response.UserProfileResponse import com.threegap.bitnagil.data.user.service.UserService import javax.inject.Inject -class UserDataSourceImpl @Inject constructor( +class UserRemoteDataSourceImpl @Inject constructor( private val userService: UserService, -) : UserDataSource { +) : UserRemoteDataSource { override suspend fun fetchUserProfile(): Result = - safeApiCall { - userService.fetchUserProfile() - } + safeApiCall { userService.fetchUserProfile() } } From 92362661122de4fb935650e94f8993fc19124930 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 11 Mar 2026 02:51:08 +0900 Subject: [PATCH 05/20] =?UTF-8?q?Feat:=20UserRepository=20=EB=82=B4=20?= =?UTF-8?q?=EC=BA=90=EC=8B=B1=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20Mutex?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20Race=20Condition=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/repositoryImpl/UserRepositoryImpl.kt | 44 +++++++++++++++++-- .../domain/user/repository/UserRepository.kt | 4 +- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt index e0506f24..0f6d9e1f 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt @@ -1,14 +1,50 @@ package com.threegap.bitnagil.data.user.repositoryImpl -import com.threegap.bitnagil.data.user.datasource.UserDataSource +import com.threegap.bitnagil.data.user.datasource.UserLocalDataSource +import com.threegap.bitnagil.data.user.datasource.UserRemoteDataSource import com.threegap.bitnagil.data.user.model.response.toDomain import com.threegap.bitnagil.domain.user.model.UserProfile import com.threegap.bitnagil.domain.user.repository.UserRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import javax.inject.Inject +import javax.inject.Singleton +@Singleton class UserRepositoryImpl @Inject constructor( - private val userDataSource: UserDataSource, + private val userLocalDataSource: UserLocalDataSource, + private val userRemoteDataSource: UserRemoteDataSource, ) : UserRepository { - override suspend fun fetchUserProfile(): Result = - userDataSource.fetchUserProfile().map { it.toDomain() } + private val fetchMutex = Mutex() + + override fun observeUserProfile(): Flow> = flow { + if (userLocalDataSource.userProfile.value == null) { + fetchMutex.withLock { + if (userLocalDataSource.userProfile.value == null) { + userRemoteDataSource.fetchUserProfile() + .onSuccess { response -> + userLocalDataSource.saveUserProfile(response.toDomain()) + } + .onFailure { + emit(Result.failure(it)) + } + } + } + } + + emitAll( + userLocalDataSource.userProfile + .filterNotNull() + .map { Result.success(it) }, + ) + } + + override fun clearCache() { + userLocalDataSource.clearCache() + } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt index de352843..5ad673fd 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt @@ -1,7 +1,9 @@ package com.threegap.bitnagil.domain.user.repository import com.threegap.bitnagil.domain.user.model.UserProfile +import kotlinx.coroutines.flow.Flow interface UserRepository { - suspend fun fetchUserProfile(): Result + fun observeUserProfile(): Flow> + fun clearCache() } From 1b40a14b2e48a447a0812891f6073fb5260f1a3e Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 11 Mar 2026 02:51:32 +0900 Subject: [PATCH 06/20] =?UTF-8?q?Refactor:=20FetchUserProfileUseCase?= =?UTF-8?q?=EB=A5=BC=20ObserveUserProfileUseCase(Flow)=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...chUserProfileUseCase.kt => ObserveUserProfileUseCase.kt} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/{FetchUserProfileUseCase.kt => ObserveUserProfileUseCase.kt} (58%) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/FetchUserProfileUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/ObserveUserProfileUseCase.kt similarity index 58% rename from domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/FetchUserProfileUseCase.kt rename to domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/ObserveUserProfileUseCase.kt index baee4a20..ed371c28 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/FetchUserProfileUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/ObserveUserProfileUseCase.kt @@ -2,11 +2,11 @@ package com.threegap.bitnagil.domain.user.usecase import com.threegap.bitnagil.domain.user.model.UserProfile import com.threegap.bitnagil.domain.user.repository.UserRepository +import kotlinx.coroutines.flow.Flow import javax.inject.Inject -class FetchUserProfileUseCase @Inject constructor( +class ObserveUserProfileUseCase @Inject constructor( private val userRepository: UserRepository, ) { - suspend operator fun invoke(): Result = - userRepository.fetchUserProfile() + operator fun invoke(): Flow> = userRepository.observeUserProfile() } From 35bf09269355aa4f8b44cc0a7a16b3f47b40fbd2 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 11 Mar 2026 02:51:51 +0900 Subject: [PATCH 07/20] =?UTF-8?q?Feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EC=8B=9C=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=BA=90=EC=8B=9C=EB=A5=BC=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt | 7 ++++++- .../bitnagil/domain/auth/usecase/WithdrawalUseCase.kt | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt index f8893314..b7418429 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt @@ -1,10 +1,15 @@ package com.threegap.bitnagil.domain.auth.usecase import com.threegap.bitnagil.domain.auth.repository.AuthRepository +import com.threegap.bitnagil.domain.user.repository.UserRepository import javax.inject.Inject class LogoutUseCase @Inject constructor( private val authRepository: AuthRepository, + private val userRepository: UserRepository, ) { - suspend operator fun invoke(): Result = authRepository.logout() + suspend operator fun invoke(): Result = + authRepository.logout().onSuccess { + userRepository.clearCache() + } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt index 956a80ed..12c578a7 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt @@ -1,10 +1,15 @@ package com.threegap.bitnagil.domain.auth.usecase import com.threegap.bitnagil.domain.auth.repository.AuthRepository +import com.threegap.bitnagil.domain.user.repository.UserRepository import javax.inject.Inject class WithdrawalUseCase @Inject constructor( private val authRepository: AuthRepository, + private val userRepository: UserRepository, ) { - suspend operator fun invoke(reason: String): Result = authRepository.withdrawal(reason) + suspend operator fun invoke(reason: String): Result = + authRepository.withdrawal(reason).onSuccess { + userRepository.clearCache() + } } From 58e9b3c350e83a0a43451265b4818acf07f0dbad Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 11 Mar 2026 02:52:52 +0900 Subject: [PATCH 08/20] =?UTF-8?q?Test:=20UserRepositoryImpl=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(=EC=BA=90=EC=8B=B1,=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=84=B1,=20=EC=BA=90=EC=8B=9C=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EA=B2=80=EC=A6=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/build.gradle.kts | 3 + .../repositoryImpl/UserRepositoryImplTest.kt | 126 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt diff --git a/data/build.gradle.kts b/data/build.gradle.kts index fcee75af..d167730e 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -20,4 +20,7 @@ dependencies { implementation(libs.bundles.retrofit) implementation(libs.play.services.location) implementation(libs.kotlinx.coroutines.play) + + testImplementation(libs.androidx.junit) + testImplementation(libs.kotlin.coroutines.test) } diff --git a/data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt b/data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt new file mode 100644 index 00000000..64289259 --- /dev/null +++ b/data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt @@ -0,0 +1,126 @@ +package com.threegap.bitnagil.data.user.repositoryImpl + +import com.threegap.bitnagil.data.user.datasource.UserLocalDataSource +import com.threegap.bitnagil.data.user.datasource.UserRemoteDataSource +import com.threegap.bitnagil.data.user.model.response.UserProfileResponse +import com.threegap.bitnagil.domain.user.model.UserProfile +import com.threegap.bitnagil.domain.user.repository.UserRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.util.concurrent.atomic.AtomicInteger + +class UserRepositoryImplTest { + + private lateinit var localDataSource: FakeUserLocalDataSource + private lateinit var remoteDataSource: FakeUserRemoteDataSource + private lateinit var userRepository: UserRepository + + @Before + fun setup() { + localDataSource = FakeUserLocalDataSource() + remoteDataSource = FakeUserRemoteDataSource() + userRepository = UserRepositoryImpl(localDataSource, remoteDataSource) + } + + @Test + fun `캐시가 비어있을 때 observeUserProfile을 구독하면 Remote에서 데이터를 가져와 캐시를 업데이트해야 한다`() = + runTest { + // given + val expectedProfile = UserProfile(nickname = "TestUser") + remoteDataSource.profileResponse = UserProfileResponse(nickname = "TestUser") + + // when + // 구독(first)이 시작되는 순간 Fetch가 발생함 + val result = userRepository.observeUserProfile().first() + + // then + assertEquals(expectedProfile, result.getOrNull()) + assertEquals(1, remoteDataSource.fetchCount.get()) + assertEquals(expectedProfile, localDataSource.userProfile.value) + } + + @Test + fun `캐시가 이미 존재할 때 observeUserProfile을 구독하면 Remote를 호출하지 않고 캐시를 반환해야 한다`() = + runTest { + // given + val cachedProfile = UserProfile(nickname = "CachedUser") + localDataSource.saveUserProfile(cachedProfile) + + // when + val result = userRepository.observeUserProfile().first() + + // then + assertEquals(cachedProfile, result.getOrNull()) + assertEquals(0, remoteDataSource.fetchCount.get()) + } + + @Test + fun `여러 코루틴이 동시에 observeUserProfile을 구독해도 Remote API는 1회만 호출되어야 한다 (Race Condition 방지)`() = + runTest { + // given + remoteDataSource.profileResponse = UserProfileResponse(nickname = "RaceUser") + remoteDataSource.delayMillis = 100L // 네트워크 지연 시뮬레이션 + + // when + // 10개의 코루틴이 동시에 구독 시작 + val jobs = List(10) { + async { userRepository.observeUserProfile().first() } + } + jobs.awaitAll() + + // then + assertEquals(1, remoteDataSource.fetchCount.get()) + assertEquals("RaceUser", localDataSource.userProfile.value?.nickname) + } + + @Test + fun `clearCache를 호출하면 로컬 캐시가 초기화되어야 한다`() = + runTest { + // given + localDataSource.saveUserProfile(UserProfile(nickname = "ToDelete")) + + // when + userRepository.clearCache() + + // then + assertEquals(null, localDataSource.userProfile.value) + } + + // --- Fake Objects --- + + private class FakeUserLocalDataSource : UserLocalDataSource { + private val _userProfile = MutableStateFlow(null) + override val userProfile: StateFlow = _userProfile.asStateFlow() + + override suspend fun saveUserProfile(userProfile: UserProfile) { + _userProfile.update { userProfile } + } + + override fun clearCache() { + _userProfile.update { null } + } + } + + private class FakeUserRemoteDataSource : UserRemoteDataSource { + var profileResponse: UserProfileResponse? = null + val fetchCount = AtomicInteger(0) + var delayMillis = 0L + + override suspend fun fetchUserProfile(): Result { + if (delayMillis > 0) delay(delayMillis) + fetchCount.incrementAndGet() + return profileResponse?.let { Result.success(it) } + ?: Result.failure(Exception("No profile set in fake")) + } + } +} From e5c743ee3c34889bfd0902f30f38f163daa582ee Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 11 Mar 2026 02:53:28 +0900 Subject: [PATCH 09/20] =?UTF-8?q?Refactor:=20Home,=20MyPage,=20OnBoarding?= =?UTF-8?q?=20ViewModel=EC=97=90=EC=84=9C=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20Flow=EB=A1=9C=20=EA=B5=AC=EB=8F=85?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/home/HomeViewModel.kt | 34 +++++++++++-------- .../screen/mypage/MyPageViewModel.kt | 34 +++++++++---------- .../screen/onboarding/OnBoardingViewModel.kt | 9 ++--- 3 files changed, 40 insertions(+), 37 deletions(-) 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..3f6ded78 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 @@ -11,7 +11,7 @@ import com.threegap.bitnagil.domain.routine.usecase.FetchWeeklyRoutinesUseCase import com.threegap.bitnagil.domain.routine.usecase.GetWriteRoutineEventFlowUseCase 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.user.usecase.ObserveUserProfileUseCase import com.threegap.bitnagil.presentation.screen.home.contract.HomeSideEffect import com.threegap.bitnagil.presentation.screen.home.contract.HomeState import com.threegap.bitnagil.presentation.screen.home.model.ToggleStrategy @@ -35,7 +35,7 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val fetchWeeklyRoutinesUseCase: FetchWeeklyRoutinesUseCase, - private val fetchUserProfileUseCase: FetchUserProfileUseCase, + private val observeUserProfileUseCase: ObserveUserProfileUseCase, private val fetchDailyEmotionUseCase: FetchDailyEmotionUseCase, private val routineCompletionUseCase: RoutineCompletionUseCase, private val getWriteRoutineEventFlowUseCase: GetWriteRoutineEventFlowUseCase, @@ -185,7 +185,7 @@ class HomeViewModel @Inject constructor( private fun initialize() { intent { coroutineScope { - launch { fetchUserProfile() } + launch { observeUserProfile() } launch { fetchDailyEmotion() } launch { fetchWeeklyRoutines(state.currentWeeks) } launch { observeWriteRoutineEvent() } @@ -246,18 +246,22 @@ class HomeViewModel @Inject constructor( } } - private suspend fun fetchUserProfile() { - subIntent { - reduce { state.copy(loadingCount = state.loadingCount + 1) } - fetchUserProfileUseCase().fold( - onSuccess = { - reduce { state.copy(userNickname = it.nickname, loadingCount = state.loadingCount - 1) } - }, - onFailure = { - Log.e("HomeViewModel", "유저 정보 가져오기 실패: ${it.message}") - reduce { state.copy(loadingCount = state.loadingCount - 1) } - }, - ) + private fun observeUserProfile() { + intent { + repeatOnSubscription { + reduce { state.copy(loadingCount = state.loadingCount + 1) } + observeUserProfileUseCase().collect { result -> + result.fold( + onSuccess = { + reduce { state.copy(userNickname = it.nickname, loadingCount = state.loadingCount - 1) } + }, + onFailure = { + Log.e("HomeViewModel", "유저 정보 가져오기 실패: ${it.message}") + reduce { state.copy(loadingCount = state.loadingCount - 1) } + }, + ) + } + } } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt index 1e417234..5b40610d 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt @@ -1,7 +1,7 @@ package com.threegap.bitnagil.presentation.screen.mypage import androidx.lifecycle.ViewModel -import com.threegap.bitnagil.domain.user.usecase.FetchUserProfileUseCase +import com.threegap.bitnagil.domain.user.usecase.ObserveUserProfileUseCase import com.threegap.bitnagil.presentation.screen.mypage.contract.MyPageSideEffect import com.threegap.bitnagil.presentation.screen.mypage.contract.MyPageState import dagger.hilt.android.lifecycle.HiltViewModel @@ -12,26 +12,24 @@ import javax.inject.Inject @HiltViewModel class MyPageViewModel @Inject constructor( - private val fetchUserProfileUseCase: FetchUserProfileUseCase, + private val observeUserProfileUseCase: ObserveUserProfileUseCase, ) : ContainerHost, ViewModel() { - override val container: Container = container(initialState = MyPageState.INIT) - init { - loadMyPageInfo() - } - - private fun loadMyPageInfo() = intent { - fetchUserProfileUseCase().fold( - onSuccess = { - reduce { - state.copy( - name = it.nickname, - profileUrl = "profileUrl", - ) + override val container: Container = + container( + initialState = MyPageState.INIT, + buildSettings = { repeatOnSubscribedStopTimeout = 5_000L }, + onCreate = { + repeatOnSubscription { + observeUserProfileUseCase().collect { result -> + result.fold( + onSuccess = { + reduce { state.copy(name = it.nickname, profileUrl = "profileUrl") } + }, + onFailure = {}, + ) + } } }, - onFailure = { - }, ) - } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt index c9e6a6ca..54ce2cf2 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt @@ -8,7 +8,7 @@ import com.threegap.bitnagil.domain.onboarding.usecase.GetOnBoardingsUseCase import com.threegap.bitnagil.domain.onboarding.usecase.GetRecommendOnBoardingRoutinesUseCase import com.threegap.bitnagil.domain.onboarding.usecase.GetUserOnBoardingUseCase import com.threegap.bitnagil.domain.onboarding.usecase.RegisterRecommendOnBoardingRoutinesUseCase -import com.threegap.bitnagil.domain.user.usecase.FetchUserProfileUseCase +import com.threegap.bitnagil.domain.user.usecase.ObserveUserProfileUseCase import com.threegap.bitnagil.presentation.screen.onboarding.contract.OnBoardingSideEffect import com.threegap.bitnagil.presentation.screen.onboarding.contract.OnBoardingState import com.threegap.bitnagil.presentation.screen.onboarding.model.OnBoardingItemUiModel @@ -22,6 +22,7 @@ import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first import kotlinx.coroutines.isActive import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost @@ -34,7 +35,7 @@ class OnBoardingViewModel @AssistedInject constructor( private val getRecommendOnBoardingRoutinesUseCase: GetRecommendOnBoardingRoutinesUseCase, private val getOnBoardingAbstractUseCase: GetOnBoardingAbstractUseCase, private val registerRecommendOnBoardingRoutinesUseCase: RegisterRecommendOnBoardingRoutinesUseCase, - private val fetchUserProfileUseCase: FetchUserProfileUseCase, + private val observeUserProfileUseCase: ObserveUserProfileUseCase, private val getUserOnBoardingUseCase: GetUserOnBoardingUseCase, @Assisted private val onBoardingArg: OnBoardingScreenArg, ) : ContainerHost, ViewModel() { @@ -71,7 +72,7 @@ class OnBoardingViewModel @AssistedInject constructor( } private fun loadIntro() = intent { - val userName = fetchUserProfileUseCase().getOrNull()?.nickname ?: "-" + val userName = observeUserProfileUseCase().first().getOrNull()?.nickname ?: "-" reduce { OnBoardingState.Idle( @@ -86,7 +87,7 @@ class OnBoardingViewModel @AssistedInject constructor( } private fun loadUserOnBoarding() = intent { - val userName = fetchUserProfileUseCase().getOrNull()?.nickname ?: "-" + val userName = observeUserProfileUseCase().first().getOrNull()?.nickname ?: "-" val userOnBoarding = getUserOnBoardingUseCase().fold( onSuccess = { it }, onFailure = { From fae478855b58dd24de9b4fec668b61c93d0b9548 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 11 Mar 2026 22:25:59 +0900 Subject: [PATCH 10/20] =?UTF-8?q?Feat:=20userProfile=20=EB=8B=A8=EC=9D=BC?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20=EC=9C=84=ED=95=9C=20GetUserPr?= =?UTF-8?q?ofileUseCase=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/repositoryImpl/UserRepositoryImpl.kt | 33 ++++++++++++------- .../domain/user/repository/UserRepository.kt | 1 + .../user/usecase/GetUserProfileUseCase.kt | 11 +++++++ .../screen/onboarding/OnBoardingViewModel.kt | 9 +++-- 4 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/GetUserProfileUseCase.kt diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt index 0f6d9e1f..19be4a5f 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt @@ -23,18 +23,9 @@ class UserRepositoryImpl @Inject constructor( private val fetchMutex = Mutex() override fun observeUserProfile(): Flow> = flow { - if (userLocalDataSource.userProfile.value == null) { - fetchMutex.withLock { - if (userLocalDataSource.userProfile.value == null) { - userRemoteDataSource.fetchUserProfile() - .onSuccess { response -> - userLocalDataSource.saveUserProfile(response.toDomain()) - } - .onFailure { - emit(Result.failure(it)) - } - } - } + fetchAndCacheIfNeeded().onFailure { + emit(Result.failure(it)) + return@flow } emitAll( @@ -44,7 +35,25 @@ class UserRepositoryImpl @Inject constructor( ) } + override suspend fun getUserProfile(): Result { + return fetchAndCacheIfNeeded() + } + override fun clearCache() { userLocalDataSource.clearCache() } + + private suspend fun fetchAndCacheIfNeeded(): Result { + userLocalDataSource.userProfile.value?.let { return Result.success(it) } + + return fetchMutex.withLock { + userLocalDataSource.userProfile.value?.let { return@withLock Result.success(it) } + + userRemoteDataSource.fetchUserProfile() + .onSuccess { response -> + userLocalDataSource.saveUserProfile(response.toDomain()) + } + .map { it.toDomain() } + } + } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt index 5ad673fd..3bc25503 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt @@ -5,5 +5,6 @@ import kotlinx.coroutines.flow.Flow interface UserRepository { fun observeUserProfile(): Flow> + suspend fun getUserProfile(): Result fun clearCache() } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/GetUserProfileUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/GetUserProfileUseCase.kt new file mode 100644 index 00000000..b07b6822 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/GetUserProfileUseCase.kt @@ -0,0 +1,11 @@ +package com.threegap.bitnagil.domain.user.usecase + +import com.threegap.bitnagil.domain.user.model.UserProfile +import com.threegap.bitnagil.domain.user.repository.UserRepository +import javax.inject.Inject + +class GetUserProfileUseCase @Inject constructor( + private val userRepository: UserRepository, +) { + suspend operator fun invoke(): Result = userRepository.getUserProfile() +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt index 54ce2cf2..03f54a07 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt @@ -8,7 +8,7 @@ import com.threegap.bitnagil.domain.onboarding.usecase.GetOnBoardingsUseCase import com.threegap.bitnagil.domain.onboarding.usecase.GetRecommendOnBoardingRoutinesUseCase import com.threegap.bitnagil.domain.onboarding.usecase.GetUserOnBoardingUseCase import com.threegap.bitnagil.domain.onboarding.usecase.RegisterRecommendOnBoardingRoutinesUseCase -import com.threegap.bitnagil.domain.user.usecase.ObserveUserProfileUseCase +import com.threegap.bitnagil.domain.user.usecase.GetUserProfileUseCase import com.threegap.bitnagil.presentation.screen.onboarding.contract.OnBoardingSideEffect import com.threegap.bitnagil.presentation.screen.onboarding.contract.OnBoardingState import com.threegap.bitnagil.presentation.screen.onboarding.model.OnBoardingItemUiModel @@ -22,7 +22,6 @@ import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.async -import kotlinx.coroutines.flow.first import kotlinx.coroutines.isActive import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost @@ -35,7 +34,7 @@ class OnBoardingViewModel @AssistedInject constructor( private val getRecommendOnBoardingRoutinesUseCase: GetRecommendOnBoardingRoutinesUseCase, private val getOnBoardingAbstractUseCase: GetOnBoardingAbstractUseCase, private val registerRecommendOnBoardingRoutinesUseCase: RegisterRecommendOnBoardingRoutinesUseCase, - private val observeUserProfileUseCase: ObserveUserProfileUseCase, + private val getUserProfileUseCase: GetUserProfileUseCase, private val getUserOnBoardingUseCase: GetUserOnBoardingUseCase, @Assisted private val onBoardingArg: OnBoardingScreenArg, ) : ContainerHost, ViewModel() { @@ -72,7 +71,7 @@ class OnBoardingViewModel @AssistedInject constructor( } private fun loadIntro() = intent { - val userName = observeUserProfileUseCase().first().getOrNull()?.nickname ?: "-" + val userName = getUserProfileUseCase().getOrNull()?.nickname ?: "-" reduce { OnBoardingState.Idle( @@ -87,7 +86,7 @@ class OnBoardingViewModel @AssistedInject constructor( } private fun loadUserOnBoarding() = intent { - val userName = observeUserProfileUseCase().first().getOrNull()?.nickname ?: "-" + val userName = getUserProfileUseCase().getOrNull()?.nickname ?: "-" val userOnBoarding = getUserOnBoardingUseCase().fold( onSuccess = { it }, onFailure = { From 9d4add3d6ed07d8008cbc8f14abac01edad5a5b2 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 12 Mar 2026 02:00:57 +0900 Subject: [PATCH 11/20] =?UTF-8?q?Refactor:=20Emotion=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=A0=88=EC=9D=B4=EC=96=B4=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=BA=90=EC=8B=B1=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/di/data/DataSourceModule.kt | 12 ++- .../datasource/EmotionLocalDataSource.kt | 10 +++ ...taSource.kt => EmotionRemoteDataSource.kt} | 2 +- .../EmotionLocalDataSourceImpl.kt | 22 ++++++ ...Impl.kt => EmotionRemoteDataSourceImpl.kt} | 6 +- .../repositoryImpl/EmotionRepositoryImpl.kt | 74 +++++++++++++------ .../emotion/repository/EmotionRepository.kt | 4 +- .../usecase/ObserveDailyEmotionUseCase.kt | 2 +- .../presentation/screen/home/HomeViewModel.kt | 11 ++- .../RecommendRoutineViewModel.kt | 2 +- 10 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt rename data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/{EmotionDataSource.kt => EmotionRemoteDataSource.kt} (93%) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionLocalDataSourceImpl.kt rename data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/{EmotionDataSourceImpl.kt => EmotionRemoteDataSourceImpl.kt} (88%) diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt index 6f59ec1a..b1a7818d 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt @@ -8,8 +8,10 @@ import com.threegap.bitnagil.data.auth.datasource.AuthLocalDataSource import com.threegap.bitnagil.data.auth.datasource.AuthRemoteDataSource import com.threegap.bitnagil.data.auth.datasourceimpl.AuthLocalDataSourceImpl import com.threegap.bitnagil.data.auth.datasourceimpl.AuthRemoteDataSourceImpl -import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource -import com.threegap.bitnagil.data.emotion.datasourceImpl.EmotionDataSourceImpl +import com.threegap.bitnagil.data.emotion.datasource.EmotionLocalDataSource +import com.threegap.bitnagil.data.emotion.datasource.EmotionRemoteDataSource +import com.threegap.bitnagil.data.emotion.datasourceImpl.EmotionLocalDataSourceImpl +import com.threegap.bitnagil.data.emotion.datasourceImpl.EmotionRemoteDataSourceImpl import com.threegap.bitnagil.data.file.datasource.FileDataSource import com.threegap.bitnagil.data.file.datasourceImpl.FileDataSourceImpl import com.threegap.bitnagil.data.onboarding.datasource.OnBoardingDataSource @@ -52,7 +54,11 @@ abstract class DataSourceModule { @Binds @Singleton - abstract fun bindEmotionDataSource(emotionDataSourceImpl: EmotionDataSourceImpl): EmotionDataSource + abstract fun bindEmotionLocalDataSource(emotionLocalDataSourceImpl: EmotionLocalDataSourceImpl): EmotionLocalDataSource + + @Binds + @Singleton + abstract fun bindEmotionRemoteDataSource(emotionRemoteDataSourceImpl: EmotionRemoteDataSourceImpl): EmotionRemoteDataSource @Binds @Singleton diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt new file mode 100644 index 00000000..0a64f25c --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt @@ -0,0 +1,10 @@ +package com.threegap.bitnagil.data.emotion.datasource + +import com.threegap.bitnagil.domain.emotion.model.DailyEmotion +import kotlinx.coroutines.flow.StateFlow + +interface EmotionLocalDataSource { + val dailyEmotion: StateFlow + fun saveDailyEmotion(dailyEmotion: DailyEmotion) + fun clearCache() +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionRemoteDataSource.kt similarity index 93% rename from data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt rename to data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionRemoteDataSource.kt index c3f81e30..d8c7e004 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionRemoteDataSource.kt @@ -4,7 +4,7 @@ import com.threegap.bitnagil.data.emotion.model.dto.EmotionDto import com.threegap.bitnagil.data.emotion.model.response.DailyEmotionResponse import com.threegap.bitnagil.data.emotion.model.response.RegisterEmotionResponse -interface EmotionDataSource { +interface EmotionRemoteDataSource { suspend fun getEmotions(): Result> suspend fun registerEmotion(emotion: String): Result suspend fun fetchDailyEmotion(currentDate: String): Result diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionLocalDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionLocalDataSourceImpl.kt new file mode 100644 index 00000000..e3f30f99 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionLocalDataSourceImpl.kt @@ -0,0 +1,22 @@ +package com.threegap.bitnagil.data.emotion.datasourceImpl + +import com.threegap.bitnagil.data.emotion.datasource.EmotionLocalDataSource +import com.threegap.bitnagil.domain.emotion.model.DailyEmotion +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +class EmotionLocalDataSourceImpl @Inject constructor() : EmotionLocalDataSource { + private val _dailyEmotion = MutableStateFlow(null) + override val dailyEmotion: StateFlow = _dailyEmotion.asStateFlow() + + override fun saveDailyEmotion(dailyEmotion: DailyEmotion) { + _dailyEmotion.update { dailyEmotion } + } + + override fun clearCache() { + _dailyEmotion.update { null } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionRemoteDataSourceImpl.kt similarity index 88% rename from data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt rename to data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionRemoteDataSourceImpl.kt index 20035b56..78effc19 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionRemoteDataSourceImpl.kt @@ -1,7 +1,7 @@ package com.threegap.bitnagil.data.emotion.datasourceImpl import com.threegap.bitnagil.data.common.safeApiCall -import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource +import com.threegap.bitnagil.data.emotion.datasource.EmotionRemoteDataSource import com.threegap.bitnagil.data.emotion.model.dto.EmotionDto import com.threegap.bitnagil.data.emotion.model.request.RegisterEmotionRequest import com.threegap.bitnagil.data.emotion.model.response.DailyEmotionResponse @@ -9,9 +9,9 @@ import com.threegap.bitnagil.data.emotion.model.response.RegisterEmotionResponse import com.threegap.bitnagil.data.emotion.service.EmotionService import javax.inject.Inject -class EmotionDataSourceImpl @Inject constructor( +class EmotionRemoteDataSourceImpl @Inject constructor( private val emotionService: EmotionService, -) : EmotionDataSource { +) : EmotionRemoteDataSource { override suspend fun getEmotions(): Result> { return safeApiCall { emotionService.getEmotions() 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 6bb5aa9b..cd72fc30 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 @@ -1,54 +1,84 @@ package com.threegap.bitnagil.data.emotion.repositoryImpl -import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource +import com.threegap.bitnagil.data.emotion.datasource.EmotionLocalDataSource +import com.threegap.bitnagil.data.emotion.datasource.EmotionRemoteDataSource 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.EmotionRecommendRoutine import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.onSubscription +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import java.time.LocalDate -import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject class EmotionRepositoryImpl @Inject constructor( - private val emotionDataSource: EmotionDataSource, + private val emotionRemoteDataSource: EmotionRemoteDataSource, + private val emotionLocalDateSource: EmotionLocalDataSource, ) : 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() - } + private val fetchMutex = Mutex() override suspend fun getEmotions(): Result> { - return emotionDataSource.getEmotions().map { response -> + return emotionRemoteDataSource.getEmotions().map { response -> response.map { it.toDomain() } } } override suspend fun registerEmotion(emotionMarbleType: String): Result> { - return emotionDataSource.registerEmotion(emotionMarbleType).map { + return emotionRemoteDataSource.registerEmotion(emotionMarbleType).map { it.recommendedRoutines.map { emotionRecommendedRoutineDto -> emotionRecommendedRoutineDto.toEmotionRecommendRoutine() } }.also { - if (it.isSuccess) fetchDailyEmotion() + if (it.isSuccess) fetchAndSaveDailyEmotion(today = LocalDate.now(), forceRefresh = true) } } - 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) + override fun observeDailyEmotion(): Flow> = flow { + fetchAndSaveDailyEmotion(LocalDate.now()) + .onFailure { + emit(Result.failure(it)) + return@flow + } + + emitAll( + emotionLocalDateSource.dailyEmotion + .filterNotNull() + .map { Result.success(it) }, + ) + } + + override fun clearCache() { + emotionLocalDateSource.clearCache() + } + + private suspend fun fetchAndSaveDailyEmotion( + today: LocalDate, + forceRefresh: Boolean = false, + ): Result { + if (!forceRefresh) { + val currentLocalData = emotionLocalDateSource.dailyEmotion.value + if (currentLocalData != null && !currentLocalData.isStale(today)) { + return Result.success(currentLocalData) + } + } + + return fetchMutex.withLock { + if (!forceRefresh) { + val doubleCheckData = emotionLocalDateSource.dailyEmotion.value + if (doubleCheckData != null && !doubleCheckData.isStale(today)) { + return@withLock Result.success(doubleCheckData) + } } - } finally { - isFetching.set(false) + emotionRemoteDataSource.fetchDailyEmotion(today.toString()) + .onSuccess { emotionLocalDateSource.saveDailyEmotion(it.toDomain(today)) } + .map { it.toDomain(today) } } } } 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 51fd97ca..a95af407 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 @@ -6,8 +6,8 @@ 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(): Result + fun observeDailyEmotion(): Flow> + fun clearCache() } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt index aa064f8d..9892334b 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt @@ -8,5 +8,5 @@ import javax.inject.Inject class ObserveDailyEmotionUseCase @Inject constructor( private val emotionRepository: EmotionRepository, ) { - operator fun invoke(): Flow = emotionRepository.dailyEmotionFlow + operator fun invoke(): Flow> = emotionRepository.observeDailyEmotion() } 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 0e199238..87b87ec6 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 @@ -59,9 +59,14 @@ class HomeViewModel @Inject constructor( private fun observeDailyEmotion() { intent { repeatOnSubscription { - observeDailyEmotionUseCase() - .map { it.toUiModel() } - .collect { reduce { state.copy(dailyEmotion = it) } } + observeDailyEmotionUseCase().collect { result -> + result.fold( + onSuccess = { dailyEmotion -> + reduce { state.copy(dailyEmotion = dailyEmotion.toUiModel()) } + }, + onFailure = {}, + ) + } } } } 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 9ff91736..83e0f741 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 @@ -31,7 +31,7 @@ class RecommendRoutineViewModel @Inject constructor( onCreate = { repeatOnSubscription { observeDailyEmotionUseCase() - .map { it.type } + .map { result -> result.getOrNull()?.type } .distinctUntilChanged() .collect { loadRecommendRoutines() } } From 12f5dec5c185a3f71f0a8192704f5d6d1fb4fb5b Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 12 Mar 2026 02:10:45 +0900 Subject: [PATCH 12/20] =?UTF-8?q?Chore:=20=EC=98=A4=ED=83=80=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../emotion/repositoryImpl/EmotionRepositoryImpl.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 cd72fc30..73ed05a1 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 @@ -19,7 +19,7 @@ import javax.inject.Inject class EmotionRepositoryImpl @Inject constructor( private val emotionRemoteDataSource: EmotionRemoteDataSource, - private val emotionLocalDateSource: EmotionLocalDataSource, + private val emotionLocalDataSource: EmotionLocalDataSource, ) : EmotionRepository { private val fetchMutex = Mutex() @@ -48,14 +48,14 @@ class EmotionRepositoryImpl @Inject constructor( } emitAll( - emotionLocalDateSource.dailyEmotion + emotionLocalDataSource.dailyEmotion .filterNotNull() .map { Result.success(it) }, ) } override fun clearCache() { - emotionLocalDateSource.clearCache() + emotionLocalDataSource.clearCache() } private suspend fun fetchAndSaveDailyEmotion( @@ -63,7 +63,7 @@ class EmotionRepositoryImpl @Inject constructor( forceRefresh: Boolean = false, ): Result { if (!forceRefresh) { - val currentLocalData = emotionLocalDateSource.dailyEmotion.value + val currentLocalData = emotionLocalDataSource.dailyEmotion.value if (currentLocalData != null && !currentLocalData.isStale(today)) { return Result.success(currentLocalData) } @@ -71,13 +71,13 @@ class EmotionRepositoryImpl @Inject constructor( return fetchMutex.withLock { if (!forceRefresh) { - val doubleCheckData = emotionLocalDateSource.dailyEmotion.value + val doubleCheckData = emotionLocalDataSource.dailyEmotion.value if (doubleCheckData != null && !doubleCheckData.isStale(today)) { return@withLock Result.success(doubleCheckData) } } emotionRemoteDataSource.fetchDailyEmotion(today.toString()) - .onSuccess { emotionLocalDateSource.saveDailyEmotion(it.toDomain(today)) } + .onSuccess { emotionLocalDataSource.saveDailyEmotion(it.toDomain(today)) } .map { it.toDomain(today) } } } From 90f5a30e94a038525edec9a2ea6341fbe0211ec5 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 8 Mar 2026 01:20:55 +0900 Subject: [PATCH 13/20] =?UTF-8?q?Refactor:=20DailyEmotion=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=ED=9D=90=EB=A6=84=EC=9D=84=20Flow=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EA=B4=80=EC=B0=B0=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/response/DailyEmotionResponse.kt | 2 + .../repositoryImpl/EmotionRepositoryImpl.kt | 31 ++++++++------ .../domain/emotion/model/DailyEmotion.kt | 18 ++++++++- .../emotion/model/EmotionChangeEvent.kt | 5 --- .../emotion/repository/EmotionRepository.kt | 5 +-- .../GetEmotionChangeEventFlowUseCase.kt | 12 ------ ...eCase.kt => ObserveDailyEmotionUseCase.kt} | 9 ++--- .../presentation/screen/home/HomeViewModel.kt | 40 ++++++------------- .../RecommendRoutineViewModel.kt | 34 ++++++++-------- 9 files changed, 71 insertions(+), 85 deletions(-) delete mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt delete mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt rename domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/{FetchDailyEmotionUseCase.kt => ObserveDailyEmotionUseCase.kt} (51%) 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..f2bfd6fd 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( @@ -23,4 +24,5 @@ fun DailyEmotionResponse.toDomain(): DailyEmotion = name = emotionMarbleName, imageUrl = imageUrl, homeMessage = emotionMarbleHomeMessage, + fetchedDate = LocalDate.now(), ) 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..d1ac495f 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,24 @@ 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 javax.inject.Inject class EmotionRepositoryImpl @Inject constructor( private val emotionDataSource: EmotionDataSource, ) : EmotionRepository { + + private val _dailyEmotionFlow = MutableStateFlow(DailyEmotion.INIT) + override val dailyEmotionFlow: Flow = _dailyEmotionFlow + .onSubscription { + if (_dailyEmotionFlow.value.isStale) fetchDailyEmotion() + } + override suspend fun getEmotions(): Result> { return emotionDataSource.getEmotions().map { response -> response.map { it.toDomain() } @@ -23,20 +30,18 @@ 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 { + val currentDate = LocalDate.now().toString() + return emotionDataSource.fetchDailyEmotion(currentDate).map { + _dailyEmotionFlow.value = it.toDomain() + } + } } 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..c27f4266 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,24 @@ 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, +) { + val isStale: Boolean + get() = fetchedDate != LocalDate.now() + + 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 3f6ded78..a655b15c 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,6 @@ 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.onboarding.usecase.GetOnBoardingRecommendRoutineEventFlowUseCase import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfo import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos @@ -35,11 +33,11 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val fetchWeeklyRoutinesUseCase: FetchWeeklyRoutinesUseCase, + private val fetchUserProfileUseCase: FetchUserProfileUseCase, private val observeUserProfileUseCase: ObserveUserProfileUseCase, 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() { @@ -51,6 +49,17 @@ class HomeViewModel @Inject constructor( init { initialize() + observeDailyEmotion() + } + + private fun observeDailyEmotion() { + intent { + repeatOnSubscription { + observeDailyEmotionUseCase() + .map { it.toUiModel() } + .collect { reduce { state.copy(dailyEmotion = it) } } + } + } } fun selectDate(data: LocalDate) { @@ -185,11 +194,11 @@ class HomeViewModel @Inject constructor( private fun initialize() { intent { coroutineScope { + launch { fetchUserProfile() } launch { observeUserProfile() } launch { fetchDailyEmotion() } launch { fetchWeeklyRoutines(state.currentWeeks) } launch { observeWriteRoutineEvent() } - launch { observeEmotionChangeEvent() } launch { observeRecommendRoutineEvent() } launch { observeWeekChanges() } launch { observeRoutineUpdates() } @@ -205,14 +214,6 @@ class HomeViewModel @Inject constructor( } } - private suspend fun observeEmotionChangeEvent() { - subIntent { - getEmotionChangeEventFlowUseCase().collect { - fetchDailyEmotion() - } - } - } - private suspend fun observeRecommendRoutineEvent() { subIntent { getOnBoardingRecommendRoutineEventFlowUseCase().collect { @@ -282,21 +283,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) } From 29f878d43d4f8a69c5a4bf80e52f985dfce78f08 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 9 Mar 2026 01:56:05 +0900 Subject: [PATCH 14/20] =?UTF-8?q?Fix:=20EmotionRepository=20=EB=82=B4=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=ED=98=B8=EC=B6=9C=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?=EB=B0=8F=20HomeViewModel=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../emotion/repositoryImpl/EmotionRepositoryImpl.kt | 13 ++++++++++--- .../presentation/screen/home/HomeViewModel.kt | 6 +++++- 2 files changed, 15 insertions(+), 4 deletions(-) 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 d1ac495f..1cb7b3c4 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 @@ -10,12 +10,14 @@ import kotlinx.coroutines.flow.Flow 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 { @@ -39,9 +41,14 @@ class EmotionRepositoryImpl @Inject constructor( } override suspend fun fetchDailyEmotion(): Result { - val currentDate = LocalDate.now().toString() - return emotionDataSource.fetchDailyEmotion(currentDate).map { - _dailyEmotionFlow.value = it.toDomain() + if (!isFetching.compareAndSet(false, true)) return Result.success(Unit) + return try { + val currentDate = LocalDate.now().toString() + emotionDataSource.fetchDailyEmotion(currentDate).map { + _dailyEmotionFlow.value = it.toDomain() + } + } finally { + isFetching.set(false) } } } 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 a655b15c..46e3377c 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 @@ -42,7 +42,11 @@ class HomeViewModel @Inject constructor( 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) From f559baca0d205d9d4753b8fe87d2552a1a6f6965 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 9 Mar 2026 23:17:24 +0900 Subject: [PATCH 15/20] =?UTF-8?q?Refactor:=20DailyEmotion=20=EB=82=B4=20is?= =?UTF-8?q?Stale=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20L?= =?UTF-8?q?ocalDate=20=EC=A3=BC=EC=9E=85=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/emotion/model/response/DailyEmotionResponse.kt | 4 ++-- .../data/emotion/repositoryImpl/EmotionRepositoryImpl.kt | 8 ++++---- .../bitnagil/domain/emotion/model/DailyEmotion.kt | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) 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 f2bfd6fd..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 @@ -18,11 +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 = LocalDate.now(), + 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 1cb7b3c4..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 @@ -21,7 +21,7 @@ class EmotionRepositoryImpl @Inject constructor( private val _dailyEmotionFlow = MutableStateFlow(DailyEmotion.INIT) override val dailyEmotionFlow: Flow = _dailyEmotionFlow .onSubscription { - if (_dailyEmotionFlow.value.isStale) fetchDailyEmotion() + if (_dailyEmotionFlow.value.isStale(LocalDate.now())) fetchDailyEmotion() } override suspend fun getEmotions(): Result> { @@ -43,9 +43,9 @@ class EmotionRepositoryImpl @Inject constructor( override suspend fun fetchDailyEmotion(): Result { if (!isFetching.compareAndSet(false, true)) return Result.success(Unit) return try { - val currentDate = LocalDate.now().toString() - emotionDataSource.fetchDailyEmotion(currentDate).map { - _dailyEmotionFlow.value = it.toDomain() + 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 c27f4266..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 @@ -9,8 +9,7 @@ data class DailyEmotion( val homeMessage: String?, val fetchedDate: LocalDate = LocalDate.MIN, ) { - val isStale: Boolean - get() = fetchedDate != LocalDate.now() + fun isStale(today: LocalDate): Boolean = fetchedDate != today companion object { val INIT = DailyEmotion( From 1de807cd9b170547583b362bf52195afdcdcd908 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 12 Mar 2026 02:00:57 +0900 Subject: [PATCH 16/20] =?UTF-8?q?Refactor:=20Emotion=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=A0=88=EC=9D=B4=EC=96=B4=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=BA=90=EC=8B=B1=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/di/data/DataSourceModule.kt | 12 ++- .../datasource/EmotionLocalDataSource.kt | 10 +++ ...taSource.kt => EmotionRemoteDataSource.kt} | 2 +- .../EmotionLocalDataSourceImpl.kt | 22 ++++++ ...Impl.kt => EmotionRemoteDataSourceImpl.kt} | 6 +- .../repositoryImpl/EmotionRepositoryImpl.kt | 74 +++++++++++++------ .../emotion/repository/EmotionRepository.kt | 4 +- .../usecase/ObserveDailyEmotionUseCase.kt | 2 +- .../presentation/screen/home/HomeViewModel.kt | 11 ++- .../RecommendRoutineViewModel.kt | 2 +- 10 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt rename data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/{EmotionDataSource.kt => EmotionRemoteDataSource.kt} (93%) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionLocalDataSourceImpl.kt rename data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/{EmotionDataSourceImpl.kt => EmotionRemoteDataSourceImpl.kt} (88%) diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt index 4c3fab8f..61535f14 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt @@ -8,8 +8,10 @@ import com.threegap.bitnagil.data.auth.datasource.AuthLocalDataSource import com.threegap.bitnagil.data.auth.datasource.AuthRemoteDataSource import com.threegap.bitnagil.data.auth.datasourceimpl.AuthLocalDataSourceImpl import com.threegap.bitnagil.data.auth.datasourceimpl.AuthRemoteDataSourceImpl -import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource -import com.threegap.bitnagil.data.emotion.datasourceImpl.EmotionDataSourceImpl +import com.threegap.bitnagil.data.emotion.datasource.EmotionLocalDataSource +import com.threegap.bitnagil.data.emotion.datasource.EmotionRemoteDataSource +import com.threegap.bitnagil.data.emotion.datasourceImpl.EmotionLocalDataSourceImpl +import com.threegap.bitnagil.data.emotion.datasourceImpl.EmotionRemoteDataSourceImpl import com.threegap.bitnagil.data.file.datasource.FileDataSource import com.threegap.bitnagil.data.file.datasourceImpl.FileDataSourceImpl import com.threegap.bitnagil.data.onboarding.datasource.OnBoardingDataSource @@ -54,7 +56,11 @@ abstract class DataSourceModule { @Binds @Singleton - abstract fun bindEmotionDataSource(emotionDataSourceImpl: EmotionDataSourceImpl): EmotionDataSource + abstract fun bindEmotionLocalDataSource(emotionLocalDataSourceImpl: EmotionLocalDataSourceImpl): EmotionLocalDataSource + + @Binds + @Singleton + abstract fun bindEmotionRemoteDataSource(emotionRemoteDataSourceImpl: EmotionRemoteDataSourceImpl): EmotionRemoteDataSource @Binds @Singleton diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt new file mode 100644 index 00000000..0a64f25c --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt @@ -0,0 +1,10 @@ +package com.threegap.bitnagil.data.emotion.datasource + +import com.threegap.bitnagil.domain.emotion.model.DailyEmotion +import kotlinx.coroutines.flow.StateFlow + +interface EmotionLocalDataSource { + val dailyEmotion: StateFlow + fun saveDailyEmotion(dailyEmotion: DailyEmotion) + fun clearCache() +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionRemoteDataSource.kt similarity index 93% rename from data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt rename to data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionRemoteDataSource.kt index c3f81e30..d8c7e004 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionRemoteDataSource.kt @@ -4,7 +4,7 @@ import com.threegap.bitnagil.data.emotion.model.dto.EmotionDto import com.threegap.bitnagil.data.emotion.model.response.DailyEmotionResponse import com.threegap.bitnagil.data.emotion.model.response.RegisterEmotionResponse -interface EmotionDataSource { +interface EmotionRemoteDataSource { suspend fun getEmotions(): Result> suspend fun registerEmotion(emotion: String): Result suspend fun fetchDailyEmotion(currentDate: String): Result diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionLocalDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionLocalDataSourceImpl.kt new file mode 100644 index 00000000..e3f30f99 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionLocalDataSourceImpl.kt @@ -0,0 +1,22 @@ +package com.threegap.bitnagil.data.emotion.datasourceImpl + +import com.threegap.bitnagil.data.emotion.datasource.EmotionLocalDataSource +import com.threegap.bitnagil.domain.emotion.model.DailyEmotion +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +class EmotionLocalDataSourceImpl @Inject constructor() : EmotionLocalDataSource { + private val _dailyEmotion = MutableStateFlow(null) + override val dailyEmotion: StateFlow = _dailyEmotion.asStateFlow() + + override fun saveDailyEmotion(dailyEmotion: DailyEmotion) { + _dailyEmotion.update { dailyEmotion } + } + + override fun clearCache() { + _dailyEmotion.update { null } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionRemoteDataSourceImpl.kt similarity index 88% rename from data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt rename to data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionRemoteDataSourceImpl.kt index 20035b56..78effc19 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionRemoteDataSourceImpl.kt @@ -1,7 +1,7 @@ package com.threegap.bitnagil.data.emotion.datasourceImpl import com.threegap.bitnagil.data.common.safeApiCall -import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource +import com.threegap.bitnagil.data.emotion.datasource.EmotionRemoteDataSource import com.threegap.bitnagil.data.emotion.model.dto.EmotionDto import com.threegap.bitnagil.data.emotion.model.request.RegisterEmotionRequest import com.threegap.bitnagil.data.emotion.model.response.DailyEmotionResponse @@ -9,9 +9,9 @@ import com.threegap.bitnagil.data.emotion.model.response.RegisterEmotionResponse import com.threegap.bitnagil.data.emotion.service.EmotionService import javax.inject.Inject -class EmotionDataSourceImpl @Inject constructor( +class EmotionRemoteDataSourceImpl @Inject constructor( private val emotionService: EmotionService, -) : EmotionDataSource { +) : EmotionRemoteDataSource { override suspend fun getEmotions(): Result> { return safeApiCall { emotionService.getEmotions() 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 6bb5aa9b..cd72fc30 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 @@ -1,54 +1,84 @@ package com.threegap.bitnagil.data.emotion.repositoryImpl -import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource +import com.threegap.bitnagil.data.emotion.datasource.EmotionLocalDataSource +import com.threegap.bitnagil.data.emotion.datasource.EmotionRemoteDataSource 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.EmotionRecommendRoutine import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.onSubscription +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import java.time.LocalDate -import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject class EmotionRepositoryImpl @Inject constructor( - private val emotionDataSource: EmotionDataSource, + private val emotionRemoteDataSource: EmotionRemoteDataSource, + private val emotionLocalDateSource: EmotionLocalDataSource, ) : 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() - } + private val fetchMutex = Mutex() override suspend fun getEmotions(): Result> { - return emotionDataSource.getEmotions().map { response -> + return emotionRemoteDataSource.getEmotions().map { response -> response.map { it.toDomain() } } } override suspend fun registerEmotion(emotionMarbleType: String): Result> { - return emotionDataSource.registerEmotion(emotionMarbleType).map { + return emotionRemoteDataSource.registerEmotion(emotionMarbleType).map { it.recommendedRoutines.map { emotionRecommendedRoutineDto -> emotionRecommendedRoutineDto.toEmotionRecommendRoutine() } }.also { - if (it.isSuccess) fetchDailyEmotion() + if (it.isSuccess) fetchAndSaveDailyEmotion(today = LocalDate.now(), forceRefresh = true) } } - 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) + override fun observeDailyEmotion(): Flow> = flow { + fetchAndSaveDailyEmotion(LocalDate.now()) + .onFailure { + emit(Result.failure(it)) + return@flow + } + + emitAll( + emotionLocalDateSource.dailyEmotion + .filterNotNull() + .map { Result.success(it) }, + ) + } + + override fun clearCache() { + emotionLocalDateSource.clearCache() + } + + private suspend fun fetchAndSaveDailyEmotion( + today: LocalDate, + forceRefresh: Boolean = false, + ): Result { + if (!forceRefresh) { + val currentLocalData = emotionLocalDateSource.dailyEmotion.value + if (currentLocalData != null && !currentLocalData.isStale(today)) { + return Result.success(currentLocalData) + } + } + + return fetchMutex.withLock { + if (!forceRefresh) { + val doubleCheckData = emotionLocalDateSource.dailyEmotion.value + if (doubleCheckData != null && !doubleCheckData.isStale(today)) { + return@withLock Result.success(doubleCheckData) + } } - } finally { - isFetching.set(false) + emotionRemoteDataSource.fetchDailyEmotion(today.toString()) + .onSuccess { emotionLocalDateSource.saveDailyEmotion(it.toDomain(today)) } + .map { it.toDomain(today) } } } } 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 51fd97ca..a95af407 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 @@ -6,8 +6,8 @@ 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(): Result + fun observeDailyEmotion(): Flow> + fun clearCache() } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt index aa064f8d..9892334b 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt @@ -8,5 +8,5 @@ import javax.inject.Inject class ObserveDailyEmotionUseCase @Inject constructor( private val emotionRepository: EmotionRepository, ) { - operator fun invoke(): Flow = emotionRepository.dailyEmotionFlow + operator fun invoke(): Flow> = emotionRepository.observeDailyEmotion() } 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 46e3377c..995f2ae4 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 @@ -59,9 +59,14 @@ class HomeViewModel @Inject constructor( private fun observeDailyEmotion() { intent { repeatOnSubscription { - observeDailyEmotionUseCase() - .map { it.toUiModel() } - .collect { reduce { state.copy(dailyEmotion = it) } } + observeDailyEmotionUseCase().collect { result -> + result.fold( + onSuccess = { dailyEmotion -> + reduce { state.copy(dailyEmotion = dailyEmotion.toUiModel()) } + }, + onFailure = {}, + ) + } } } } 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 9ff91736..83e0f741 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 @@ -31,7 +31,7 @@ class RecommendRoutineViewModel @Inject constructor( onCreate = { repeatOnSubscription { observeDailyEmotionUseCase() - .map { it.type } + .map { result -> result.getOrNull()?.type } .distinctUntilChanged() .collect { loadRecommendRoutines() } } From 2f889926da2dbfd52d38c6832564ed360fdd89a1 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 12 Mar 2026 02:10:45 +0900 Subject: [PATCH 17/20] =?UTF-8?q?Chore:=20=EC=98=A4=ED=83=80=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../emotion/repositoryImpl/EmotionRepositoryImpl.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 cd72fc30..73ed05a1 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 @@ -19,7 +19,7 @@ import javax.inject.Inject class EmotionRepositoryImpl @Inject constructor( private val emotionRemoteDataSource: EmotionRemoteDataSource, - private val emotionLocalDateSource: EmotionLocalDataSource, + private val emotionLocalDataSource: EmotionLocalDataSource, ) : EmotionRepository { private val fetchMutex = Mutex() @@ -48,14 +48,14 @@ class EmotionRepositoryImpl @Inject constructor( } emitAll( - emotionLocalDateSource.dailyEmotion + emotionLocalDataSource.dailyEmotion .filterNotNull() .map { Result.success(it) }, ) } override fun clearCache() { - emotionLocalDateSource.clearCache() + emotionLocalDataSource.clearCache() } private suspend fun fetchAndSaveDailyEmotion( @@ -63,7 +63,7 @@ class EmotionRepositoryImpl @Inject constructor( forceRefresh: Boolean = false, ): Result { if (!forceRefresh) { - val currentLocalData = emotionLocalDateSource.dailyEmotion.value + val currentLocalData = emotionLocalDataSource.dailyEmotion.value if (currentLocalData != null && !currentLocalData.isStale(today)) { return Result.success(currentLocalData) } @@ -71,13 +71,13 @@ class EmotionRepositoryImpl @Inject constructor( return fetchMutex.withLock { if (!forceRefresh) { - val doubleCheckData = emotionLocalDateSource.dailyEmotion.value + val doubleCheckData = emotionLocalDataSource.dailyEmotion.value if (doubleCheckData != null && !doubleCheckData.isStale(today)) { return@withLock Result.success(doubleCheckData) } } emotionRemoteDataSource.fetchDailyEmotion(today.toString()) - .onSuccess { emotionLocalDateSource.saveDailyEmotion(it.toDomain(today)) } + .onSuccess { emotionLocalDataSource.saveDailyEmotion(it.toDomain(today)) } .map { it.toDomain(today) } } } From b12d67dde8794b05438af5df731808cb0520e9aa Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 15 Mar 2026 16:34:02 +0900 Subject: [PATCH 18/20] =?UTF-8?q?Refactor:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83/=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20=EC=8B=9C=20?= =?UTF-8?q?=EA=B0=90=EC=A0=95=20=EC=BA=90=EC=8B=9C=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt | 3 +++ .../bitnagil/domain/auth/usecase/WithdrawalUseCase.kt | 3 +++ .../bitnagil/presentation/screen/home/HomeViewModel.kt | 6 ++---- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt index b7418429..d5b5ddac 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt @@ -1,15 +1,18 @@ package com.threegap.bitnagil.domain.auth.usecase import com.threegap.bitnagil.domain.auth.repository.AuthRepository +import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import com.threegap.bitnagil.domain.user.repository.UserRepository import javax.inject.Inject class LogoutUseCase @Inject constructor( private val authRepository: AuthRepository, private val userRepository: UserRepository, + private val emotionRepository: EmotionRepository, ) { suspend operator fun invoke(): Result = authRepository.logout().onSuccess { userRepository.clearCache() + emotionRepository.clearCache() } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt index 12c578a7..880b8e3a 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt @@ -1,15 +1,18 @@ package com.threegap.bitnagil.domain.auth.usecase import com.threegap.bitnagil.domain.auth.repository.AuthRepository +import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import com.threegap.bitnagil.domain.user.repository.UserRepository import javax.inject.Inject class WithdrawalUseCase @Inject constructor( private val authRepository: AuthRepository, private val userRepository: UserRepository, + private val emotionRepository: EmotionRepository, ) { suspend operator fun invoke(reason: String): Result = authRepository.withdrawal(reason).onSuccess { userRepository.clearCache() + emotionRepository.clearCache() } } 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 995f2ae4..639da290 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,6 +2,7 @@ package com.threegap.bitnagil.presentation.screen.home import android.util.Log import androidx.lifecycle.ViewModel +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 @@ -33,9 +34,8 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val fetchWeeklyRoutinesUseCase: FetchWeeklyRoutinesUseCase, - private val fetchUserProfileUseCase: FetchUserProfileUseCase, private val observeUserProfileUseCase: ObserveUserProfileUseCase, - private val fetchDailyEmotionUseCase: FetchDailyEmotionUseCase, + private val observeDailyEmotionUseCase: ObserveDailyEmotionUseCase, private val routineCompletionUseCase: RoutineCompletionUseCase, private val getWriteRoutineEventFlowUseCase: GetWriteRoutineEventFlowUseCase, private val getOnBoardingRecommendRoutineEventFlowUseCase: GetOnBoardingRecommendRoutineEventFlowUseCase, @@ -203,9 +203,7 @@ class HomeViewModel @Inject constructor( private fun initialize() { intent { coroutineScope { - launch { fetchUserProfile() } launch { observeUserProfile() } - launch { fetchDailyEmotion() } launch { fetchWeeklyRoutines(state.currentWeeks) } launch { observeWriteRoutineEvent() } launch { observeRecommendRoutineEvent() } From a41759429e4eaaee907b492259ccbdb962891175 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 15 Mar 2026 19:20:16 +0900 Subject: [PATCH 19/20] =?UTF-8?q?Refactor:=20UserLocalDataSource=20?= =?UTF-8?q?=EB=82=B4=20saveUserProfile=EC=9D=98=20suspend=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/data/user/datasource/UserLocalDataSource.kt | 2 +- .../data/user/datasourceImpl/UserLocalDataSourceImpl.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt index 2edeebcf..e8856ecf 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt @@ -5,6 +5,6 @@ import kotlinx.coroutines.flow.StateFlow interface UserLocalDataSource { val userProfile: StateFlow - suspend fun saveUserProfile(userProfile: UserProfile) + fun saveUserProfile(userProfile: UserProfile) fun clearCache() } diff --git a/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt index 6d12a9a1..7c8bfc78 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt @@ -14,7 +14,7 @@ class UserLocalDataSourceImpl @Inject constructor() : UserLocalDataSource { private val _userProfile = MutableStateFlow(null) override val userProfile: StateFlow = _userProfile.asStateFlow() - override suspend fun saveUserProfile(userProfile: UserProfile) { + override fun saveUserProfile(userProfile: UserProfile) { _userProfile.update { userProfile } } From ae207778635cd4ec9c53f3cbcf5fc5592e0f4e67 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 15 Mar 2026 19:24:23 +0900 Subject: [PATCH 20/20] =?UTF-8?q?Chore:=20=EB=88=84=EB=9D=BD=ED=95=9C=20su?= =?UTF-8?q?spend=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt b/data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt index 64289259..a8cca845 100644 --- a/data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt +++ b/data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt @@ -102,7 +102,7 @@ class UserRepositoryImplTest { private val _userProfile = MutableStateFlow(null) override val userProfile: StateFlow = _userProfile.asStateFlow() - override suspend fun saveUserProfile(userProfile: UserProfile) { + override fun saveUserProfile(userProfile: UserProfile) { _userProfile.update { userProfile } }