diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 742620eb..6ae513ba 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -64,8 +64,6 @@ dependencies {
implementation(libs.compose.material)
implementation(libs.kotlin.reflect)
- implementation(libs.hilt.worker)
- implementation(libs.androidx.work.runtime)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.analytics)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e7abd75a..59cbd0b8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -80,14 +80,5 @@
android:name="com.yapp.alarm.services.AlarmService"
android:foregroundServiceType="mediaPlayback" />
-
-
-
diff --git a/app/src/main/java/com/yapp/orbit/OrbitApplication.kt b/app/src/main/java/com/yapp/orbit/OrbitApplication.kt
index b06538f8..61f01f70 100644
--- a/app/src/main/java/com/yapp/orbit/OrbitApplication.kt
+++ b/app/src/main/java/com/yapp/orbit/OrbitApplication.kt
@@ -1,24 +1,13 @@
package com.yapp.orbit
import android.app.Application
-import androidx.hilt.work.HiltWorkerFactory
-import androidx.work.Configuration
import com.google.android.gms.ads.MobileAds
import dagger.hilt.android.HiltAndroidApp
-import javax.inject.Inject
@HiltAndroidApp
-class OrbitApplication() : Application(), Configuration.Provider {
-
- @Inject lateinit var workerFactory: HiltWorkerFactory
-
+class OrbitApplication() : Application() {
override fun onCreate() {
super.onCreate()
MobileAds.initialize(this)
}
-
- override val workManagerConfiguration: Configuration
- get() = Configuration.Builder()
- .setWorkerFactory(workerFactory)
- .build()
}
diff --git a/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt b/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt
index dec5fc7a..36727c95 100644
--- a/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt
+++ b/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt
@@ -13,9 +13,7 @@ internal fun Project.configureHiltAndroid() {
dependencies {
"implementation"(libs.findLibrary("hilt.android").get())
"ksp"(libs.findLibrary("hilt.android.compiler").get())
- "ksp"(libs.findLibrary("androidx-hilt-compiler").get())
"implementation"(libs.findLibrary("hilt-navigation-compose").get())
- "implementation"(libs.findLibrary("hilt-worker").get())
}
}
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 c3faf948..030027be 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,9 +6,10 @@ 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.FortuneCreationState
import com.yapp.domain.model.MissionType
import com.yapp.domain.repository.FortuneRepository
+import com.yapp.domain.tracker.FortuneCreationTracker
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -23,6 +24,9 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity)
@Inject
lateinit var fortuneRepository: FortuneRepository
+ @Inject
+ lateinit var fortuneCreationTracker: FortuneCreationTracker
+
override fun onReceive(context: Context?, intent: Intent?) {
val isSnoozed = intent?.getBooleanExtra(AlarmConstants.EXTRA_IS_SNOOZED, false) ?: false
@@ -46,16 +50,15 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity)
CoroutineScope(Dispatchers.Main).launch {
try {
if (!hasValidMissionData) {
- val (fortuneCreateStatus, hasUnseenFortune) = withContext(Dispatchers.IO) {
- val status = fortuneRepository.fortuneCreateStatusFlow.first()
+ val (fortuneCreationState, hasUnseenFortune) = withContext(Dispatchers.IO) {
+ val todayFortune = fortuneCreationTracker.state.first()
val unseen = fortuneRepository.hasUnseenFortuneFlow.first()
- status to unseen
+
+ todayFortune to unseen
}
- when (fortuneCreateStatus) {
- is FortuneCreateStatus.Creating,
- is FortuneCreateStatus.Failure,
- -> {
+ when (fortuneCreationState) {
+ is FortuneCreationState.Start -> {
context?.let { ctx ->
val uri = "orbitapp://fortune".toUri()
val fortuneIntent = Intent(Intent.ACTION_VIEW, uri).apply {
@@ -65,8 +68,7 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity)
ctx.startActivity(fortuneIntent)
}
}
-
- is FortuneCreateStatus.Success -> {
+ is FortuneCreationState.Success -> {
if (hasUnseenFortune) {
context?.let { ctx ->
val uri = "orbitapp://fortune".toUri()
@@ -79,8 +81,7 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity)
}
}
}
-
- FortuneCreateStatus.Idle -> { }
+ is FortuneCreationState.Failure -> { }
}
} else {
context?.let { ctx ->
diff --git a/core/alarm/src/main/java/com/yapp/alarm/scheduler/PostFortuneTaskScheduler.kt b/core/alarm/src/main/java/com/yapp/alarm/scheduler/PostFortuneTaskScheduler.kt
deleted file mode 100644
index e16e8dd4..00000000
--- a/core/alarm/src/main/java/com/yapp/alarm/scheduler/PostFortuneTaskScheduler.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.yapp.alarm.scheduler
-
-interface PostFortuneTaskScheduler {
- fun enqueueOnceForToday()
-}
diff --git a/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt b/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt
index abdc8692..0b7e42e2 100644
--- a/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt
+++ b/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt
@@ -23,17 +23,20 @@ import com.yapp.alarm.pendingIntent.interaction.createAlarmAlertPendingIntent
import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissPendingIntent
import com.yapp.alarm.pendingIntent.interaction.createAlarmSnoozePendingIntent
import com.yapp.alarm.pendingIntent.interaction.createNavigateToMissionPendingIntent
-import com.yapp.alarm.scheduler.PostFortuneTaskScheduler
import com.yapp.domain.model.Alarm
import com.yapp.domain.model.AlarmDay
import com.yapp.domain.model.MissionType
import com.yapp.domain.repository.AlarmRepository
+import com.yapp.domain.repository.FortuneRepository
+import com.yapp.domain.repository.UserInfoRepository
+import com.yapp.domain.tracker.FortuneCreationTracker
import com.yapp.media.sound.SoundPlayer
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -52,7 +55,13 @@ class AlarmService : Service() {
lateinit var androidAlarmScheduler: AndroidAlarmScheduler
@Inject
- lateinit var postFortuneTaskScheduler: PostFortuneTaskScheduler
+ lateinit var fortuneRepository: FortuneRepository
+
+ @Inject
+ lateinit var userInfoRepository: UserInfoRepository
+
+ @Inject
+ lateinit var fortuneCreationTracker: FortuneCreationTracker
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@@ -79,7 +88,7 @@ class AlarmService : Service() {
super.onDestroy()
}
- private fun handleIntent(intent: Intent) {
+ private suspend fun handleIntent(intent: Intent) {
val alarm: Alarm? = intent.getStringExtra(AlarmConstants.EXTRA_ALARM)?.let(Alarm::fromJson)
if (alarm == null) {
@@ -117,7 +126,19 @@ class AlarmService : Service() {
turnOffAlarm(alarmId = notificationId)
}
- postFortuneTaskScheduler.enqueueOnceForToday()
+ val shouldPostFortune = !fortuneRepository.hasTodayFortune()
+ if (shouldPostFortune) {
+ val userId = userInfoRepository.userIdFlow.first()
+ userId?.let {
+ fortuneCreationTracker.start()
+ fortuneRepository.postFortune(userId).onSuccess { fortune ->
+ fortuneCreationTracker.succeed(fortune.id)
+ fortuneRepository.markFortuneAsCreated(fortune.id)
+ }.onFailure {
+ fortuneCreationTracker.fail()
+ }
+ }
+ }
}
private fun shouldNavigateToMission(
diff --git a/core/common/src/main/java/com/yapp/common/di/FortuneTrackerModule.kt b/core/common/src/main/java/com/yapp/common/di/FortuneTrackerModule.kt
new file mode 100644
index 00000000..025180b6
--- /dev/null
+++ b/core/common/src/main/java/com/yapp/common/di/FortuneTrackerModule.kt
@@ -0,0 +1,19 @@
+package com.yapp.common.di
+
+import com.yapp.common.tracker.InMemoryFortuneCreationTracker
+import com.yapp.domain.tracker.FortuneCreationTracker
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class FortuneTrackerModule {
+ @Binds
+ @Singleton
+ abstract fun bindFortuneCreationTracker(
+ impl: InMemoryFortuneCreationTracker,
+ ): FortuneCreationTracker
+}
diff --git a/core/common/src/main/java/com/yapp/common/tracker/InMemoryFortuneCreationTracker.kt b/core/common/src/main/java/com/yapp/common/tracker/InMemoryFortuneCreationTracker.kt
new file mode 100644
index 00000000..49b315bd
--- /dev/null
+++ b/core/common/src/main/java/com/yapp/common/tracker/InMemoryFortuneCreationTracker.kt
@@ -0,0 +1,27 @@
+package com.yapp.common.tracker
+
+import com.yapp.domain.model.FortuneCreationState
+import com.yapp.domain.tracker.FortuneCreationTracker
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class InMemoryFortuneCreationTracker @Inject constructor() : FortuneCreationTracker {
+ private val _state = MutableStateFlow(FortuneCreationState.Start)
+ override val state: StateFlow = _state.asStateFlow()
+
+ override fun start() {
+ _state.value = FortuneCreationState.Start
+ }
+
+ override fun succeed(fortuneId: Long) {
+ _state.value = FortuneCreationState.Success(fortuneId)
+ }
+
+ override fun fail() {
+ _state.value = FortuneCreationState.Failure
+ }
+}
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 fde9d5b7..1079067a 100644
--- a/core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt
+++ b/core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt
@@ -7,15 +7,12 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
-import androidx.datastore.preferences.core.stringPreferencesKey
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.transformLatest
import java.time.Clock
-import java.time.Instant
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@@ -33,20 +30,11 @@ class FortunePreferences @Inject constructor(
val SEEN = booleanPreferencesKey("fortune_seen")
val TOOLTIP_SHOWN = booleanPreferencesKey("fortune_tooltip_shown")
- 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")
- val EXPIRES_AT = longPreferencesKey("fortune_expires_at")
-
val FIRST_ALARM_DISMISSED_TODAY = booleanPreferencesKey("first_alarm_dismissed_today")
val FIRST_ALARM_DISMISSED_DATE_EPOCH = longPreferencesKey("first_alarm_dismissed_date_epoch")
}
private fun todayEpoch(): Long = LocalDate.now(clock).toEpochDay()
- private fun nowMillis(): Long = Instant.now(clock).toEpochMilli()
val fortuneIdFlow: Flow = dataStore.data
.catch { emit(emptyPreferences()) }
@@ -85,59 +73,6 @@ class FortunePreferences @Inject constructor(
}
.distinctUntilChanged()
- @OptIn(ExperimentalCoroutinesApi::class)
- val isFortuneCreatingFlow: Flow = dataStore.data
- .catch { emit(emptyPreferences()) }
- .map { pref ->
- Triple(
- pref[Keys.CREATING] ?: false,
- pref[Keys.EXPIRES_AT] ?: 0L,
- pref[Keys.ATTEMPT_ID],
- )
- }
- .transformLatest { (creating, expiresAt, attemptId) ->
- if (creating) {
- val legacy = (expiresAt <= 0L) || attemptId.isNullOrEmpty()
- val expired = (!legacy && nowMillis() > expiresAt)
-
- if (legacy || expired) {
- // 레거시(만료정보 없음) 또는 만료 → 실패로 교정
- dataStore.edit { pref ->
- pref[Keys.CREATING] = false
- pref[Keys.FAILED] = true
- pref[Keys.FAILED_DATE] = todayEpoch()
- }
- emit(false)
- return@transformLatest
- }
- }
- emit(creating)
- }
- .distinctUntilChanged()
-
- val isFortuneFailedFlow: Flow = dataStore.data
- .catch { emit(emptyPreferences()) }
- .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
.catch { emit(emptyPreferences()) }
.map { pref ->
@@ -147,69 +82,18 @@ class FortunePreferences @Inject constructor(
}
.distinctUntilChanged()
- suspend fun markFortuneCreating(
- attemptId: String,
- lease: Long,
- ) {
- val now = nowMillis()
+ suspend fun markFortuneCreated(fortuneId: Long) {
dataStore.edit { pref ->
- pref[Keys.CREATING] = true
- pref[Keys.ATTEMPT_ID] = attemptId
- pref[Keys.STARTED_AT] = now
- pref[Keys.EXPIRES_AT] = now + lease
- }
- }
+ val today = todayEpoch()
+ val prevDate = pref[Keys.DATE]
+ val isNewForToday = (pref[Keys.ID] != fortuneId) || (prevDate != today)
- suspend fun markFortuneCreatedIfAttemptMatches(
- attemptId: String,
- fortuneId: Long,
- ) {
- dataStore.edit { pref ->
- val currentAttempt = pref[Keys.ATTEMPT_ID]
- val isCreating = pref[Keys.CREATING] ?: false
- val expiresAt = pref[Keys.EXPIRES_AT] ?: 0L
-
- if (isCreating) {
- val legacy = (expiresAt <= 0L) || currentAttempt.isNullOrEmpty()
- val expired = (!legacy && nowMillis() > expiresAt)
-
- if (legacy || expired) {
- // 만료된 상태라면 성공 처리 거부
- return@edit
- }
- }
+ pref[Keys.ID] = fortuneId
+ pref[Keys.DATE] = today
- if (isCreating && currentAttempt == attemptId) {
- val today = todayEpoch()
- val prevDate = pref[Keys.DATE]
- val isNewForToday = (pref[Keys.ID] != fortuneId) || (prevDate != today)
-
- pref[Keys.ID] = fortuneId
- 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)
-
- if (isNewForToday) {
- pref[Keys.SEEN] = false
- pref[Keys.TOOLTIP_SHOWN] = false
- }
- }
- }
- }
-
- suspend fun markFortuneFailedIfAttemptMatches(attemptId: String) {
- dataStore.edit { pref ->
- 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)
+ if (isNewForToday) {
+ pref[Keys.SEEN] = false
+ pref[Keys.TOOLTIP_SHOWN] = false
}
}
}
@@ -245,9 +129,11 @@ class FortunePreferences @Inject constructor(
pref.remove(Keys.SCORE)
pref.remove(Keys.SEEN)
pref.remove(Keys.TOOLTIP_SHOWN)
- pref.remove(Keys.CREATING)
- pref.remove(Keys.FAILED)
- pref.remove(Keys.FAILED_DATE)
}
}
+
+ suspend fun hasTodayFortune(): Boolean {
+ val prefs = dataStore.data.first()
+ return prefs[Keys.ID] != null && prefs[Keys.DATE] == todayEpoch()
+ }
}
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 af669911..a80abb32 100644
--- a/core/datastore/src/test/kotlin/com/yapp/datastore/FortunePreferencesTest.kt
+++ b/core/datastore/src/test/kotlin/com/yapp/datastore/FortunePreferencesTest.kt
@@ -3,19 +3,15 @@ package com.yapp.datastore
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
-import kotlinx.coroutines.flow.first
+import junit.framework.TestCase.assertFalse
import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.time.Clock
import java.time.Instant
-import java.time.LocalDate
import java.time.ZoneOffset
-import java.util.UUID
class FortunePreferencesTest {
@@ -25,15 +21,10 @@ class FortunePreferencesTest {
private val fixedZoneOffsetUtc = ZoneOffset.UTC
private val baseInstantAtT0: Instant = Instant.parse("2025-09-17T00:00:00Z")
- private val baseInstantAtT0Plus2Seconds: Instant = Instant.parse("2025-09-17T00:00:02Z")
+ private val nextDayInstant: Instant = Instant.parse("2025-09-18T00:00:00Z")
private val fixedClockAtT0: Clock = Clock.fixed(baseInstantAtT0, fixedZoneOffsetUtc)
- private val fixedClockAtT0Plus2Seconds: Clock =
- Clock.fixed(baseInstantAtT0Plus2Seconds, fixedZoneOffsetUtc)
-
- private val referenceInstantForAnyDay: Instant = Instant.parse("2025-09-17T00:00:00Z")
- private val fixedClockForReferenceDay: Clock =
- Clock.fixed(referenceInstantForAnyDay, fixedZoneOffsetUtc)
+ private val fixedClockNextDay: Clock = Clock.fixed(nextDayInstant, fixedZoneOffsetUtc)
private fun createNewDataStoreWithFile(fileName: String): DataStore =
PreferenceDataStoreFactory.create {
@@ -46,254 +37,31 @@ class FortunePreferencesTest {
): FortunePreferences = FortunePreferences(dataStore, fixedClock)
@Test
- fun `운세_생성_상태_Creating_만료_시_Failure로_교정된다`() = runTest {
- // given: t0 시점에서 Creating(lease 1초) 설정
- val dataStoreAtT0 = createNewDataStoreWithFile("prefs_expire.preferences_pb")
- val preferencesAtT0 =
- createFortunePreferencesWithClock(dataStoreAtT0, fixedClockAtT0)
-
- val generatedAttemptId = UUID.randomUUID().toString()
- preferencesAtT0.markFortuneCreating(attemptId = generatedAttemptId, lease = 1_000L)
-
- // when: t0 + 2초 경과 후 같은 DataStore를 새로운 Clock으로 읽음
- val preferencesAtT0Plus2Seconds =
- createFortunePreferencesWithClock(dataStoreAtT0, fixedClockAtT0Plus2Seconds)
-
- // then: Creating → false, Failed → true
- val creating = preferencesAtT0Plus2Seconds.isFortuneCreatingFlow.first()
- assertEquals(false, creating)
- val failed = preferencesAtT0Plus2Seconds.isFortuneFailedFlow.first()
- assertEquals(true, failed)
- }
-
- @Test
- fun `만료_정보_없는_운세_생성_상태_Creating은_즉시_Failure로_교정된다`() = runTest {
- // given: 과거 버전 데이터 (Creating=true만 존재)
- val dataStoreWithLegacyCreating = createNewDataStoreWithFile("prefs_legacy.preferences_pb")
- val keyCreatingOnly = booleanPreferencesKey("fortune_creating")
- dataStoreWithLegacyCreating.edit { it[keyCreatingOnly] = true }
-
- val preferencesFromLegacyData =
- createFortunePreferencesWithClock(dataStoreWithLegacyCreating, fixedClockForReferenceDay)
-
- // when: Failure로 교정 로직이 표시된 Flow 구독 시작
-
- // then: 즉시 Creating → false, Failed → true
- val creating = preferencesFromLegacyData.isFortuneCreatingFlow.first()
- assertEquals(false, creating)
- val failed = preferencesFromLegacyData.isFortuneFailedFlow.first()
- assertEquals(true, failed)
- }
-
- @Test
- fun `생성_성공_시_attemptId가_일치할_때만_운세_생성_상태가_Creating에서_Success로_전환된다`() = runTest {
- // given: 운세 Creating 상태
- val dataStore = createNewDataStoreWithFile("prefs_success.preferences_pb")
- val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay)
- val validAttemptId = "ATTEMPT_VALID"
- val invalidAttemptId = "ATTEMPT_INVALID"
- val createdFortuneId = 99L
- preferences.markFortuneCreating(attemptId = validAttemptId, lease = 60_000L)
-
- // when: 잘못된 attemptId로 생성 성공 처리 시도
- preferences.markFortuneCreatedIfAttemptMatches(
- attemptId = invalidAttemptId,
- fortuneId = createdFortuneId
- )
-
- // then: 여전히 Creating 상태 (무시됨)
- val stillCreating = preferences.isFortuneCreatingFlow.first()
- assertEquals(true, stillCreating)
-
- // when: 올바른 attemptId로 생성 성공 처리 시도
- preferences.markFortuneCreatedIfAttemptMatches(
- attemptId = validAttemptId,
- fortuneId = createdFortuneId
- )
-
- // then: Creating → false, Failed → false, fortuneId 및 날짜 설정
- val creatingAfterSuccess = preferences.isFortuneCreatingFlow.first()
- assertEquals(false, creatingAfterSuccess)
- val failedAfterSuccess = preferences.isFortuneFailedFlow.first()
- assertEquals(false, failedAfterSuccess)
- val savedId = preferences.fortuneIdFlow.first()
- assertEquals(createdFortuneId, savedId)
- val savedEpoch = preferences.fortuneDateEpochFlow.first()
- assertEquals(LocalDate.now(fixedClockForReferenceDay).toEpochDay(), savedEpoch)
- }
-
- @Test
- fun `운세_생성_실패_시_attemptId가_일치할_때만_Failure로_전환된다`() = runTest {
- // given: 운세 Creating 상태
- val dataStore = createNewDataStoreWithFile("prefs_fail.preferences_pb")
- val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay)
- val validAttemptId = "ATTEMPT_VALID"
- val invalidAttemptId = "ATTEMPT_INVALID"
- preferences.markFortuneCreating(attemptId = validAttemptId, lease = 60_000L)
-
- // when: 잘못된 attemptId로 실패 처리 시도
- preferences.markFortuneFailedIfAttemptMatches(invalidAttemptId)
-
- // then: 아직 Creating 상태 (무시됨)
- val stillCreating = preferences.isFortuneCreatingFlow.first()
- assertEquals(true, stillCreating)
-
- // when: 올바른 attemptId로 실패 처리 시도
- preferences.markFortuneFailedIfAttemptMatches(validAttemptId)
-
- // then: Creating → false, Failed → true
- val creatingAfterFail = preferences.isFortuneCreatingFlow.first()
- assertEquals(false, creatingAfterFail)
- val failed = preferences.isFortuneFailedFlow.first()
- 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초) 설정
- val dataStore = createNewDataStoreWithFile("prefs_expired_success_guard.preferences_pb")
- val prefsAtT0 = createFortunePreferencesWithClock(dataStore, fixedClockAtT0)
-
- val attemptId = UUID.randomUUID().toString()
- val fortuneId = 999L
- prefsAtT0.markFortuneCreating(attemptId = attemptId, lease = 1_000L)
-
- // when: t0+2초(만료 이후)로 시계를 바꾸고, 같은 DataStore로 성공 처리 시도
- val prefsAtT0Plus2 = createFortunePreferencesWithClock(dataStore, fixedClockAtT0Plus2Seconds)
- // 만료된 상태이므로, 아래 호출은 내부에서 return@edit 되어 성공 반영이 되면 안 된다.
- prefsAtT0Plus2.markFortuneCreatedIfAttemptMatches(
- attemptId = attemptId,
- fortuneId = fortuneId
- )
-
- // then: 성공 반영이 거부되었으므로 fortuneId는 여전히 null이어야 한다
- val savedId = prefsAtT0Plus2.fortuneIdFlow.first()
- assertEquals(null, savedId)
-
- // 그리고 isFortuneCreatingFlow 구독 시 만료 교정 로직이 작동하여
- // CREATING → false, FAILED → true 로 자동 교정되어야 한다.
- val creatingAfter = prefsAtT0Plus2.isFortuneCreatingFlow.first()
- assertEquals(false, creatingAfter)
-
- val failedAfter = prefsAtT0Plus2.isFortuneFailedFlow.first()
- assertEquals(true, failedAfter)
- }
-
- @Test
- fun `오늘_운세가_있고_확인한_경우_hasUnseenFortune가_false`() = runTest {
- // given: 오늘 운세가 생성되어 있고(미확인)
- val dataStore = createNewDataStoreWithFile("prefs_seen.preferences_pb")
- val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay)
- val attemptId = "ATTEMPT_FOR_SEEN"
- val fortuneId = 777L
- preferences.markFortuneCreating(attemptId = attemptId, lease = 60_000L)
- preferences.markFortuneCreatedIfAttemptMatches(attemptId = attemptId, fortuneId = fortuneId)
-
- // when: 사용자가 오늘 운세를 확인
- preferences.markFortuneSeen()
-
- // then: hasUnseenFortune → false
- val unseen = preferences.hasUnseenFortuneFlow.first()
- assertEquals(false, unseen)
- }
-
- @Test
- fun `오늘_운세가_있고_아직_확인하지_않은_경우_hasUnseenFortune가_true`() = runTest {
- // given: 오늘 운세가 생성되어 있는 상태(미확인)
- val dataStore = createNewDataStoreWithFile("prefs_unseen.preferences_pb")
- val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay)
- val attemptId = "ATTEMPT_FOR_UNSEEN"
- val fortuneId = 123L
- preferences.markFortuneCreating(attemptId = attemptId, lease = 60_000L)
- preferences.markFortuneCreatedIfAttemptMatches(attemptId = attemptId, fortuneId = fortuneId)
+ fun `오늘_운세를_생성했다면_hasTodayFortune이_참이다`() = runTest {
+ // given
+ val dataStore = createNewDataStoreWithFile("today.preferences_pb")
+ val preferences = createFortunePreferencesWithClock(dataStore, fixedClockAtT0)
- // when: hasUnseenFortuneFlow 구독
+ // when
+ preferences.markFortuneCreated(fortuneId = 1L)
- // then: 오늘 운세 존재 + 아직 읽지 않음 = hasUnseenFortune → true
- val unseen = preferences.hasUnseenFortuneFlow.first()
- assertEquals(true, unseen)
+ // then
+ val result = preferences.hasTodayFortune()
+ assertTrue(result)
}
@Test
- fun `오늘_운세가_있고_Tooltip을_보여주었다면_shouldShowFortuneToolTip이_false`() = runTest {
- // given: 오늘 운세가 생성되어 있는 상태(툴팁 미표시)
- val dataStore = createNewDataStoreWithFile("prefs_tooltip_true.preferences_pb")
- val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay)
- val attemptId = "ATTEMPT_FOR_TOOLTIP_TRUE"
- val fortuneId = 888L
- preferences.markFortuneCreating(attemptId = attemptId, lease = 60_000L)
- preferences.markFortuneCreatedIfAttemptMatches(attemptId = attemptId, fortuneId = fortuneId)
-
- // when: ToolTip을 보여줌
- preferences.markFortuneTooltipShown()
-
- // then: shouldShowFortuneToolTip → false
- val showTooltip = preferences.shouldShowFortuneToolTipFlow.first()
- assertEquals(false, showTooltip)
- }
-
- @Test
- fun `오늘_운세가_있고_Tooltip을_아직_보여주지_않았다면_shouldShowFortuneToolTip이_true`() = runTest {
- // given: 오늘 운세가 생성되어 있는 상태(툴팁 미표시)
- val dataStore = createNewDataStoreWithFile("prefs_tooltip.preferences_pb")
- val preferences = createFortunePreferencesWithClock(dataStore, fixedClockForReferenceDay)
- val attemptId = "ATTEMPT_FOR_TOOLTIP"
- val fortuneId = 456L
- preferences.markFortuneCreating(attemptId = attemptId, lease = 60_000L)
- preferences.markFortuneCreatedIfAttemptMatches(attemptId = attemptId, fortuneId = fortuneId)
-
- // when: shouldShowFortuneToolTipFlow 구독
-
- // then: 오늘 운세 존재 + 툴팁 미표시 = shouldShowFortuneToolTip → true
- val showTooltip = preferences.shouldShowFortuneToolTipFlow.first()
- assertEquals(true, showTooltip)
+ fun `운세_생성일과_현재_날짜가_다르면_hasTodayFortune이_거짓이다`() = runTest {
+ // given
+ val dataStore = createNewDataStoreWithFile("yesterday.preferences_pb")
+ val preferencesAtT0 = createFortunePreferencesWithClock(dataStore, fixedClockAtT0)
+ val preferencesNextDay = createFortunePreferencesWithClock(dataStore, fixedClockNextDay)
+
+ // when
+ preferencesAtT0.markFortuneCreated(fortuneId = 1L)
+
+ // then
+ val result = preferencesNextDay.hasTodayFortune()
+ assertFalse(result)
}
}
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 2cd0f60d..693176a3 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,6 +1,5 @@
package com.yapp.data.local.datasource
-import com.yapp.domain.model.FortuneCreateStatus
import kotlinx.coroutines.flow.Flow
interface FortuneLocalDataSource {
@@ -12,11 +11,7 @@ interface FortuneLocalDataSource {
val shouldShowFortuneToolTipFlow: Flow
val isFirstAlarmDismissedTodayFlow: Flow
- val fortuneCreateStatusFlow: Flow
-
- suspend fun markFortuneCreating(attemptId: String, leaseMillis: Long)
- suspend fun markFortuneCreated(attemptId: String, fortuneId: Long)
- suspend fun markFortuneFailed(attemptId: String)
+ suspend fun markFortuneCreated(fortuneId: Long)
suspend fun markFortuneSeen()
suspend fun markFortuneTooltipShown()
suspend fun saveFortuneImageId(imageResId: Int)
@@ -24,4 +19,6 @@ interface FortuneLocalDataSource {
suspend fun markFirstAlarmDismissedToday()
suspend fun clearFortuneData()
+
+ suspend fun hasTodayFortune(): Boolean
}
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 a3178e11..1066166c 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,10 +1,6 @@
package com.yapp.data.local.datasource
import com.yapp.datastore.FortunePreferences
-import com.yapp.domain.model.FortuneCreateStatus
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import java.time.LocalDate
import javax.inject.Inject
class FortuneLocalDataSourceImpl @Inject constructor(
@@ -19,32 +15,8 @@ class FortuneLocalDataSourceImpl @Inject constructor(
override val shouldShowFortuneToolTipFlow = fortunePreferences.shouldShowFortuneToolTipFlow
override val isFirstAlarmDismissedTodayFlow = fortunePreferences.isFirstAlarmDismissedTodayFlow
- override val fortuneCreateStatusFlow = combine(
- fortunePreferences.fortuneIdFlow,
- fortunePreferences.fortuneDateEpochFlow,
- fortunePreferences.isFortuneCreatingFlow,
- fortunePreferences.isFortuneFailedFlow,
- ) { fortuneId, fortuneDate, isCreating, isFailed ->
- when {
- isFailed -> FortuneCreateStatus.Failure
- isCreating -> FortuneCreateStatus.Creating
- fortuneId != null && fortuneDate == todayEpoch() -> FortuneCreateStatus.Success(fortuneId)
- else -> FortuneCreateStatus.Idle
- }
- }.distinctUntilChanged()
-
- private fun todayEpoch(): Long = LocalDate.now().toEpochDay()
-
- override suspend fun markFortuneCreating(attemptId: String, leaseMillis: Long) {
- fortunePreferences.markFortuneCreating(attemptId, leaseMillis)
- }
-
- override suspend fun markFortuneCreated(attemptId: String, fortuneId: Long) {
- fortunePreferences.markFortuneCreatedIfAttemptMatches(attemptId, fortuneId)
- }
-
- override suspend fun markFortuneFailed(attemptId: String) {
- fortunePreferences.markFortuneFailedIfAttemptMatches(attemptId)
+ override suspend fun markFortuneCreated(fortuneId: Long) {
+ fortunePreferences.markFortuneCreated(fortuneId)
}
override suspend fun markFortuneSeen() {
@@ -70,4 +42,8 @@ class FortuneLocalDataSourceImpl @Inject constructor(
override suspend fun clearFortuneData() {
fortunePreferences.clearFortuneData()
}
+
+ override suspend fun hasTodayFortune(): Boolean {
+ return fortunePreferences.hasTodayFortune()
+ }
}
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 8849da81..b4271611 100644
--- a/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt
+++ b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt
@@ -4,7 +4,6 @@ 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
@@ -22,11 +21,7 @@ class FortuneRepositoryImpl @Inject constructor(
override val shouldShowFortuneToolTipFlow: Flow = fortuneLocalDataSource.shouldShowFortuneToolTipFlow
override val isFirstAlarmDismissedTodayFlow: Flow = fortuneLocalDataSource.isFirstAlarmDismissedTodayFlow
- override val fortuneCreateStatusFlow: Flow = fortuneLocalDataSource.fortuneCreateStatusFlow
-
- override suspend fun markFortuneAsCreating(attemptId: String, leaseMillis: Long) = fortuneLocalDataSource.markFortuneCreating(attemptId, leaseMillis)
- override suspend fun markFortuneAsCreated(attemptId: String, fortuneId: Long) = fortuneLocalDataSource.markFortuneCreated(attemptId, fortuneId)
- override suspend fun markFortuneAsFailed(attemptId: String) = fortuneLocalDataSource.markFortuneFailed(attemptId)
+ override suspend fun markFortuneAsCreated(fortuneId: Long) = fortuneLocalDataSource.markFortuneCreated(fortuneId)
override suspend fun markFortuneSeen() = fortuneLocalDataSource.markFortuneSeen()
override suspend fun markFortuneTooltipShown() = fortuneLocalDataSource.markFortuneTooltipShown()
override suspend fun saveFortuneImageId(imageResId: Int) = fortuneLocalDataSource.saveFortuneImageId(imageResId)
@@ -35,6 +30,8 @@ class FortuneRepositoryImpl @Inject constructor(
override suspend fun clearFortuneData() = fortuneLocalDataSource.clearFortuneData()
+ override suspend fun hasTodayFortune() = fortuneLocalDataSource.hasTodayFortune()
+
override suspend fun postFortune(userId: Long): Result {
return fortuneRemoteDataSource.postFortune(userId)
.mapCatching { it.toDomain() }
diff --git a/domain/src/main/java/com/yapp/domain/model/FortuneCreateStatus.kt b/domain/src/main/java/com/yapp/domain/model/FortuneCreateStatus.kt
deleted file mode 100644
index 27ae9ad0..00000000
--- a/domain/src/main/java/com/yapp/domain/model/FortuneCreateStatus.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-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/model/FortuneCreationState.kt b/domain/src/main/java/com/yapp/domain/model/FortuneCreationState.kt
new file mode 100644
index 00000000..bf53a30b
--- /dev/null
+++ b/domain/src/main/java/com/yapp/domain/model/FortuneCreationState.kt
@@ -0,0 +1,7 @@
+package com.yapp.domain.model
+
+sealed class FortuneCreationState {
+ data object Start : FortuneCreationState()
+ data class Success(val fortuneId: Long) : FortuneCreationState()
+ data object Failure : FortuneCreationState()
+}
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 7edc5396..937556d8 100644
--- a/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt
+++ b/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt
@@ -1,7 +1,6 @@
package com.yapp.domain.repository
import com.yapp.domain.model.Fortune
-import com.yapp.domain.model.FortuneCreateStatus
import kotlinx.coroutines.flow.Flow
interface FortuneRepository {
@@ -13,11 +12,7 @@ interface FortuneRepository {
val shouldShowFortuneToolTipFlow: Flow
val isFirstAlarmDismissedTodayFlow: Flow
- val fortuneCreateStatusFlow: Flow
-
- suspend fun markFortuneAsCreating(attemptId: String, leaseMillis: Long = 2 * 60_000L)
- suspend fun markFortuneAsCreated(attemptId: String, fortuneId: Long)
- suspend fun markFortuneAsFailed(attemptId: String)
+ suspend fun markFortuneAsCreated(fortuneId: Long)
suspend fun markFortuneSeen()
suspend fun markFortuneTooltipShown()
suspend fun saveFortuneImageId(imageResId: Int)
@@ -26,6 +21,8 @@ interface FortuneRepository {
suspend fun clearFortuneData()
+ suspend fun hasTodayFortune(): Boolean
+
suspend fun postFortune(userId: Long): Result
suspend fun getFortune(fortuneId: Long): Result
}
diff --git a/domain/src/main/java/com/yapp/domain/tracker/FortuneCreationTracker.kt b/domain/src/main/java/com/yapp/domain/tracker/FortuneCreationTracker.kt
new file mode 100644
index 00000000..72708316
--- /dev/null
+++ b/domain/src/main/java/com/yapp/domain/tracker/FortuneCreationTracker.kt
@@ -0,0 +1,11 @@
+package com.yapp.domain.tracker
+
+import com.yapp.domain.model.FortuneCreationState
+import kotlinx.coroutines.flow.StateFlow
+
+interface FortuneCreationTracker {
+ val state: StateFlow
+ fun start()
+ fun succeed(fortuneId: Long)
+ fun fail()
+}
diff --git a/feature/fortune/build.gradle.kts b/feature/fortune/build.gradle.kts
index 543510e8..b161f68f 100644
--- a/feature/fortune/build.gradle.kts
+++ b/feature/fortune/build.gradle.kts
@@ -17,9 +17,6 @@ dependencies {
implementation(libs.orbit.compose)
implementation(libs.orbit.viewmodel)
implementation(libs.coil.compose)
- implementation(libs.androidx.work.runtime)
- testImplementation(libs.androidx.work.testing)
- androidTestImplementation(libs.androidx.work.testing)
implementation(projects.domain)
implementation(projects.core.media)
}
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 0bc19414..c267c811 100644
--- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt
+++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt
@@ -4,8 +4,9 @@ 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.model.FortuneCreationState
import com.yapp.domain.repository.FortuneRepository
+import com.yapp.domain.tracker.FortuneCreationTracker
import com.yapp.fortune.page.toFortunePages
import com.yapp.media.decoder.ImageUtils
import com.yapp.media.storage.ImageSaver
@@ -25,6 +26,7 @@ class FortuneViewModel @Inject constructor(
private val application: Application,
private val fortuneRepository: FortuneRepository,
private val imageSaver: ImageSaver,
+ private val fortuneCreationTracker: FortuneCreationTracker,
) : ViewModel(), ContainerHost {
override val container: Container = container(
@@ -51,20 +53,20 @@ class FortuneViewModel @Inject constructor(
}
private fun observeFortune() = intent {
- fortuneRepository.fortuneCreateStatusFlow.collect { status ->
+ fortuneCreationTracker.state.collect { status ->
when (status) {
- is FortuneCreateStatus.Creating -> {
+ is FortuneCreationState.Start -> {
reduce { state.copy(isLoading = true) }
}
- is FortuneCreateStatus.Success -> {
+ is FortuneCreationState.Success -> {
fetchAndUpdateFortune(
fortuneId = status.fortuneId,
isFirstAlarmDismissedToday = fortuneRepository.isFirstAlarmDismissedTodayFlow.first(),
)
}
- is FortuneCreateStatus.Failure -> {
+ FortuneCreationState.Failure -> {
reduce {
state.copy(
isLoading = false,
@@ -72,12 +74,6 @@ class FortuneViewModel @Inject constructor(
)
}
}
-
- is FortuneCreateStatus.Idle -> {
- if (!state.isCreateFailureDialogVisible) {
- postSideEffect(FortuneContract.SideEffect.NavigateToHome)
- }
- }
}
}
}
diff --git a/feature/fortune/src/main/java/com/yapp/fortune/di/SchedulerModule.kt b/feature/fortune/src/main/java/com/yapp/fortune/di/SchedulerModule.kt
deleted file mode 100644
index 47fbd0b0..00000000
--- a/feature/fortune/src/main/java/com/yapp/fortune/di/SchedulerModule.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.yapp.fortune.di
-
-import com.yapp.alarm.scheduler.PostFortuneTaskScheduler
-import com.yapp.fortune.scheduler.WorkManagerPostFortuneTaskScheduler
-import dagger.Binds
-import dagger.Module
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
-
-@Module
-@InstallIn(SingletonComponent::class)
-abstract class SchedulerModule {
- @Binds
- @Singleton
- abstract fun bindsPostFortuneTaskScheduler(
- postFortuneTaskScheduler: WorkManagerPostFortuneTaskScheduler,
- ): PostFortuneTaskScheduler
-}
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
deleted file mode 100644
index 2aadbe87..00000000
--- a/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.yapp.fortune.scheduler
-
-import android.content.Context
-import androidx.work.BackoffPolicy
-import androidx.work.ExistingWorkPolicy
-import androidx.work.OneTimeWorkRequestBuilder
-import androidx.work.WorkManager
-import com.yapp.alarm.scheduler.PostFortuneTaskScheduler
-import com.yapp.fortune.worker.PostFortuneWorker
-import dagger.hilt.android.qualifiers.ApplicationContext
-import java.time.LocalDate
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-
-class WorkManagerPostFortuneTaskScheduler @Inject constructor(
- @ApplicationContext private val context: Context,
-) : PostFortuneTaskScheduler {
- override fun enqueueOnceForToday() {
- val name = "post_fortune_${LocalDate.now()}"
- val req = OneTimeWorkRequestBuilder()
- .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.SECONDS)
- .build()
- WorkManager.getInstance(context)
- .enqueueUniqueWork(name, ExistingWorkPolicy.KEEP, req)
- }
-}
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
deleted file mode 100644
index 45f31753..00000000
--- a/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.yapp.fortune.worker
-
-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
-import dagger.assisted.AssistedInject
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.firstOrNull
-import java.util.UUID
-
-@HiltWorker
-class PostFortuneWorker @AssistedInject constructor(
- @Assisted appContext: Context,
- @Assisted params: WorkerParameters,
- private val fortuneRepository: FortuneRepository,
- private val userInfoRepository: UserInfoRepository,
-) : CoroutineWorker(appContext, params) {
-
- override suspend fun doWork(): Result {
- // 이미 진행 중이거나(다른 워커) 오늘 운세가 성공 상태면 중복 실행 방지
- when (fortuneRepository.fortuneCreateStatusFlow.first()) {
- is FortuneCreateStatus.Creating,
- is FortuneCreateStatus.Success,
- -> return Result.success()
- is FortuneCreateStatus.Failure -> return Result.failure()
- FortuneCreateStatus.Idle -> { /* 계속 진행 */ }
- }
-
- val userId = userInfoRepository.userIdFlow.firstOrNull()
- ?: run {
- return Result.failure()
- }
-
- val attemptId = UUID.randomUUID().toString()
-
- return try {
- fortuneRepository.markFortuneAsCreating(attemptId)
-
- val result = fortuneRepository.postFortune(userId)
-
- result.fold(
- onSuccess = { fortune ->
- fortuneRepository.markFortuneAsCreated(attemptId, fortune.id)
- fortuneRepository.saveFortuneScore(fortune.avgFortuneScore)
- Result.success()
- },
- onFailure = {
- fortuneRepository.markFortuneAsFailed(attemptId)
- Result.retry()
- },
- )
- } catch (ce: CancellationException) {
- fortuneRepository.markFortuneAsFailed(attemptId)
- throw ce
- } catch (_: Throwable) {
- fortuneRepository.markFortuneAsFailed(attemptId)
- 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 f88d9321..d4a4c364 100644
--- a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt
+++ b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt
@@ -6,7 +6,6 @@ import androidx.lifecycle.ViewModel
import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissIntent
import com.yapp.analytics.AnalyticsEvent
import com.yapp.analytics.AnalyticsHelper
-import com.yapp.domain.model.FortuneCreateStatus
import com.yapp.domain.model.MissionMode
import com.yapp.domain.model.MissionType
import com.yapp.domain.repository.FortuneRepository
@@ -139,16 +138,10 @@ class MissionViewModel @Inject constructor(
return@intent
}
- val fortuneCreateStatus = fortuneRepository.fortuneCreateStatusFlow.first()
+ val hasTodayFortune = fortuneRepository.hasTodayFortune()
val hasUnseenFortune = fortuneRepository.hasUnseenFortuneFlow.first()
- val shouldOpenFortune = when (fortuneCreateStatus) {
- is FortuneCreateStatus.Creating,
- is FortuneCreateStatus.Failure,
- -> true
- is FortuneCreateStatus.Success -> hasUnseenFortune
- FortuneCreateStatus.Idle -> false
- }
+ val shouldOpenFortune = !hasTodayFortune || hasUnseenFortune
postSideEffect(
if (shouldOpenFortune) {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ea5df3af..2afbecdc 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -29,23 +29,21 @@ androidx-app-compat = "1.7.0"
androidx-core = "1.15.0"
androidx-datastore = "1.1.1"
androidx-room = "2.7.2"
-androidx-work = "2.10.3"
androidx-lifecycle = "2.9.4"
annotation = "1.9.1"
## Compose
-compose-bom = "2024.11.00"
-compose-navigation = "2.8.4"
-compose-material3 = "1.3.1"
-compose-ui = "1.7.6"
+compose-bom = "2025.02.00"
+compose-navigation = "2.8.5"
+compose-material3 = "1.4.0"
+compose-ui = "1.8.3"
activity-compose = "1.9.3"
## Hilt
hilt = "2.57.2"
hilt-navigation-compose = "1.3.0"
-hilt-work = "1.3.0"
## Third Party
okhttp = "4.12.0"
@@ -109,9 +107,6 @@ androidx-room-compiler = { group = "androidx.room", name = "room-compiler", vers
androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "androidx-room" }
androidx-room-paging = { group = "androidx.room", name = "room-paging", version.ref = "androidx-room" }
androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
-androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hilt-work" }
-androidx-work-runtime = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidx-work" }
-androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidx-work" }
## Compose Libraries
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
@@ -139,7 +134,6 @@ hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
-hilt-worker = { group = "androidx.hilt", name = "hilt-work", version.ref = "hilt-work" }
# Orbit
orbit-core = { group = "org.orbit-mvi", name = "orbit-core", version.ref = "orbit" }