Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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 {
Expand All @@ -78,7 +80,7 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity)
}
}

FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> { }
FortuneCreateStatus.Idle -> { }
}
} else {
context?.let { ctx ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -115,7 +117,25 @@ class FortunePreferences @Inject constructor(

val isFortuneFailedFlow: Flow<Boolean> = 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<Boolean> = dataStore.data
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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초) 설정
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ sealed class FortuneContract {
val avgFortuneScore: Int = 0,
val fortunePages: List<FortunePageData> = emptyList(),
val fortuneImageId: Int? = null,
val isCreateFailureDialogVisible: Boolean = false,
) : com.yapp.ui.base.UiState

sealed class Action {
Expand Down
12 changes: 12 additions & 0 deletions feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 18 additions & 2 deletions feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}
Expand Down Expand Up @@ -94,6 +105,7 @@ class FortuneViewModel @Inject constructor(
fortunePages = fortune.toFortunePages(),
fortuneImageId = imageId,
hasReward = isFirstAlarmDismissedToday,
isCreateFailureDialogVisible = false,
)
}
}.onFailure { error ->
Expand Down Expand Up @@ -122,6 +134,10 @@ class FortuneViewModel @Inject constructor(
}

private fun navigateToHome() = intent {
if (state.isCreateFailureDialogVisible) {
reduce { state.copy(isCreateFailureDialogVisible = false) }
}

postSideEffect(FortuneContract.SideEffect.NavigateToHome)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<PostFortuneWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -57,6 +56,7 @@ class PostFortuneWorker @AssistedInject constructor(
},
)
} catch (ce: CancellationException) {
fortuneRepository.markFortuneAsFailed(attemptId)
throw ce
} catch (_: Throwable) {
fortuneRepository.markFortuneAsFailed(attemptId)
Expand Down
6 changes: 6 additions & 0 deletions feature/fortune/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fortune_failure_dialog_title">운세 생성 실패</string>
<string name="fortune_failure_dialog_message">운세 생성에 실패했어요.\n네트워크 연결을 확인해주세요.</string>
<string name="fortune_failure_dialog_confirm">확인</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down