From 451d7f85b34667a847b0582ee800973896733732 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Wed, 24 Sep 2025 20:38:33 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[BUGFIX/#262]=20=EC=9A=B4=EC=84=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EB=A5=BC=20=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AlarmInteractionActivityReceiver.kt | 6 ++++-- .../java/com/yapp/fortune/FortuneContract.kt | 1 + .../java/com/yapp/fortune/FortuneScreen.kt | 12 +++++++++++ .../java/com/yapp/fortune/FortuneViewModel.kt | 20 +++++++++++++++++-- .../fortune/src/main/res/values/strings.xml | 6 ++++++ .../java/com/yapp/mission/MissionViewModel.kt | 11 ++++++---- 6 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 feature/fortune/src/main/res/values/strings.xml 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 029072a4..c3faf948 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 @@ -53,7 +53,9 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity) } when (fortuneCreateStatus) { - is FortuneCreateStatus.Creating -> { + is FortuneCreateStatus.Creating, + is FortuneCreateStatus.Failure, + -> { context?.let { ctx -> val uri = "orbitapp://fortune".toUri() val fortuneIntent = Intent(Intent.ACTION_VIEW, uri).apply { @@ -78,7 +80,7 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity) } } - FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> { } + FortuneCreateStatus.Idle -> { } } } else { context?.let { ctx -> diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneContract.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneContract.kt index 6c7528e0..e900b6b7 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneContract.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneContract.kt @@ -15,6 +15,7 @@ sealed class FortuneContract { val avgFortuneScore: Int = 0, val fortunePages: List = emptyList(), val fortuneImageId: Int? = null, + val isCreateFailureDialogVisible: Boolean = false, ) : com.yapp.ui.base.UiState sealed class Action { diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt index 1eb97abc..9417e908 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -36,7 +37,9 @@ import com.yapp.designsystem.theme.OrbitTheme import com.yapp.fortune.component.FortuneTopAppBar import com.yapp.fortune.component.SlidingIndicator import com.yapp.fortune.page.FortunePager +import com.yapp.ui.component.dialog.OrbitDialog import com.yapp.ui.component.lottie.LottieAnimation +import feature.fortune.R import kotlinx.coroutines.delay import java.math.BigDecimal import java.math.RoundingMode @@ -184,6 +187,15 @@ fun FortuneScreen( } } } + + if (state.isCreateFailureDialogVisible) { + OrbitDialog( + title = stringResource(id = R.string.fortune_failure_dialog_title), + message = stringResource(id = R.string.fortune_failure_dialog_message), + confirmText = stringResource(id = R.string.fortune_failure_dialog_confirm), + onConfirm = onNavigateToHome, + ) + } } @Composable 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 4a83a561..0bc19414 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt @@ -64,8 +64,19 @@ class FortuneViewModel @Inject constructor( ) } - is FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> { - postSideEffect(FortuneContract.SideEffect.NavigateToHome) + is FortuneCreateStatus.Failure -> { + reduce { + state.copy( + isLoading = false, + isCreateFailureDialogVisible = true, + ) + } + } + + is FortuneCreateStatus.Idle -> { + if (!state.isCreateFailureDialogVisible) { + postSideEffect(FortuneContract.SideEffect.NavigateToHome) + } } } } @@ -94,6 +105,7 @@ class FortuneViewModel @Inject constructor( fortunePages = fortune.toFortunePages(), fortuneImageId = imageId, hasReward = isFirstAlarmDismissedToday, + isCreateFailureDialogVisible = false, ) } }.onFailure { error -> @@ -122,6 +134,10 @@ class FortuneViewModel @Inject constructor( } private fun navigateToHome() = intent { + if (state.isCreateFailureDialogVisible) { + reduce { state.copy(isCreateFailureDialogVisible = false) } + } + postSideEffect(FortuneContract.SideEffect.NavigateToHome) } diff --git a/feature/fortune/src/main/res/values/strings.xml b/feature/fortune/src/main/res/values/strings.xml new file mode 100644 index 00000000..0584c356 --- /dev/null +++ b/feature/fortune/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + 운세 생성 실패 + 운세 생성에 실패했어요. 네트워크 연결 상태를 확인해주세요. + 확인 + 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 b5e1c45d..f26f893c 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt @@ -142,10 +142,13 @@ class MissionViewModel @Inject constructor( val fortuneCreateStatus = fortuneRepository.fortuneCreateStatusFlow.first() val hasUnseenFortune = fortuneRepository.hasUnseenFortuneFlow.first() - val shouldOpenFortune = ( - fortuneCreateStatus is FortuneCreateStatus.Creating || - fortuneCreateStatus is FortuneCreateStatus.Success && hasUnseenFortune - ) + val shouldOpenFortune = when (fortuneCreateStatus) { + is FortuneCreateStatus.Creating, + is FortuneCreateStatus.Failure, + -> true + is FortuneCreateStatus.Success -> hasUnseenFortune + FortuneCreateStatus.Idle -> false + } postSideEffect( if (shouldOpenFortune) { From 42019710f812aba226e355a5a17f835a3b49c8f0 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Wed, 24 Sep 2025 20:39:09 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[BUGFIX/#262]=20=EC=9A=B4=EC=84=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8B=A4=ED=8C=A8=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=98=A4=EB=8A=98=20=EA=B8=B0=EC=A4=80=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/yapp/datastore/FortunePreferences.kt | 26 +++++++++- .../yapp/datastore/FortunePreferencesTest.kt | 48 +++++++++++++++++++ .../WorkManagerPostFortuneTaskScheduler.kt | 6 --- .../yapp/fortune/worker/PostFortuneWorker.kt | 6 +-- 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt b/core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt index 98ebc7c5..fde9d5b7 100644 --- a/core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt +++ b/core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt @@ -35,6 +35,7 @@ class FortunePreferences @Inject constructor( val CREATING = booleanPreferencesKey("fortune_creating") val FAILED = booleanPreferencesKey("fortune_failed") + val FAILED_DATE = longPreferencesKey("fortune_failed_date_epoch") val ATTEMPT_ID = stringPreferencesKey("fortune_attempt_id") val STARTED_AT = longPreferencesKey("fortune_started_at") @@ -104,6 +105,7 @@ class FortunePreferences @Inject constructor( dataStore.edit { pref -> pref[Keys.CREATING] = false pref[Keys.FAILED] = true + pref[Keys.FAILED_DATE] = todayEpoch() } emit(false) return@transformLatest @@ -115,7 +117,25 @@ class FortunePreferences @Inject constructor( val isFortuneFailedFlow: Flow = dataStore.data .catch { emit(emptyPreferences()) } - .map { it[Keys.FAILED] ?: false } + .map { pref -> + val failed = pref[Keys.FAILED] ?: false + val failedDate = pref[Keys.FAILED_DATE] + failed to failedDate + } + .transformLatest { (failed, failedDate) -> + if (failed) { + val isToday = failedDate == todayEpoch() + if (!isToday) { + dataStore.edit { pref -> + pref[Keys.FAILED] = false + pref.remove(Keys.FAILED_DATE) + } + emit(false) + return@transformLatest + } + } + emit(failed) + } .distinctUntilChanged() val isFirstAlarmDismissedTodayFlow: Flow = dataStore.data @@ -134,7 +154,6 @@ class FortunePreferences @Inject constructor( val now = nowMillis() dataStore.edit { pref -> pref[Keys.CREATING] = true - pref[Keys.FAILED] = false pref[Keys.ATTEMPT_ID] = attemptId pref[Keys.STARTED_AT] = now pref[Keys.EXPIRES_AT] = now + lease @@ -169,6 +188,7 @@ class FortunePreferences @Inject constructor( pref[Keys.DATE] = today pref[Keys.CREATING] = false pref[Keys.FAILED] = false + pref.remove(Keys.FAILED_DATE) pref.remove(Keys.ATTEMPT_ID) pref.remove(Keys.STARTED_AT) pref.remove(Keys.EXPIRES_AT) @@ -186,6 +206,7 @@ class FortunePreferences @Inject constructor( if (pref[Keys.ATTEMPT_ID] == attemptId) { pref[Keys.CREATING] = false pref[Keys.FAILED] = true + pref[Keys.FAILED_DATE] = todayEpoch() pref.remove(Keys.ATTEMPT_ID) pref.remove(Keys.STARTED_AT) pref.remove(Keys.EXPIRES_AT) @@ -226,6 +247,7 @@ class FortunePreferences @Inject constructor( pref.remove(Keys.TOOLTIP_SHOWN) pref.remove(Keys.CREATING) pref.remove(Keys.FAILED) + pref.remove(Keys.FAILED_DATE) } } } diff --git a/core/datastore/src/test/kotlin/com/yapp/datastore/FortunePreferencesTest.kt b/core/datastore/src/test/kotlin/com/yapp/datastore/FortunePreferencesTest.kt index b9410965..af669911 100644 --- a/core/datastore/src/test/kotlin/com/yapp/datastore/FortunePreferencesTest.kt +++ b/core/datastore/src/test/kotlin/com/yapp/datastore/FortunePreferencesTest.kt @@ -148,6 +148,54 @@ class FortunePreferencesTest { assertEquals(true, failed) } + @Test + fun `운세_생성_실패_후_재시도해도_성공하기_전까지_Failure가_유지된다`() = runTest { + // given: 첫 번째 시도에서 실패 상태로 전환 + val dataStore = createNewDataStoreWithFile("prefs_fail_retry.preferences_pb") + val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay) + val firstAttemptId = "ATTEMPT_FIRST" + val retryAttemptId = "ATTEMPT_RETRY" + val fortuneId = 101L + + preferences.markFortuneCreating(attemptId = firstAttemptId, lease = 60_000L) + preferences.markFortuneFailedIfAttemptMatches(firstAttemptId) + + // when: 새로운 attemptId로 재시도 + preferences.markFortuneCreating(attemptId = retryAttemptId, lease = 60_000L) + + // then: 성공 전까지는 Failure 상태 유지 + val failedDuringRetry = preferences.isFortuneFailedFlow.first() + assertEquals(true, failedDuringRetry) + + // when: 재시도가 성공적으로 완료되면 + preferences.markFortuneCreatedIfAttemptMatches( + attemptId = retryAttemptId, + fortuneId = fortuneId, + ) + + // then: Failure 플래그가 해제된다 + val failedAfterSuccess = preferences.isFortuneFailedFlow.first() + assertEquals(false, failedAfterSuccess) + } + + @Test + fun `이전_날짜의_운세_실패_상태는_자동_초기화된다`() = runTest { + // given: 기준일에 실패 상태 기록 + val dataStore = createNewDataStoreWithFile("prefs_fail_old.preferences_pb") + val preferencesToday = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay) + val attemptId = "ATTEMPT_OLD_FAIL" + preferencesToday.markFortuneCreating(attemptId = attemptId, lease = 60_000L) + preferencesToday.markFortuneFailedIfAttemptMatches(attemptId) + + // when: 다음 날 시점에서 상태 확인 + val nextDayClock = Clock.fixed(referenceInstantForAnyDay.plusSeconds(86_400), fixedZoneOffsetUtc) + val preferencesNextDay = createFortunePreferencesWithClock(dataStore, nextDayClock) + + // then: 실패 상태가 자동으로 해제된다 + val failedNextDay = preferencesNextDay.isFortuneFailedFlow.first() + assertEquals(false, failedNextDay) + } + @Test fun `운세_생성_상태_Creating_만료_시_Success_처리는_거부되고_Failure로_교정된다`() = runTest { // given: t0에서 Creating(lease 1초) 설정 diff --git a/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt b/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt index 36e49c4f..2aadbe87 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt @@ -2,9 +2,7 @@ package com.yapp.fortune.scheduler import android.content.Context import androidx.work.BackoffPolicy -import androidx.work.Constraints import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import com.yapp.alarm.scheduler.PostFortuneTaskScheduler @@ -19,11 +17,7 @@ class WorkManagerPostFortuneTaskScheduler @Inject constructor( ) : PostFortuneTaskScheduler { override fun enqueueOnceForToday() { val name = "post_fortune_${LocalDate.now()}" - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() val req = OneTimeWorkRequestBuilder() - .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.SECONDS) .build() WorkManager.getInstance(context) 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 d5feb56c..45f31753 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 @@ -28,9 +28,8 @@ class PostFortuneWorker @AssistedInject constructor( is FortuneCreateStatus.Creating, is FortuneCreateStatus.Success, -> return Result.success() - FortuneCreateStatus.Failure, - FortuneCreateStatus.Idle, - -> { /* 계속 진행 */ } + is FortuneCreateStatus.Failure -> return Result.failure() + FortuneCreateStatus.Idle -> { /* 계속 진행 */ } } val userId = userInfoRepository.userIdFlow.firstOrNull() @@ -57,6 +56,7 @@ class PostFortuneWorker @AssistedInject constructor( }, ) } catch (ce: CancellationException) { + fortuneRepository.markFortuneAsFailed(attemptId) throw ce } catch (_: Throwable) { fortuneRepository.markFortuneAsFailed(attemptId) From 9dd762e662153a0b586f442cd7482ed238de25a2 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Wed, 24 Sep 2025 20:40:41 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[CHORE/#262]=20=EC=9A=B4=EC=84=B8=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=95=88=EB=82=B4=20=EB=AC=B8=EA=B5=AC=20?= =?UTF-8?q?=EA=B0=9C=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/fortune/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/fortune/src/main/res/values/strings.xml b/feature/fortune/src/main/res/values/strings.xml index 0584c356..a4f7b162 100644 --- a/feature/fortune/src/main/res/values/strings.xml +++ b/feature/fortune/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ 운세 생성 실패 - 운세 생성에 실패했어요. 네트워크 연결 상태를 확인해주세요. + 운세 생성에 실패했어요.\n네트워크 연결을 확인해주세요. 확인