-
Notifications
You must be signed in to change notification settings - Fork 1
[BUGFIX] 운세 생성 상태에 만료 기한 추가하여 유령 Creating 문제 해결 #257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughFortune 관련 상태를 UserPreferences에서 분리해 새로운 FortunePreferences로 이동하고, 운세 생성에 attemptId와 만료(leaseMillis)를 도입해 per-attempt 상태 추적 및 만료 기반 자동 교정 로직을 추가했습니다. LocalDataSource, Repository, Worker의 관련 메서드 시그니처가 attemptId/leaseMillis를 받도록 변경되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor System
participant Worker as PostFortuneWorker
participant Repo as FortuneRepository
participant LDS as FortuneLocalDataSource
participant Pref as FortunePreferences(DataStore)
System->>Worker: run()
Worker->>Worker: attemptId = UUID.randomUUID()
Worker->>Repo: markFortuneAsCreating(attemptId, leaseMillis)
Repo->>LDS: markFortuneCreating(attemptId, leaseMillis)
LDS->>Pref: set CREATING=true, ATTEMPT_ID, STARTED_AT, EXPIRES_AT
alt 성공
Worker->>Repo: markFortuneAsCreated(attemptId, fortuneId)
Repo->>LDS: markFortuneCreated(attemptId, fortuneId)
LDS->>Pref: update ID/DATE, CREATING=false, FAILED=false, reset SEEN/TOOLTIP(오늘)
Worker->>Pref: saveFortuneScore(score)
Worker-->>System: Result.success
else 실패/예외
Worker->>Repo: markFortuneAsFailed(attemptId)
Repo->>LDS: markFortuneFailed(attemptId)
LDS->>Pref: set FAILED=true, CREATING=false (attempt 일치 시)
Worker-->>System: Result.retry 또는 Result.failure
end
sequenceDiagram
participant Pref as FortunePreferences
participant UI as UI/VM
note over Pref,UI: Flow 정책: catch -> emptyPreferences(), distinctUntilChanged()\nisFortuneCreatingFlow: EXPIRES_AT < now -> 자동으로 Failure로 교정
Pref-->>UI: fortuneIdFlow, fortuneDateEpochFlow, isFortuneCreatingFlow, isFortuneFailedFlow, hasUnseenFortuneFlow, shouldShowFortuneToolTipFlow
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is ❌ Your project status has failed because the head coverage (5.85%) is below the target coverage (60.00%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## develop #257 +/- ##
============================================
+ Coverage 4.06% 5.85% +1.79%
- Complexity 53 67 +14
============================================
Files 51 53 +2
Lines 4670 4847 +177
Branches 689 706 +17
============================================
+ Hits 190 284 +94
- Misses 4470 4537 +67
- Partials 10 26 +16
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (10)
data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt (1)
17-19: 시그니처 변경 방향 적절 + 계약(Contract) 명확화 제안attemptId/leaseMillis 도입은 문제 정의에 정확히 부합합니다. 향후 혼선을 막기 위해 아래를 인터페이스 KDoc에 명시해 주세요: 단위(ms), 유효 범위(>0), 시계 기준(벽시계 vs 단조 시계), idempotency(동일 attemptId 재호출 시 기대 동작).
interface FortuneLocalDataSource { @@ - suspend fun markFortuneCreating(attemptId: String, leaseMillis: Long) + /** + * 운세 생성 시도를 Creating으로 표시합니다. + * @param attemptId 시도 식별자(비어있지 않아야 함). 동일 id 재호출 시 idempotent 동작을 기대. + * @param leaseMillis ms 단위 임대기간(>0). 임대 만료 시 Creating은 자동 교정 대상이 됩니다. + * 시계 기준: System.currentTimeMillis() 기반인지(벽시계 변동 영향) 명시 필요. + */ + suspend fun markFortuneCreating(attemptId: String, leaseMillis: Long) @@ - suspend fun markFortuneCreated(attemptId: String, fortuneId: Long) + /** attemptId가 일치할 때에만 Created 전이 */ + suspend fun markFortuneCreated(attemptId: String, fortuneId: Long) @@ - suspend fun markFortuneFailed(attemptId: String) + /** attemptId가 일치할 때에만 Failed 전이 */ + suspend fun markFortuneFailed(attemptId: String)feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt (2)
35-38: userId 없음 시 Result.failure() 재검토이 워커가 PeriodicWork이면 다음 주기엔 재시도되니 괜찮지만, OneTimeWork라면 로그인 후 자동 재시도가 안 됩니다. 의도대로라면
Result.retry()가 더 맞을 수 있습니다. 스케줄링 정책(Unique/Periodic/OneTime)을 확인해 주세요.
53-57: 재시도 조건 세분화 제안모든 실패를
Result.retry()로 취급하면 영구 재시도 루프가 날 수 있습니다. HTTP 4xx/도메인 불변 위반 등 비가역 오류는Result.failure()로 분기하는 에러 매핑을 고려해 주세요(예: RemoteDataSource에서 도메인 예외로 변환).data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt (1)
22-35: 상태 결합 로직 합리적 + 오늘 판정 시계 주입 고려
- Failure > Creating > Success > Idle 우선순위는 명확합니다. LGTM.
todayEpoch()가LocalDate.now()(시스템 타임존/벽시계)에 의존합니다. 테스트 용이성과 시간 변경(사용자 수동 변경/서머타임) 영향 완화를 위해Clock주입 또는 래핑을 권장합니다.-import java.time.LocalDate +import java.time.Clock +import java.time.LocalDate @@ -class FortuneLocalDataSourceImpl @Inject constructor( - private val fortunePreferences: FortunePreferences, -) : FortuneLocalDataSource { +class FortuneLocalDataSourceImpl @Inject constructor( + private val fortunePreferences: FortunePreferences, + private val clock: Clock, +) : FortuneLocalDataSource { @@ - private fun todayEpoch(): Long = LocalDate.now().toEpochDay() + private fun todayEpoch(): Long = LocalDate.now(clock).toEpochDay()DI에
Clock.systemDefaultZone()바인딩만 추가하면 됩니다.domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt (1)
18-20: 확인: 만료 후에도 ATTEMPT_ID 검사로 created/failed가 정상 반영됩니다 — Duration 사용 권장FortunePreferences의 markFortuneCreatedIfAttemptMatches / markFortuneFailedIfAttemptMatches는 ATTEMPT_ID만 비교하므로 만료된 이후에도(또는 isFortuneCreatingFlow가 만료를 교정한 이후에도) 서버 응답이 정상 반영됩니다. isFortuneCreatingFlow는 만료 시 CREATING=false·FAILED=true로 교정하지만 ATTEMPT_ID는 삭제하지 않습니다. (파일: core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt — isFortuneCreatingFlow, markFortuneCreatedIfAttemptMatches, markFortuneFailedIfAttemptMatches)
선택적 권장: API 가독성/오용 방지를 위해 leaseMillis: Long → kotlin.time.Duration 사용 검토.
-import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.Flow +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes @@ - suspend fun markFortuneAsCreating(attemptId: String, leaseMillis: Long = 2 * 60_000L) + suspend fun markFortuneAsCreating(attemptId: String, lease: Duration = 2.minutes)core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt (5)
102-109: 만료/레거시 교정 시에도 시도 메타데이터 정리교정 후 ATTEMPT_ID/STARTED_AT/EXPIRES_AT가 남아 후속 로직을 혼동시킬 수 있습니다.
if (legacy || expired) { // 레거시(만료정보 없음) 또는 만료 → 실패로 교정 dataStore.edit { pref -> pref[Keys.CREATING] = false pref[Keys.FAILED] = true + // 교정 종료 → 시도 메타데이터 정리 + pref.remove(Keys.ATTEMPT_ID) + pref.remove(Keys.STARTED_AT) + pref.remove(Keys.EXPIRES_AT) } emit(false) return@transformLatest }
200-210: clearFortuneData가 시도 메타데이터를 남김사용자 데이터 초기화 시 시도 메타데이터도 함께 제거하는 편이 직관적입니다.
suspend fun clearFortuneData() { dataStore.edit { pref -> pref.remove(Keys.ID) pref.remove(Keys.DATE) pref.remove(Keys.IMAGE_ID) pref.remove(Keys.SCORE) pref.remove(Keys.SEEN) pref.remove(Keys.TOOLTIP_SHOWN) pref.remove(Keys.CREATING) pref.remove(Keys.FAILED) + // 시도 메타데이터도 정리 + pref.remove(Keys.ATTEMPT_ID) + pref.remove(Keys.STARTED_AT) + pref.remove(Keys.EXPIRES_AT) } }
50-86: DataStore 예외를 전부 삼키는 패턴: 최소한의 로깅 고려
catch { emit(emptyPreferences()) }는 안전하지만, 반복되는 오류를 파악하기 어렵습니다. 공용 연산자(예:private fun <T> Flow<Preferences>.safeMap(...))로 감싸며 로깅(디버그/에러 레벨) 추가를 고려하세요.
47-49: todayEpoch()/nowMillis() 중복 유틸 통일 제안다른 모듈에도 동일 유틸이 존재합니다(
FortuneLocalDataSourceImpl.kt,UserPreferences.kt). 공용 유틸로 추출해 시계 주입 일관성을 높이세요.
130-141: lease 파라미터를 leaseMillis로 이름 통일 — 네임드 인자 충돌 예방data 모듈의 인터페이스/구현부가 leaseMillis를 사용합니다. 저장소(FortunePreferences.kt)의 파라미터명을 일치시켜 주세요 — repo에서 명시적 네임드 호출(leaseMillis=...)은 발견되지 않았지만 일관성 유지 권장.
- suspend fun markFortuneCreating( - attemptId: String, - lease: Long, - ) { + suspend fun markFortuneCreating( + attemptId: String, + leaseMillis: Long, + ) { 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 + pref[Keys.EXPIRES_AT] = now + leaseMillis } }대안(선택): java.time.Duration 사용으로 단위 혼동 방지.
참고 파일: data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt, data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt, core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt(1 hunks)core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt(0 hunks)data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt(1 hunks)data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt(2 hunks)data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt(1 hunks)domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt(1 hunks)feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt(2 hunks)
💤 Files with no reviewable changes (1)
- core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-14T15:32:44.064Z
Learnt from: DongChyeon
PR: YAPP-Github/Orbit-Android#252
File: domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt:16-17
Timestamp: 2025-09-14T15:32:44.064Z
Learning: Fortune creation requests have at least a 1-minute gap between them in the Orbit Android app, making atomic guards for race condition prevention unnecessary in the fortune creation flow.
Applied to files:
data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.ktdomain/src/main/java/com/yapp/domain/repository/FortuneRepository.ktdata/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt
🧬 Code graph analysis (1)
core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt (2)
data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt (1)
todayEpoch(36-36)core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt (1)
todayEpoch(31-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (4)
data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt (1)
27-29: 위임 구현 깔끔합니다시그니처 변경에 맞춘 단순 위임으로 일관성 좋습니다.
feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt (1)
25-33: 초기 가드 OK, 단 “Creating 만료 교정”과의 레이스만 확인
Creating/Success면 조기 종료하는 정책은 타당합니다. 다만 isFortuneCreatingFlow가 만료 교정을 즉시 반영하지 못하는 타이밍에서는 조기 종료로 인해 시도가 생략될 수 있으니, 교정이 플로우에 즉시 반영됨을 확인해 주세요.data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt (2)
14-21: 위임 플로우 교체 LGTMUserPreferences → FortunePreferences 전환과 각 Flow 위임은 일관되고 변경 목적에 부합합니다.
38-48: 만료(TTL) 후 지연 응답 수용 여부 확인 — 현재 ATTEMPT_ID 일치만으로 상태 전이됨FortunePreferences의 markFortuneCreatedIfAttemptMatches / markFortuneFailedIfAttemptMatches는 pref[Keys.ATTEMPT_ID] == attemptId만 검사합니다. isFortuneCreatingFlow가 만료 시 CREATING=false, FAILED=true로 교정하지만 ATTEMPT_ID는 제거하지 않으므로, TTL 경과 후 성공 응답이 동일한 attemptId로 오면 실패로 교정된 상태가 성공으로 덮어씌워집니다.
위치: core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt — markFortuneCreatedIfAttemptMatches / markFortuneFailedIfAttemptMatches / isFortuneCreatingFlow
권장 조치(의도에 따라 하나 선택): 1) 만료 교정 시 ATTEMPT_ID 제거, 2) markFortuneCreatedIfAttemptMatches에 CREATING==true 또는 expiresAt 검사 추가, 3) 현재 동작이 의도라면 주석/테스트로 의도 명시.
feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (3)
core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt (2)
159-161: 이전 피드백 반영: 성공 시 시도 메타데이터 정리 확인성공 완료 후 ATTEMPT_ID/STARTED_AT/EXPIRES_AT 제거가 반영되었습니다. 굿.
171-179: 실패 마킹도 CREATING 상태에서만 진행해야 함성공 후 지연 실패 신호가 덮어쓰지 않도록 CREATING==true 가드가 필요합니다. (이전 코멘트 연장선)
적용 패치:
- if (pref[Keys.ATTEMPT_ID] == attemptId) { + if (pref[Keys.ATTEMPT_ID] == attemptId && (pref[Keys.CREATING] == true)) { pref[Keys.CREATING] = false pref[Keys.FAILED] = true pref.remove(Keys.ATTEMPT_ID) pref.remove(Keys.STARTED_AT) pref.remove(Keys.EXPIRES_AT) }feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt (1)
59-61: CancellationException 재전파 반영 확인취소를 실패/재시도로 오인하지 않도록 한 처리 굿.
🧹 Nitpick comments (4)
core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt (4)
102-111: 만료/레거시 교정 시 시도 메타데이터도 함께 정리하세요교정 후 attempt 메타데이터가 남아 있으면 추후 레이스에서 오작동 여지가 있습니다.
적용 패치:
if (legacy || expired) { // 레거시(만료정보 없음) 또는 만료 → 실패로 교정 dataStore.edit { pref -> pref[Keys.CREATING] = false pref[Keys.FAILED] = true + pref.remove(Keys.ATTEMPT_ID) + pref.remove(Keys.STARTED_AT) + pref.remove(Keys.EXPIRES_AT) } emit(false) return@transformLatest }Also applies to: 104-107
206-216: clearFortuneData에서 attempt 메타데이터 누락운세 관련 정리 시도 시 ATTEMPT_ID/STARTED_AT/EXPIRES_AT도 함께 제거해야 일관성이 유지됩니다.
적용 패치:
dataStore.edit { pref -> pref.remove(Keys.ID) pref.remove(Keys.DATE) pref.remove(Keys.IMAGE_ID) pref.remove(Keys.SCORE) pref.remove(Keys.SEEN) pref.remove(Keys.TOOLTIP_SHOWN) pref.remove(Keys.CREATING) pref.remove(Keys.FAILED) + pref.remove(Keys.ATTEMPT_ID) + pref.remove(Keys.STARTED_AT) + pref.remove(Keys.EXPIRES_AT) }
100-101: 만료 경계 비교는 >=가 안전합니다동일 시각(밀리초)에서 경계 미스 방지.
- val expired = (!legacy && nowMillis() > expiresAt) + val expired = (!legacy && nowMillis() >= expiresAt)
131-141: 파라미터 명확성: lease → leaseMillis로 명명단위 명시로 오용 방지. 호출부 영향이 크면 스킵 가능.
- suspend fun markFortuneCreating( - attemptId: String, - lease: Long, - ) { + suspend fun markFortuneCreating( + attemptId: String, + leaseMillis: Long, + ) { val now = nowMillis() dataStore.edit { pref -> @@ - pref[Keys.EXPIRES_AT] = now + lease + pref[Keys.EXPIRES_AT] = now + leaseMillis } }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt(1 hunks)feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt(2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-14T15:32:44.064Z
Learnt from: DongChyeon
PR: YAPP-Github/Orbit-Android#252
File: domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt:16-17
Timestamp: 2025-09-14T15:32:44.064Z
Learning: Fortune creation requests have at least a 1-minute gap between them in the Orbit Android app, making atomic guards for race condition prevention unnecessary in the fortune creation flow.
Applied to files:
feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt
🧬 Code graph analysis (1)
core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt (2)
data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt (1)
todayEpoch(36-36)core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt (1)
todayEpoch(31-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (2)
feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt (2)
25-34: 중복 실행 가드 적절Creating/Success 시 조기 종료로 중복 워크 방지 OK.
41-45: lease 전달 경로 확인 — 정상검증 결과: Worker에서 markFortuneAsCreating(attemptId) 호출 시 도메인 인터페이스의 기본 leaseMillis(2 * 60_000L)가 적용되며, FortuneRepositoryImpl → FortuneLocalDataSourceImpl → FortunePreferences.markFortuneCreating(attemptId, lease)로 전달되어 EXPIRES_AT에 now + lease로 저장됩니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (5)
core/datastore/src/test/kotlin/com/yapp/datastore/FortunePreferencesTest.kt (5)
10-13: 불리언 비교는 assertTrue/assertFalse를 사용해 가독성 향상불리언에 대한
assertEquals(true/false, …)대신assertTrue/False를 사용하면 실패 메시지가 더 명확하고 관례적입니다.적용 diff 제안:
@@ -import org.junit.Assert.assertEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue @@ - assertEquals(false, creating) + assertFalse(creating) @@ - assertEquals(true, failed) + assertTrue(failed) @@ - assertEquals(false, creating) + assertFalse(creating) @@ - assertEquals(true, failed) + assertTrue(failed) @@ - assertEquals(true, stillCreating) + assertTrue(stillCreating) @@ - assertEquals(false, creatingAfterSuccess) + assertFalse(creatingAfterSuccess) @@ - assertEquals(false, failedAfterSuccess) + assertFalse(failedAfterSuccess) @@ - assertEquals(true, stillCreating) + assertTrue(stillCreating) @@ - assertEquals(false, creatingAfterFail) + assertFalse(creatingAfterFail) @@ - assertEquals(true, failed) + assertTrue(failed) @@ - assertEquals(false, unseen) + assertFalse(unseen) @@ - assertEquals(true, unseen) + assertTrue(unseen) @@ - assertEquals(false, showTooltip) + assertFalse(showTooltip) @@ - assertEquals(true, showTooltip) + assertTrue(showTooltip)Also applies to: 63-67, 82-86, 105-106, 115-118, 138-139, 145-148, 165-166, 182-183, 200-201, 217-218
38-41: DataStore scope를 명시하면 테스트 결정성이 더 좋아집니다현재
PreferenceDataStoreFactory.create { ... }의 기본 scope(IO+SupervisorJob)를 사용합니다. 테스트에서는StandardTestDispatcher(testScheduler)기반의 테스트 scope를 명시해주면 스케줄링이 일관되어 드문 타이밍 플레이키를 줄일 수 있습니다.적용 예(개략):
- 파일 상단에
private val testDispatcher = StandardTestDispatcher()정의PreferenceDataStoreFactory.create(scope = CoroutineScope(testDispatcher + SupervisorJob())) { newFile(...) }- 각 테스트는
runTest(testDispatcher) { ... }로 실행
48-67: 만료 교정 경계 테스트 추가 제안(만료 전 유지 확인)만료 이후 교정은 검증했지만, 만료 전에는 Creating이 유지됨을 보장하는 테스트가 있으면 회귀 방지에 도움이 됩니다.
예시 테스트:
@Test fun `운세_생성_상태_Creating_만료_전에는_유지된다`() = runTest { val ds = createNewDataStoreWithFile("prefs_not_expired.preferences_pb") val prefs = createFortunePreferencesWithClock(ds, fixedClockAtT0) // t0 val attemptId = UUID.randomUUID().toString() prefs.markFortuneCreating(attemptId = attemptId, lease = 60_000L) // 60초 리스 // 만료 전: Creating 유지, Failed 아님 assertTrue(prefs.isFortuneCreatingFlow.first()) assertFalse(prefs.isFortuneFailedFlow.first()) }
88-123: 만료 후 성공 신호 무시 시나리오도 추가하면 완결됩니다attemptId 일치 성공 케이스는 검증됐습니다. 여기에 “만료된 Creating 상태에서 동일 attemptId의 성공 신호가 와도 Success로 전환되지 않고 Failure 유지”를 추가하면 비정상 종료 후 지연된 콜백이 도착하는 현실 시나리오를 커버할 수 있습니다.
예시 테스트:
@Test fun `만료된_Creating_상태에서는_success_호출이_무시되고_Failure_유지된다`() = runTest { val ds = createNewDataStoreWithFile("prefs_expired_then_success.preferences_pb") val attemptId = UUID.randomUUID().toString() val prefsT0 = createFortunePreferencesWithClock(ds, fixedClockAtT0) prefsT0.markFortuneCreating(attemptId = attemptId, lease = 1_000L) // t0+2s 시점(만료 후)에서 성공 신호가 와도 교정 로직상 Failure 유지 val prefsT0Plus2 = createFortunePreferencesWithClock(ds, fixedClockAtT0Plus2Seconds) prefsT0Plus2.markFortuneCreatedIfAttemptMatches(attemptId = attemptId, fortuneId = 123L) assertFalse(prefsT0Plus2.isFortuneCreatingFlow.first()) assertTrue(prefsT0Plus2.isFortuneFailedFlow.first()) }
69-86: 레거시 키 명 하드코딩은 유지하되, 키 충돌 회피 주석 추가 권장레거시 교정을 검증하려면 하드코딩이 불가피합니다. 다만 실제 키명 변경 시 테스트 오탑을 빠르게 파악하기 위해 “레거시 스냅샷 의도”를 주석으로 남겨두면 좋습니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
core/datastore/src/test/kotlin/com/yapp/datastore/FortunePreferencesTest.kt(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (1)
core/datastore/src/test/kotlin/com/yapp/datastore/FortunePreferencesTest.kt (1)
20-47: 전반적으로 테스트 시나리오 구성 훌륭합니다만료/레거시 교정/attemptId 매칭/읽음 상태/툴팁 노출까지 핵심 플로우가 잘 커버되었습니다. PR 목표(#256) 충족을 검증하는 테스트로 적절합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt (1)
184-194: CREATING 가드 누락 — 성공 이후 지연 실패 신호 방지현재는 ATTEMPT_ID 일치만 보면 실패로 전환합니다. 성공 시 ATTEMPT_ID를 지우지만, 교정 경로 등 특수 케이스에서 안전하게 하려면 CREATING==true 가드까지 요구하는 편이 일관적입니다. (이 이슈는 과거에도 지적됨)
- dataStore.edit { pref -> - if (pref[Keys.ATTEMPT_ID] == attemptId) { + dataStore.edit { pref -> + if ((pref[Keys.CREATING] == true) && (pref[Keys.ATTEMPT_ID] == attemptId)) { pref[Keys.CREATING] = false pref[Keys.FAILED] = true pref.remove(Keys.ATTEMPT_ID) pref.remove(Keys.STARTED_AT) pref.remove(Keys.EXPIRES_AT) } }
🧹 Nitpick comments (8)
core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt (5)
102-109: 만료/교정 시 시도 메타데이터도 함께 정리 필요만료/레거시 교정 시 CREATING=false, FAILED=true만 설정하고 ATTEMPT_ID/STARTED_AT/EXPIRES_AT는 남깁니다. 후속 처리 로직 혼선을 줄이기 위해 교정 시에도 시도 메타데이터를 제거하세요.
if (legacy || expired) { // 레거시(만료정보 없음) 또는 만료 → 실패로 교정 dataStore.edit { pref -> pref[Keys.CREATING] = false pref[Keys.FAILED] = true + pref.remove(Keys.ATTEMPT_ID) + pref.remove(Keys.STARTED_AT) + pref.remove(Keys.EXPIRES_AT) } emit(false) return@transformLatest }
219-230: clearFortuneData가 시도 메타데이터를 남깁니다사용자 초기화/로그아웃 등에서 남은 ATTEMPT_ID/STARTED_AT/EXPIRES_AT는 이후 흐름에 간섭할 수 있습니다. 함께 제거하세요.
dataStore.edit { pref -> pref.remove(Keys.ID) pref.remove(Keys.DATE) pref.remove(Keys.IMAGE_ID) pref.remove(Keys.SCORE) pref.remove(Keys.SEEN) pref.remove(Keys.TOOLTIP_SHOWN) pref.remove(Keys.CREATING) pref.remove(Keys.FAILED) + pref.remove(Keys.ATTEMPT_ID) + pref.remove(Keys.STARTED_AT) + pref.remove(Keys.EXPIRES_AT) }
130-141: lease 파라미터 검증 추가 제안(음수/과대값 방지)음수/과도한 lease 입력을 방치하면 즉시 만료되거나 비정상적으로 긴 임대가 설정될 수 있습니다. 최소/최대 범위를 강제하세요.
) { 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 + val safeLease = lease.coerceIn(1_000L, 10 * 60_000L) // 1초 ~ 10분 (예시) + pref[Keys.EXPIRES_AT] = now + safeLease } }필요 시 상한값은 상수로 분리해 도메인 정책에 맞춰 주세요.
87-115: 만료 자동 교정이 ‘구독 시점’에만 실행됨isFortuneCreatingFlow는 DataStore 변경이 없으면 만료 시각 도래 후에도 재평가되지 않습니다. 장시간 실행 중인 화면에서 자동 교정이 지연될 수 있습니다. transformLatest 내에서 만료까지 delay 후 재확인하거나, expiresAt을 시간 틱 Flow와 결합하는 방식을 고려하세요.
간단한 방향:
- creating && !legacy && !expired인 경우, (expiresAt - now)만큼 delay 후 재검사 → 만료 시 교정.
- 또는 expiresAt을 방출하는 Flow와 combine하여 경과 시점을 트리거로 사용.
50-69: 에러 핸들링 가시성.catch { emit(emptyPreferences()) }로 에러를 삼키면 원인 추적이 어려워집니다. 최소한 로깅/크래시 리포팅 훅을 추가하세요.
예: .catch { e -> log.warn("DataStore read failed", e); emit(emptyPreferences()) }
Also applies to: 116-120, 121-129
core/datastore/src/test/kotlin/com/yapp/datastore/FortunePreferencesTest.kt (3)
48-67: 자정 경계 테스트 추가 제안오늘 의존 Flow가 자정 이후에도 즉시 갱신되는지 검증이 없습니다. 날짜 변화 후 hasUnseenFortuneFlow/shouldShowFortuneToolTipFlow가 재평가되는 테스트를 추가하세요(현재 구현은 갱신되지 않음).
원하시면 todayEpochTickerFlow 결합을 전제로 한 테스트 케이스 초안을 제공하겠습니다.
Also applies to: 151-180
125-149: 실패 마킹 가드 테스트 보강CREATING==true 가드가 추가되면(제안) 성공 처리 후 동일 attemptId의 지연 실패가 무시되는지 검증 테스트를 보강하세요.
예) 성공 후 markFortuneFailedIfAttemptMatches(validAttemptId) 호출 → FAILED가 false로 유지되는지 assert.
182-215: clearFortuneData 동작 테스트 추가초기화 후 ATTEMPT_ID/STARTED_AT/EXPIRES_AT까지 제거되는지 확인하는 테스트가 있으면 회귀를 막을 수 있습니다.
필요 시 테스트 스캐폴드를 드리겠습니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt(1 hunks)core/datastore/src/test/kotlin/com/yapp/datastore/FortunePreferencesTest.kt(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-14T15:32:44.064Z
Learnt from: DongChyeon
PR: YAPP-Github/Orbit-Android#252
File: domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt:16-17
Timestamp: 2025-09-14T15:32:44.064Z
Learning: Fortune creation requests have at least a 1-minute gap between them in the Orbit Android app, making atomic guards for race condition prevention unnecessary in the fortune creation flow.
Applied to files:
core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt
🧬 Code graph analysis (1)
core/datastore/src/main/java/com/yapp/datastore/FortunePreferences.kt (2)
data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt (1)
todayEpoch(36-36)core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt (1)
todayEpoch(31-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
Related issue 🛠
closed #256
어떤 변경사항이 있었나요?
CheckPoint ✅
PR이 다음 요구 사항을 충족하는지 확인하세요.
Work Description ✏️
isFortuneCreatingFlow에서expiresAt <= 0또는attemptId == null인 레거시 데이터를 즉시 Failure로 교정expiresAt)이 지났을 경우에도 동일하게 Failure 처리Uncompleted Tasks 😅
To Reviewers 📢
Summary by CodeRabbit