Skip to content

기념일 설정 화면 폴링 처리 추가#166

Merged
chanho0908 merged 1 commit into
developfrom
feature/#162-onboarding-anniversary-policy
May 30, 2026
Merged

기념일 설정 화면 폴링 처리 추가#166
chanho0908 merged 1 commit into
developfrom
feature/#162-onboarding-anniversary-policy

Conversation

@chanho0908
Copy link
Copy Markdown
Contributor

@chanho0908 chanho0908 commented May 21, 2026

이슈 번호

작업내용

  • 프로필 설정 -> 기념일 설정 화면 이동 단계에서 이동 전 온보딩 상태를 체크해 상대방이 기념일을 설정한 경우 기념일 설정 화면 Skip
    -온폴링을 통해 온보딩 상태가 기념일 설정 단계에서 완료 상태로 변경되면 즉시 홈으로 이동하도록 처리
  • 이 경우 안내 토스트를 노출하도록 문구를 추가
  • D-day 화면 진입/이탈 시 온보딩 상태 polling을 시작/중단하도록 적용
  • 초대 코드와 D-day 화면에서 사용하는 상태 polling 로직을 공통화

결과물

@chanho0908 chanho0908 self-assigned this May 21, 2026
@chanho0908 chanho0908 added the Feature Extra attention is needed label May 21, 2026
@chanho0908 chanho0908 changed the base branch from develop to feature/#157-loading-error-ui May 21, 2026 10:31
@chanho0908 chanho0908 force-pushed the feature/#162-onboarding-anniversary-policy branch from 374a8dd to 0ba0a22 Compare May 21, 2026 10:38
@chanho0908 chanho0908 changed the base branch from feature/#157-loading-error-ui to develop May 21, 2026 10:38
@chanho0908 chanho0908 changed the title 온보딩 기념일 등록 완료 처리 추가 기념일 설정 화면 폴링 처리 추가 May 21, 2026
@Keepiluv Keepiluv deleted a comment from coderabbitai Bot May 21, 2026
@chanho0908
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

온보딩 기념일 정책 변경 작업으로서, 상대방이 먼저 기념일을 등록한 경우를 감지하는 기능을 구현합니다. D-day 화면에 상태 폴링 메커니즘을 추가하여 온보딩 상태가 COMPLETED로 변경될 때까지 주기적으로 서버에서 상태를 확인합니다. 폴링 중 COMPLETED 상태를 감지하면 "짝꿍이 기념일을 먼저 등록했어요" 토스트 메시지를 표시한 후 사용자를 홈 화면으로 이동시킵니다. 온보딩 ViewModel의 폴링 로직을 startStatusPolling 통합 메서드로 리팩터링하여 커플 연결 대기와 기념일 등록 완료 대기 두 가지 시나리오를 조건 기반으로 처리합니다.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed 코드 변경사항이 #162의 모든 주요 요구사항을 충족합니다: 프로필 설정 후 COMPLETED 상태 시 기념일 페이지 스킵, 안내 토스트 추가, D-day 화면의 폴링 처리, 상대방 등록 완료 감지 시 토스트 표시 후 홈 이동.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #162의 요구사항 범위 내에 있으며, 기념일 설정 화면 폴링 처리와 관련된 필수 변경들로 구성되어 있습니다.
Title check ✅ Passed PR 제목이 변경사항의 주요 부분인 '기념일 설정 화면에서의 폴링 상태 처리'를 정확하게 반영하고 있으며, 온보딩 완료 감지 및 토스트 표시 기능 추가라는 핵심 의도를 명확하게 전달합니다.
Description check ✅ Passed PR 설명이 변경 사항과 관련성이 있으며, 이슈 #162 해결을 위한 구체적인 작업 내용(기념일 폴링, 토스트 표시, 상태 폴링 통합)을 포함하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#162-onboarding-anniversary-policy

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt (1)

67-76: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

초기 통계 fetch는 동시 실행 방지가 필요합니다.

Line [67]~Line [76], Line [210]에서 fetchInitialStats()가 호출될 때마다 새 코루틴을 시작해, 빠른 재시도 시 요청이 중첩되고 늦게 끝난 이전 응답이 최신 상태를 덮어쓸 수 있습니다. 초기 fetch Job을 1개로 관리해 취소/중복방지 처리해 주세요.

제안 diff
 import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.async
@@
 class StatsDetailViewModel(
@@
 ) : BaseViewModel<StatsDetailUiState, StatsDetailIntent, StatsDetailSideEffect>(
         StatsDetailUiState(),
     ) {
+    private var initialFetchJob: Job? = null
@@
     private fun fetchInitialStats() {
+        if (initialFetchJob?.isActive == true) return
         val initialDate = LocalDate.parse(argDate).let(YearMonth::from)
 
-        viewModelScope.launch {
+        initialFetchJob = viewModelScope.launch {
             reduce {
                 copyState(isLoading = true, error = null) as StatsDetailUiState
             }
             val (summary, detail) = fetchStats(initialDate)
             handleFetchStatsDetailResult(summary, detail, initialDate)
         }
     }
As per coding guidelines "feature/**: 단방향 데이터 플로우(Intent → ViewModel → State → View)가 유지되는가?".

Also applies to: 210-210

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt`
around lines 67 - 76, Add single-job management to fetchInitialStats by
introducing a private Job property (e.g., initialFetchJob) and use it to prevent
concurrent fetches: before calling viewModelScope.launch in fetchInitialStats
cancel any previous job or return when initialFetchJob?.isActive == true, then
assign the new coroutine to initialFetchJob; ensure the coroutine still updates
state and calls fetchStats(initialDate) and
handleFetchStatsDetailResult(summary, detail, initialDate). This keeps
viewModelScope.launch usage but ensures duplicate/cancel behavior for
fetchInitialStats (also applied to the other call site at line ~210).
feature/notification/src/main/java/com/twix/notification/NotificationScreen.kt (1)

92-106: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

LaunchedEffect 내부에서 uiState.canLoadNextPage가 오래된 값으로 캡처될 수 있어 페이징 트리거가 흔들립니다(중복 Intent/요청 위험).

NotificationScreen.ktLaunchedEffect(listState, uiState.notificationList.size)는 키에 canLoadNextPage(= hasNext && !isLoading)가 없어서, 로딩/다음페이지 여부가 바뀌어도 effect 내부의 if (uiState.canLoadNextPage)는 이전 캡처값을 사용할 수 있습니다. 그 결과 스크롤 조건이 true → false → true로 다시 변하는 경우 로딩 중이거나 hasNext=false인데도 FetchNextPage가 다시 dispatch될 수 있습니다(뷰모델의 currentState.hasNext/isLoading 가드로 완전한 중복 요청은 완화되더라도, 타이밍 의존의 불필요한 트리거가 생길 여지가 있습니다).

rememberUpdatedState로 최신 canLoadNextPage를 effect 내부에서 참조하도록 바꾸는 방식이 가장 안전합니다.

수정 예시
-    LaunchedEffect(listState, uiState.notificationList.size) {
+    val canLoadNextPage by rememberUpdatedState(uiState.canLoadNextPage)
+
+    LaunchedEffect(listState, uiState.notificationList.size, canLoadNextPage) {
         snapshotFlow {
             val layoutInfo = listState.layoutInfo
             val totalCount = layoutInfo.totalItemsCount
             val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1

             // 마지막 3개쯤 보일 때 미리 조회
             totalCount > 0 && lastVisibleIndex >= totalCount - 3
         }.distinctUntilChanged()
             .filter { it }
             .collect {
-                if (uiState.canLoadNextPage) {
+                if (canLoadNextPage) {
                     onNextPage()
                 }
             }
     }

로딩 중에도 마지막 아이템 근처 스크롤이 다시 “조건 true”로 바뀌면, 추가 fetch를 막는 게 의도된 동작인가요?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/notification/src/main/java/com/twix/notification/NotificationScreen.kt`
around lines 92 - 106, The effect captures an outdated uiState.canLoadNextPage
causing spurious triggers; wrap uiState.canLoadNextPage with
rememberUpdatedState (e.g. val canLoadNextPageState =
rememberUpdatedState(uiState.canLoadNextPage)) and then inside the
LaunchedEffect(listState, uiState.notificationList.size) replace direct checks
of uiState.canLoadNextPage with canLoadNextPageState.value so
snapshotFlow/collect uses the latest value when deciding to call onNextPage();
keep the existing keys (listState and uiState.notificationList.size) for the
LaunchedEffect and only swap the conditional to use the remembered updated state
to avoid stale captures.
feature/photolog/editor/src/main/java/com/twix/photolog/editor/PhotologEditorViewModel.kt (1)

57-75: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

저장 요청 중복 실행 방지 가드가 필요합니다.

왜 문제인지: Line 63-75는 isSaving을 표시만 하고, 재진입을 막지는 않습니다. 사용자가 빠르게 저장을 연타하면 modifyPhotolog가 중복 호출될 수 있어요.
어떻게 개선할지: modifyComment() 시작 시 currentState.isSaving 가드를 추가해 단일 in-flight 요청만 허용해 주세요.

수정 예시
 private fun modifyComment() {
+    if (currentState.isSaving) return
+
     if (currentState.comment.canUpload.not()) {
         showToast(R.string.toast_comment_length_guide, ToastType.DEFAULT)
     } else if (currentState.isCommentNotChanged) {
         showToast(R.string.toast_comment_not_modified, ToastType.DEFAULT)
     } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/photolog/editor/src/main/java/com/twix/photolog/editor/PhotologEditorViewModel.kt`
around lines 57 - 75, Add a reentrancy guard at the start of modifyComment():
check currentState.isSaving and return immediately if true so only one in-flight
save is allowed; keep the existing reduce { copy(isSaving = true) }/onFinally
logic and ensure the guard references the same state flag (currentState.isSaving
/ isSaving) used around launchResult and launchModifyComment so rapid repeated
taps won’t trigger duplicate launches.
🧹 Nitpick comments (4)
feature/login/src/main/java/com/twix/login/LoginScreen.kt (1)

76-89: 💤 Low value

상태 기반 로딩 가드 구현이 잘 되어 있습니다.

uiState.isLoading 가드와 버튼 레벨의 isAuthenticating 체크를 통해 중복 로그인 요청을 이중으로 방지하고 있어 안정적입니다.

한 가지 참고 사항: OAuth 제공자(kakaoLoginProvider.login(), googleLoginProvider.login()) 호출 중에는 ViewModel의 isLoading 상태가 아직 true가 아니므로, 이 구간에서 빠른 더블 클릭이 발생할 수 있습니다. 다만 대부분의 OAuth SDK가 내부적으로 중복 요청을 처리하므로 실질적인 문제는 적을 것으로 보입니다. 더 엄격한 제어가 필요하다면 로컬 상태나 MutableStateFlow<Boolean>을 활용한 락 패턴을 고려해볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/login/src/main/java/com/twix/login/LoginScreen.kt` around lines 76 -
89, The current double-click protection uses uiState.isLoading and the
LoginScreen isAuthenticating prop but because uiState.isLoading may not be set
true until after kakaoLoginProvider.login()/googleLoginProvider.login() returns,
rapid double clicks can trigger multiple OAuth calls; fix by adding a local
in-scope guard (e.g., a boolean or a MutableStateFlow<Boolean> "isSubmitting" or
lock) around the coroutineScope.launch branch so you set the lock immediately
before calling kakaoLoginProvider.login()/googleLoginProvider.login() and clear
it after dispatching viewModel.dispatch(LoginIntent.Login(result)), ensuring
LoginScreen/coroutineScope.launch and the provider calls are protected
atomically from rapid re-entry.
feature/goal-manage/src/main/java/com/twix/goal_manage/GoalManageScreen.kt (1)

96-123: ⚡ Quick win

로딩/에러 분기 추가에 맞춘 상태별 Preview를 함께 두면 유지보수가 더 쉬워집니다.

Line 150 이후로 showLoading/showError/else 분기가 생겨 UI 회귀 가능성이 커졌는데, 현재 파일 기준으로 상태별 Preview가 없습니다. loading, error, content 3가지를 분리해서 Preview를 추가해두면 QA 전에 빠르게 확인할 수 있어요. 이 방향으로 보강해보실까요?

As per coding guidelines feature/**: "Preview Composable이 제공되는가?"

Also applies to: 150-257

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/goal-manage/src/main/java/com/twix/goal_manage/GoalManageScreen.kt`
around lines 96 - 123, Add three Preview composables for GoalManageScreen to
cover loading, error, and content states: create `@Preview` functions that call
GoalManageScreen with GoalManageUiState variants (e.g., a uiState representing
loading (isLoading=true), an error state (error message set), and a populated
content state), supply openedMenuGoalId as null or a sample id and use no-op
lambdas for onBack/onRetry/onSelectDate/etc.; ensure each Preview is named
clearly (e.g., PreviewGoalManageLoading, PreviewGoalManageError,
PreviewGoalManageContent) so they live alongside the
GoalManageScreen/GoalManageUiState definitions and make UI regressions visible.
core/ui/src/main/java/com/twix/ui/base/BaseViewModel.kt (1)

218-226: 💤 Low value

중복 타입 체크로 인한 불필요한 코드

라인 219에서 currentState !is DefaultLoadableState를 체크하고 early return하므로, 라인 222의 safe cast this as? DefaultLoadableState는 항상 성공합니다. 따라서 null 체크가 불필요합니다.

개선 제안:

♻️ 중복 체크 제거
 private inline fun reduceLoadableState(crossinline reducer: DefaultLoadableState.() -> DefaultLoadableState) {
     if (currentState !is DefaultLoadableState) return
 
     reduce {
-        val loadableState = this as? DefaultLoadableState ?: return@reduce this
+        val loadableState = this as DefaultLoadableState
         `@Suppress`("UNCHECKED_CAST")
         loadableState.reducer() as S
     }
 }

참고: 라인 224의 UNCHECKED_CASTDefaultLoadableState.copyState의 반환 타입 설계 이슈로 인한 것이므로, DefaultLoadableState.kt의 self-type 패턴 적용으로 근본적으로 해결할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/ui/src/main/java/com/twix/ui/base/BaseViewModel.kt` around lines 218 -
226, 현재 reduceLoadableState에서 외부에선 currentState 타입을 확인한 뒤(reduceLoadableState)
내부 reduce 블록에서 다시 안전 캐스트(this as? DefaultLoadableState)를 수행하고 있어 중복 검사와 불필요한
null handling이 발생합니다; reduce 람다 내의 캐스트를 안전 캐스트 대신 단순 non-null 캐스트로 변경하여(val
loadableState = this as DefaultLoadableState) 불필요한 반환 경로를 제거하고 기존
`@Suppress`("UNCHECKED_CAST")는 유지한 채 DefaultLoadableState.copyState 관련 self-type
이슈는 별도 파일에서 해결하도록 남겨두세요 (참조 심볼: reduceLoadableState, DefaultLoadableState,
reduce).
feature/settings/src/main/java/com/twix/settings/account/SettingsAccountScreen.kt (1)

141-150: ⚡ Quick win

CommonDialog의 confirm/dismiss 의미를 표준에 맞춰 정렬해 주세요.

Line [143]~Line [149]처럼 버튼 텍스트와 콜백을 역으로 매핑하면, 지금은 동작해도 유지보수 시 실수 가능성이 커집니다. confirm=탈퇴, dismiss=취소로 맞추는 쪽이 안전합니다. 이 방향으로 통일해보실까요?

제안 diff
             CommonDialog(
                 visible = showWithdrawDialog,
-                confirmText = stringResource(R.string.word_cancel),
-                dismissText = stringResource(R.string.action_withdraw_account),
+                confirmText = stringResource(R.string.action_withdraw_account),
+                dismissText = stringResource(R.string.word_cancel),
                 onDismiss = {
                     showWithdrawDialog = false
-                    onWithdrawAccount()
                 },
-                onConfirm = { showWithdrawDialog = false },
+                onConfirm = {
+                    showWithdrawDialog = false
+                    onWithdrawAccount()
+                },
                 onDismissRequest = { showWithdrawDialog = false },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/settings/src/main/java/com/twix/settings/account/SettingsAccountScreen.kt`
around lines 141 - 150, The confirm/dismiss labels and callbacks in CommonDialog
are reversed; update CommonDialog invocation so confirmText is the withdraw
label and triggers the withdraw action, and dismissText is the cancel label and
only hides the dialog: set confirmText to
stringResource(R.string.action_withdraw_account) and call onWithdrawAccount()
from onConfirm (then hide dialog), set dismissText to
stringResource(R.string.word_cancel) and make onDismiss only set
showWithdrawDialog = false; keep onDismissRequest = { showWithdrawDialog = false
} as-is. Reference: CommonDialog, showWithdrawDialog, onWithdrawAccount.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/ui/src/main/java/com/twix/ui/base/DefaultLoadableState.kt`:
- Around line 15-18: The copyState method currently returns the concrete
DefaultLoadableState interface which loses concrete types and forces unsafe
casts in BaseViewModel.reduceLoadableState; change DefaultLoadableState to a
generic self-type (e.g., interface DefaultLoadableState<S :
DefaultLoadableState<S>>) and update copyState to return S so each
implementation (e.g., LoginUiState) can return its own type, then update
BaseViewModel.reduceLoadableState to work with the generic S and remove the
`@Suppress`("UNCHECKED_CAST")/as S casts; adjust all implementing classes to
implement DefaultLoadableState<TheirType> and implement copyState returning
their concrete type.

In
`@feature/photolog/detail/src/main/java/com/twix/photolog/detail/contract/PhotologDetailUiState.kt`:
- Line 182: The current fallback returns PhotologDetailUiState(isLoading =
false) when the goal lookup fails, causing goalId to become -1L and selectedDate
to default to now(), which leads to invalid subsequent actions; instead modify
the fallback in the logic that builds PhotologDetailUiState (the branch that
currently does "?: return PhotologDetailUiState(isLoading = false)") to preserve
the original route inputs (goalId, selectedDate, betweenUs) and set an explicit
error state (e.g., isError = true or an errorMessage) so callers know the goal
was missing rather than receiving reset default identifiers; update the
construction of PhotologDetailUiState accordingly to use the incoming parameters
and an error flag so upload/edit flows won't proceed with invalid params.

In
`@feature/photolog/detail/src/main/java/com/twix/photolog/detail/PhotologDetailViewModel.kt`:
- Around line 201-204: The handlePokeCooldown function currently only clears
isPoking and shows a toast but doesn't persist the remaining cooldown to state;
update the reduce call inside handlePokeCooldown to set pokeCooldownRemaining =
remainingMs while keeping isPoking = false, then continue to emit the
PhotologDetailSideEffect.ShowPokeCooldownToast(remainingMs) as before (function:
handlePokeCooldown, state field: pokeCooldownRemaining).
- Around line 172-176: Add a guard to prevent concurrent poke executions in
pokeToPartner(): check an isPoking boolean at the start (return early if true),
set isPoking = true immediately before launching the operation (or before
calling startPokeLoading()), and ensure isPoking is reset to false in all
completion paths after handlePokeResult returns (and optionally implement a
short cooldown if desired). Update references to pokeToPartner(),
viewModelScope.launch, startPokeLoading(), handlePokeResult, and
pokeGoalUseCase.invoke(argGoalId) so the flag wraps the invoke call to guarantee
only one in-flight poke.

---

Outside diff comments:
In
`@feature/notification/src/main/java/com/twix/notification/NotificationScreen.kt`:
- Around line 92-106: The effect captures an outdated uiState.canLoadNextPage
causing spurious triggers; wrap uiState.canLoadNextPage with
rememberUpdatedState (e.g. val canLoadNextPageState =
rememberUpdatedState(uiState.canLoadNextPage)) and then inside the
LaunchedEffect(listState, uiState.notificationList.size) replace direct checks
of uiState.canLoadNextPage with canLoadNextPageState.value so
snapshotFlow/collect uses the latest value when deciding to call onNextPage();
keep the existing keys (listState and uiState.notificationList.size) for the
LaunchedEffect and only swap the conditional to use the remembered updated state
to avoid stale captures.

In
`@feature/photolog/editor/src/main/java/com/twix/photolog/editor/PhotologEditorViewModel.kt`:
- Around line 57-75: Add a reentrancy guard at the start of modifyComment():
check currentState.isSaving and return immediately if true so only one in-flight
save is allowed; keep the existing reduce { copy(isSaving = true) }/onFinally
logic and ensure the guard references the same state flag (currentState.isSaving
/ isSaving) used around launchResult and launchModifyComment so rapid repeated
taps won’t trigger duplicate launches.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt`:
- Around line 67-76: Add single-job management to fetchInitialStats by
introducing a private Job property (e.g., initialFetchJob) and use it to prevent
concurrent fetches: before calling viewModelScope.launch in fetchInitialStats
cancel any previous job or return when initialFetchJob?.isActive == true, then
assign the new coroutine to initialFetchJob; ensure the coroutine still updates
state and calls fetchStats(initialDate) and
handleFetchStatsDetailResult(summary, detail, initialDate). This keeps
viewModelScope.launch usage but ensures duplicate/cancel behavior for
fetchInitialStats (also applied to the other call site at line ~210).

---

Nitpick comments:
In `@core/ui/src/main/java/com/twix/ui/base/BaseViewModel.kt`:
- Around line 218-226: 현재 reduceLoadableState에서 외부에선 currentState 타입을 확인한
뒤(reduceLoadableState) 내부 reduce 블록에서 다시 안전 캐스트(this as? DefaultLoadableState)를
수행하고 있어 중복 검사와 불필요한 null handling이 발생합니다; reduce 람다 내의 캐스트를 안전 캐스트 대신 단순
non-null 캐스트로 변경하여(val loadableState = this as DefaultLoadableState) 불필요한 반환 경로를
제거하고 기존 `@Suppress`("UNCHECKED_CAST")는 유지한 채 DefaultLoadableState.copyState 관련
self-type 이슈는 별도 파일에서 해결하도록 남겨두세요 (참조 심볼: reduceLoadableState,
DefaultLoadableState, reduce).

In `@feature/goal-manage/src/main/java/com/twix/goal_manage/GoalManageScreen.kt`:
- Around line 96-123: Add three Preview composables for GoalManageScreen to
cover loading, error, and content states: create `@Preview` functions that call
GoalManageScreen with GoalManageUiState variants (e.g., a uiState representing
loading (isLoading=true), an error state (error message set), and a populated
content state), supply openedMenuGoalId as null or a sample id and use no-op
lambdas for onBack/onRetry/onSelectDate/etc.; ensure each Preview is named
clearly (e.g., PreviewGoalManageLoading, PreviewGoalManageError,
PreviewGoalManageContent) so they live alongside the
GoalManageScreen/GoalManageUiState definitions and make UI regressions visible.

In `@feature/login/src/main/java/com/twix/login/LoginScreen.kt`:
- Around line 76-89: The current double-click protection uses uiState.isLoading
and the LoginScreen isAuthenticating prop but because uiState.isLoading may not
be set true until after kakaoLoginProvider.login()/googleLoginProvider.login()
returns, rapid double clicks can trigger multiple OAuth calls; fix by adding a
local in-scope guard (e.g., a boolean or a MutableStateFlow<Boolean>
"isSubmitting" or lock) around the coroutineScope.launch branch so you set the
lock immediately before calling
kakaoLoginProvider.login()/googleLoginProvider.login() and clear it after
dispatching viewModel.dispatch(LoginIntent.Login(result)), ensuring
LoginScreen/coroutineScope.launch and the provider calls are protected
atomically from rapid re-entry.

In
`@feature/settings/src/main/java/com/twix/settings/account/SettingsAccountScreen.kt`:
- Around line 141-150: The confirm/dismiss labels and callbacks in CommonDialog
are reversed; update CommonDialog invocation so confirmText is the withdraw
label and triggers the withdraw action, and dismissText is the cancel label and
only hides the dialog: set confirmText to
stringResource(R.string.action_withdraw_account) and call onWithdrawAccount()
from onConfirm (then hide dialog), set dismissText to
stringResource(R.string.word_cancel) and make onDismiss only set
showWithdrawDialog = false; keep onDismissRequest = { showWithdrawDialog = false
} as-is. Reference: CommonDialog, showWithdrawDialog, onWithdrawAccount.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: dbadb2d2-d9ed-4801-9ad9-a4782e724d24

📥 Commits

Reviewing files that changed from the base of the PR and between b29d219 and 374a8dd.

📒 Files selected for processing (58)
  • .gitignore
  • app/src/main/AndroidManifest.xml
  • core/design-system/src/main/res/values/strings.xml
  • core/result/src/main/java/com/twix/result/Extension.kt
  • core/share/src/main/java/com/twix/share/InviteLaunchDispatcher.kt
  • core/ui/src/main/java/com/twix/ui/base/BaseViewModel.kt
  • core/ui/src/main/java/com/twix/ui/base/ContentLoadableState.kt
  • core/ui/src/main/java/com/twix/ui/base/DefaultLoadableState.kt
  • core/ui/src/main/java/com/twix/ui/base/LoadableState.kt
  • feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt
  • feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt
  • feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt
  • feature/goal-manage/src/main/java/com/twix/goal_manage/GoalManageIntent.kt
  • feature/goal-manage/src/main/java/com/twix/goal_manage/GoalManageScreen.kt
  • feature/goal-manage/src/main/java/com/twix/goal_manage/GoalManageViewModel.kt
  • feature/goal-manage/src/main/java/com/twix/goal_manage/model/GoalManageUiState.kt
  • feature/login/src/main/java/com/twix/login/LoginScreen.kt
  • feature/login/src/main/java/com/twix/login/LoginViewModel.kt
  • feature/login/src/main/java/com/twix/login/contract/LoginUiState.kt
  • feature/main/src/main/java/com/twix/home/HomeScreen.kt
  • feature/main/src/main/java/com/twix/home/HomeViewModel.kt
  • feature/main/src/main/java/com/twix/home/model/HomeUiState.kt
  • feature/main/src/main/java/com/twix/stats/StatsScreen.kt
  • feature/main/src/main/java/com/twix/stats/StatsViewModel.kt
  • feature/main/src/main/java/com/twix/stats/contract/StatsIntent.kt
  • feature/main/src/main/java/com/twix/stats/contract/StatsUiState.kt
  • feature/main/src/main/java/com/twix/stats/preview/StatsUiStatePreviewProvider.kt
  • feature/notification/src/main/java/com/twix/notification/NotificationScreen.kt
  • feature/notification/src/main/java/com/twix/notification/NotificationViewModel.kt
  • feature/notification/src/main/java/com/twix/notification/contract/NotificationIntent.kt
  • feature/notification/src/main/java/com/twix/notification/contract/NotificationUiState.kt
  • feature/onboarding/src/main/java/com/twix/onboarding/OnBoardingViewModel.kt
  • feature/onboarding/src/main/java/com/twix/onboarding/contract/OnBoardingIntent.kt
  • feature/onboarding/src/main/java/com/twix/onboarding/contract/OnBoardingLoadingAction.kt
  • feature/onboarding/src/main/java/com/twix/onboarding/contract/OnBoardingUiState.kt
  • feature/onboarding/src/main/java/com/twix/onboarding/couple/CoupleConnectScreen.kt
  • feature/onboarding/src/main/java/com/twix/onboarding/dday/DdayRoute.kt
  • feature/onboarding/src/main/java/com/twix/onboarding/invite/InviteCodeScreen.kt
  • feature/onboarding/src/main/java/com/twix/onboarding/profile/ProfileScreen.kt
  • feature/photolog/detail/src/main/java/com/twix/photolog/detail/PhotologDetailScreen.kt
  • feature/photolog/detail/src/main/java/com/twix/photolog/detail/PhotologDetailViewModel.kt
  • feature/photolog/detail/src/main/java/com/twix/photolog/detail/contract/PhotologDetailIntent.kt
  • feature/photolog/detail/src/main/java/com/twix/photolog/detail/contract/PhotologDetailUiState.kt
  • feature/photolog/detail/src/main/java/com/twix/photolog/detail/preview/PhotologDetailPreviewProvider.kt
  • feature/photolog/editor/src/main/java/com/twix/photolog/editor/PhotologEditorRoute.kt
  • feature/photolog/editor/src/main/java/com/twix/photolog/editor/PhotologEditorViewModel.kt
  • feature/photolog/editor/src/main/java/com/twix/photolog/editor/contract/PhotologEditorIntent.kt
  • feature/photolog/editor/src/main/java/com/twix/photolog/editor/contract/PhotologEditorUiState.kt
  • feature/settings/src/main/java/com/twix/settings/SettingsIntent.kt
  • feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt
  • feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt
  • feature/settings/src/main/java/com/twix/settings/account/SettingsAccountScreen.kt
  • feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt
  • feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt
  • feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt
  • feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailIntent.kt
  • feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailUiState.kt
  • feature/stats/detail/src/main/java/com/twix/stats/detail/preview/StatsDetailUiStatePreviewProvider.kt
💤 Files with no reviewable changes (1)
  • core/ui/src/main/java/com/twix/ui/base/LoadableState.kt

Comment on lines +15 to +18
fun copyState(
isLoading: Boolean = this.isLoading,
error: AppError? = this.error,
): DefaultLoadableState
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

copyState 반환 타입으로 인한 타입 안정성 저하 우려

현재 copyState 메서드가 DefaultLoadableState 인터페이스 타입을 반환하도록 정의되어 있는데, 이로 인해 몇 가지 문제가 발생할 수 있습니다:

  1. 구체 타입 손실: 구현체(예: data class)가 copyState를 호출할 때 자신의 타입이 아닌 인터페이스 타입을 받게 됩니다
  2. Unsafe cast 강제: BaseViewModel.reduceLoadableState (line 224)에서 @Suppress("UNCHECKED_CAST")as S 캐스팅이 필수적으로 필요합니다
  3. 런타임 오류 위험: 타입 캐스팅이 컴파일 타임에 검증되지 않아 잠재적 ClassCastException 위험이 있습니다

개선 방안: Self-type 패턴을 사용하여 구현체가 자신의 타입을 반환하도록 개선할 수 있습니다:

♻️ Self-type 패턴 적용 예시
-interface DefaultLoadableState : State {
+interface DefaultLoadableState<out T : DefaultLoadableState<T>> : State {
     val isLoading: Boolean
     val error: AppError?
 
     fun copyState(
         isLoading: Boolean = this.isLoading,
         error: AppError? = this.error,
-    ): DefaultLoadableState
+    ): T
 }

구현 예시:

data class LoginUiState(
    override val isLoading: Boolean = false,
    override val error: AppError? = null,
    val isAuthenticating: Boolean = false
) : DefaultLoadableState<LoginUiState> {
    override fun copyState(
        isLoading: Boolean,
        error: AppError?
    ): LoginUiState = copy(isLoading = isLoading, error = error)
}

이렇게 하면 BaseViewModel에서 unsafe cast 없이 타입 안전하게 처리할 수 있습니다.

다만 이미 여러 화면에서 구현이 진행 중인 것으로 보이므로, 팀 내 논의를 통해 마이그레이션 범위와 우선순위를 결정하는 것이 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/ui/src/main/java/com/twix/ui/base/DefaultLoadableState.kt` around lines
15 - 18, The copyState method currently returns the concrete
DefaultLoadableState interface which loses concrete types and forces unsafe
casts in BaseViewModel.reduceLoadableState; change DefaultLoadableState to a
generic self-type (e.g., interface DefaultLoadableState<S :
DefaultLoadableState<S>>) and update copyState to return S so each
implementation (e.g., LoginUiState) can return its own type, then update
BaseViewModel.reduceLoadableState to work with the generic S and remove the
`@Suppress`("UNCHECKED_CAST")/as S casts; adjust all implementing classes to
implement DefaultLoadableState<TheirType> and implement copyState returning
their concrete type.

goals.firstOrNull {
it.goalId == goalId
} ?: return PhotologDetailUiState()
} ?: return PhotologDetailUiState(isLoading = false)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

목표 미조회 fallback에서 식별자/날짜가 초기화되어 잘못된 후속 액션으로 이어질 수 있습니다.

Line 182는 goal을 찾지 못했을 때 기본 상태를 반환해서 goalId = -1L, selectedDate = now()가 됩니다. 이 상태에서 업로드/수정 액션이 실행되면 잘못된 파라미터로 이동/요청될 수 있습니다.
fallback에서도 라우트 입력값(goalId, selectedDate, betweenUs)은 유지하고, 필요하면 에러 상태로 전환하는 쪽이 안전합니다. 이 케이스는 에러 화면으로 보내는 게 더 의도에 맞을까요?

수정 예시
-        } ?: return PhotologDetailUiState(isLoading = false)
+        } ?: return PhotologDetailUiState(
+            goalId = goalId,
+            currentShow = BetweenUs.valueOf(betweenUs),
+            selectedDate = selectedDate,
+            isCompletedGoal = isCompletedGoal,
+            hasLoadedContent = true,
+            isLoading = false,
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/photolog/detail/src/main/java/com/twix/photolog/detail/contract/PhotologDetailUiState.kt`
at line 182, The current fallback returns PhotologDetailUiState(isLoading =
false) when the goal lookup fails, causing goalId to become -1L and selectedDate
to default to now(), which leads to invalid subsequent actions; instead modify
the fallback in the logic that builds PhotologDetailUiState (the branch that
currently does "?: return PhotologDetailUiState(isLoading = false)") to preserve
the original route inputs (goalId, selectedDate, betweenUs) and set an explicit
error state (e.g., isError = true or an errorMessage) so callers know the goal
was missing rather than receiving reset default identifiers; update the
construction of PhotologDetailUiState accordingly to use the incoming parameters
and an error flag so upload/edit flows won't proceed with invalid params.

Comment on lines 172 to 176
private fun pokeToPartner() {
viewModelScope.launch {
reduce { copy(isPoking = true) }
when (val result = pokeGoalUseCase.invoke(argGoalId)) {
is PokeGoalResult.Success -> {
reduce { copy(isPoking = false, pokeCooldownRemaining = PokeGoalUseCase.COOLDOWN_MS) }
tryEmitSideEffect(PhotologDetailSideEffect.ShowPokeToast)
}
is PokeGoalResult.OnCooldown -> {
reduce { copy(isPoking = false) }
tryEmitSideEffect(PhotologDetailSideEffect.ShowPokeCooldownToast(result.remainingMs))
}
PokeGoalResult.Error -> {
reduce { copy(isPoking = false) }
showToast(R.string.toast_poke_goal_failed, ToastType.ERROR)
}
}
startPokeLoading()
handlePokeResult(pokeGoalUseCase.invoke(argGoalId))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Poke 중복 실행을 막는 가드가 필요합니다.

왜 문제인지: Line 173-175 구간은 탭 연타 시 pokeGoalUseCase.invoke(argGoalId)가 병렬로 여러 번 실행될 수 있어요. 외부 쓰기 호출 중복으로 토스트/상태가 꼬일 수 있습니다.
어떻게 개선할지: isPoking(및 필요 시 cooldown) 기준으로 조기 반환 가드를 넣어 단일 실행만 허용해 주세요.

수정 예시
 private fun pokeToPartner() {
+    if (currentState.isPoking || currentState.pokeCooldownRemaining > 0L) return
     viewModelScope.launch {
         startPokeLoading()
         handlePokeResult(pokeGoalUseCase.invoke(argGoalId))
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/photolog/detail/src/main/java/com/twix/photolog/detail/PhotologDetailViewModel.kt`
around lines 172 - 176, Add a guard to prevent concurrent poke executions in
pokeToPartner(): check an isPoking boolean at the start (return early if true),
set isPoking = true immediately before launching the operation (or before
calling startPokeLoading()), and ensure isPoking is reset to false in all
completion paths after handlePokeResult returns (and optionally implement a
short cooldown if desired). Update references to pokeToPartner(),
viewModelScope.launch, startPokeLoading(), handlePokeResult, and
pokeGoalUseCase.invoke(argGoalId) so the flag wraps the invoke call to guarantee
only one in-flight poke.

Comment on lines +201 to +204
private fun handlePokeCooldown(remainingMs: Long) {
reduce { copy(isPoking = false) }
tryEmitSideEffect(PhotologDetailSideEffect.ShowPokeCooldownToast(remainingMs))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cooldown 응답 시 남은 시간을 상태에 반영해야 합니다.

왜 문제인지: Line 201-204에서 remainingMs를 토스트에만 쓰고 상태(pokeCooldownRemaining)에는 저장하지 않아, UI가 cooldown 정보를 잃을 수 있습니다. 그 결과 불필요한 재요청이 반복될 수 있어요.
어떻게 개선할지: cooldown 분기에서 pokeCooldownRemaining = remainingMs를 함께 업데이트해 주세요.

수정 예시
 private fun handlePokeCooldown(remainingMs: Long) {
-    reduce { copy(isPoking = false) }
+    reduce {
+        copy(
+            isPoking = false,
+            pokeCooldownRemaining = remainingMs,
+        )
+    }
     tryEmitSideEffect(PhotologDetailSideEffect.ShowPokeCooldownToast(remainingMs))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/photolog/detail/src/main/java/com/twix/photolog/detail/PhotologDetailViewModel.kt`
around lines 201 - 204, The handlePokeCooldown function currently only clears
isPoking and shows a toast but doesn't persist the remaining cooldown to state;
update the reduce call inside handlePokeCooldown to set pokeCooldownRemaining =
remainingMs while keeping isPoking = false, then continue to emit the
PhotologDetailSideEffect.ShowPokeCooldownToast(remainingMs) as before (function:
handlePokeCooldown, state field: pokeCooldownRemaining).

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
feature/onboarding/src/main/java/com/twix/onboarding/OnBoardingViewModel.kt (1)

104-129: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

첫 상태 조회는 지연 없이 시작해 주세요.

Line 111에서 첫 fetchOnboardingStatus() 전에 delay()를 걸어서, 프로필 단계 직후 상태가 COMPLETED로 바뀐 레이스에서는 D-day 화면이 최대 3초 동안 노출됩니다. 요구사항이 “즉시” 홈 이동이라면 첫 회 조회는 바로 실행하고, 이후 반복에만 interval을 두는 쪽이 안전합니다.

예시 수정
     onboardingStatusJob =
         viewModelScope.launch {
             var consecutiveFailureCount = 0

             while (isActive) {
-                delay(POLLING_INTERVAL_MS)
                 when (val result = onBoardingRepository.fetchOnboardingStatus()) {
                     is AppResult.Success -> {
                         consecutiveFailureCount = 0
                         if (onStatusFetched(result.data)) {
                             onboardingStatusJob = null
                             break
                         }
                     }

                     is AppResult.Error -> {
                         if (++consecutiveFailureCount >= MAX_POLLING_FAILURE_COUNT) {
                             onboardingStatusJob = null
                             break
                         }
                     }
                 }
+                delay(POLLING_INTERVAL_MS)
             }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feature/onboarding/src/main/java/com/twix/onboarding/OnBoardingViewModel.kt`
around lines 104 - 129, The polling currently delays before the first fetch
which can hide an immediate COMPLETED status; in startStatusPolling, call
onBoardingRepository.fetchOnboardingStatus() immediately once (invoking
onStatusFetched with the result and handling success/error and
consecutiveFailureCount), and then enter the while(isActive) loop that delays
POLLING_INTERVAL_MS before subsequent fetches; ensure you still set
onboardingStatusJob = null when onStatusFetched returns true or when
consecutiveFailureCount >= MAX_POLLING_FAILURE_COUNT, and preserve existing
error handling and job lifecycle behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@feature/onboarding/src/main/java/com/twix/onboarding/OnBoardingViewModel.kt`:
- Around line 104-129: The polling currently delays before the first fetch which
can hide an immediate COMPLETED status; in startStatusPolling, call
onBoardingRepository.fetchOnboardingStatus() immediately once (invoking
onStatusFetched with the result and handling success/error and
consecutiveFailureCount), and then enter the while(isActive) loop that delays
POLLING_INTERVAL_MS before subsequent fetches; ensure you still set
onboardingStatusJob = null when onStatusFetched returns true or when
consecutiveFailureCount >= MAX_POLLING_FAILURE_COUNT, and preserve existing
error handling and job lifecycle behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 272e56f6-d422-429a-bb33-2e4ced3d4065

📥 Commits

Reviewing files that changed from the base of the PR and between 374a8dd and 0ba0a22.

📒 Files selected for processing (4)
  • core/design-system/src/main/res/values/strings.xml
  • feature/onboarding/src/main/java/com/twix/onboarding/OnBoardingViewModel.kt
  • feature/onboarding/src/main/java/com/twix/onboarding/contract/OnBoardingIntent.kt
  • feature/onboarding/src/main/java/com/twix/onboarding/dday/DdayRoute.kt
✅ Files skipped from review due to trivial changes (1)
  • core/design-system/src/main/res/values/strings.xml

Copy link
Copy Markdown
Contributor

@dogmania dogmania left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다~!

@chanho0908 chanho0908 merged commit dbbd63a into develop May 30, 2026
7 checks passed
@chanho0908 chanho0908 deleted the feature/#162-onboarding-anniversary-policy branch May 30, 2026 06:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

온보딩 기념일 정책 변경

2 participants