diff --git a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt index 8d2389b7..029072a4 100644 --- a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt +++ b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt @@ -6,6 +6,7 @@ import android.content.Intent import androidx.activity.ComponentActivity import androidx.core.net.toUri import com.yapp.alarm.AlarmConstants +import com.yapp.domain.model.FortuneCreateStatus import com.yapp.domain.model.MissionType import com.yapp.domain.repository.FortuneRepository import dagger.hilt.android.AndroidEntryPoint @@ -45,31 +46,51 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity) CoroutineScope(Dispatchers.Main).launch { try { if (!hasValidMissionData) { - val hasUnseenFortune = withContext(Dispatchers.IO) { - fortuneRepository.hasUnseenFortuneFlow.first() + val (fortuneCreateStatus, hasUnseenFortune) = withContext(Dispatchers.IO) { + val status = fortuneRepository.fortuneCreateStatusFlow.first() + val unseen = fortuneRepository.hasUnseenFortuneFlow.first() + status to unseen } - if (hasUnseenFortune) { - context?.let { ctx -> - val uri = "orbitapp://fortune".toUri() - val fortuneIntent = Intent(Intent.ACTION_VIEW, uri).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - setPackage(ctx.packageName) + + when (fortuneCreateStatus) { + is FortuneCreateStatus.Creating -> { + context?.let { ctx -> + val uri = "orbitapp://fortune".toUri() + val fortuneIntent = Intent(Intent.ACTION_VIEW, uri).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + setPackage(ctx.packageName) + } + ctx.startActivity(fortuneIntent) } - ctx.startActivity(fortuneIntent) } - } - return@launch - } - context?.let { ctx -> - val uriString = - "orbitapp://mission?notificationId=$notificationId&missionType=${missionType.value}&missionCount=$missionCount" - val missionIntent = - Intent(Intent.ACTION_VIEW, uriString.toUri()).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - setPackage(ctx.packageName) + is FortuneCreateStatus.Success -> { + if (hasUnseenFortune) { + context?.let { ctx -> + val uri = "orbitapp://fortune".toUri() + val fortuneIntent = + Intent(Intent.ACTION_VIEW, uri).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + setPackage(ctx.packageName) + } + ctx.startActivity(fortuneIntent) + } + } } - ctx.startActivity(missionIntent) + + FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> { } + } + } else { + context?.let { ctx -> + val uriString = + "orbitapp://mission?notificationId=$notificationId&missionType=${missionType.value}&missionCount=$missionCount" + val missionIntent = + Intent(Intent.ACTION_VIEW, uriString.toUri()).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + setPackage(ctx.packageName) + } + ctx.startActivity(missionIntent) + } } } finally { pending.finish() diff --git a/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt b/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt index 220dfa55..27387c9f 100644 --- a/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt +++ b/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt @@ -131,20 +131,6 @@ class UserPreferences @Inject constructor( } } - suspend fun tryMarkFortuneCreating(): Boolean { - var canStart = false - dataStore.edit { pref -> - val hasTodayFortune = pref[Keys.FORTUNE_DATE] == today() && pref[Keys.FORTUNE_ID] != null - val creating = pref[Keys.FORTUNE_CREATING] ?: false - if (!hasTodayFortune && !creating) { - pref[Keys.FORTUNE_CREATING] = true - pref[Keys.FORTUNE_FAILED] = false - canStart = true - } - } - return canStart - } - suspend fun markFortuneCreating() { dataStore.edit { pref -> pref[Keys.FORTUNE_CREATING] = true diff --git a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt index 7b574bc0..853420c6 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt +++ b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt @@ -1,5 +1,6 @@ package com.yapp.data.local.datasource +import com.yapp.domain.model.FortuneCreateStatus import kotlinx.coroutines.flow.Flow interface FortuneLocalDataSource { @@ -9,11 +10,10 @@ interface FortuneLocalDataSource { val fortuneScoreFlow: Flow val hasUnseenFortuneFlow: Flow val shouldShowFortuneToolTipFlow: Flow - val isFortuneCreatingFlow: Flow - val isFortuneFailedFlow: Flow val isFirstAlarmDismissedTodayFlow: Flow - suspend fun tryMarkFortuneCreating(): Boolean + val fortuneCreateStatusFlow: Flow + suspend fun markFortuneCreating() suspend fun markFortuneCreated(fortuneId: Long) suspend fun markFortuneFailed() diff --git a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt index 80712f16..0a14cb17 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt +++ b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt @@ -1,6 +1,11 @@ package com.yapp.data.local.datasource import com.yapp.datastore.UserPreferences +import com.yapp.domain.model.FortuneCreateStatus +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import java.time.LocalDate +import java.time.format.DateTimeFormatter import javax.inject.Inject class FortuneLocalDataSourceImpl @Inject constructor( @@ -13,13 +18,23 @@ class FortuneLocalDataSourceImpl @Inject constructor( override val fortuneScoreFlow = userPreferences.fortuneScoreFlow override val hasUnseenFortuneFlow = userPreferences.hasUnseenFortuneFlow override val shouldShowFortuneToolTipFlow = userPreferences.shouldShowFortuneToolTipFlow - override val isFortuneCreatingFlow = userPreferences.isFortuneCreatingFlow - override val isFortuneFailedFlow = userPreferences.isFortuneFailedFlow override val isFirstAlarmDismissedTodayFlow = userPreferences.isFirstAlarmDismissedTodayFlow - override suspend fun tryMarkFortuneCreating(): Boolean { - return userPreferences.tryMarkFortuneCreating() - } + override val fortuneCreateStatusFlow = combine( + userPreferences.fortuneIdFlow, + userPreferences.fortuneDateFlow, + userPreferences.isFortuneCreatingFlow, + userPreferences.isFortuneFailedFlow, + ) { fortuneId, fortuneDate, isCreating, isFailed -> + when { + isFailed -> FortuneCreateStatus.Failure + isCreating -> FortuneCreateStatus.Creating + fortuneId != null && fortuneDate == today() -> FortuneCreateStatus.Success(fortuneId) + else -> FortuneCreateStatus.Idle + } + }.distinctUntilChanged() + + private fun today(): String = LocalDate.now().format(DateTimeFormatter.ISO_DATE) override suspend fun markFortuneCreating() { userPreferences.markFortuneCreating() diff --git a/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt index 23271c5f..8afe824e 100644 --- a/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt +++ b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt @@ -4,6 +4,7 @@ import com.yapp.data.local.datasource.FortuneLocalDataSource import com.yapp.data.remote.datasource.FortuneDataSource import com.yapp.data.remote.dto.response.toDomain import com.yapp.domain.model.Fortune +import com.yapp.domain.model.FortuneCreateStatus import com.yapp.domain.repository.FortuneRepository import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -19,11 +20,10 @@ class FortuneRepositoryImpl @Inject constructor( override val fortuneScoreFlow: Flow = fortuneLocalDataSource.fortuneScoreFlow override val hasUnseenFortuneFlow: Flow = fortuneLocalDataSource.hasUnseenFortuneFlow override val shouldShowFortuneToolTipFlow: Flow = fortuneLocalDataSource.shouldShowFortuneToolTipFlow - override val isFortuneCreatingFlow: Flow = fortuneLocalDataSource.isFortuneCreatingFlow - override val isFortuneFailedFlow: Flow = fortuneLocalDataSource.isFortuneFailedFlow override val isFirstAlarmDismissedTodayFlow: Flow = fortuneLocalDataSource.isFirstAlarmDismissedTodayFlow - override suspend fun tryMarkFortuneCreating() = fortuneLocalDataSource.tryMarkFortuneCreating() + override val fortuneCreateStatusFlow: Flow = fortuneLocalDataSource.fortuneCreateStatusFlow + override suspend fun markFortuneAsCreating() = fortuneLocalDataSource.markFortuneCreating() override suspend fun markFortuneAsCreated(fortuneId: Long) = fortuneLocalDataSource.markFortuneCreated(fortuneId) override suspend fun markFortuneAsFailed() = fortuneLocalDataSource.markFortuneFailed() diff --git a/domain/src/main/java/com/yapp/domain/model/FortuneCreateStatus.kt b/domain/src/main/java/com/yapp/domain/model/FortuneCreateStatus.kt new file mode 100644 index 00000000..27ae9ad0 --- /dev/null +++ b/domain/src/main/java/com/yapp/domain/model/FortuneCreateStatus.kt @@ -0,0 +1,8 @@ +package com.yapp.domain.model + +sealed class FortuneCreateStatus { + data object Idle : FortuneCreateStatus() + data object Creating : FortuneCreateStatus() + data class Success(val fortuneId: Long) : FortuneCreateStatus() + data object Failure : FortuneCreateStatus() +} diff --git a/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt b/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt index 89cc4a67..29024ae6 100644 --- a/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt +++ b/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt @@ -1,6 +1,7 @@ package com.yapp.domain.repository import com.yapp.domain.model.Fortune +import com.yapp.domain.model.FortuneCreateStatus import kotlinx.coroutines.flow.Flow interface FortuneRepository { @@ -10,11 +11,10 @@ interface FortuneRepository { val fortuneScoreFlow: Flow val hasUnseenFortuneFlow: Flow val shouldShowFortuneToolTipFlow: Flow - val isFortuneCreatingFlow: Flow - val isFortuneFailedFlow: Flow val isFirstAlarmDismissedTodayFlow: Flow - suspend fun tryMarkFortuneCreating(): Boolean + val fortuneCreateStatusFlow: Flow + suspend fun markFortuneAsCreating() suspend fun markFortuneAsCreated(fortuneId: Long) suspend fun markFortuneAsFailed() diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt index 003aa09d..4a83a561 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt @@ -4,12 +4,13 @@ import android.app.Application import android.util.Log import androidx.annotation.DrawableRes import androidx.lifecycle.ViewModel +import com.yapp.domain.model.FortuneCreateStatus import com.yapp.domain.repository.FortuneRepository import com.yapp.fortune.page.toFortunePages import com.yapp.media.decoder.ImageUtils import com.yapp.media.storage.ImageSaver import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost @@ -50,22 +51,21 @@ class FortuneViewModel @Inject constructor( } private fun observeFortune() = intent { - combine( - fortuneRepository.fortuneIdFlow, - fortuneRepository.isFirstAlarmDismissedTodayFlow, - fortuneRepository.isFortuneCreatingFlow, - ) { fortuneId: Long?, isFirstAlarmDismissedToday: Boolean, isCreating: Boolean -> - Triple(fortuneId, isFirstAlarmDismissedToday, isCreating) - }.collect { (fortuneId, isFirstAlarmDismissedToday, isCreating) -> - when { - isCreating -> { + fortuneRepository.fortuneCreateStatusFlow.collect { status -> + when (status) { + is FortuneCreateStatus.Creating -> { reduce { state.copy(isLoading = true) } } - fortuneId != null -> { - fetchAndUpdateFortune(fortuneId, isFirstAlarmDismissedToday) + + is FortuneCreateStatus.Success -> { + fetchAndUpdateFortune( + fortuneId = status.fortuneId, + isFirstAlarmDismissedToday = fortuneRepository.isFirstAlarmDismissedTodayFlow.first(), + ) } - else -> { - reduce { state.copy(isLoading = false) } + + is FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> { + postSideEffect(FortuneContract.SideEffect.NavigateToHome) } } } diff --git a/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt b/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt index 751ae536..2dd14f72 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import com.yapp.domain.model.FortuneCreateStatus import com.yapp.domain.repository.FortuneRepository import com.yapp.domain.repository.UserInfoRepository import dagger.assisted.Assisted @@ -20,35 +21,43 @@ class PostFortuneWorker @AssistedInject constructor( ) : CoroutineWorker(appContext, params) { override suspend fun doWork(): Result { - val hasUnseenFortune = fortuneRepository.hasUnseenFortuneFlow.first() - if (hasUnseenFortune) return Result.success() - - val acquired = fortuneRepository.tryMarkFortuneCreating() - if (!acquired) return Result.success() - - val userId = userInfoRepository.userIdFlow.firstOrNull() - ?: run { - fortuneRepository.markFortuneAsFailed() - return Result.failure() + when (fortuneRepository.fortuneCreateStatusFlow.first()) { + is FortuneCreateStatus.Creating, + is FortuneCreateStatus.Success, + -> { + return Result.success() } + FortuneCreateStatus.Failure, + FortuneCreateStatus.Idle, + -> { + val userId = userInfoRepository.userIdFlow.firstOrNull() + ?: run { + // 사용자 없으면 실패 상태 표시 후 실패 반환 + fortuneRepository.markFortuneAsFailed() + return Result.failure() + } - return try { - fortuneRepository.markFortuneAsCreating() - val result = fortuneRepository.postFortune(userId) - result.fold( - onSuccess = { fortune -> - fortuneRepository.markFortuneAsCreated(fortune.id) - fortuneRepository.saveFortuneScore(fortune.avgFortuneScore) - Result.success() - }, - onFailure = { e -> + return try { + fortuneRepository.markFortuneAsCreating() + + val result = fortuneRepository.postFortune(userId) + result.fold( + onSuccess = { fortune -> + fortuneRepository.markFortuneAsCreated(fortune.id) + fortuneRepository.saveFortuneScore(fortune.avgFortuneScore) + Result.success() + }, + onFailure = { + fortuneRepository.markFortuneAsFailed() + // WM 백오프 규칙에 따라 재시도 + Result.retry() + }, + ) + } catch (_: Throwable) { fortuneRepository.markFortuneAsFailed() Result.retry() - }, - ) - } catch (_: Throwable) { - fortuneRepository.markFortuneAsFailed() - Result.retry() + } + } } } } diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt index f54f58e4..eeccad37 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt @@ -7,6 +7,7 @@ import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissIntent import com.yapp.analytics.AnalyticsEvent import com.yapp.analytics.AnalyticsHelper import com.yapp.domain.MissionMode +import com.yapp.domain.model.FortuneCreateStatus import com.yapp.domain.model.MissionType import com.yapp.domain.repository.FortuneRepository import com.yapp.media.haptic.HapticFeedbackManager @@ -133,13 +134,15 @@ class MissionViewModel @Inject constructor( performHapticSuccess() logMissionSuccess(type) if (state.missionMode == MissionMode.REAL) { - val hasUnseenFortune = fortuneRepository.hasUnseenFortuneFlow.first() - val isFortuneCreating = fortuneRepository.isFortuneCreatingFlow.first() - - if (hasUnseenFortune || isFortuneCreating) { - postSideEffect(MissionContract.SideEffect.NavigateToFortune) - } else { - postSideEffect(MissionContract.SideEffect.NavigateBack) + val fortuneCreateStaus = fortuneRepository.fortuneCreateStatusFlow.first() + + when (fortuneCreateStaus) { + is FortuneCreateStatus.Creating, is FortuneCreateStatus.Success -> { + postSideEffect(MissionContract.SideEffect.NavigateToFortune) + } + FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> { + postSideEffect(MissionContract.SideEffect.NavigateBack) + } } } else { postSideEffect(MissionContract.SideEffect.NavigateBack)