Skip to content

목표 생성, 편집 화면 구현#54

Merged
chanho0908 merged 46 commits intodevelopfrom
feat/#47-goal-editor-screen
Feb 5, 2026
Merged

목표 생성, 편집 화면 구현#54
chanho0908 merged 46 commits intodevelopfrom
feat/#47-goal-editor-screen

Conversation

@dogmania
Copy link
Member

@dogmania dogmania commented Feb 4, 2026

이슈 번호

작업내용

  • 목표 생성/수정 UI를 구현했습니다.
  • 목표 생성 상태 처리를 구현했습니다.

결과물

2026-02-04.5.40.17.mov

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

수정은 이후에 홈 화면에서 수정으로 넘어가는 기능 구현할 때 네비게이션 파라미터 넘기는 방식으로 뷰모델에서 상태를 초기화시키는 로직을 구현할 예정입니다!
디자인 상으로는 바텀시트에서 목표 생성으로 넘어가야 하는데 이것도 홈 화면 작업 이후에 바텀시트를 중간에 끼워넣는 걸로 작업할 예정입니다.

@dogmania dogmania requested a review from chanho0908 February 4, 2026 08:41
@dogmania dogmania self-assigned this Feb 4, 2026
@dogmania dogmania added the Feature Extra attention is needed label Feb 4, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

goal-editor 기능 모듈이 새로 추가되었습니다. GoalEditorScreen과 관련 컴포저블(TopBar, TextField, InfoCard, EmojiPicker 등), GoalEditorViewModel, Intent, SideEffect, UiState, 네비게이션(Graph/Route) 구현 및 Koin DI 모듈이 포함되어 있으며, 앱 모듈과 메인/홈 라우트가 해당 모듈을 참조하도록 업데이트되었습니다. 디자인 시스템에는 CommonSwitch, CommonDialog, UnderlineTextField가 추가되고 여러 벡터 drawable과 문자열 리소스가 추가되었으며, 도메인에는 GoalIconType과 RepeatType enum이 추가되었습니다. 입력 처리 보조 유틸(dismissKeyboardOnTap)과 BottomSheet BackHandler 동작 변경도 포함됩니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
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.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경 사항의 핵심을 명확하게 설명하고 있습니다. '목표 생성, 편집 화면 구현'이라는 제목은 구현된 주요 기능(Goal Editor Screen)을 직관적으로 전달합니다.
Description check ✅ Passed PR 설명이 작업 내용과 결과물을 구체적으로 설명하고 있으며, 향후 계획도 명시되어 있어 변경 사항과 충분히 관련되어 있습니다.
Linked Issues check ✅ Passed 연관된 이슈 #47의 TODO 항목인 GoalEditorScreen, ViewModel, UiState, Intent, SideEffect, GoalEditorGraph 구현 요구사항이 모두 충족되었습니다.
Out of Scope Changes check ✅ Passed HomeScreen, MainScreen의 네비게이션 업데이트는 새로운 Goal Editor 기능을 통합하기 위한 필수적인 변경으로, 요구사항 범위 내에 있습니다. CommonBottomSheet의 BackHandler 수정도 디자인시스템 개선으로 범위 내입니다.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#47-goal-editor-screen

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

@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

🤖 Fix all issues with AI agents
In
`@core/design-system/src/main/java/com/twix/designsystem/components/common/CommonSwitch.kt`:
- Around line 41-48: The Box in CommonSwitch.kt uses clickable which doesn't
expose switch semantics; replace clickable with Modifier.toggleable(value =
checked, onValueChange = { onClick(it) }, role = Role.Switch) (and import
androidx.compose.ui.semantics.Role) so TalkBack/VoiceOver announces "switch,
on/off"; ensure the existing size/clip/background/border modifiers remain and
remove the clickable call so the Composable (CommonSwitch) exposes proper
accessibility state via toggleable.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/dialog/CommonDialog.kt`:
- Around line 113-116: The Surface in CommonDialog (around the Surface call
using shape = RoundedCornerShape(20.dp), color = CommonColor.White, and any
usages of GrayColor.C500) hardcodes light colors and breaks Dark Mode; update
the component to use semantic Material 3 colors
(MaterialTheme.colorScheme.surface / onSurface / primary / onPrimary as
appropriate) or the project's TwixTheme CompositionLocals instead of
CommonColor.White and GrayColor.C500 so colors respond to light/dark themes, and
ensure any text/icon colors inside CommonDialog also use semantic colorScheme
values rather than hardcoded colors.
- Around line 36-71: The CommonDialog currently lacks accessibility semantics:
update the dialog surface (inside DialogContent or wherever the dialog
Root/Surface is rendered) to include Modifier.semantics { role = Role.Dialog;
contentDescription = /* meaningful description or title */ } and add semantic
labels for action buttons provided via confirmText/dismissText (e.g., accessible
text or contentDescription for buttons in DialogContent). Also wire a
FocusRequester in CommonDialog (create and remember it, pass to DialogContent)
and requestFocus() when visible becomes true so screen readers and keyboard
focus move into the dialog; keep BackHandler(onDismissRequest) as-is. Locate and
modify the CommonDialog, DialogContent and button renderings to apply these
changes.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt`:
- Around line 39-42: The passed modifier parameter is being applied to internal
children instead of the root Column; move the incoming modifier to the outermost
Column composable (the Column in UnderlineTextField) so callers can control
layout, and replace usages of that parameter inside the internal Box and
HorizontalDivider with the default Modifier (i.e., use Modifier for those
children); update any references in the UnderlineTextField function signature
and usages to ensure modifier is applied only to the root Column.

In `@core/design-system/src/main/res/drawable/ic_plus.xml`:
- Around line 2-10: ic_plus.xml 하드코딩된 strokeColor("#171717") 때문에
GoalEditorScreen.kt에서 ColorFilter.tint(CommonColor.White)을 적용해도 의도대로 색이 바뀌지 않을 수
있으니 아이콘 색상을 리소스로 추출하고 다크/라이트 변형을 제공하도록 수정하세요: ic_plus.xml과 ic_minus.xml의
android:strokeColor 값을 직접 색상 코드 대신 `@color/icon_primary로` 바꾸고
res/values/colors.xml 및 res/values-night/colors.xml에 각각 icon_primary를 `#171717와`
`#FFFFFF로` 정의한 뒤 GoalEditorScreen.kt에서 기존 ColorFilter.tint(CommonColor.White) 사용이
필요 없다면 제거하거나 리소스 기반 색상 로딩으로 통일하세요.

In `@feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt`:
- Around line 309-312: When switching RepeatType (e.g., in the noRippleClickable
handler that sets internalSelectedRepeatType = RepeatType.WEEKLY), don't reset
internalRepeatCount to 0; set it to 1 so the repeat count has a sensible
minimum. Update all similar handlers (the other case at the other occurrence
around the block that currently sets internalRepeatCount = 0) to assign 1
instead, and ensure any UI/state that treats 0 as "unset" is adjusted or
clarified if 0 must remain meaningful.
- Around line 112-116: The internalSelectedIcon state in GoalEditorScreen is
only initialized from uiState.selectedIcon and won't update when
uiState.selectedIcon changes; change the remember usage to include
uiState.selectedIcon as a key (e.g., remember(uiState.selectedIcon) {
mutableStateOf(uiState.selectedIcon) }) so internalSelectedIcon is
re-created/synced when the external uiState.selectedIcon updates, ensuring
correct synchronization when entering edit mode or when the selected icon is
loaded/changed.

In
`@feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt`:
- Around line 64-69: The code block in GoalEditorViewModel uses a hardcoded
Korean toast string and an unnecessary coroutine launch; change the side-effect
to carry a message key (e.g., an enum or sealed class like
GoalEditorSideEffect.ToastMessageKey or a ToastType payload) instead of the raw
string so the UI layer resolves the string resource, and remove the
viewModelScope.launch by calling emitSideEffect(...) directly (since
handleIntent and emitSideEffect are suspend), i.e., when
currentState.endDateEnabled &&
currentState.endDate.isBefore(currentState.startDate) return by emitting a
ToastMessageKey via
emitSideEffect(GoalEditorSideEffect.ShowToast(ToastMessageKey.EndBeforeStart,
ToastType.ERROR)) without wrapping in viewModelScope.launch.
🧹 Nitpick comments (31)
core/design-system/src/main/java/com/twix/designsystem/components/bottomsheet/CommonBottomSheet.kt (3)

159-168: 다크 모드 지원을 위해 하드코딩된 색상 대신 테마 색상 사용을 고려해 주세요.

현재 SurfacecolorCommonColor.White로 하드코딩되어 있어 다크 모드에서 적절히 대응하지 못합니다. 디자인 시스템 컴포넌트는 다양한 테마 환경에서 재사용되므로, 테마 기반 색상을 사용하는 것이 권장됩니다.

Material Design 3에서는 Bottom Sheet에 surfaceContainerLow 또는 surface 색상을 권장합니다. 만약 프로젝트에서 커스텀 테마 시스템을 사용 중이라면, 해당 시스템에서 라이트/다크 모드에 따라 변경되는 색상을 사용하는 것이 좋습니다.

♻️ 제안하는 수정 방향
 Surface(
     shape = config.shape,
     tonalElevation = 0.dp,
     shadowElevation = 16.dp,
-    color = CommonColor.White,
+    color = MaterialTheme.colorScheme.surface, // 또는 프로젝트 테마 색상
     modifier =
         Modifier
             .fillMaxWidth()
             .heightIn(max = sheetMaxHeight),
 )

또는 CommonBottomSheetConfigsheetColor 파라미터를 추가하여 외부에서 제어할 수 있도록 하는 방법도 있습니다.

As per coding guidelines: core/design-system/** - Dark Mode 지원 여부를 검토해야 합니다.


191-204: 접근성 개선을 위해 SheetHandle에 시맨틱 정보 추가를 고려해 보세요.

현재 SheetHandle은 시각적 요소만 있고 접근성 정보가 없습니다. 스크린 리더 사용자에게 이 요소가 드래그 핸들임을 알려주거나, 순수 장식용이라면 Modifier.semantics { invisibleToUser() }를 통해 스크린 리더에서 건너뛰도록 설정할 수 있습니다.

♻️ 접근성 개선 예시
 `@Composable`
 private fun SheetHandle(modifier: Modifier = Modifier) {
     Box(
         modifier =
             modifier
                 .padding(vertical = 11.dp)
                 .width(44.dp)
                 .height(6.dp)
+                .semantics { invisibleToUser() } // 장식용 요소로 표시
                 .background(
                     color = GrayColor.C100,
                     shape = RoundedCornerShape(2.55.dp),
                 ),
     )
 }

또는 드래그 기능이 있다면:

.semantics { 
    contentDescription = "드래그하여 시트 닫기"
}

As per coding guidelines: core/design-system/** - 접근성(Accessibility) 고려 여부를 검토해야 합니다.


71-75: 애니메이션 지속 시간을 상수로 관리하면 유지보수성이 향상됩니다.

delay(200)이 하드코딩되어 있는데, 이 값은 exit 애니메이션 시간(slideOut: 180ms, fadeOut: 120ms)과 관련이 있습니다. 현재 200ms는 애니메이션이 완료되기에 충분하지만, 나중에 애니메이션 시간을 변경할 때 이 delay도 함께 수정해야 한다는 것을 놓치기 쉽습니다.

애니메이션 관련 상수들을 companion object나 파일 상단에 정의하면 일관성을 유지하기 쉬워집니다.

♻️ 상수 추출 예시
private object BottomSheetAnimationDefaults {
    const val SLIDE_IN_DURATION = 220
    const val SLIDE_OUT_DURATION = 180
    const val FADE_IN_DURATION = 140
    const val FADE_OUT_DURATION = 120
    const val SCRIM_FADE_DURATION = 160
    const val DISMISS_DELAY = 200L // SLIDE_OUT_DURATION 이상이어야 함
}
core/design-system/src/main/java/com/twix/designsystem/components/common/CommonSwitch.kt (3)

46-46: Dark Mode 지원이 누락되었습니다.

현재 색상이 CommonColor.White, GrayColor.C500으로 하드코딩되어 있어 다크 모드에서도 동일한 색상이 적용됩니다. 다크 모드에서 흰색 배경의 스위치는 시각적으로 튀거나 가독성 문제가 발생할 수 있습니다.

테마 시스템에서 다크/라이트 모드에 따라 적응하는 색상을 사용하거나, 별도의 다크 모드 색상 세트를 정의하는 것을 권장합니다. 프로젝트의 다른 컴포넌트들이 다크 모드를 어떻게 처리하는지 확인해 주시겠어요?

As per coding guidelines, 디자인 시스템 리뷰 가이드에서 Dark Mode 지원 여부를 검토해야 합니다.

Also applies to: 58-58


33-33: 불필요한 연산을 단순화할 수 있습니다.

0.dp.toPx()는 항상 0f를 반환합니다. density 변환이 필요하지 않으므로 직접 0f를 사용하는 것이 더 명확합니다.

✨ 단순화 제안
-    val minBound = with(density) { 0.dp.toPx() }
+    val minBound = 0f

37-37: 애니메이션 지속 시간이 다소 길어 보입니다.

500ms는 스위치 토글 애니메이션으로는 비교적 긴 편입니다. 일반적으로 스위치 애니메이션은 150-300ms 정도가 즉각적인 피드백을 제공하면서도 부드러운 전환을 보여줍니다. Material Design에서는 약 200ms를 권장합니다.

의도적으로 느린 애니메이션을 선택하신 것인지 확인 부탁드립니다. 사용자 테스트 결과 현재 속도가 적절하다면 유지하셔도 됩니다!

⚡ 애니메이션 속도 조정 제안
     val state by animateFloatAsState(
         targetValue = if (checked) maxBound else minBound,
-        animationSpec = tween(durationMillis = 500),
+        animationSpec = tween(durationMillis = 200),
         label = "common switch",
     )
core/ui/src/main/java/com/twix/ui/extension/DismissKeyboardOnTap.kt (1)

3-25: onDismiss 최신 상태 캡처 보장 필요

왜 문제인가요: pointerInput(Unit)은 키가 고정이라 onDismiss가 상태에 따라 바뀌어도 이전 람다가 계속 호출될 수 있습니다(예: 화면 상태에 따라 다른 동작을 기대할 때).
어떻게 개선하나요: rememberUpdatedState로 최신 콜백을 참조하거나 pointerInput 키에 onDismiss를 포함해 재시작되도록 해주세요. onDismiss가 변경될 가능성이 있나요?

🔧 개선 예시
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@
     val focusManager = LocalFocusManager.current
     val keyboardController = LocalSoftwareKeyboardController.current
+    val onDismissState = rememberUpdatedState(onDismiss)
 
     return this.pointerInput(Unit) {
         detectTapGestures(
             onTap = {
                 focusManager.clearFocus()
                 keyboardController?.hide()
-                onDismiss()
+                onDismissState.value()
             },
         )
     }
 }
core/design-system/src/main/java/com/twix/designsystem/components/dialog/CommonDialog.kt (2)

78-78: fadeDuration 상수 중복

fadeDuration = 160DialogScrim(line 78)과 DialogContent(line 106)에서 각각 정의되어 있습니다. 두 값이 동기화되어야 하는 상황에서 하나만 수정하면 애니메이션 타이밍이 어긋날 수 있습니다.

♻️ 상수 추출 제안
+private const val DIALOG_FADE_DURATION_MS = 160
+
 `@Composable`
 private fun DialogScrim(
     visible: Boolean,
     onDismissRequest: () -> Unit,
 ) {
-    val fadeDuration = 160
-
     AnimatedVisibility(
         visible = visible,
-        enter = fadeIn(animationSpec = tween(fadeDuration)),
-        exit = fadeOut(animationSpec = tween(fadeDuration)),
+        enter = fadeIn(animationSpec = tween(DIALOG_FADE_DURATION_MS)),
+        exit = fadeOut(animationSpec = tween(DIALOG_FADE_DURATION_MS)),

Also applies to: 106-106


135-146: dismissTextonDismiss 파라미터 일관성 검토

현재 dismissTextonDismiss가 각각 독립적인 nullable 파라미터로 정의되어 있지만, line 135에서 두 값 모두 non-null일 때만 dismiss 버튼이 표시됩니다.

이로 인해 호출부에서 dismissText만 전달하거나 onDismiss만 전달할 경우 버튼이 표시되지 않아 혼란을 줄 수 있습니다.

개선 방안 (선택사항):

  1. 두 파라미터를 하나의 data class로 묶어 명시적으로 처리
  2. KDoc 주석으로 두 파라미터를 함께 사용해야 함을 명시

현재 구현이 의도된 것이라면, 문서화를 통해 사용법을 명확히 해주시면 좋겠습니다.

core/design-system/src/main/res/drawable/ic_chevron_down_circle.xml (1)

6-15: 하드코딩된 색상에 대한 Dark Mode 지원 검토가 필요합니다.

현재 #171717(배경)과 #ffffff(chevron)가 하드코딩되어 있어 Dark Mode에서 테마에 따라 자동으로 변경되지 않습니다.

의도된 디자인이라면 괜찮지만, 테마 대응이 필요하다면 다음 방안을 고려해 주세요:

  • res/drawable-night/ 에 Dark Mode용 리소스 추가
  • 또는 테마 색상 attribute 사용 (예: ?attr/colorOnSurface)

디자이너와 의도를 확인해 보시는 것을 권장드립니다.

core/design-system/src/main/res/drawable/ic_arrow3_left.xml (1)

6-12: 네비게이션 아이콘의 Dark Mode 지원을 권장드립니다.

뒤로 가기 버튼처럼 기능적으로 중요한 네비게이션 아이콘에 #171717이 하드코딩되어 있습니다. Dark Mode에서 어두운 배경 위에 이 색상이 표시되면 거의 보이지 않을 수 있습니다.

다음 방안 중 하나를 고려해 주세요:

  1. 테마 색상 attribute 사용 (권장):
android:strokeColor="?attr/colorOnSurface"
  1. night 리소스 분리:
    res/drawable-night/ic_arrow3_left.xml에 밝은 색상 버전 추가

기능적 아이콘이므로 테마 대응을 통해 모든 모드에서 시인성을 확보하는 것이 사용자 경험에 중요합니다.

core/design-system/src/main/res/drawable/ic_minus.xml (1)

10-10: 다크 모드 지원을 위해 하드코딩된 색상 대신 테마 색상 참조를 고려해 주세요.

현재 #171717 색상이 직접 하드코딩되어 있습니다. 다크 모드에서는 어두운 배경 위에 이 아이콘이 거의 보이지 않게 됩니다.

개선 방안:

  • android:tint 속성과 함께 색상 리소스를 사용하거나
  • Compose에서 아이콘 사용 시 tint 파라미터로 테마 색상을 적용하는 방식을 권장합니다.

다른 새로 추가된 아이콘들(ic_plus.xml, ic_health.xml 등)에도 동일한 고려가 필요합니다.

core/design-system/src/main/res/drawable/ic_health.xml (1)

30-59: 중복된 path 요소들이 있습니다.

동일한 pathData를 가진 path 요소들이 반복되고 있습니다:

  • Lines 32-34, 36-39, 40-44: "M2,6a6,3 0,1 0,12 0a6,3 0,1 0,-12 0z" 3회 반복
  • Lines 45-49, 50-54, 55-59: "M14,6V9C14,10.657..." 3회 반복

이는 디자인 도구에서 내보낼 때 발생한 것으로 보이며, 불필요한 중복은 앱 크기와 렌더링 성능에 미미하지만 영향을 줄 수 있습니다. 중복된 path를 제거하는 것이 좋습니다.

core/design-system/src/main/res/drawable/ic_exercise.xml (1)

1-31: [전체 아이콘 리소스 요약] 다크 모드 대응 전략을 팀 내에서 논의해 보시면 좋겠습니다.

이번 PR에서 추가된 아이콘들은 일러스트 스타일로 다채로운 색상을 사용하고 있습니다. 이런 경우 일반적으로 두 가지 접근 방식이 있습니다:

  1. 단일 아이콘 유지: 다크/라이트 모드 모두에서 동일한 아이콘 사용 (현재 방식)
  2. 모드별 아이콘 분리: drawable-night 폴더에 다크 모드용 아이콘 별도 제공

브랜드 아이덴티티상 동일한 색상 유지가 필요하다면 현재 방식도 괜찮지만, 가시성 테스트는 꼭 진행해 주세요.

core/design-system/src/main/res/drawable/ic_empty_emoji.xml (1)

12-35: 중복된 path 정의가 있습니다. 의도된 것인지 확인이 필요합니다.

동일한 pathData가 여러 번 반복되어 정의되어 있습니다:

  • Lines 12-23: "M10,31L11.5,26H25.5L26,31H10Z" 경로가 4번 반복
  • Lines 24-35: "M29,31L28.375,26H31L33,31H29Z" 경로가 4번 반복

마지막 fillColor만 최종적으로 보이므로 불필요한 중복일 수 있습니다. SVG 내보내기 과정에서 생긴 아티팩트인지, 또는 의도된 레이어링인지 확인해 주세요. 불필요하다면 제거하여 리소스 크기를 줄일 수 있습니다.

core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt (2)

48-76: 접근성(Accessibility) 개선을 고려해 주세요.

BasicTextField와 별도의 placeholder AppText가 시맨틱하게 연결되어 있지 않습니다. 스크린 리더 사용자를 위해 placeholder 힌트를 제공하는 것이 좋습니다.

♿ 접근성 개선 제안
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.contentDescription

         Box(
             modifier =
                 Modifier
-                    .padding(horizontal = 8.dp, vertical = 10.dp),
+                    .padding(horizontal = 8.dp, vertical = 10.dp)
+                    .semantics { 
+                        if (value.isBlank()) contentDescription = placeHolder 
+                    },
             contentAlignment = Alignment.CenterStart,
         ) {

As per coding guidelines: 디자인 시스템 리뷰 가이드 - 접근성(Accessibility) 고려 여부


23-82: Preview Composable 추가를 권장합니다.

디자인 시스템 컴포넌트에 @Preview 함수가 없습니다. Preview를 추가하면 개발 중 컴포넌트의 다양한 상태(빈 값, 입력된 값, disabled 상태 등)를 시각적으로 확인할 수 있습니다.

👁️ Preview 추가 예시
`@Preview`(showBackground = true)
`@Composable`
private fun UnderlineTextFieldPreview() {
    UnderlineTextField(
        value = "",
        placeHolder = "목표를 입력하세요",
        onValueChange = {},
    )
}

`@Preview`(showBackground = true)
`@Composable`
private fun UnderlineTextFieldFilledPreview() {
    UnderlineTextField(
        value = "운동하기",
        placeHolder = "목표를 입력하세요",
        onValueChange = {},
    )
}

As per coding guidelines: Feature 모듈 - MVI 패턴 리뷰 가이드 - Preview Composable이 제공되는가?

feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt (1)

15-17: 테스트 용이성을 위한 고려사항입니다.

LocalDate.now()를 기본값으로 사용하면 테스트 시 날짜에 의존적인 결과가 발생할 수 있습니다. 현재 구현은 초기 상태 생성에만 영향을 주므로 큰 문제는 아니지만, 테스트 코드 작성 시 명시적으로 날짜를 전달하는 것이 좋습니다.

// 테스트 예시
val testState = GoalEditorUiState(
    startDate = LocalDate.of(2026, 2, 4),
    endDate = LocalDate.of(2026, 2, 28),
)

추후 테스트 작성 시 참고해 주세요.

feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalEditorTopBar.kt (2)

53-61: 접근성: contentDescription에 하드코딩된 영어 문자열 사용

contentDescription"back"으로 하드코딩되어 있습니다. 스크린 리더 사용자를 위해 다국어 지원이 필요합니다. stringResource를 사용하여 리소스 문자열로 변경하는 것이 좋습니다.

♻️ 개선 제안
 Image(
     painter = painterResource(R.drawable.ic_arrow3_left),
-    contentDescription = "back",
+    contentDescription = stringResource(R.string.content_description_back),
     modifier =
         Modifier
             .padding(18.dp)
             .size(24.dp)
             .noRippleClickable(onClick = onBack),
 )

그리고 strings.xml에 다음을 추가하세요:

<string name="content_description_back">뒤로 가기</string>

26-87: Preview Composable 추가 권장

디자인 시스템 가이드라인에 따르면 Preview Composable이 제공되어야 합니다. Preview를 추가하면 개발 중 UI 검증이 용이해지고, 다크 모드 등 다양한 상태를 빠르게 확인할 수 있습니다.

♻️ Preview 추가 예시
`@Preview`(showBackground = true)
`@Composable`
private fun GoalEditorTopBarPreview() {
    GoalEditorTopBar(
        isEdit = false,
        onBack = {},
    )
}

`@Preview`(showBackground = true)
`@Composable`
private fun GoalEditorTopBarEditModePreview() {
    GoalEditorTopBar(
        isEdit = true,
        onBack = {},
    )
}
feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt (1)

61-70: save() 함수의 실제 저장 로직 구현 필요

현재 save() 함수는 유효성 검사만 수행하고 실제 저장 로직이 구현되어 있지 않습니다. PR 설명에 따르면 추후 구현 예정인 것으로 보이지만, TODO 주석을 추가하여 미완성 상태임을 명시하면 다른 개발자가 코드를 이해하는 데 도움이 됩니다.

TODO 주석 추가를 원하시면 말씀해 주세요. 또는 저장 로직 구현을 위한 이슈를 생성해 드릴 수 있습니다.

feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt (5)

247-253: RepeatType.label() 확장 함수 위치 검토

RepeatType.label() 확장 함수가 @Composable로 선언되어 있고 public으로 노출되어 있습니다. 이 함수는 다른 곳에서도 재사용될 수 있으므로, 별도의 파일(예: RepeatTypeExtensions.kt)로 분리하거나 domain 모듈의 RepeatType 근처로 이동하는 것을 고려해 보세요.

현재 위치에서는 GoalInfoCard.kt를 찾아봐야만 이 확장 함수를 발견할 수 있어 코드 탐색이 어려울 수 있습니다.


155-159: 하드코딩된 포맷 문자열을 리소스로 이동 권장

"%s %s번" 형태의 포맷 문자열이 하드코딩되어 있습니다. 다국어 지원과 일관성을 위해 strings.xml의 포맷 문자열을 사용하는 것이 좋습니다.

♻️ 개선 제안

strings.xml에 추가:

<string name="repeat_count_format">%1$s %2$d번</string>

코드 변경:

 AppText(
-    text = "%s %s번".format(selectedRepeatType.label(), repeatCount),
+    text = stringResource(R.string.repeat_count_format, selectedRepeatType.label(), repeatCount),
     style = AppTextStyle.B2,
     color = GrayColor.C500,
 )

198-202: 날짜 포맷의 다국어 지원 검토

"%s월 %s일" 형태의 날짜 포맷이 하드코딩되어 있습니다. 한국어에서는 자연스럽지만, 향후 다국어 지원 시 각 언어별로 날짜 표현 방식이 다를 수 있습니다.

DateTimeFormatterstrings.xml의 포맷 문자열을 조합하거나, Android의 DateUtils 또는 java.time.format.DateTimeFormatter.ofLocalizedDate()를 사용하는 것을 고려해 보세요.

♻️ 대안 예시
// strings.xml
<string name="date_format_month_day">%1$d월 %2$d일</string>

// 또는 DateTimeFormatter 사용
val formatter = DateTimeFormatter.ofPattern("M월 d일", Locale.KOREA)
text = date.format(formatter)

161-167: contentDescription 접근성 개선 필요

이미지의 contentDescription"repeat type", "date" 등 영어로 하드코딩되어 있습니다. 스크린 리더 사용자를 위해 의미 있는 한국어 설명을 stringResource로 제공하는 것이 좋습니다.

♻️ 개선 제안
 Image(
     painter = painterResource(R.drawable.ic_chevron_down_circle),
-    contentDescription = "repeat type",
+    contentDescription = stringResource(R.string.content_description_select_repeat_count),
     ...
 )
 Image(
     painter = painterResource(R.drawable.ic_chevron_down_circle),
-    contentDescription = "date",
+    contentDescription = stringResource(R.string.content_description_select_date),
     ...
 )

Also applies to: 204-210


40-96: Preview Composable 추가 권장

GoalInfoCard는 복잡한 UI 컴포넌트입니다. Preview를 추가하면 다양한 상태(endDateEnabled true/false, 다른 RepeatType 등)에서의 렌더링을 빠르게 확인할 수 있습니다.

♻️ Preview 추가 예시
`@Preview`(showBackground = true)
`@Composable`
private fun GoalInfoCardPreview() {
    GoalInfoCard(
        selectedRepeatType = RepeatType.DAILY,
        repeatCount = 1,
        startDate = LocalDate.now(),
        endDateEnabled = false,
        endDate = LocalDate.now().plusMonths(1),
        onSelectedRepeatType = {},
        onShowRepeatCountBottomSheet = {},
        onShowCalendarBottomSheet = {},
        onToggleEndDateEnabled = {},
    )
}

`@Preview`(showBackground = true)
`@Composable`
private fun GoalInfoCardWithEndDatePreview() {
    GoalInfoCard(
        selectedRepeatType = RepeatType.WEEKLY,
        repeatCount = 3,
        startDate = LocalDate.now(),
        endDateEnabled = true,
        endDate = LocalDate.now().plusMonths(1),
        onSelectedRepeatType = {},
        onShowRepeatCountBottomSheet = {},
        onShowCalendarBottomSheet = {},
        onToggleEndDateEnabled = {},
    )
}
core/design-system/src/main/res/drawable/ic_pencil.xml (1)

1-75: 다크 모드/테마 대응 여부를 한 번 확인해 주세요.
현재 모든 색상이 HEX 하드코딩이라 다크 모드에서 대비가 떨어질 수 있습니다. 필요하면 @color/... 리소스로 분리하고 night 변형을 두는 방향이 안전합니다.

As per coding guidelines, “Dark Mode 지원 여부” 및 “색상과 테마가 디자인 시스템에 포함되는가?”.

core/design-system/src/main/res/drawable/ic_laptop.xml (1)

6-32: 동일 pathData 중복으로 불필요한 overdraw가 생깁니다.
같은 pathData가 여러 번 반복되어 최종 출력은 마지막 색만 남고 앞선 레이어는 의미가 없습니다. 의도된 레이어링이 아니라면 중복 제거를 권장합니다. 혹시 색상 레이어링 의도인가요?

♻️ 중복 path 제거 예시
-  <path
-      android:pathData="M5.039,25.769L6.468,21.004H19.809L20.286,25.769H5.039Z"
-      android:fillColor="#FF96AD"/>
-  <path
-      android:pathData="M5.039,25.769L6.468,21.004H19.809L20.286,25.769H5.039Z"
-      android:fillColor="#FF96AD"/>
+  <path
+      android:pathData="M5.039,25.769L6.468,21.004H19.809L20.286,25.769H5.039Z"
+      android:fillColor="#FF96AD"/>
feature/goal-editor/src/main/java/com/twix/goal_editor/component/EmojiPicker.kt (1)

44-55: toRes() 함수에서 @Composable 어노테이션이 불필요합니다.

이 함수는 Compose 런타임 기능(remember, LaunchedEffect 등)을 사용하지 않고 단순 매핑만 수행하므로, 일반 확장 함수로 선언하는 것이 적절합니다. @Composable로 선언하면 Composable 컨텍스트 외부에서 호출할 수 없는 불필요한 제약이 생깁니다.

♻️ 일반 확장 함수로 변경
-@Composable
 fun GoalIconType.toRes(): Int =
     when (this) {
         GoalIconType.DEFAULT -> R.drawable.ic_default
feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt (1)

22-48: 포커스 기반 커밋 패턴이 잘 구현되어 있습니다.

wasFocused 플래그를 사용하여 초기 무의미한 커밋을 방지하고, 포커스 해제 시에만 값을 커밋하는 패턴이 UX 측면에서 적절합니다. rememberSaveable(value)를 키로 사용하여 외부 상태 변경 시 내부 상태가 동기화되는 것도 올바른 접근입니다.

다만, UI 컴포넌트의 독립적인 테스트와 디자인 확인을 위해 @Preview Composable을 추가하면 좋겠습니다.

💡 Preview 추가 제안 (선택사항)
`@Preview`(showBackground = true)
`@Composable`
private fun GoalTextFieldPreview() {
    TwixTheme {
        GoalTextField(
            value = "목표 제목",
            onCommitTitle = {},
        )
    }
}
feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt (1)

267-273: 접근성(Accessibility)을 위한 contentDescription 개선을 제안합니다.

현재 contentDescription이 "emoji"로 하드코딩되어 있어, 스크린 리더 사용자가 어떤 아이콘인지 구분하기 어렵습니다. 각 아이콘 타입에 맞는 설명을 제공하면 접근성이 향상됩니다.

♻️ 접근성 개선 제안

GoalIconType에 label을 추가하거나, 기존 리소스를 활용하여 의미 있는 설명을 제공하세요:

                     Image(
                         painter = painterResource(it.toRes()),
-                        contentDescription = "emoji",
+                        contentDescription = it.name.lowercase(),
                         modifier =

또는 별도의 확장 함수로 각 아이콘에 대한 설명을 정의할 수 있습니다.

Copy link
Member

@chanho0908 chanho0908 left a comment

Choose a reason for hiding this comment

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

고생했어 현수야 !
이미지 파일 때문에 크기가 어마 무시해졌군 ㅇㅅㅇ ,,,

전체적으로 테스트 해봤을 때 문제 없어서 작은 의견 하나만 남겼어 !

verticalAlignment = Alignment.CenterVertically,
) {
AppText(
text = "%s월 %s일".format(date.monthValue, date.dayOfMonth),
Copy link
Member

Choose a reason for hiding this comment

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

strings resource로 분리할 수 있을 것 같아 !

Copy link
Member Author

Choose a reason for hiding this comment

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

수정했습니다!

Comment on lines 65 to 67
viewModelScope.launch {
emitSideEffect(GoalEditorSideEffect.ShowToast("종료 날짜가 시작 날짜보다 이전입니다.", ToastType.ERROR))
}
Copy link
Member

Choose a reason for hiding this comment

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

ViewModel은 SideEffect만 발생시키고 이를 어떻게 처리할지는 Ui에서 관리하도록 관심사를 분리하는건 어떨까 ?

출력할 메시지를 Ui에서 관리하면 strings로도 분리할 수 있어서 추후 다국어 대응할 때도 좀 더
유지보수성이 편리해질 것 같아 !

Copy link
Member Author

Choose a reason for hiding this comment

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

수정했습니다!

Comment on lines +23 to +24
@Composable
fun UnderlineTextField(
Copy link
Member

Choose a reason for hiding this comment

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

온보딩 닉네임 입력/날짜 선택 화면에서 trailing Icon이 필요한 경우가 있어서 추가해줄 수 있을까 ?

Copy link
Member Author

@dogmania dogmania Feb 5, 2026

Choose a reason for hiding this comment

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

일단 피그마 보고 대충 추가해봤는데 trailing 파라미터에 Image 컴포저블 넘겨서 디테일한 padding이나 클릭 이벤트 처리하면 될 것 같아요!

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: 4

🤖 Fix all issues with AI agents
In
`@feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt`:
- Around line 198-210: Replace the hardcoded Korean date format and decorative
icon description in GoalInfoCard: instead of AppText(text = "%s월
%s일".format(...)), use a localized formatted string (e.g.,
stringResource(R.string.date_format, date.monthValue, date.dayOfMonth)) or
format the date with DateTimeFormatter.ofLocalizedDate/style with the current
Locale to support i18n in the AppText call; and for the chevron Image
(painterResource(R.drawable.ic_chevron_down_circle)), set contentDescription =
null if it is purely decorative so it is ignored by accessibility tools. Ensure
you update or add the corresponding string resource key (date_format) if using
stringResource and keep AppText and Image modifiers unchanged.
- Around line 155-167: The Image composable in GoalInfoCard.kt currently uses a
hardcoded English contentDescription ("repeat type") which breaks
accessibility/localization; update the Image call that renders
ic_chevron_down_circle to use contentDescription = null if the icon is purely
decorative, or replace the hardcoded string with a localized string via
stringResource(R.string.<appropriate_key>) (and add the new string resource key
if needed) so the description is localizable and accessible.

In
`@feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt`:
- Around line 34-38: 현재 setTitle(title: String)에서 title.isBlank()로 빈 문자열을 차단하고
있어 사용자가 필드를 모두 지워도 상태가 업데이트되지 않습니다; setTitle 함수에서 title.isBlank() 체크를 제거하고 항상
reduce { copy(goalTitle = title) }를 호출해 빈 값도 상태에 반영하도록 하세요 (또는, 만약 의도적으로 빈 입력을
금지하려면 이 로직 대신 별도의 검증/오류 상태를 관리하는 흐름을 만들어 setTitle은 상태 반영만 담당하도록 수정하세요; 참고 심볼:
setTitle, reduce, goalTitle).
- Around line 44-48: The setter setRepeatCount currently ignores zero/blank
inputs because it early-returns when repeatCount <= 0; change it to accept
intermediate/empty states so UX doesn't jump: remove the early-return in
setRepeatCount (keep a guard only for negative values if desired) and always
call reduce { copy(repeatCount = repeatCount) } for zero values so the view
reflects the cleared/zero input, and move validation (enforce >=1) to the
save/submit path or expose an explicit validation state; refer to setRepeatCount
and the reduce { copy(repeatCount = ...) } usage when making the change.
🧹 Nitpick comments (1)
feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt (1)

40-96: 프리뷰가 없어 UI 검증 비용이 커집니다.
왜 문제냐면, @Preview가 없으면 디자이너/QA가 화면을 빠르게 확인하기 어렵습니다. 어떻게 개선하냐면 GoalInfoCard 전용 Preview를 추가해 샘플 상태를 고정해 주세요. 프리뷰 추가 가능할까요?

🧩 제안 코드 (Preview 추가)
+import androidx.compose.ui.tooling.preview.Preview
+
+@Preview(showBackground = true)
+@Composable
+private fun GoalInfoCardPreview() {
+    GoalInfoCard(
+        selectedRepeatType = RepeatType.DAILY,
+        repeatCount = 1,
+        startDate = LocalDate.of(2026, 2, 5),
+        endDateEnabled = true,
+        endDate = LocalDate.of(2026, 2, 12),
+        onSelectedRepeatType = {},
+        onShowRepeatCountBottomSheet = {},
+        onShowCalendarBottomSheet = {},
+        onToggleEndDateEnabled = {},
+    )
+}

As per coding guidelines, "Preview Composable이 제공되는가?"

Comment on lines +155 to +167
AppText(
text = stringResource(R.string.repeat_count, selectedRepeatType.label(), repeatCount),
style = AppTextStyle.B2,
color = GrayColor.C500,
)

Image(
painter = painterResource(R.drawable.ic_chevron_down_circle),
contentDescription = "repeat type",
modifier =
Modifier
.size(24.dp),
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

아이콘 contentDescription이 영문 하드코딩이라 접근성/현지화가 깨집니다.
왜 문제냐면 스크린리더가 “repeat type” 같은 영문을 그대로 읽어 UX가 나빠집니다. 어떻게 개선하냐면 장식용 아이콘이라면 contentDescription을 null로, 의미 전달이 필요하면 stringResource로 로컬라이즈 해주세요.

✅ 제안 수정
-                    Image(
-                        painter = painterResource(R.drawable.ic_chevron_down_circle),
-                        contentDescription = "repeat type",
+                    Image(
+                        painter = painterResource(R.drawable.ic_chevron_down_circle),
+                        contentDescription = null, // 또는 stringResource(R.string.cd_repeat_count)
                         modifier =
                             Modifier
                                 .size(24.dp),
                     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AppText(
text = stringResource(R.string.repeat_count, selectedRepeatType.label(), repeatCount),
style = AppTextStyle.B2,
color = GrayColor.C500,
)
Image(
painter = painterResource(R.drawable.ic_chevron_down_circle),
contentDescription = "repeat type",
modifier =
Modifier
.size(24.dp),
)
AppText(
text = stringResource(R.string.repeat_count, selectedRepeatType.label(), repeatCount),
style = AppTextStyle.B2,
color = GrayColor.C500,
)
Image(
painter = painterResource(R.drawable.ic_chevron_down_circle),
contentDescription = null, // 또는 stringResource(R.string.cd_repeat_count)
modifier =
Modifier
.size(24.dp),
)
🤖 Prompt for AI Agents
In
`@feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt`
around lines 155 - 167, The Image composable in GoalInfoCard.kt currently uses a
hardcoded English contentDescription ("repeat type") which breaks
accessibility/localization; update the Image call that renders
ic_chevron_down_circle to use contentDescription = null if the icon is purely
decorative, or replace the hardcoded string with a localized string via
stringResource(R.string.<appropriate_key>) (and add the new string resource key
if needed) so the description is localizable and accessible.

Comment on lines +198 to +210
AppText(
text = "%s월 %s일".format(date.monthValue, date.dayOfMonth),
style = AppTextStyle.B2,
color = GrayColor.C500,
)

Image(
painter = painterResource(R.drawable.ic_chevron_down_circle),
contentDescription = "date",
modifier =
Modifier
.size(24.dp),
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

날짜 포맷 하드코딩으로 i18n 확장성이 떨어집니다.
왜 문제냐면 "%s월 %s일"은 번역 리소스를 우회해 로케일 전환 시 형식이 깨집니다. 어떻게 개선하냐면 stringResource 포맷으로 이동하거나 DateTimeFormatter+Locale를 사용하세요. 아래 아이콘도 장식용이면 contentDescription을 null로 두는 게 접근성에 더 좋습니다.

✅ 제안 수정
-            AppText(
-                text = "%s월 %s일".format(date.monthValue, date.dayOfMonth),
+            AppText(
+                text = stringResource(R.string.format_month_day, date.monthValue, date.dayOfMonth),
                 style = AppTextStyle.B2,
                 color = GrayColor.C500,
             )
 
             Image(
                 painter = painterResource(R.drawable.ic_chevron_down_circle),
-                contentDescription = "date",
+                contentDescription = null, // 또는 stringResource(R.string.cd_date_picker)
                 modifier =
                     Modifier
                         .size(24.dp),
             )
🤖 Prompt for AI Agents
In
`@feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt`
around lines 198 - 210, Replace the hardcoded Korean date format and decorative
icon description in GoalInfoCard: instead of AppText(text = "%s월
%s일".format(...)), use a localized formatted string (e.g.,
stringResource(R.string.date_format, date.monthValue, date.dayOfMonth)) or
format the date with DateTimeFormatter.ofLocalizedDate/style with the current
Locale to support i18n in the AppText call; and for the chevron Image
(painterResource(R.drawable.ic_chevron_down_circle)), set contentDescription =
null if it is purely decorative so it is ignored by accessibility tools. Ensure
you update or add the corresponding string resource key (date_format) if using
stringResource and keep AppText and Image modifiers unchanged.

Comment on lines +34 to +38
private fun setTitle(title: String) {
if (title.isBlank()) return

reduce { copy(goalTitle = title) }
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

빈 문자열 입력을 막으면 필드 삭제가 불가능해질 수 있습니다.

왜 문제인가요: 사용자가 전체 삭제(공백/빈 값)를 시도할 때 상태가 업데이트되지 않아 UI가 이전 값으로 되돌아갈 수 있습니다.
어떻게 개선하나요: 빈 값도 상태에 반영하고, 검증/저장은 별도 로직에서 처리하는 편이 UX에 안전합니다. 의도적으로 빈 값 금지라면 오류 상태를 보여주는 흐름이 더 명확하지 않을까요?

🔧 제안 수정
-    private fun setTitle(title: String) {
-        if (title.isBlank()) return
-
-        reduce { copy(goalTitle = title) }
-    }
+    private fun setTitle(title: String) {
+        reduce { copy(goalTitle = title) }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun setTitle(title: String) {
if (title.isBlank()) return
reduce { copy(goalTitle = title) }
}
private fun setTitle(title: String) {
reduce { copy(goalTitle = title) }
}
🤖 Prompt for AI Agents
In
`@feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt`
around lines 34 - 38, 현재 setTitle(title: String)에서 title.isBlank()로 빈 문자열을 차단하고
있어 사용자가 필드를 모두 지워도 상태가 업데이트되지 않습니다; setTitle 함수에서 title.isBlank() 체크를 제거하고 항상
reduce { copy(goalTitle = title) }를 호출해 빈 값도 상태에 반영하도록 하세요 (또는, 만약 의도적으로 빈 입력을
금지하려면 이 로직 대신 별도의 검증/오류 상태를 관리하는 흐름을 만들어 setTitle은 상태 반영만 담당하도록 수정하세요; 참고 심볼:
setTitle, reduce, goalTitle).

Comment on lines +44 to +48
private fun setRepeatCount(repeatCount: Int) {
if (repeatCount <= 0) return

reduce { copy(repeatCount = repeatCount) }
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

0/빈값 입력이 중간에 무시되면 숫자 편집 UX가 끊길 수 있습니다.

왜 문제인가요: 사용자가 값을 지우는 순간(0/빈 값) 상태가 반영되지 않아 커서/입력이 튀는 현상이 생길 수 있습니다.
어떻게 개선하나요: 입력 중간 상태는 허용하고, 저장 시점에 검증하거나 오류 상태를 표시하는 쪽이 안정적입니다. 현재 의도적으로 1 이상만 허용하는 정책인가요?

🔧 제안 수정
-    private fun setRepeatCount(repeatCount: Int) {
-        if (repeatCount <= 0) return
-
-        reduce { copy(repeatCount = repeatCount) }
-    }
+    private fun setRepeatCount(repeatCount: Int) {
+        reduce { copy(repeatCount = repeatCount) }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun setRepeatCount(repeatCount: Int) {
if (repeatCount <= 0) return
reduce { copy(repeatCount = repeatCount) }
}
private fun setRepeatCount(repeatCount: Int) {
reduce { copy(repeatCount = repeatCount) }
}
🤖 Prompt for AI Agents
In
`@feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt`
around lines 44 - 48, The setter setRepeatCount currently ignores zero/blank
inputs because it early-returns when repeatCount <= 0; change it to accept
intermediate/empty states so UX doesn't jump: remove the early-return in
setRepeatCount (keep a guard only for negative values if desired) and always
call reduce { copy(repeatCount = repeatCount) } for zero values so the view
reflects the cleared/zero input, and move validation (enforce >=1) to the
save/submit path or expose an explicit validation state; refer to setRepeatCount
and the reduce { copy(repeatCount = ...) } usage when making the change.

@dogmania dogmania requested a review from chanho0908 February 5, 2026 09:38
Copy link
Member

@chanho0908 chanho0908 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 50a10b3 into develop Feb 5, 2026
10 of 11 checks passed
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

🤖 Fix all issues with AI agents
In
`@core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt`:
- Around line 71-87: The BasicTextField in UnderlineTextField is missing a size
modifier so when value is empty its measured width collapses and becomes hard to
tap; update the BasicTextField invocation (in UnderlineTextField) to include a
modifier such as Modifier.weight(1f) or Modifier.fillMaxWidth() (combined with
any existing modifiers) so the input area fills available horizontal space and
remains easily focusable/tappable.
🧹 Nitpick comments (3)
feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt (3)

174-182: 완료 버튼의 활성화 상태가 제어되지 않습니다.

AI 요약에 따르면 GoalEditorUiStateisEnabled computed property가 있는데, 이 값이 완료 버튼에 반영되지 않고 있습니다. 사용자가 유효하지 않은 상태(예: 빈 제목)에서도 완료 버튼을 누를 수 있습니다.

의도적으로 ViewModel에서 유효성 검사를 하고 Toast로 피드백을 주는 방식이라면 현재 구현도 괜찮지만, UX 관점에서 버튼 비활성화를 통한 시각적 피드백도 고려해 보시는 것은 어떨까요?

💡 버튼 활성화 상태 적용 제안
 AppButton(
     onClick = onComplete,
+    enabled = uiState.isEnabled,
     modifier =
         Modifier
             .padding(horizontal = 20.dp)
             .padding(vertical = 8.dp)
             .fillMaxWidth(),
     text = stringResource(R.string.word_completion),
 )

만약 AppButtonenabled 파라미터를 지원하지 않는다면, 해당 컴포넌트에 추가하는 것을 권장드립니다.


235-287: IconEditorDialogContent 구현이 깔끔합니다! 👍

LazyVerticalGrid에서 원형 아이템 레이아웃을 위해 중첩 Box를 사용한 이유가 주석으로 잘 설명되어 있습니다. 상태 호이스팅도 적절히 적용되어 있습니다.

다만, 접근성(Accessibility) 측면에서 contentDescription = "emoji" 대신 선택된 아이콘의 의미를 전달하는 것이 더 좋을 수 있습니다. 예를 들어 GoalIconType에 label이 있다면 활용해 보시는 건 어떨까요?


295-296: RepeatCountBottomSheetContent의 내부 상태도 외부 상태 변경을 반영하지 않을 수 있습니다.

IconEditorDialogContent와 유사하게, internalRepeatCountinternalSelectedRepeatType도 초기값만 캡처합니다. 수정 모드에서 기존 목표 데이터가 비동기로 로드된 후 바텀시트를 열면, 로드된 값이 아닌 이전 기본값이 표시될 수 있습니다.

현재 바텀시트가 열릴 때마다 새로 컴포지션되어 문제가 발생하지 않을 수도 있지만, 향후 수정 기능 구현 시 이 부분을 확인해 보시는 것이 좋겠습니다.

💡 key를 추가하여 상태 동기화 보장
-    var internalRepeatCount by remember { mutableIntStateOf(repeatCount) }
-    var internalSelectedRepeatType by remember { mutableStateOf(selectedRepeatType) }
+    var internalRepeatCount by remember(repeatCount) { mutableIntStateOf(repeatCount) }
+    var internalSelectedRepeatType by remember(selectedRepeatType) { mutableStateOf(selectedRepeatType) }

Comment on lines +71 to +87
Row(
modifier =
Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
BasicTextField(
value = value,
textStyle = textStyle.toTextStyle(typo).copy(color = GrayColor.C500),
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
singleLine = singleLine,
maxLines = maxLines,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

빈 값에서 입력 영역이 너무 좁아 탭이 어려울 수 있어요.
왜 문제가 되는지: BasicTextField에 크기 modifier가 없어 텍스트 길이만큼만 측정됩니다. 값이 비어 있으면 폭이 거의 0에 가까워져 포커스가 잘 안 잡힐 수 있습니다(접근성/UX 저하).
어떻게 개선할지: Modifier.weight(1f) 또는 fillMaxWidth()를 적용해 입력 영역과 탭 영역을 확보하세요.

🛠️ 수정 제안
 BasicTextField(
+    modifier = Modifier.weight(1f),
     value = value,
     textStyle = textStyle.toTextStyle(typo).copy(color = GrayColor.C500),
     onValueChange = onValueChange,
     enabled = enabled,
     readOnly = readOnly,
     singleLine = singleLine,
     maxLines = maxLines,
     keyboardOptions = keyboardOptions,
     keyboardActions = keyboardActions,
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Row(
modifier =
Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
BasicTextField(
value = value,
textStyle = textStyle.toTextStyle(typo).copy(color = GrayColor.C500),
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
singleLine = singleLine,
maxLines = maxLines,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
Row(
modifier =
Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
BasicTextField(
modifier = Modifier.weight(1f),
value = value,
textStyle = textStyle.toTextStyle(typo).copy(color = GrayColor.C500),
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
singleLine = singleLine,
maxLines = maxLines,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
🤖 Prompt for AI Agents
In
`@core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt`
around lines 71 - 87, The BasicTextField in UnderlineTextField is missing a size
modifier so when value is empty its measured width collapses and becomes hard to
tap; update the BasicTextField invocation (in UnderlineTextField) to include a
modifier such as Modifier.weight(1f) or Modifier.fillMaxWidth() (combined with
any existing modifiers) so the input area fills available horizontal space and
remains easily focusable/tappable.

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