Skip to content

통계 상세 화면 기본 세팅#97

Merged
chanho0908 merged 45 commits intodevelopfrom
feat/#88-stats-detail
Feb 25, 2026
Merged

통계 상세 화면 기본 세팅#97
chanho0908 merged 45 commits intodevelopfrom
feat/#88-stats-detail

Conversation

@chanho0908
Copy link
Member

이슈 번호

#88

작업내용

  • API 연동전 UI 구현을 위한 FakeRepository 구현
  • 통계 상세 화면 Calendar 구현
  • UiState, Intent, SideEffect 기본 구조 구현
  • 통계 목록 화면 -> 통계 상세 화면 이동 기능 구현

결과물

캘린더 Topbar
스크린샷 2026-02-24 오전 10 14 22 스크린샷 2026-02-24 오전 10 16 28

리뷰어에게 추가로 요구하는 사항 (선택)

PR이 너무 커져서 나누기 위해 간단한 세팅 먼저 PR 만들었어 !

- `StatsDetailGraph` 및 `StatsDetailRoute`를 `NavRoutes`에 정의
- `StatsDetailRoute`에 `goalId`와 `date`를 인자로 받는 `createRoute` 함수 추가
- `StatsRepository`에 월별 통계 상세 정보를 조회하는 `fetchStatsDetail` 추가
- 테스트용 `FakeStatsRepository`에 `fetchStatsDetail` 구현
- `StatsRoute`에 `navigateToDetail` 람다 함수를 추가하여 화면 전환 로직을 구현
- 각 통계 카드(진행 중, 완료)에 `goalId`와 `LocalDate`를 전달하는 클릭 이벤트 핸들러 추가
- `statsDetailModule`을 새로 추가하고 `NavGraphContributor`를 주입하여 통계 상세 화면으로의 네비게이션을 구현
- `FeatureModules`에 `statsDetailModule`을 추가하여 앱 전체에 적용
@chanho0908 chanho0908 self-assigned this Feb 24, 2026
@chanho0908 chanho0908 added the Feature Extra attention is needed label Feb 24, 2026
@chanho0908
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 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.

Copy link

@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: 8

Caution

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

⚠️ Outside diff range comments (2)
core/design-system/src/main/java/com/twix/designsystem/components/calendar/CalendarNavigator.kt (2)

40-49: ⚠️ Potential issue | 🟠 Major

비활성화된 화살표가 여전히 클릭 이벤트를 발생시킵니다

hasPrevious = false일 때 화살표가 시각적으로 회색으로 처리되지만, noRippleClickable(onClick = onPreviousMonth)는 조건 없이 항상 적용되어 있습니다. 즉, 사용자가 비활성화된 화살표를 탭하면 onPreviousMonth가 그대로 호출됩니다.

이는 시각적 피드백과 실제 동작이 불일치하는 UX 버그입니다. 아래와 같이 hasPreviousfalse일 때 콜백이 발동되지 않도록 처리하는 방법을 권장합니다:

🐛 수정 제안
 Image(
     painter = painterResource(R.drawable.ic_arrow_m_left),
     contentDescription = "previous month",
     modifier =
         Modifier
-            .noRippleClickable(onClick = onPreviousMonth)
+            .then(
+                if (hasPrevious) Modifier.noRippleClickable(onClick = onPreviousMonth)
+                else Modifier
+            )
             .padding(6.dp)
             .size(24.dp),
     colorFilter = ColorFilter.tint(if (hasPrevious) GrayColor.C500 else GrayColor.C200),
 )

같은 방식으로 Line 58–66의 hasNext 화살표에도 동일한 수정이 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/calendar/CalendarNavigator.kt`
around lines 40 - 49, The left and right arrow Image composables in
CalendarNavigator.kt use noRippleClickable unconditionally so tapping a visually
disabled arrow still calls onPreviousMonth/onNextMonth; update the modifier
application to only attach the clickable when hasPrevious/hasNext is true (or
wrap the callback to a no-op when false) so the onPreviousMonth and onNextMonth
callbacks are not invoked for disabled arrows, and keep the existing tint logic
(GrayColor.C200 when disabled) unchanged; apply the same change for the hasNext
branch (lines referencing onPreviousMonth, onNextMonth, hasPrevious, hasNext,
and the Image modifiers).

40-48: ⚠️ Potential issue | 🟡 Minor

접근성(Accessibility): 비활성화 상태가 contentDescription에 반영되어 있지 않습니다

현재 contentDescription = "previous month" / "next month"로 고정되어 있어, 접근성 서비스(TalkBack 등)가 비활성화된 화살표도 일반 버튼과 동일하게 안내합니다. 비활성화 상태일 때 사용자에게 의미 있는 정보를 제공하거나, 클릭 불가 요소로 명확히 표시하는 것이 좋습니다.

-contentDescription = "previous month",
+contentDescription = if (hasPrevious) "previous month" else "previous month (disabled)",

Also applies to: 58-66

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/calendar/CalendarNavigator.kt`
around lines 40 - 48, Update the Image composables used for month navigation
(the ones using painterResource(R.drawable.ic_arrow_m_left)/ic_arrow_m_right and
modifiers with
noRippleClickable(onPreviousMonth)/noRippleClickable(onNextMonth)) so their
contentDescription reflects disabled state instead of a fixed "previous
month"/"next month": conditionally set contentDescription to include "disabled"
(e.g., "previous month, disabled") or set it to null and add a semantics
modifier marking the element as disabled when hasPrevious/hasNext is false, and
ensure the semantics/role reflects non-clickable when disabled so accessibility
services announce the correct state.
🧹 Nitpick comments (11)
feature/stats/detail/src/main/java/com/yapp/detail/contract/StatsDetailSideEffect.kt (1)

6-12: ShowToast.message 타입 안정성 강화 제안
현재 message: Int는 문자열 리소스 ID로 보이는데, 타입만으로는 보장이 없어 잘못된 리소스나 임의의 Int가 들어갈 위험이 있습니다. 리소스 ID를 의도하신 거라면 @StringRes를 붙이거나 아예 String으로 바꿔 전달 책임을 명확히 하는 편이 안전합니다. 이런 방향이 맞는지 확인해 주실 수 있을까요?

🔧 예시 수정안
+import androidx.annotation.StringRes
 import com.twix.designsystem.components.toast.model.ToastType
 import com.twix.ui.base.SideEffect

 sealed interface StatsDetailSideEffect : SideEffect {
     data class ShowToast(
-        val message: Int,
+        `@StringRes` val message: Int,
         val type: ToastType,
     ) : StatsDetailSideEffect
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/yapp/detail/contract/StatsDetailSideEffect.kt`
around lines 6 - 12, ShowToast의 message가 Int로 선언되어 있어 리소스 ID가 아닌 임의의 정수가 들어갈 수
있으니 타입 안정성을 강화해 주세요: 의도대로 문자열 리소스 ID를 전달하려면 StatsDetailSideEffect.ShowToast의
message에 `@StringRes` 어노테이션을 추가해 컴파일 시 검증되게 하거나, 리소스를 호출한 쪽에서 문자열을 resolve하도록 하고
message 타입을 String으로 변경해 호출 책임을 명확히 하세요; 변경 대상는 sealed interface
StatsDetailSideEffect 내 data class ShowToast의 message 프로퍼티입니다.
feature/stats/detail/src/main/java/com/yapp/detail/contract/StatsDetailIntent.kt (1)

5-5: StatsDetailIntent에 사용자 액션이 아직 정의되지 않았습니다.

현재 sealed interface가 비어 있어, 실질적인 사용자 액션 계약이 존재하지 않습니다. MVI 가이드라인에서는 "Intent가 사용자 액션 단위로 명확히 정의되어 있는가?" 를 중요 기준으로 삼고 있습니다.

PR 스크린샷을 보면, 이미 달력 UI에서 월 이동(이전/다음)날짜 선택 등의 인터랙션이 보입니다. 향후 PR에서 아래와 같은 Intent 항목들을 추가해 주시면 계약이 명확해질 것 같습니다:

 sealed interface StatsDetailIntent : Intent {
+    data object OnPreviousMonthClick : StatsDetailIntent
+    data object OnNextMonthClick : StatsDetailIntent
+    data class OnDateSelected(val date: LocalDate) : StatsDetailIntent
 }

초기 스켈레톤 세팅임을 감안해 지금 당장 필수는 아니지만, 다음 PR에서 UI 인터랙션이 추가될 때 함께 채워 주시면 리뷰하기 훨씬 수월할 것 같습니다. 😊

As per coding guidelines, feature/** 경로에 대해 "Intent가 사용자 액션 단위로 명확히 정의되어 있는가?" 를 확인합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/yapp/detail/contract/StatsDetailIntent.kt`
at line 5, The sealed interface StatsDetailIntent is currently empty—define
explicit user-action intents to represent calendar interactions (e.g., month
navigation and date selection) so the MVI contract is clear: add implementations
under StatsDetailIntent such as objects for NavigateToPreviousMonth and
NavigateToNextMonth and a data class (e.g., DateSelected) carrying the selected
LocalDate (or equivalent) and optionally an Initialize/Load intent for initial
screen load; update any callers (view/event handlers) to emit these specific
intents instead of raw events.
domain/src/main/java/com/twix/domain/model/stats/detail/StatsSummary.kt (1)

16-27: EMPTYstartDateLocalDate.now() 사용 — 날짜가 고정될 수 있습니다

companion objectval EMPTY는 JVM에서 클래스가 처음 로드될 때 딱 한 번만 초기화됩니다. 즉, LocalDate.now()는 앱 실행 시점의 날짜로 고정되고, 이후에 EMPTY를 참조해도 날짜가 갱신되지 않습니다.

이는 테스트 환경에서 날짜에 의존하는 단위 테스트를 작성할 때 문제가 되거나, 자정을 넘어 앱이 실행 중인 경우 예기치 않은 동작을 일으킬 수 있습니다.

EMPTY의 의도가 "빈 기본값(sentinel)"이라면, 아래 두 가지 방식 중 하나를 권장합니다:

♻️ 개선 방법

방법 1: 고정된 sentinel 날짜 사용 (불변, 결정적)

-startDate = LocalDate.now(),
+startDate = LocalDate.of(1970, 1, 1),

방법 2: fun empty() 팩토리 함수로 변경 (매번 새 인스턴스 생성)

-companion object {
-    val EMPTY =
-        StatsSummary(
-            myNickname = "",
-            partnerNickname = "",
-            totalCount = 0,
-            myCompletedCount = 0,
-            partnerCompletedCount = 0,
-            repeatCycle = RepeatCycle.DAILY,
-            startDate = LocalDate.now(),
-            endDate = null,
-        )
-}
+companion object {
+    fun empty() = StatsSummary(
+        myNickname = "",
+        partnerNickname = "",
+        totalCount = 0,
+        myCompletedCount = 0,
+        partnerCompletedCount = 0,
+        repeatCycle = RepeatCycle.DAILY,
+        startDate = LocalDate.now(),
+        endDate = null,
+    )
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@domain/src/main/java/com/twix/domain/model/stats/detail/StatsSummary.kt`
around lines 16 - 27, The EMPTY companion val in StatsSummary currently sets
startDate = LocalDate.now() at class-load time which freezes the date; change
this to a safe sentinel by either (A) using a fixed constant date (e.g.,
LocalDate.MIN or a clearly documented constant) for EMPTY.startDate, or (B)
convert EMPTY into a factory function fun empty(): StatsSummary that constructs
a new instance with startDate = LocalDate.now() each call; update the companion
object to replace val EMPTY with your chosen approach and ensure callers use
StatsSummary.EMPTY or StatsSummary.empty() accordingly.
core/design-system/src/main/java/com/twix/designsystem/components/stats/PictureDayCell.kt (1)

41-42: borderColorelse 분기가 사용되지 않는 코드입니다

borderColorif (showBackgroundCard) 블록 안에서만 사용되므로, showBackgroundCardfalse일 때 계산되는 GrayColor.C400은 실제로 참조되지 않는 데드 코드입니다.

현재 구조를 보면 hasImagetrue이지만 showBackgroundCardfalse인 경우(이미지가 하나만 있는 경우)에도 테두리가 전혀 표시되지 않습니다. 이것이 의도된 디자인이라면 변수를 단순화하거나 인라인하는 게 더 명확합니다. 만약 단일 이미지 케이스에도 테두리를 보여주려는 의도였다면 Modifier 조건을 수정해야 합니다.

♻️ 의도가 "백그라운드 카드일 때만 흰색 테두리"라면 단순화 가능
-val borderColor = if (showBackgroundCard) CommonColor.White else GrayColor.C400
 ...
 .then(
     if (showBackgroundCard) {
-        Modifier.border(1.dp, borderColor, cornerShape)
+        Modifier.border(1.dp, CommonColor.White, cornerShape)
     } else {
         Modifier
     },
 ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/stats/PictureDayCell.kt`
around lines 41 - 42, The else branch for borderColor is dead because
borderColor is only used when showBackgroundCard is true; locate the borderColor
declaration and its usage (borderColor, showBackgroundCard, hasImage in
PictureDayCell) and either inline/simplify by setting borderColor =
CommonColor.White only when showBackgroundCard is true (removing the
GrayColor.C400 else) or change the Modifier condition so the border is applied
when you also want a border for single-image cases (e.g., use hasImage ||
showBackgroundCard to decide on the border color) — pick the approach matching
the intended design and adjust the border usage accordingly.
domain/src/main/java/com/twix/domain/model/stats/detail/StatsDetail.kt (1)

15-26: EMPTYmonthDateLocalDate.now() 사용 시 주의가 필요합니다.

companion objectval EMPTY는 클래스 로드 시점에 한 번만 평가됩니다. 따라서 LocalDate.now()가 클래스 최초 접근 시점의 날짜로 고정되어, 이후에는 항상 같은 값을 반환합니다.

왜 문제인가:

  • 테스트에서 비결정적(non-deterministic) 동작을 유발할 수 있습니다.
  • 앱이 자정을 넘겨 실행되더라도 EMPTY의 날짜는 갱신되지 않습니다.

개선 방향: 고정된 sentinel 값(예: LocalDate.MIN 또는 LocalDate.of(1970, 1, 1))을 사용하거나, EMPTY 대신 팩토리 함수를 제공하는 방법을 고려해 보시면 어떨까요?

💡 예시
         val EMPTY =
             StatsDetail(
                 goalId = -1,
                 goalName = "",
                 goalIcon = GoalIconType.DEFAULT,
                 status = "",
-                monthDate = LocalDate.now(),
+                monthDate = LocalDate.MIN,
                 completedDate = emptyList(),
                 statsSummary = StatsSummary.EMPTY,
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@domain/src/main/java/com/twix/domain/model/stats/detail/StatsDetail.kt`
around lines 15 - 26, The companion object singleton EMPTY in StatsDetail
currently sets monthDate = LocalDate.now(), which is evaluated once at class
load and causes non-deterministic/frozen dates; change EMPTY to use a fixed
sentinel date (e.g., LocalDate.MIN or LocalDate.of(1970,1,1)) for the monthDate
field, or instead remove the singleton and add a factory (e.g., a function like
StatsDetail.empty() or StatsDetail.withDefaultDate()) that returns a new
instance using LocalDate.now() when needed; update references to
StatsDetail.EMPTY accordingly if you choose the factory approach.
feature/main/src/main/java/com/twix/stats/component/InProgressStatsContent.kt (1)

63-73: EndStatsContent와 동일한 패턴으로 클릭 처리가 일관되게 구현되어 있습니다.

named parameter 사용과 noRippleClickable modifier 적용이 깔끔합니다. Preview에도 새 파라미터가 반영되어 있어 좋습니다. 👍

한 가지 사소한 제안으로, items 블록의 trailing lambda에서 it을 사용하고 있는데, it.goalId가 클릭 람다 안에서도 쓰이다 보니 가독성 측면에서 named parameter(예: statsGoal ->)를 고려해볼 수 있습니다. 이 부분은 EndStatsContent에서도 동일하게 적용됩니다.

✨ 가독성 개선 예시 (optional)
             items(
                 items = stats.statsGoals,
                 key = { it.goalId },
-            ) {
+            ) { statsGoal ->
                 StatsGoalCard(
-                    statsGoal = it,
+                    statsGoal = statsGoal,
                     showStamp = true,
                     modifier =
                         Modifier
-                            .noRippleClickable(onClick = { onClickStatsCard(it.goalId) }),
+                            .noRippleClickable(onClick = { onClickStatsCard(statsGoal.goalId) }),
                 )
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/main/src/main/java/com/twix/stats/component/InProgressStatsContent.kt`
around lines 63 - 73, The trailing lambda for items(items = stats.statsGoals,
key = { it.goalId }) uses the implicit it which reduces readability when reused
inside the click lambda; change the lambda signature to a named parameter (e.g.,
statsGoal ->) in the items block and update usages inside the block
(StatsGoalCard(statsGoal = statsGoal, ... .noRippleClickable(onClick = {
onClickStatsCard(statsGoal.goalId) }))) and apply the same change in
EndStatsContent so both components consistently use a named parameter instead of
implicit it.
feature/stats/detail/src/main/java/com/yapp/detail/navigation/StatsDetailGraph.kt (1)

12-38: 네비게이션 그래프 등록 구조가 적절합니다.

단일 화면 feature 모듈에서 navigation() 블록 없이 composable()을 직접 등록하는 방식은 심플하고 효과적입니다. nav argument 정의도 ARG_GOAL_ID(LongType)과 ARG_DATE(StringType, nullable)로 명확하게 구성되어 있습니다.

다만, 앞서 StatsDetailRoute.kt에서 언급한 것처럼 nav argument를 composable 블록 내에서 추출하여 StatsDetailRoute에 전달하는 부분이 누락되어 있습니다. 현재 backStackEntry(it)의 arguments에서 goalId와 date를 꺼내지 않고 있으므로, 화면 구현 시 반드시 추가해주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/yapp/detail/navigation/StatsDetailGraph.kt`
around lines 12 - 38, The composable registration in StatsDetailGraph is missing
extraction of nav arguments from the backStackEntry and passing them into
StatsDetailRoute; update the composable lambda to read val goalId =
it.arguments?.getLong(NavRoutes.StatsDetailRoute.ARG_GOAL_ID) and val date =
it.arguments?.getString(NavRoutes.StatsDetailRoute.ARG_DATE) (handling nullable
date and absent goalId appropriately), then pass these values into
StatsDetailRoute along with onBack = navController::popBackStack so
StatsDetailRoute receives the expected goalId and date.
feature/stats/detail/src/main/java/com/yapp/detail/StatsDetailRoute.kt (1)

5-9: 네비게이션 인자(goalId, date)가 Route에 전달되지 않고 있습니다.

StatsDetailGraph에서 ARG_GOAL_ID(Long)과 ARG_DATE(String?)를 네비게이션 인자로 정의하고 있지만, StatsDetailRoute에는 onBack 콜백만 전달되고 있습니다. 초기 세팅이라 빈 상태인 것은 이해하지만, 이후 ViewModel이나 화면 구성 시 이 인자들이 반드시 필요하므로 지금부터 파라미터로 받아두면 나중에 누락 없이 연결할 수 있습니다.

StatsDetailGraph.kt의 composable 블록에서 backStackEntry를 통해 인자를 추출하고 Route에 전달하는 패턴을 미리 준비해두시면 좋겠습니다.

💡 제안하는 방향

StatsDetailRoute 시그니처 확장:

 `@Composable`
 fun StatsDetailRoute(
+    goalId: Long,
+    date: String?,
     onBack: () -> Unit,
 ) {
 }

StatsDetailGraph.kt에서 인자 전달:

 ) {
+    val goalId = it.arguments?.getLong(NavRoutes.StatsDetailRoute.ARG_GOAL_ID) ?: return@composable
+    val date = it.arguments?.getString(NavRoutes.StatsDetailRoute.ARG_DATE)
     StatsDetailRoute(
+        goalId = goalId,
+        date = date,
         onBack = navController::popBackStack,
     )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/stats/detail/src/main/java/com/yapp/detail/StatsDetailRoute.kt`
around lines 5 - 9, StatsDetailRoute currently only accepts onBack but must also
receive the navigation args: add parameters goalId: Long and date: String? to
the StatsDetailRoute signature and use them in the UI/ViewModel setup; in the
composable that registers the route (StatsDetailGraph) extract ARG_GOAL_ID and
ARG_DATE from backStackEntry (using the same keys ARG_GOAL_ID and ARG_DATE) and
pass those values into StatsDetailRoute when calling it so the route receives
the required arguments for future ViewModel/screen wiring.
feature/stats/detail/build.gradle.kts (1)

5-6: Android namespace와 소스 파일의 package를 정렬하세요.

현재 빌드 파일의 namespace는 com.yapp.stats.detail로 설정되어 있지만, 모든 소스 파일의 package 선언은 com.yapp.detail 형태입니다 (예: StatsDetailRoute.kt, StatsDetailGraph.kt, StatsDetailModule.kt 등이 모두 com.yapp.detail.* 패키지 사용).

namespace와 package명이 일치하지 않으면 R 클래스 참조 시 어느 네임스페이스에서 자동 생성되는지 혼란을 줄 수 있고, 모듈 간 패키지 충돌 가능성도 있습니다. 다음 중 하나로 정렬하는 것을 권장합니다:

  • Option 1: namespace를 com.yapp.detail로 수정
  • Option 2: 모든 소스 파일의 package를 com.yapp.stats.detail.*로 통일

특히 모듈 경로가 feature/stats/detail이므로, Option 2로 package를 com.yapp.stats.detail로 통일하면 모듈 구조와의 일관성도 높아집니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/stats/detail/build.gradle.kts` around lines 5 - 6, The Android module
namespace in build.gradle.kts is com.yapp.stats.detail but the Kotlin source
files (e.g., StatsDetailRoute.kt, StatsDetailGraph.kt, StatsDetailModule.kt)
declare package com.yapp.detail; align them by either changing the namespace
value in the android { namespace = "..."} to com.yapp.detail or (preferred for
module path consistency) update the package declarations in those source files
to com.yapp.stats.detail so package and namespace match across the module.
feature/stats/detail/src/main/java/com/yapp/detail/component/StatsDetailTopbar.kt (1)

40-41: onClickAction 콜백이 두 가지 서로 다른 역할을 담당합니다.

isInProgressStatsDetail = true일 때는 팝업 토글, false일 때는 삭제 액션으로 사용됩니다. 동일한 콜백이 상황에 따라 다른 역할을 하면 ViewModel의 Intent 처리 시 어떤 동작을 해야 하는지 구분하기 어렵고, 향후 요구사항 변화 시 혼란을 초래할 수 있습니다.

onTogglePopupMenu: () -> UnitonClickDelete: () -> Unit처럼 명시적으로 분리하는 것을 고려해보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/yapp/detail/component/StatsDetailTopbar.kt`
around lines 40 - 41, The onClickAction callback in StatsDetailTopbar is being
used for two different responsibilities depending on isInProgressStatsDetail
(toggling a popup vs performing delete), which makes intent handling ambiguous;
replace the single onClickAction with two explicit callbacks (e.g.,
onTogglePopupMenu: () -> Unit and onClickDelete: () -> Unit) in the
StatsDetailTopbar signature and body (use onTogglePopupMenu when
isInProgressStatsDetail is true, onClickDelete when false), then update all call
sites and the ViewModel/intent wiring to call the correct new callback so each
action has a single clear handler.
feature/stats/detail/src/main/java/com/yapp/detail/preview/StatsDetailUiStatePreviewProvider.kt (1)

13-74: LocalDate.now() 사용으로 프리뷰 데이터가 매일 달라집니다.

LocalDate.now()를 기반으로 날짜를 계산하면 프리뷰가 날마다 다른 결과를 보여줍니다. 특히 캘린더 UI처럼 날짜에 민감한 컴포넌트에서는 고정된 날짜를 사용하는 것이 재현 가능하고 안정적인 프리뷰를 보장합니다.

+private val fixedDate = LocalDate.of(2026, 2, 1)

 private val completedDates =
     listOf(
         CompletedDate(
-            date = LocalDate.now().minusDays(1),
+            date = fixedDate.minusDays(1),
             ...
         ),
         ...
     )

또한 이 클래스는 프리뷰 전용이므로 모듈 외부에서 불필요하게 노출되지 않도록 internal로 선언하는 것도 고려해보세요.

-class StatsDetailUiStatePreviewProvider : PreviewParameterProvider<StatsDetailUiState> {
+internal class StatsDetailUiStatePreviewProvider : PreviewParameterProvider<StatsDetailUiState> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/yapp/detail/preview/StatsDetailUiStatePreviewProvider.kt`
around lines 13 - 74, The preview uses LocalDate.now() in many places
(completedDates, baseDetail.monthDate/startDate,
baseCalendarUiModel.currentDate, StatsDetailUiState.selectedDate) which makes
previews non-deterministic; replace all LocalDate.now() usages in
StatsDetailUiStatePreviewProvider with a fixed constant date (e.g., val
fixedDate = LocalDate.of(2025, 1, 15)) and compute minusDays/minusMonths from
that fixedDate for completedDates, baseDetail.monthDate/startDate,
baseCalendarUiModel creation and the selectedDate values, and also change the
class declaration of StatsDetailUiStatePreviewProvider to internal so the
preview provider is not exposed outside the module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/yapp/twix/di/FeatureModules.kt`:
- Line 12: The import for statsDetailModule is using the wrong package
namespace; update the import in FeatureModules.kt to reference the module's
actual package com.yapp.stats.detail.di and ensure the statsDetailModule symbol
is imported from there so the DI package mirrors the project's namespace
convention (replace import com.yapp.detail.di.statsDetailModule with import
com.yapp.stats.detail.di.statsDetailModule).

In
`@core/design-system/src/main/java/com/twix/designsystem/components/stats/PictureDayCell.kt`:
- Around line 31-37: PictureDayCell에 Preview가 빠져 있으니 PictureDayCell을 호출하는
`@Preview` Composable을 추가하세요; 예를 들어 PictureDayCellPreview라는
`@Preview`(showBackground = true) 함수 내에서 TwixTheme로 감싸고 Row로 여러 상태(완료 없음,
myImageUrl만 있는 CompletedDate 등)를 렌더링하여 date, completed, onDateSelected, modifier
인자를 전달해 미리보기를 확인할 수 있게 만드세요; Preview 이름과 내부 케이스는 PictureDayCell, CompletedDate
같은 식별자를 사용해 쉽게 찾을 수 있게 구현하세요.
- Around line 45-49: The Box in PictureDayCell is clickable via
noRippleClickable but lacks accessibility semantics; update PictureDayCell to
add semantics on the clickable container (use
androidx.compose.ui.semantics.semantics and set contentDescription with the date
label and semantics { this.role = Role.Button } or semantics {
contentDescription = ...; role = Role.Button }) so TalkBack announces which date
and that it is clickable, and import androidx.compose.ui.semantics.semantics and
androidx.compose.ui.semantics.Role accordingly.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/stats/StatsCalendar.kt`:
- Around line 41-64: The layout misalignment comes from inconsistent Modifiers
inside the Row: PictureDayCell uses Modifier.weight(1f) while null placeholders
(Box with Modifier.size(44.dp)) and trailing empty boxes use fixed size, so
cells get different widths; update the null branch in the uiModel.weeks.forEach
loop and the trailing repeat block to use Box(modifier = Modifier.weight(1f))
(matching PictureDayCell’s Modifier.weight(1f)) so every column shares equal
width; touch the code around PictureDayCell, the null placeholder Box, and the
trailing repeat to ensure all calendar cells use Modifier.weight(1f).

In
`@feature/stats/detail/src/main/java/com/yapp/detail/component/StatsDetailTopbar.kt`:
- Around line 91-99: The Image in StatsDetailTopbar is interactive but sets
contentDescription = null, which breaks accessibility; change it to provide a
meaningful description (preferably via stringResource) and wire it into the
Image's contentDescription (e.g., a "more options" or "open menu" label) so
TalkBack users know the control's purpose; locate the Image
painterResource(R.drawable.ic_meatball) with Modifier.noRippleClickable(onClick
= onClickAction) and replace the null description with an appropriate
stringResource reference.
- Around line 50-58: In StatsDetailTopbar, the Image composable uses a hardcoded
contentDescription and has the click modifier applied after size, making the
clickable area only 24×24 dp; replace the hardcoded string with a string
resource via stringResource(R.string.<appropriate_back>) for accessibility and
localization, and move the noRippleClickable(onClick = onBack) modifier to come
before padding (i.e., apply clickable then padding/size) so the touch target
covers the full padded area (meeting the 48dp minimum).
- Around line 101-103: The popup anchor currently uses hardcoded pixel offsets
(IntOffset(x = -100, y = 140)) which vary with screen density; in
StatsDetailTopbar where CommonPopup is called (visible = popupMenuVisibility,
anchorOffset = ...), replace the literal IntOffset with values converted from Dp
to pixels using LocalDensity.current (e.g. convert Dp(-100).dp and 140.dp via
LocalDensity.current.run { dp.toPx().roundToInt() } or equivalent) so the
anchorOffset uses density-correct IntOffset; ensure you import
androidx.compose.ui.unit.dp and kotlin.math.roundToInt and perform the
conversion in a composable scope before passing anchorOffset to CommonPopup.

In
`@feature/stats/detail/src/main/java/com/yapp/detail/contract/StatsDetailUiState.kt`:
- Around line 15-16: The computed property isInProgressStatsDetail (based on
selectedDate) can be false during first composition because selectedDate
defaults to null, causing StatsDetailTopbar to briefly show the wrong action;
instead add an explicit boolean (e.g., isInProgress) to the State or derive the
value from detail.status inside StatsDetailUiState (replace selectedDate != null
usage), and update the ViewModel to initialize and emit State with that explicit
in-progress value (or compute it from detail.status) so the top bar consistently
shows the correct UI from first composition.

---

Outside diff comments:
In
`@core/design-system/src/main/java/com/twix/designsystem/components/calendar/CalendarNavigator.kt`:
- Around line 40-49: The left and right arrow Image composables in
CalendarNavigator.kt use noRippleClickable unconditionally so tapping a visually
disabled arrow still calls onPreviousMonth/onNextMonth; update the modifier
application to only attach the clickable when hasPrevious/hasNext is true (or
wrap the callback to a no-op when false) so the onPreviousMonth and onNextMonth
callbacks are not invoked for disabled arrows, and keep the existing tint logic
(GrayColor.C200 when disabled) unchanged; apply the same change for the hasNext
branch (lines referencing onPreviousMonth, onNextMonth, hasPrevious, hasNext,
and the Image modifiers).
- Around line 40-48: Update the Image composables used for month navigation (the
ones using painterResource(R.drawable.ic_arrow_m_left)/ic_arrow_m_right and
modifiers with
noRippleClickable(onPreviousMonth)/noRippleClickable(onNextMonth)) so their
contentDescription reflects disabled state instead of a fixed "previous
month"/"next month": conditionally set contentDescription to include "disabled"
(e.g., "previous month, disabled") or set it to null and add a semantics
modifier marking the element as disabled when hasPrevious/hasNext is false, and
ensure the semantics/role reflects non-clickable when disabled so accessibility
services announce the correct state.

---

Nitpick comments:
In
`@core/design-system/src/main/java/com/twix/designsystem/components/stats/PictureDayCell.kt`:
- Around line 41-42: The else branch for borderColor is dead because borderColor
is only used when showBackgroundCard is true; locate the borderColor declaration
and its usage (borderColor, showBackgroundCard, hasImage in PictureDayCell) and
either inline/simplify by setting borderColor = CommonColor.White only when
showBackgroundCard is true (removing the GrayColor.C400 else) or change the
Modifier condition so the border is applied when you also want a border for
single-image cases (e.g., use hasImage || showBackgroundCard to decide on the
border color) — pick the approach matching the intended design and adjust the
border usage accordingly.

In `@domain/src/main/java/com/twix/domain/model/stats/detail/StatsDetail.kt`:
- Around line 15-26: The companion object singleton EMPTY in StatsDetail
currently sets monthDate = LocalDate.now(), which is evaluated once at class
load and causes non-deterministic/frozen dates; change EMPTY to use a fixed
sentinel date (e.g., LocalDate.MIN or LocalDate.of(1970,1,1)) for the monthDate
field, or instead remove the singleton and add a factory (e.g., a function like
StatsDetail.empty() or StatsDetail.withDefaultDate()) that returns a new
instance using LocalDate.now() when needed; update references to
StatsDetail.EMPTY accordingly if you choose the factory approach.

In `@domain/src/main/java/com/twix/domain/model/stats/detail/StatsSummary.kt`:
- Around line 16-27: The EMPTY companion val in StatsSummary currently sets
startDate = LocalDate.now() at class-load time which freezes the date; change
this to a safe sentinel by either (A) using a fixed constant date (e.g.,
LocalDate.MIN or a clearly documented constant) for EMPTY.startDate, or (B)
convert EMPTY into a factory function fun empty(): StatsSummary that constructs
a new instance with startDate = LocalDate.now() each call; update the companion
object to replace val EMPTY with your chosen approach and ensure callers use
StatsSummary.EMPTY or StatsSummary.empty() accordingly.

In
`@feature/main/src/main/java/com/twix/stats/component/InProgressStatsContent.kt`:
- Around line 63-73: The trailing lambda for items(items = stats.statsGoals, key
= { it.goalId }) uses the implicit it which reduces readability when reused
inside the click lambda; change the lambda signature to a named parameter (e.g.,
statsGoal ->) in the items block and update usages inside the block
(StatsGoalCard(statsGoal = statsGoal, ... .noRippleClickable(onClick = {
onClickStatsCard(statsGoal.goalId) }))) and apply the same change in
EndStatsContent so both components consistently use a named parameter instead of
implicit it.

In `@feature/stats/detail/build.gradle.kts`:
- Around line 5-6: The Android module namespace in build.gradle.kts is
com.yapp.stats.detail but the Kotlin source files (e.g., StatsDetailRoute.kt,
StatsDetailGraph.kt, StatsDetailModule.kt) declare package com.yapp.detail;
align them by either changing the namespace value in the android { namespace =
"..."} to com.yapp.detail or (preferred for module path consistency) update the
package declarations in those source files to com.yapp.stats.detail so package
and namespace match across the module.

In
`@feature/stats/detail/src/main/java/com/yapp/detail/component/StatsDetailTopbar.kt`:
- Around line 40-41: The onClickAction callback in StatsDetailTopbar is being
used for two different responsibilities depending on isInProgressStatsDetail
(toggling a popup vs performing delete), which makes intent handling ambiguous;
replace the single onClickAction with two explicit callbacks (e.g.,
onTogglePopupMenu: () -> Unit and onClickDelete: () -> Unit) in the
StatsDetailTopbar signature and body (use onTogglePopupMenu when
isInProgressStatsDetail is true, onClickDelete when false), then update all call
sites and the ViewModel/intent wiring to call the correct new callback so each
action has a single clear handler.

In
`@feature/stats/detail/src/main/java/com/yapp/detail/contract/StatsDetailIntent.kt`:
- Line 5: The sealed interface StatsDetailIntent is currently empty—define
explicit user-action intents to represent calendar interactions (e.g., month
navigation and date selection) so the MVI contract is clear: add implementations
under StatsDetailIntent such as objects for NavigateToPreviousMonth and
NavigateToNextMonth and a data class (e.g., DateSelected) carrying the selected
LocalDate (or equivalent) and optionally an Initialize/Load intent for initial
screen load; update any callers (view/event handlers) to emit these specific
intents instead of raw events.

In
`@feature/stats/detail/src/main/java/com/yapp/detail/contract/StatsDetailSideEffect.kt`:
- Around line 6-12: ShowToast의 message가 Int로 선언되어 있어 리소스 ID가 아닌 임의의 정수가 들어갈 수
있으니 타입 안정성을 강화해 주세요: 의도대로 문자열 리소스 ID를 전달하려면 StatsDetailSideEffect.ShowToast의
message에 `@StringRes` 어노테이션을 추가해 컴파일 시 검증되게 하거나, 리소스를 호출한 쪽에서 문자열을 resolve하도록 하고
message 타입을 String으로 변경해 호출 책임을 명확히 하세요; 변경 대상는 sealed interface
StatsDetailSideEffect 내 data class ShowToast의 message 프로퍼티입니다.

In
`@feature/stats/detail/src/main/java/com/yapp/detail/navigation/StatsDetailGraph.kt`:
- Around line 12-38: The composable registration in StatsDetailGraph is missing
extraction of nav arguments from the backStackEntry and passing them into
StatsDetailRoute; update the composable lambda to read val goalId =
it.arguments?.getLong(NavRoutes.StatsDetailRoute.ARG_GOAL_ID) and val date =
it.arguments?.getString(NavRoutes.StatsDetailRoute.ARG_DATE) (handling nullable
date and absent goalId appropriately), then pass these values into
StatsDetailRoute along with onBack = navController::popBackStack so
StatsDetailRoute receives the expected goalId and date.

In
`@feature/stats/detail/src/main/java/com/yapp/detail/preview/StatsDetailUiStatePreviewProvider.kt`:
- Around line 13-74: The preview uses LocalDate.now() in many places
(completedDates, baseDetail.monthDate/startDate,
baseCalendarUiModel.currentDate, StatsDetailUiState.selectedDate) which makes
previews non-deterministic; replace all LocalDate.now() usages in
StatsDetailUiStatePreviewProvider with a fixed constant date (e.g., val
fixedDate = LocalDate.of(2025, 1, 15)) and compute minusDays/minusMonths from
that fixedDate for completedDates, baseDetail.monthDate/startDate,
baseCalendarUiModel creation and the selectedDate values, and also change the
class declaration of StatsDetailUiStatePreviewProvider to internal so the
preview provider is not exposed outside the module.

In `@feature/stats/detail/src/main/java/com/yapp/detail/StatsDetailRoute.kt`:
- Around line 5-9: StatsDetailRoute currently only accepts onBack but must also
receive the navigation args: add parameters goalId: Long and date: String? to
the StatsDetailRoute signature and use them in the UI/ViewModel setup; in the
composable that registers the route (StatsDetailGraph) extract ARG_GOAL_ID and
ARG_DATE from backStackEntry (using the same keys ARG_GOAL_ID and ARG_DATE) and
pass those values into StatsDetailRoute when calling it so the route receives
the required arguments for future ViewModel/screen wiring.

ℹ️ Review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3973b11 and 5a19e99.

📒 Files selected for processing (31)
  • app/build.gradle.kts
  • app/src/main/java/com/yapp/twix/di/FeatureModules.kt
  • core/design-system/src/main/java/com/twix/designsystem/components/calendar/CalendarNavigator.kt
  • core/design-system/src/main/java/com/twix/designsystem/components/stats/PictureDayCell.kt
  • core/design-system/src/main/java/com/twix/designsystem/components/stats/StatsCalendar.kt
  • core/design-system/src/main/java/com/twix/designsystem/components/stats/model/StatsCalendarUiModel.kt
  • core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt
  • data/src/main/java/com/twix/data/repository/FakeStatsRepository.kt
  • domain/src/main/java/com/twix/domain/model/stats/detail/CompletedDate.kt
  • domain/src/main/java/com/twix/domain/model/stats/detail/StatsDetail.kt
  • domain/src/main/java/com/twix/domain/model/stats/detail/StatsSummary.kt
  • domain/src/main/java/com/twix/domain/repository/StatsRepository.kt
  • feature/main/src/main/java/com/twix/main/MainScreen.kt
  • feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt
  • feature/main/src/main/java/com/twix/stats/StatsScreen.kt
  • feature/main/src/main/java/com/twix/stats/component/EndStatsContent.kt
  • feature/main/src/main/java/com/twix/stats/component/InProgressStatsContent.kt
  • feature/stats/detail/.gitignore
  • feature/stats/detail/build.gradle.kts
  • feature/stats/detail/consumer-rules.pro
  • feature/stats/detail/proguard-rules.pro
  • feature/stats/detail/src/main/AndroidManifest.xml
  • feature/stats/detail/src/main/java/com/yapp/detail/StatsDetailRoute.kt
  • feature/stats/detail/src/main/java/com/yapp/detail/component/StatsDetailTopbar.kt
  • feature/stats/detail/src/main/java/com/yapp/detail/contract/StatsDetailIntent.kt
  • feature/stats/detail/src/main/java/com/yapp/detail/contract/StatsDetailSideEffect.kt
  • feature/stats/detail/src/main/java/com/yapp/detail/contract/StatsDetailUiState.kt
  • feature/stats/detail/src/main/java/com/yapp/detail/di/StatsDetailModule.kt
  • feature/stats/detail/src/main/java/com/yapp/detail/navigation/StatsDetailGraph.kt
  • feature/stats/detail/src/main/java/com/yapp/detail/preview/StatsDetailUiStatePreviewProvider.kt
  • settings.gradle.kts

- 고정된 `44.dp` 크기를 사용하는 대신 `weight(1f)`를 적용하여 유연하게 채워지도록 수정
- `LocalDensity`를 사용하여 dp 단위를 px로 변환하도록 수정
- `Column`을 `Box`로 변경하고 `contentAlignment`를 사용하여 중앙 정렬
- `com.yapp.detail.navigation.StatsDetailGraph`에서 `com.yapp.stats.detail.navigation.StatsDetailGraph`로 잘못된 임포트 경로를 수정
- `isEnd` 상태에 따라 시작 날짜와 종료 날짜를 다르게 설정
- 종료된 챌린지의 경우 월간 목표 횟수를 15로 수정
- 시작일과 종료일을 고정된 날짜로 설정하여 테스트 데이터 일관성 유지
- 이전/다음 달로 이동이 가능한지 확인하기 위해 `hasNext`와 `hasPrevious` 속성을 추가
- 기존 `isInProgressStatsDetail` 로직을 `selectedDate` null 체크에서 초기 로딩 상태를 나타내는 boolean 값으로 변경
- 이전/다음 달로 이동이 가능한지 확인하기 위해 `hasNext`와 `hasPrevious` 속성을 추가
- 기존 `isInProgressStatsDetail` 로직을 `selectedDate` null 체크에서 초기 로딩 상태를 나타내는 boolean 값으로 변경
@YAPP-Github YAPP-Github deleted a comment from coderabbitai bot Feb 24, 2026
@chanho0908 chanho0908 requested a review from dogmania February 24, 2026 08:21
# Conflicts:
#	feature/main/src/main/java/com/twix/stats/StatsScreen.kt
Base automatically changed from feat/#88-stats-component to develop February 24, 2026 13:45
coderabbitai[bot]

This comment was marked as resolved.

@YAPP-Github YAPP-Github deleted a comment from coderabbitai bot Feb 24, 2026
coderabbitai[bot]

This comment was marked as resolved.

Copy link
Member

@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.

고생하셧습니다!!

uiModel.weeks.forEach { week ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround,
Copy link
Member

Choose a reason for hiding this comment

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

이미 안쪽에서 weight(1f)를 주고 있어서 이 옵션은 제거해도 될 거 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

리뷰 반영 커밋 : 336c69f

Comment on lines 81 to 85
ImageRequest
.Builder(LocalContext.current)
.data(displayImageUrl)
.crossfade(true)
.build(),
Copy link
Member

Choose a reason for hiding this comment

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

이 부분은 따로 remember로 감싸서 재구성될 때마다 불필요하게 객체 생성하지 않도록 수정할 수 있을 거 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

리뷰 반영 커밋 : f8bab45

Copy link
Member

Choose a reason for hiding this comment

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

Image

이게 지금 프리뷰로 봤을 때 팝업 배경에 튀어나온 흰색 부분이 보이는데 확인 부탁드려요!

Copy link
Member Author

Choose a reason for hiding this comment

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

Image

실제 UI에선 흰색 부분이 안 보이는 것 같아 궁금해서 팝업 사용하는 곳을 프리뷰로 확인해봤는데
동일한 현상이 발생하는 것 같아 ! 프리뷰에서만 그러는 것 같은데 왜일까 ... 😅

Copy link
Member

Choose a reason for hiding this comment

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

그르네요...넘어가도 될 거 같아요!

}

android {
namespace = "com.yapp.stats.detail"
Copy link
Member

Choose a reason for hiding this comment

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

다른 feature들은 twix로 되어있는데 여기만 yapp이라서 수정해주세요!

Copy link
Member Author

Choose a reason for hiding this comment

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

리뷰 반영 커밋 : cb4471d

PictureDayCell(
date = date,
completed = uiModel.completedDateMap[date],
onDateSelected = { onSelectedDate(it) },
Copy link
Member

Choose a reason for hiding this comment

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

래퍼 제거해주세요!

Copy link
Member Author

Choose a reason for hiding this comment

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

리뷰 반영 커밋 : 235443b

@YAPP-Github YAPP-Github deleted a comment from coderabbitai bot Feb 25, 2026
coderabbitai[bot]

This comment was marked as resolved.

- `com.yapp.stats.detail` 패키지 이름을 `com.twix.stats.detail`로 변경
@YAPP-Github YAPP-Github deleted a comment from coderabbitai bot Feb 25, 2026
@YAPP-Github YAPP-Github deleted a comment from coderabbitai bot Feb 25, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":404,"request":{"method":"PATCH","url":"https://api.github.com/repos/YAPP-Github/Twix-Android/issues/comments/3957243337","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\n새로운 `stats.detail` 기능 모듈을 추가하여 통계 상세 조회 기능을 구현합니다. 이를 위해 도메인 레이어에 `CompletedDate`, `StatsDetail`, `StatsSummary` 데이터 클래스를 정의하고, 디자인 시스템에 `CalendarNavigator` 개선, `PictureDayCell`, `StatsCalendar` 컴포넌트를 추가합니다. 기존 Stats 화면에 네비게이션 콜백을 연결하고, 새로운 `StatsDetailRoute` 및 관련 라우팅 로직을 네비게이션 계층에 등록합니다. stats.detail 모듈에서 TopBar, UI 상태, DI 설정을 구현합니다.\n\n## Estimated code review effort\n\n🎯 4 (Complex) | ⏱️ ~50 minutes\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 2 | ❌ 1</summary>\n\n### ❌ Failed checks (1 warning)\n\n|     Check name     | Status     | Explanation                                                                          | Resolution                                                                         |\n| :----------------: | :--------- | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- |\n| Docstring Coverage | ⚠️ Warning | Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |\n\n<details>\n<summary>✅ Passed checks (2 passed)</summary>\n\n|     Check name    | Status   | Explanation                                                                                                  |\n| :---------------: | :------- | :----------------------------------------------------------------------------------------------------------- |\n|    Title check    | ✅ Passed | PR 제목 '통계 상세 화면 기본 세팅'은 pull request의 주요 변경 사항을 명확하게 설명하고 있습니다. 통계 상세 화면의 기본 구조 설정이라는 핵심 내용이 간결하게 담겨 있습니다.   |\n| Description check | ✅ Passed | PR 설명에는 이슈 번호(`#88`), 구현된 주요 기능(FakeRepository, 캘린더, MVI 구조, 네비게이션), 그리고 스크린샷이 포함되어 있어 변경 사항과 직접적으로 연관되어 있습니다. |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> 📝 Generate docstrings (stacked PR)\n- [ ] <!-- {\"checkboxId\": \"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98\"} --> 📝 Generate docstrings (commit on current branch)\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `feat/#88-stats-detail`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=YAPP-Github/Twix-Android&utm_content=97)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":1}},"response":{"url":"https://api.github.com/repos/YAPP-Github/Twix-Android/issues/comments/3957243337","status":404,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","content-encoding":"gzip","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Wed, 25 Feb 2026 07:04:40 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","transfer-encoding":"chunked","vary":"Accept-Encoding, Accept, X-Requested-With","x-accepted-github-permissions":"issues=write; pull_requests=write","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-api-version-selected":"2022-11-28","x-github-media-type":"github.v3; format=json","x-github-request-id":"804F:4E1B1:131F0E:518CA5:699E9F07","x-ratelimit-limit":"12500","x-ratelimit-remaining":"12408","x-ratelimit-reset":"1772006127","x-ratelimit-resource":"core","x-ratelimit-used":"92","x-xss-protection":"0"},"data":{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#update-an-issue-comment","status":"404"}}}

Copy link

@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: 1

♻️ Duplicate comments (5)
feature/stats/detail/src/main/java/com/twix/stats/detail/di/StatsDetailModule.kt (1)

11-11: ⚠️ Potential issue | 🟡 Minor

NavGraph qualifier를 Graph 기준으로 통일해 주세요.

왜 문제인가: Line 11에서 StatsDetailRoute.route로 qualifier를 등록하면, StatsDetailGraph 기준으로 조회/확장하는 흐름과 불일치가 생길 수 있습니다.
어떻게 개선하나: qualifier를 StatsDetailGraph.graphRoute(또는 NavRoutes.StatsDetailGraph.route)로 맞춰 일관성을 유지해 주세요.

수정 예시
-        single<NavGraphContributor>(named(NavRoutes.StatsDetailRoute.route)) { StatsDetailGraph }
+        single<NavGraphContributor>(named(StatsDetailGraph.graphRoute)) { StatsDetailGraph }

As per coding guidelines, 3. 의존성 주입 - Koin을 통한 DI가 책임에 맞게 구성되었는가?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/di/StatsDetailModule.kt`
at line 11, The DI qualifier is registered using
NavRoutes.StatsDetailRoute.route but the contributor is StatsDetailGraph; change
the qualifier to the graph-based route to keep lookups consistent (use
StatsDetailGraph.graphRoute or NavRoutes.StatsDetailGraph.route) in the
single<NavGraphContributor> registration so the provider and consumer use the
same unique identifier (update the registration that currently references
NavRoutes.StatsDetailRoute.route to reference the StatsDetailGraph route symbol
instead).
feature/stats/detail/src/main/java/com/twix/stats/detail/navigation/StatsDetailGraph.kt (1)

19-38: ⚠️ Potential issue | 🟠 Major

중첩 그래프 래핑이 없어 graphRoute/startDestination 선언 의도가 코드에 반영되지 않습니다.

왜 문제인가요: 현재 구조는 목적지 하나만 직접 등록하고 있어 NavGraphContributor로 선언한 그래프 경계가 약해집니다. 결과적으로 백스택/그래프 단위 관리가 기대와 달라질 수 있습니다.
어떻게 개선할까요: 기존 composable 등록을 navigation(route, startDestination) 내부로 감싸 중첩 그래프로 명확히 등록해 주세요. 이 방식이면 선언한 graphRoute, startDestination가 실제 동작에 연결됩니다.

🔧 제안 코드
 import androidx.navigation.NavGraphBuilder
 import androidx.navigation.NavHostController
 import androidx.navigation.NavType
 import androidx.navigation.compose.composable
+import androidx.navigation.compose.navigation
 import androidx.navigation.navArgument
 import com.twix.navigation.NavRoutes
 import com.twix.navigation.base.NavGraphContributor
 import com.twix.stats.detail.StatsDetailRoute
@@
     override fun NavGraphBuilder.registerGraph(navController: NavHostController) {
-        composable(
-            route = NavRoutes.StatsDetailRoute.route,
-            arguments =
-                listOf(
-                    navArgument(NavRoutes.StatsDetailRoute.ARG_GOAL_ID) {
-                        type = NavType.LongType
-                    },
-                    navArgument(NavRoutes.StatsDetailRoute.ARG_DATE) {
-                        type = NavType.StringType
-                        nullable = true
-                        defaultValue = null
-                    },
-                ),
-        ) {
-            StatsDetailRoute(
-                onBack = navController::popBackStack,
-            )
+        navigation(
+            route = graphRoute.route,
+            startDestination = startDestination,
+        ) {
+            composable(
+                route = NavRoutes.StatsDetailRoute.route,
+                arguments =
+                    listOf(
+                        navArgument(NavRoutes.StatsDetailRoute.ARG_GOAL_ID) {
+                            type = NavType.LongType
+                        },
+                        navArgument(NavRoutes.StatsDetailRoute.ARG_DATE) {
+                            type = NavType.StringType
+                            nullable = true
+                            defaultValue = null
+                        },
+                    ),
+            ) {
+                StatsDetailRoute(
+                    onBack = navController::popBackStack,
+                )
+            }
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/navigation/StatsDetailGraph.kt`
around lines 19 - 38, The current registerGraph implementation registers a
single composable directly so the declared graph boundary
(graphRoute/startDestination) from NavRoutes.StatsDetailRoute is not applied;
wrap the existing composable(...) call inside a navigation(route =
NavRoutes.StatsDetailRoute.graphRoute, startDestination =
NavRoutes.StatsDetailRoute.startDestination) block so the
NavGraphBuilder.registerGraph creates a nested graph; keep the existing
composable that calls StatsDetailRoute(onBack = navController::popBackStack)
unchanged but move it into the navigation { ... } closure to ensure graph-level
routing and backstack behavior are honored.
feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailRoute.kt (1)

5-7: ⚠️ Potential issue | 🟠 Major

빈 Route 구현으로 상세 화면 진입 시 아무것도 렌더링되지 않습니다.

왜 문제인가요: 네비게이션은 연결되어도 실제 UI가 없어 사용자 입장에서는 “고장난 화면”처럼 보입니다.
어떻게 개선할까요: 최소한 StatsDetailScreen을 연결하거나, 준비 중이라면 임시 플레이스홀더라도 렌더링해 주세요. 지금 단계에서 의도적으로 비워둔 상태라면 연결 시점을 TODO로 남겨두는 방식이 좋겠습니다.

🔧 제안 코드
 import androidx.compose.runtime.Composable

 `@Composable`
 fun StatsDetailRoute(onBack: () -> Unit) {
+    // TODO: 실제 StatsDetailScreen 연결 전까지 임시 UI라도 렌더링
+    StatsDetailScreen(
+        onBack = onBack,
+    )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailRoute.kt`
around lines 5 - 7, StatsDetailRoute is currently empty so navigation lands on a
blank screen; update StatsDetailRoute to render the actual UI (call
StatsDetailScreen(onBack = onBack)) or a temporary placeholder composable
indicating "Coming soon" and keep a TODO comment if intentionally unfinished;
ensure you pass through the onBack lambda to the rendered screen (refer to
StatsDetailRoute and StatsDetailScreen) so the back navigation continues to
work.
feature/stats/detail/src/main/java/com/twix/stats/detail/component/StatsDetailTopbar.kt (2)

91-103: ⚠️ Potential issue | 🟡 Minor

메뉴 아이콘도 접근성 라벨과 터치 영역 보강이 필요합니다.

왜 문제인지: Line 97의 contentDescription = null은 클릭 가능한 요소의 목적을 스크린리더가 전달하지 못합니다. 또한 현재 클릭 영역이 24dp라 접근성 터치 타겟에 불리합니다.
어떻게 개선할지: 의미 있는 문자열 리소스를 넣고, 클릭 처리를 부모 Box(60dp 영역)로 올려 터치 타겟을 넓히는 구성이 좋습니다.

수정 예시
 Box(
     modifier =
         Modifier
-            .fillMaxSize(),
+            .fillMaxSize()
+            .noRippleClickable(onClick = onClickAction),
     contentAlignment = Alignment.Center,
 ) {
     Image(
         painter = painterResource(R.drawable.ic_meatball),
-        contentDescription = null,
+        contentDescription = stringResource(R.string.content_description_more_options),
         modifier =
             Modifier
                 .size(24.dp)
-                .rotate(90f)
-                .noRippleClickable(onClick = onClickAction),
+                .rotate(90f),
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/component/StatsDetailTopbar.kt`
around lines 91 - 103, The menu Image in StatsDetailTopbar currently uses
contentDescription = null and a 24.dp click target; change it to provide a
meaningful accessibility label (use a string resource, e.g.,
R.string.menu_description) and move the click handler from the Image to the
parent Box so the Box handles onClickAction with a larger touch area (wrap Box
with Modifier.size(60.dp).noRippleClickable(onClick = onClickAction) or
equivalent), leaving the Image as a purely decorative element (no clickable) and
ensuring the Box has contentDescription/semantics for screen readers.

50-58: ⚠️ Potential issue | 🟡 Minor

뒤로가기 접근성 문구와 터치 영역을 함께 보완해 주세요.

왜 문제인지: Line 52의 "back" 하드코딩은 로컬라이징/접근성 대응이 어렵고, Line 55-57의 modifier 순서로 인해 실제 클릭 영역이 아이콘(24dp) 중심으로 축소됩니다.
어떻게 개선할지: 문자열 리소스로 교체하고, 클릭 modifier를 padding 앞에 배치해 최소 터치 영역을 확보하는 쪽이 안전합니다.

수정 예시
 Image(
     painter = painterResource(R.drawable.ic_arrow3_left),
-    contentDescription = "back",
+    contentDescription = stringResource(R.string.content_description_back),
     modifier =
         Modifier
+            .noRippleClickable(onClick = onBack)
             .padding(18.dp)
-            .size(24.dp)
-            .noRippleClickable(onClick = onBack),
+            .size(24.dp),
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/component/StatsDetailTopbar.kt`
around lines 50 - 58, Replace the hardcoded contentDescription "back" with a
localized string resource (use stringResource(R.string.some_back_label)) and
move the click modifier so the tappable area includes padding—apply
noRippleClickable(onClick = onBack) (or Modifier.clickable) before padding/size
in the Modifier chain for the Image in StatsDetailTopbar; update the resource
name (e.g., R.string.back) and ensure the Image composable (painterResource,
contentDescription, modifier) uses the reordered modifiers so the touch target
meets minimum accessibility size.
🧹 Nitpick comments (5)
feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailSideEffect.kt (1)

7-10: ShowToastStatsSideEffect와 동일하게 중복 정의되어 있습니다.

feature/main/src/main/java/com/twix/stats/contract/StatsSideEffect.ktShowToast와 구조가 완전히 동일합니다. 현재는 모듈 독립성을 위한 의도적 중복일 수 있지만, 여러 feature 모듈에서 동일한 패턴이 반복될 경우 유지보수 비용이 증가할 수 있습니다.

공통 UI 기반 모듈(core:ui 혹은 designsystem 등)이 이미 존재한다면, 아래와 같이 공용 ShowToast side effect를 해당 모듈로 추출하는 것을 고려해 보실 수 있습니다.

💡 토론 포인트: 앞으로 feature 모듈이 더 추가될 때도 동일 패턴이 반복될 가능성이 높은데, 공통 SideEffect 추상화를 지금 시점에 논의해 보시는 건 어떨까요?

#!/bin/bash
# Description: 공통 ShowToast 또는 유사 구조가 다른 feature 모듈에도 존재하는지 확인
rg -n "data class ShowToast" --type kotlin -A 3
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailSideEffect.kt`
around lines 7 - 10, The ShowToast data class is duplicated between
StatsDetailSideEffect and StatsSideEffect; extract a single shared ShowToast
(e.g., data class ShowToast(val message: Int, val type: ToastType)) into a
common UI module (core:ui or designsystem) and replace the local definitions:
remove the ShowToast in StatsDetailSideEffect, update references to use the
shared ShowToast type, and ensure the shared class implements the appropriate
SideEffect sealed interface (or make it a standalone shared SideEffect type) so
StatsDetailSideEffect and the feature/main StatsSideEffect can reference the
common symbol.
feature/stats/detail/src/main/java/com/twix/stats/detail/preview/StatsDetailUiStatePreviewProvider.kt (2)

14-57: 프리뷰 기준일은 고정값으로 통일하는 것을 권장합니다.

LocalDate.now()를 여러 곳에서 호출하면 날짜 경계 시점에 프리뷰 결과가 흔들릴 수 있습니다. 기준일(referenceDate) 하나를 두고 파생시키면 재현성이 좋아집니다.

예시 변경안
 class StatsDetailUiStatePreviewProvider : PreviewParameterProvider<StatsDetailUiState> {
+    private val referenceDate = LocalDate.of(2026, 2, 1)
+
     private val completedDates =
         listOf(
             CompletedDate(
-                date = LocalDate.now().minusDays(1),
+                date = referenceDate.minusDays(1),
                 myImageUrl = "https://picsum.photos/200",
                 partnerImageUrl = "https://picsum.photos/201",
             ),
             CompletedDate(
-                date = LocalDate.now().minusDays(3),
+                date = referenceDate.minusDays(3),
                 myImageUrl = "https://picsum.photos/202",
                 partnerImageUrl = null,
             ),
             CompletedDate(
-                date = LocalDate.now().minusDays(5),
+                date = referenceDate.minusDays(5),
                 myImageUrl = null,
                 partnerImageUrl = "https://picsum.photos/203",
             ),
         )
@@
-            monthDate = LocalDate.now(),
+            monthDate = referenceDate,
@@
-                    startDate = LocalDate.now().minusMonths(1),
+                    startDate = referenceDate.minusMonths(1),
@@
         StatsCalendarUiModel.create(
-            currentDate = LocalDate.now(),
+            currentDate = referenceDate,
             completedDate = completedDates,
         )
@@
-                selectedDate = LocalDate.now(),
+                selectedDate = referenceDate,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/preview/StatsDetailUiStatePreviewProvider.kt`
around lines 14 - 57, The preview uses LocalDate.now() in multiple places (see
completedDates, baseDetail, baseCalendarUiModel), which can cause
non-deterministic previews; introduce a single referenceDate val (e.g.,
referenceDate = LocalDate.of(...) or LocalDate.now()) at top of the file and
replace all LocalDate.now() usages in CompletedDate(date),
StatsDetail(monthDate, startDate), and StatsCalendarUiModel.create(currentDate,
completedDate derivations) with values derived from that referenceDate so the
preview is stable and reproducible.

18-30: 프리뷰 이미지 URL은 네트워크 비의존 형태도 함께 고려해보면 좋겠습니다.

외부 URL 기반 프리뷰는 오프라인/느린 네트워크에서 화면 확인이 불안정해질 수 있습니다. 로컬 placeholder 리소스나 null 케이스 중심으로도 한 세트를 추가해보는 건 어떨까요?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/preview/StatsDetailUiStatePreviewProvider.kt`
around lines 18 - 30, Update the preview data in
StatsDetailUiStatePreviewProvider to include non-network image cases: add
CompletedDate entries that use null for myImageUrl and partnerImageUrl and at
least one entry that references a local placeholder resource (e.g., a drawable
resource name or local URI) so previews render offline; ensure the preview
consumer code that reads CompletedDate (the preview list used by the provider)
handles nulls and local resource identifiers consistently.
feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailUiState.kt (1)

16-16: 파생 상태명은 실제 의미에 맞게 더 명확히 바꾸는 것을 권장합니다.

현재 값은 “진행중 여부”가 아니라 “날짜 선택 여부”를 나타내므로, 이후 유지보수 시 오해로 잘못된 분기 로직이 생길 수 있습니다. hasSelectedDate 같은 이름으로 바꾸는 방향은 어떨까요?

예시 변경안
-    val isInProgressStatsDetail get() = selectedDate != null
+    val hasSelectedDate: Boolean get() = selectedDate != null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailUiState.kt`
at line 16, The derived-state property isInProgressStatsDetail misleads by
implying "in-progress" when it simply checks selectedDate; rename the property
to something clearer like hasSelectedDate and update its definition to use
selectedDate (e.g., val hasSelectedDate get() = selectedDate != null), then
refactor all usages of isInProgressStatsDetail across the codebase (including
view bindings, viewmodels, tests, and any serialization or mappings) to the new
name to avoid breaking references and keep behavior identical.
feature/stats/detail/src/main/java/com/twix/stats/detail/component/StatsDetailTopbar.kt (1)

154-169: Preview 제공 좋습니다.

Topbar 상태를 빠르게 확인할 수 있게 Preview를 포함한 점이 좋습니다. 이후 케이스별(isInProgressStatsDetail=false, popupMenuVisibility=false) Preview도 추가하면 회귀 확인이 더 쉬워집니다.

As per coding guidelines Compose UI - Preview Composable이 제공되는가?.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/component/StatsDetailTopbar.kt`
around lines 154 - 169, Add additional `@Preview` composables to cover the other
states of StatsDetailTopbar so reviewers can visually verify regressions: create
at least one preview with isInProgressStatsDetail = false (e.g.,
StatsDetailTopbarPreview_NotInProgress) and one with popupMenuVisibility = false
(e.g., StatsDetailTopbarPreview_PopupHidden); each should call StatsDetailTopbar
with the same stub callbacks used in StatsDetailTopbarPreview so the UI
variations for those boolean flags are visible in the IDE.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/component/StatsDetailTopbar.kt`:
- Around line 86-90: The popup uses a fixed IntOffset (popupOffset with
LocalDensity and -40.dp/55.dp) which can misplace the CommonPopup relative to
its trigger; instead, measure the trigger icon's position using
onGloballyPositioned on the Box/Icon composable to capture its window
coordinates, convert those coordinates with LocalDensity if needed, and compute
a dynamic IntOffset for popupOffset based on the measured x/y (and size) so the
CommonPopup is positioned relative to the actual icon location across devices
and rotations.

---

Duplicate comments:
In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/component/StatsDetailTopbar.kt`:
- Around line 91-103: The menu Image in StatsDetailTopbar currently uses
contentDescription = null and a 24.dp click target; change it to provide a
meaningful accessibility label (use a string resource, e.g.,
R.string.menu_description) and move the click handler from the Image to the
parent Box so the Box handles onClickAction with a larger touch area (wrap Box
with Modifier.size(60.dp).noRippleClickable(onClick = onClickAction) or
equivalent), leaving the Image as a purely decorative element (no clickable) and
ensuring the Box has contentDescription/semantics for screen readers.
- Around line 50-58: Replace the hardcoded contentDescription "back" with a
localized string resource (use stringResource(R.string.some_back_label)) and
move the click modifier so the tappable area includes padding—apply
noRippleClickable(onClick = onBack) (or Modifier.clickable) before padding/size
in the Modifier chain for the Image in StatsDetailTopbar; update the resource
name (e.g., R.string.back) and ensure the Image composable (painterResource,
contentDescription, modifier) uses the reordered modifiers so the touch target
meets minimum accessibility size.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/di/StatsDetailModule.kt`:
- Line 11: The DI qualifier is registered using NavRoutes.StatsDetailRoute.route
but the contributor is StatsDetailGraph; change the qualifier to the graph-based
route to keep lookups consistent (use StatsDetailGraph.graphRoute or
NavRoutes.StatsDetailGraph.route) in the single<NavGraphContributor>
registration so the provider and consumer use the same unique identifier (update
the registration that currently references NavRoutes.StatsDetailRoute.route to
reference the StatsDetailGraph route symbol instead).

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/navigation/StatsDetailGraph.kt`:
- Around line 19-38: The current registerGraph implementation registers a single
composable directly so the declared graph boundary (graphRoute/startDestination)
from NavRoutes.StatsDetailRoute is not applied; wrap the existing
composable(...) call inside a navigation(route =
NavRoutes.StatsDetailRoute.graphRoute, startDestination =
NavRoutes.StatsDetailRoute.startDestination) block so the
NavGraphBuilder.registerGraph creates a nested graph; keep the existing
composable that calls StatsDetailRoute(onBack = navController::popBackStack)
unchanged but move it into the navigation { ... } closure to ensure graph-level
routing and backstack behavior are honored.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailRoute.kt`:
- Around line 5-7: StatsDetailRoute is currently empty so navigation lands on a
blank screen; update StatsDetailRoute to render the actual UI (call
StatsDetailScreen(onBack = onBack)) or a temporary placeholder composable
indicating "Coming soon" and keep a TODO comment if intentionally unfinished;
ensure you pass through the onBack lambda to the rendered screen (refer to
StatsDetailRoute and StatsDetailScreen) so the back navigation continues to
work.

---

Nitpick comments:
In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/component/StatsDetailTopbar.kt`:
- Around line 154-169: Add additional `@Preview` composables to cover the other
states of StatsDetailTopbar so reviewers can visually verify regressions: create
at least one preview with isInProgressStatsDetail = false (e.g.,
StatsDetailTopbarPreview_NotInProgress) and one with popupMenuVisibility = false
(e.g., StatsDetailTopbarPreview_PopupHidden); each should call StatsDetailTopbar
with the same stub callbacks used in StatsDetailTopbarPreview so the UI
variations for those boolean flags are visible in the IDE.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailSideEffect.kt`:
- Around line 7-10: The ShowToast data class is duplicated between
StatsDetailSideEffect and StatsSideEffect; extract a single shared ShowToast
(e.g., data class ShowToast(val message: Int, val type: ToastType)) into a
common UI module (core:ui or designsystem) and replace the local definitions:
remove the ShowToast in StatsDetailSideEffect, update references to use the
shared ShowToast type, and ensure the shared class implements the appropriate
SideEffect sealed interface (or make it a standalone shared SideEffect type) so
StatsDetailSideEffect and the feature/main StatsSideEffect can reference the
common symbol.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailUiState.kt`:
- Line 16: The derived-state property isInProgressStatsDetail misleads by
implying "in-progress" when it simply checks selectedDate; rename the property
to something clearer like hasSelectedDate and update its definition to use
selectedDate (e.g., val hasSelectedDate get() = selectedDate != null), then
refactor all usages of isInProgressStatsDetail across the codebase (including
view bindings, viewmodels, tests, and any serialization or mappings) to the new
name to avoid breaking references and keep behavior identical.

In
`@feature/stats/detail/src/main/java/com/twix/stats/detail/preview/StatsDetailUiStatePreviewProvider.kt`:
- Around line 14-57: The preview uses LocalDate.now() in multiple places (see
completedDates, baseDetail, baseCalendarUiModel), which can cause
non-deterministic previews; introduce a single referenceDate val (e.g.,
referenceDate = LocalDate.of(...) or LocalDate.now()) at top of the file and
replace all LocalDate.now() usages in CompletedDate(date),
StatsDetail(monthDate, startDate), and StatsCalendarUiModel.create(currentDate,
completedDate derivations) with values derived from that referenceDate so the
preview is stable and reproducible.
- Around line 18-30: Update the preview data in
StatsDetailUiStatePreviewProvider to include non-network image cases: add
CompletedDate entries that use null for myImageUrl and partnerImageUrl and at
least one entry that references a local placeholder resource (e.g., a drawable
resource name or local URI) so previews render offline; ensure the preview
consumer code that reads CompletedDate (the preview list used by the provider)
handles nulls and local resource identifiers consistently.

ℹ️ Review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8bab45 and cb4471d.

📒 Files selected for processing (9)
  • app/src/main/java/com/yapp/twix/di/FeatureModules.kt
  • feature/stats/detail/build.gradle.kts
  • feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailRoute.kt
  • feature/stats/detail/src/main/java/com/twix/stats/detail/component/StatsDetailTopbar.kt
  • feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailSideEffect.kt
  • feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailUiState.kt
  • feature/stats/detail/src/main/java/com/twix/stats/detail/di/StatsDetailModule.kt
  • feature/stats/detail/src/main/java/com/twix/stats/detail/navigation/StatsDetailGraph.kt
  • feature/stats/detail/src/main/java/com/twix/stats/detail/preview/StatsDetailUiStatePreviewProvider.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • feature/stats/detail/build.gradle.kts

@chanho0908 chanho0908 requested a review from dogmania February 25, 2026 09:40
Copy link
Member

@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 689d800 into develop Feb 25, 2026
17 checks passed
@chanho0908 chanho0908 deleted the feat/#88-stats-detail branch February 25, 2026 15:44
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