Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e96a6e7
Refactor: DailyEmotion 데이터 흐름을 Flow 기반 관찰 방식으로 변경
wjdrjs00 Mar 7, 2026
ead9462
Fix: EmotionRepository 내 중복 호출 방지 및 HomeViewModel 초기화 로직 수정
wjdrjs00 Mar 8, 2026
3edbfd1
Refactor: DailyEmotion 내 isStale 로직 개선 및 LocalDate 주입 방식 변경
wjdrjs00 Mar 9, 2026
46ea194
Refactor: UserDataSource를 UserRemoteDataSource와 UserLocalDataSource로 분리
wjdrjs00 Mar 10, 2026
9236266
Feat: UserRepository 내 캐싱 로직 및 Mutex를 이용한 Race Condition 방지 로직 구현
wjdrjs00 Mar 10, 2026
1b40a14
Refactor: FetchUserProfileUseCase를 ObserveUserProfileUseCase(Flow)로 변경
wjdrjs00 Mar 10, 2026
35bf092
Feat: 로그아웃 및 회원탈퇴 성공 시 유저 정보 캐시를 초기화하도록 수정
wjdrjs00 Mar 10, 2026
58e9b3c
Test: UserRepositoryImpl에 대한 단위 테스트 추가 (캐싱, 동시성, 캐시 초기화 검증)
wjdrjs00 Mar 10, 2026
e5c743e
Refactor: Home, MyPage, OnBoarding ViewModel에서 유저 정보를 Flow로 구독하도록 변경
wjdrjs00 Mar 10, 2026
fae4788
Feat: userProfile 단일 조회를 위한 GetUserProfileUseCase 추가
wjdrjs00 Mar 11, 2026
9d4add3
Refactor: Emotion 데이터 레이어 구조 개선 및 캐싱 로직 수정
wjdrjs00 Mar 11, 2026
12f5dec
Chore: 오타수정
wjdrjs00 Mar 11, 2026
64eb15e
Merge pull request #195 from YAPP-Github/refactor/#194-userprofile-flow
wjdrjs00 Mar 14, 2026
90f5a30
Refactor: DailyEmotion 데이터 흐름을 Flow 기반 관찰 방식으로 변경
wjdrjs00 Mar 7, 2026
29f878d
Fix: EmotionRepository 내 중복 호출 방지 및 HomeViewModel 초기화 로직 수정
wjdrjs00 Mar 8, 2026
f559bac
Refactor: DailyEmotion 내 isStale 로직 개선 및 LocalDate 주입 방식 변경
wjdrjs00 Mar 9, 2026
1de807c
Refactor: Emotion 데이터 레이어 구조 개선 및 캐싱 로직 수정
wjdrjs00 Mar 11, 2026
2f88992
Chore: 오타수정
wjdrjs00 Mar 11, 2026
b12d67d
Refactor: 로그아웃/회원탈퇴 시 감정 캐시 초기화
wjdrjs00 Mar 15, 2026
b9e2e88
Merge branch 'refactor/#192-daily-emotion-flow' of https://github.com…
wjdrjs00 Mar 15, 2026
a417594
Refactor: UserLocalDataSource 내 saveUserProfile의 suspend 키워드 제거
wjdrjs00 Mar 15, 2026
ae20777
Chore: 누락한 suspend 제거
wjdrjs00 Mar 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,8 +22,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
Expand Down Expand Up @@ -52,11 +56,19 @@ abstract class DataSourceModule {

@Binds
@Singleton
abstract fun bindEmotionDataSource(emotionDataSourceImpl: EmotionDataSourceImpl): EmotionDataSource
abstract fun bindEmotionLocalDataSource(emotionLocalDataSourceImpl: EmotionLocalDataSourceImpl): EmotionLocalDataSource

@Binds
@Singleton
abstract fun bindUserDataSource(userDataSourceImpl: UserDataSourceImpl): UserDataSource
abstract fun bindEmotionRemoteDataSource(emotionRemoteDataSourceImpl: EmotionRemoteDataSourceImpl): EmotionRemoteDataSource

@Binds
@Singleton
abstract fun bindUserLocalDataSource(userLocalDataSourceImpl: UserLocalDataSourceImpl): UserLocalDataSource

@Binds
@Singleton
abstract fun bindUserRemoteDataSource(userRemoteDataSourceImpl: UserRemoteDataSourceImpl): UserRemoteDataSource

@Binds
@Singleton
Expand Down
3 changes: 3 additions & 0 deletions data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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<DailyEmotion?>
fun saveDailyEmotion(dailyEmotion: DailyEmotion)
fun clearCache()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<EmotionDto>>
suspend fun registerEmotion(emotion: String): Result<RegisterEmotionResponse>
suspend fun fetchDailyEmotion(currentDate: String): Result<DailyEmotionResponse>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DailyEmotion?>(null)
override val dailyEmotion: StateFlow<DailyEmotion?> = _dailyEmotion.asStateFlow()

override fun saveDailyEmotion(dailyEmotion: DailyEmotion) {
_dailyEmotion.update { dailyEmotion }
}

override fun clearCache() {
_dailyEmotion.update { null }
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
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
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<List<EmotionDto>> {
return safeApiCall {
emotionService.getEmotions()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -17,10 +18,11 @@ data class DailyEmotionResponse(
val emotionMarbleHomeMessage: String?,
)

fun DailyEmotionResponse.toDomain(): DailyEmotion =
fun DailyEmotionResponse.toDomain(fetchedDate: LocalDate): DailyEmotion =
DailyEmotion(
type = emotionMarbleType,
name = emotionMarbleName,
imageUrl = imageUrl,
homeMessage = emotionMarbleHomeMessage,
fetchedDate = fetchedDate,
)
Original file line number Diff line number Diff line change
@@ -1,42 +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.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.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 javax.inject.Inject

class EmotionRepositoryImpl @Inject constructor(
private val emotionDataSource: EmotionDataSource,
private val emotionRemoteDataSource: EmotionRemoteDataSource,
private val emotionLocalDataSource: EmotionLocalDataSource,
) : EmotionRepository {

private val fetchMutex = Mutex()

override suspend fun getEmotions(): Result<List<Emotion>> {
return emotionDataSource.getEmotions().map { response ->
return emotionRemoteDataSource.getEmotions().map { response ->
response.map { it.toDomain() }
}
}

override suspend fun registerEmotion(emotionMarbleType: String): Result<List<EmotionRecommendRoutine>> {
return emotionDataSource.registerEmotion(emotionMarbleType).map {
it.recommendedRoutines.map {
emotionRecommendedRoutineDto ->
return emotionRemoteDataSource.registerEmotion(emotionMarbleType).map {
it.recommendedRoutines.map { emotionRecommendedRoutineDto ->
emotionRecommendedRoutineDto.toEmotionRecommendRoutine()
}
}.also {
if (it.isSuccess) {
_emotionChangeEventFlow.emit(EmotionChangeEvent.ChangeEmotion(emotionMarbleType))
}
if (it.isSuccess) fetchAndSaveDailyEmotion(today = LocalDate.now(), forceRefresh = true)
}
}

override suspend fun fetchDailyEmotion(currentDate: String): Result<DailyEmotion> =
emotionDataSource.fetchDailyEmotion(currentDate).map { it.toDomain() }
override fun observeDailyEmotion(): Flow<Result<DailyEmotion>> = flow {
fetchAndSaveDailyEmotion(LocalDate.now())
.onFailure {
emit(Result.failure(it))
return@flow
}

emitAll(
emotionLocalDataSource.dailyEmotion
.filterNotNull()
.map { Result.success(it) },
)
}

override fun clearCache() {
emotionLocalDataSource.clearCache()
}

private suspend fun fetchAndSaveDailyEmotion(
today: LocalDate,
forceRefresh: Boolean = false,
): Result<DailyEmotion> {
if (!forceRefresh) {
val currentLocalData = emotionLocalDataSource.dailyEmotion.value
if (currentLocalData != null && !currentLocalData.isStale(today)) {
return Result.success(currentLocalData)
}
}

private val _emotionChangeEventFlow = MutableSharedFlow<EmotionChangeEvent>()
override suspend fun getEmotionChangeEventFlow(): Flow<EmotionChangeEvent> = _emotionChangeEventFlow.asSharedFlow()
return fetchMutex.withLock {
if (!forceRefresh) {
val doubleCheckData = emotionLocalDataSource.dailyEmotion.value
if (doubleCheckData != null && !doubleCheckData.isStale(today)) {
return@withLock Result.success(doubleCheckData)
}
}
emotionRemoteDataSource.fetchDailyEmotion(today.toString())
.onSuccess { emotionLocalDataSource.saveDailyEmotion(it.toDomain(today)) }
.map { it.toDomain(today) }
}
}
}
Original file line number Diff line number Diff line change
@@ -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<UserProfile?>
fun saveUserProfile(userProfile: UserProfile)
fun clearCache()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserProfileResponse>
}
Original file line number Diff line number Diff line change
@@ -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<UserProfile?>(null)
override val userProfile: StateFlow<UserProfile?> = _userProfile.asStateFlow()

override fun saveUserProfile(userProfile: UserProfile) {
_userProfile.update { userProfile }
}

override fun clearCache() {
_userProfile.update { null }
}
}
Original file line number Diff line number Diff line change
@@ -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<UserProfileResponse> =
safeApiCall {
userService.fetchUserProfile()
}
safeApiCall { userService.fetchUserProfile() }
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,59 @@
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<UserProfile> =
userDataSource.fetchUserProfile().map { it.toDomain() }
private val fetchMutex = Mutex()

override fun observeUserProfile(): Flow<Result<UserProfile>> = flow {
fetchAndCacheIfNeeded().onFailure {
emit(Result.failure(it))
return@flow
}

emitAll(
userLocalDataSource.userProfile
.filterNotNull()
.map { Result.success(it) },
)
}

override suspend fun getUserProfile(): Result<UserProfile> {
return fetchAndCacheIfNeeded()
}

override fun clearCache() {
userLocalDataSource.clearCache()
}

private suspend fun fetchAndCacheIfNeeded(): Result<UserProfile> {
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() }
}
}
}
Loading
Loading