[Feat] #50 랜덤 포즈 추천 기능 구현#54
Conversation
기존 `NumberOfPeople` enum의 이름을 `PeopleCount`로 변경하고, 선택되지 않은 상태를 `UNSELECTED` case 대신 nullable 타입으로 표현하도록 수정했습니다. [feat] #18: 랜덤 포즈 추천 인원수 선택 BottomSheet 추가 랜덤 포즈 추천 시 인원수를 선택하는 `RandomPosePeopleCountBottomSheet`를 구현하고, 관련 상태(`isShowRandomPosePeopleCountBottomSheet`)와 로직을 `PoseState`에 추가했습니다.
`DoubleButtonOptionBottomSheet` 컴포저블에 `buttonEnabled` 파라미터를 추가하여, 외부에서 버튼의 활성화 상태를 제어할 수 있도록 수정합니다.
- selectedCount를 selectedRandomPosePeopleCount로 변경 - 선택 버튼 클릭 시 바텀시트 닫기 로직 추가
`PoseNavKey`에 인원 수를 포함하는 `RandomPose`를 추가하고, `Navigator` 확장 함수 `navigateToRandomPose`를 정의하여 랜덤 포즈 추천 화면으로 이동할 수 있도록 구현합니다.
`DoubleButtonOptionBottomSheet` 컴포저블의 버튼 텍스트를 커스텀할 수 있도록 `primaryButtonText`와 `secondaryButtonText` 파라미터를 추가합니다. 이를 통해 기존에 "삭제하기", "취소"로 고정되어 있던 버튼의 텍스트를 호출하는 쪽에서 직접 지정할 수 있도록 수정했습니다.
- `RandomPosePeopleCountBottomSheet` ("선택하기" / "취소")
- `AlbumDetailScreen` ("삭제하기" / "취소")
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Walkthrough포즈 기능이 메인/디테일/랜덤으로 분리되고 RandomPose 흐름(화면·뷰모델·계약·컴포넌트)이 추가되었습니다. 모델 필드명( Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant PoseScreen as PoseScreen
participant PoseVM as PoseViewModel
participant Store as MviIntentStore
participant NavAPI as Navigator
User->>PoseScreen: 인원수 칩 클릭
PoseScreen->>PoseVM: ClickPeopleCountChip
PoseVM->>Store: dispatch Intent
Store->>Store: reduce -> isShowPeopleCountBottomSheet = true
Store-->>PoseScreen: 상태 반영
User->>PoseScreen: 랜덤포즈 추천 클릭
PoseScreen->>PoseVM: ClickRandomPoseRecommendation
PoseVM->>Store: dispatch Intent
Store->>Store: reduce -> isShowRandomPosePeopleCountBottomSheet = true
Store-->>PoseScreen: 상태 반영
User->>PoseScreen: 랜덤 인원 선택 후 선택 버튼
PoseScreen->>PoseVM: ClickRandomPoseBottomSheetSelectButton
PoseVM->>Store: dispatch Intent (resolve selection, hide sheet)
Store->>NavAPI: effect NavigateToRandomPose(peopleCount)
NavAPI-->>User: RandomPose 화면으로 이동
sequenceDiagram
participant User as 사용자
participant RandomPoseScreen as RandomPoseScreen
participant RandomPoseVM as RandomPoseViewModel
participant Store as MviIntentStore
participant NavAPI as Navigator
User->>RandomPoseScreen: 화면 진입
RandomPoseScreen->>RandomPoseVM: EnterRandomPoseScreen
RandomPoseVM->>Store: dispatch Intent
Store->>Store: reduce -> currentPose, randomPoseList 초기화
Store-->>RandomPoseScreen: 상태 반영
User->>RandomPoseScreen: 상세보기 버튼 클릭
RandomPoseScreen->>RandomPoseVM: ClickGoToDetailIcon
RandomPoseVM->>Store: emit effect NavigateToDetail(pose)
Store-->>NavAPI: navigateToPoseDetail(pose)
NavAPI-->>User: PoseDetail 화면으로 이동
User->>RandomPoseScreen: 스크랩 클릭
RandomPoseScreen->>RandomPoseVM: ClickScrapIcon
RandomPoseVM->>Store: reduce -> currentPose.isScrapped 토글
Store-->>RandomPoseScreen: 상태 반영
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In
`@core/ui/src/main/java/com/neki/android/core/ui/component/DoubleButtonOptionBottomSheet.kt`:
- Around line 112-116: The cancel button (CTAButtonGray) inside
DoubleButtonOptionBottomSheet is currently using the shared buttonEnabled flag
so when buttonEnabled is false the cancel action is also disabled; change
CTAButtonGray to always be enabled (or use a separate flag) by removing or
overriding the shared enabled parameter for the cancel button so that
CTAButtonGray (the secondary button using secondaryButtonText and onClickCancel)
remains clickable even when the primary/buttonEnabled is false.
In
`@feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/detail/PoseDetailViewModel.kt`:
- Around line 36-41: The reduce block for PoseDetailIntent.ClickScrapIcon flips
the injected constructor variable pose instead of the current UI state, so
repeated toggles always invert the initial value; change the copy to reference
the current state's pose (e.g., use state.pose or the ViewModel's current state
accessor) inside reduce so it becomes reduce { copy(pose =
state.pose.copy(isScrapped = !state.pose.isScrapped)) } (replace state.pose with
your actual state accessor) ensuring you update the same symbols:
PoseDetailIntent.ClickScrapIcon, reduce, and the pose.copy(...) expression.
In
`@feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/RandomPoseFloatingBar.kt`:
- Around line 36-50: The passed-in modifier parameter in RandomPoseFloatingBar
is currently applied to the inner Row instead of the root Box; move the external
modifier onto the root Box (combine it with background, alpha, padding via
modifier.then(...) or chained Modifier calls) and give the Row its own local
Modifier (e.g., Modifier.fillMaxWidth() or other internal modifiers) so that the
component respects parent sizing and layout control from the caller.
In
`@feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/RandomPoseTutorialOverlay.kt`:
- Around line 42-58: VerticalSpacer is being called with Float literals (e.g.,
275f, 243f) but the composable overload expects a Dp; update the calls to pass
Dp by appending .dp (e.g., change VerticalSpacer(275f) -> VerticalSpacer(275.dp)
and VerticalSpacer(243f) -> VerticalSpacer(243.dp)); keep HorizontalSpacer(...)
calls as-is since those are RowScope.HorizontalSpacer(weight: Float) usages and
are correct.
In
`@feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/RandomPoseContract.kt`:
- Around line 23-24: Remove the unused ClickBackIcon intent and consolidate to
ClickCloseIcon: delete the data object ClickBackIcon from RandomPoseIntent,
replace any usages/references of ClickBackIcon in the ViewModel and UI code to
ClickCloseIcon, and run/adjust any imports, tests, and switch
statements/exhaustiveness checks that referenced ClickBackIcon so the code
compiles and behavior still emits the NavigateBack effect from ClickCloseIcon.
In
`@feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/RandomPoseScreen.kt`:
- Around line 50-87: The tutorial overlay currently only draws a background and
doesn't block touch events, so when uiState.isShowTutorial is true, wrap
RandomPoseTutorialOverlay with a full-size modifier that consumes all pointer
input (e.g., add Modifier.fillMaxSize() plus a pointer-consuming modifier such
as pointerInput or a non-indicating clickable) so it intercepts taps and
prevents touches from reaching RandomPoseFloatingBarContent; update the
RandomPoseScreen code where RandomPoseTutorialOverlay is composed to apply this
consuming modifier.
🧹 Nitpick comments (22)
feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/StartRandomPoseButton.kt (2)
30-39: 하드코딩된 색상을 테마 색상으로 대체하는 것을 권장합니다.Line 35의
Color(0xFFFF334B)는 다른 gradient 색상들이NekiTheme.colorScheme을 사용하는 것과 달리 직접 하드코딩되어 있습니다. 디자인 시스템 일관성과 다크 테마 등 향후 테마 변경 대응을 위해 테마 색상으로 정의하는 것이 좋습니다.♻️ 제안하는 수정
.background( brush = Brush.horizontalGradient( colorStops = arrayOf( 0f to NekiTheme.colorScheme.primary400, 0.53f to NekiTheme.colorScheme.primary600, - 0.96f to Color(0xFFFF334B), + 0.96f to NekiTheme.colorScheme.primary700, // 또는 적절한 테마 색상 ), ), shape = RoundedCornerShape(12.dp), )
54-58: 문자열 리소스 사용을 고려해주세요."랜덤 포즈 시작하기" 텍스트가 하드코딩되어 있습니다. 다국어 지원(i18n) 또는 문자열 중앙 관리를 위해
strings.xml리소스로 분리하는 것을 권장합니다.♻️ 제안하는 수정
Text( - text = "랜덤 포즈 시작하기", + text = stringResource(R.string.start_random_pose), style = NekiTheme.typography.body16SemiBold, color = NekiTheme.colorScheme.white, )feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/VerticalDashedDivider.kt (1)
36-44: 색상 파라미터화 고려현재 그라디언트 색상이
Color.White로 하드코딩되어 있습니다.internal컴포넌트이고 현재 용도에는 문제없지만, 향후 재사용성을 위해 색상을 파라미터로 받는 것을 고려해 볼 수 있습니다.♻️ 제안: 색상 파라미터 추가
`@Composable` internal fun VerticalDashedDivider( modifier: Modifier = Modifier, + color: Color = Color.White, strokeWidth: Dp = 1.dp, dashOn: Dp = 5.dp, dashOff: Dp = 5.dp, ) { Canvas(modifier = modifier.width(strokeWidth)) { // ... val brush = linearGradient( colorStops = arrayOf( - 0f to Color.White.copy(alpha = 0f), - 0.5f to Color.White.copy(alpha = 1f), - 1f to Color.White.copy(alpha = 0f), + 0f to color.copy(alpha = 0f), + 0.5f to color.copy(alpha = 1f), + 1f to color.copy(alpha = 0f), ), // ... ) } }feature/pose/impl/src/main/res/drawable/icon_seek_before_pose.xml (1)
6-15: 불필요한strokeAlpha속성이 존재합니다.Lines 8, 13의
strokeAlpha="0.5"속성은 해당 path들에 stroke가 정의되어 있지 않으므로 (strokeColor,strokeWidth없음) 실제로 적용되지 않습니다. 코드 명확성을 위해 제거를 고려해 주세요.♻️ 제안하는 수정
<path android:pathData="M73.2,18.51m18.51,0a18.51,18.51 45,1 0,-37.02 -0a18.51,18.51 45,1 0,37.02 0" - android:strokeAlpha="0.5" android:fillColor="#FF311F" android:fillAlpha="0.5"/> <path android:pathData="M73.2,18.51m11.31,0a11.31,11.31 135,1 0,-22.62 -0a11.31,11.31 135,1 0,22.62 0" - android:strokeAlpha="0.5" android:fillColor="#FF311F" android:fillAlpha="0.5"/>feature/pose/impl/src/main/res/drawable/icon_seek_after_pose.xml (1)
6-15: 불필요한strokeAlpha속성이 존재합니다.
icon_seek_before_pose.xml과 동일하게, Lines 8, 13의strokeAlpha="0.5"속성은 stroke가 정의되지 않은 path에서 불필요합니다.♻️ 제안하는 수정
<path android:pathData="M28.8,18.51m-18.51,0a18.51,18.51 0,1 1,37.02 0a18.51,18.51 0,1 1,-37.02 0" - android:strokeAlpha="0.5" android:fillColor="#FF311F" android:fillAlpha="0.5"/> <path android:pathData="M28.8,18.51m-11.31,0a11.31,11.31 0,1 1,22.62 0a11.31,11.31 0,1 1,-22.62 0" - android:strokeAlpha="0.5" android:fillColor="#FF311F" android:fillAlpha="0.5"/>feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/RandomPoseFloatingBar.kt (1)
104-107: Drawable 리소스 네이밍 불일치.
ic_scrap_selected와icon_scrap_unselected의 prefix가 다릅니다 (ic_vsicon_). 일관된 네이밍 컨벤션 적용을 권장합니다.feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/RandomPoseTutorialOverlay.kt (1)
73-91: 하드코딩된 Spacer 값은 다양한 화면 크기에서 레이아웃 문제를 유발할 수 있음
VerticalSpacer(275f)와VerticalSpacer(243f)는 고정된 값으로, 다양한 화면 크기와 밀도에서 아이콘과 라벨이 의도한 위치에 표시되지 않을 수 있습니다. 특히 작은 화면에서는 콘텐츠가 잘리거나 겹칠 수 있습니다.
weight()modifier나Arrangement.SpaceEvenly를 사용하여 비율 기반 레이아웃으로 변경하는 것을 고려해 주세요.♻️ 비율 기반 레이아웃 제안
`@Composable` private fun TutorialGuideItem( iconRes: Int, label: String, modifier: Modifier = Modifier, ) { Column( - modifier = modifier, + modifier = modifier.fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, ) { - VerticalSpacer(275f) Icon( imageVector = ImageVector.vectorResource(iconRes), contentDescription = null, modifier = Modifier.size(80.dp), tint = Color.Unspecified, ) VerticalSpacer(6.dp) Text( text = label, style = NekiTheme.typography.body16SemiBold, color = NekiTheme.colorScheme.white, ) - VerticalSpacer(243f) } }feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/component/PoseListContent.kt (2)
47-52:items에key파라미터 추가를 권장합니다.
key파라미터 없이items를 사용하면 리스트 항목이 변경될 때 불필요한 recomposition이 발생할 수 있습니다.pose.id를 key로 사용하면 성능이 개선됩니다.♻️ 권장 수정안
- items(poseList) { pose -> + items(poseList, key = { it.id }) { pose -> PoseItem( pose = pose, onClickItem = onClickItem, ) }
62-69: 접근성을 위해contentDescription추가를 고려해주세요.
contentDescription = null은 스크린 리더 사용자에게 이미지 정보를 제공하지 않습니다. 포즈 이미지에 대한 설명을 추가하면 접근성이 개선됩니다.♻️ 권장 수정안
AsyncImage( modifier = modifier .clip(RoundedCornerShape(12.dp)) .noRippleClickable { onClickItem(pose) }, model = pose.poseImageUrl, - contentDescription = null, + contentDescription = "포즈 이미지", contentScale = ContentScale.FillWidth, )feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/component/FilterBar.kt (1)
69-69:derivedStateOf사용이 불필요합니다.단순한 null 체크에
remember+derivedStateOf조합은 과도합니다.peopleCount != null은 비용이 거의 없는 연산이므로 직접 사용해도 됩니다.♻️ 간소화 제안
- val isSelected by remember(peopleCount) { derivedStateOf { peopleCount != null } } + val isSelected = peopleCount != nullfeature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/detail/PoseDetailContract.kt (1)
15-18:ShowToast메시지에 대해 다국어 지원을 고려해주세요.
ShowToast(val message: String)대신 문자열 리소스 ID를 사용하면 향후 다국어 지원에 유리합니다. 현재 앱이 한국어 전용이라면 지금 구현도 괜찮습니다.♻️ 다국어 지원을 위한 대안
// 옵션 1: 문자열 리소스 ID 사용 data class ShowToast(`@StringRes` val messageResId: Int) : PoseDetailSideEffect // 옵션 2: sealed class로 메시지 타입 정의 sealed class ToastMessage { data object ScrapSuccess : ToastMessage() data object ScrapFailed : ToastMessage() } data class ShowToast(val message: ToastMessage) : PoseDetailSideEffectfeature/pose/api/src/main/java/com/neki/android/feature/pose/api/PoseNavKey.kt (1)
17-18: 네비게이션 키에 전체Pose객체 전달 시 주의사항
Pose객체 전체를 네비게이션 키로 전달하면 직렬화된 데이터가 커질 수 있고, 프로세스 재생성 시 상태 복원에 문제가 발생할 수 있습니다.Pose모델에 필드가 추가되면 직렬화 크기가 증가합니다.ID만 전달하고 상세 화면에서 데이터를 조회하는 방식도 고려해 보세요. 현재 구현이 의도된 것이라면 무시해도 됩니다.
feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/navigation/PoseEntryProvider.kt (1)
37-37:navigateToNotification이 빈 람다로 설정됨알림 화면으로의 네비게이션이 아직 구현되지 않은 것으로 보입니다. 의도된 것이라면 TODO 주석을 추가하여 추후 구현이 필요함을 명시하는 것이 좋습니다.
feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/detail/PoseDetailScreen.kt (1)
79-93: 스크랩 아이콘의tint가 상태에 따라 변경되지 않음아이콘 리소스는
isScrapped상태에 따라 변경되지만,tint는 항상gray500으로 고정되어 있습니다. 스크랩된 상태를 더 명확히 구분하려면tint도 상태에 따라 다르게 적용하는 것이 좋습니다.♻️ 수정 제안
NekiIconButton( modifier = Modifier .align(Alignment.End) .padding(vertical = 1.dp, horizontal = 8.dp), onClick = { onIntent(PoseDetailIntent.ClickScrapIcon) }, ) { Icon( imageVector = ImageVector.vectorResource( if (uiState.pose.isScrapped) R.drawable.ic_scrap_selected else R.drawable.icon_scrap_unselected, ), contentDescription = null, - tint = NekiTheme.colorScheme.gray500, + tint = if (uiState.pose.isScrapped) NekiTheme.colorScheme.primary + else NekiTheme.colorScheme.gray500, ) }디자인 시스템에서 정의된 스크랩 아이콘 색상이 별도로 있다면 해당 색상을 사용하세요.
core/model/src/main/java/com/neki/android/core/model/PoseContract.kt (2)
6-29: 더미 데이터가 core/model 모듈에 위치해 있습니다.
dummyPoseList와scrappedDummyList가private으로 선언되어 있지만,PoseState의 기본값으로 사용되고 있어 프로덕션 코드에 더미 데이터가 포함됩니다. 실제 API 연동 시 이 더미 데이터를 제거하거나, 테스트/프리뷰 전용 모듈로 분리하는 것이 좋습니다.Also applies to: 31-74
76-85: PoseState의 기본값으로 더미 데이터 사용 시 주의가 필요합니다.
randomPoseList와scrappedPoseList가 더미 데이터를 기본값으로 사용합니다. 실제 데이터 로딩 전까지 빈 리스트(persistentListOf())를 사용하고, ViewModel에서 API 응답으로 업데이트하는 패턴이 더 안전합니다.♻️ 권장 수정안
data class PoseState( val isLoading: Boolean = false, val selectedPeopleCount: PeopleCount? = null, val selectedRandomPosePeopleCount: PeopleCount? = null, val isShowScrappedPose: Boolean = false, - val randomPoseList: ImmutableList<Pose> = dummyPoseList, - val scrappedPoseList: ImmutableList<Pose> = scrappedDummyList, + val randomPoseList: ImmutableList<Pose> = persistentListOf(), + val scrappedPoseList: ImmutableList<Pose> = persistentListOf(), val isShowPeopleCountBottomSheet: Boolean = false, val isShowRandomPosePeopleCountBottomSheet: Boolean = false, )feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/PoseViewModel.kt (1)
63-65: fetchInitialData가 실제 데이터를 가져오지 않습니다.현재
reduce { copy() }는 상태를 변경하지 않아 no-op입니다. API 연동 시 이 부분에 데이터 로딩 로직이 추가되어야 할 것으로 보입니다. TODO 주석을 추가하여 추후 작업임을 명시하는 것이 좋겠습니다.♻️ 제안
private fun fetchInitialData(reduce: (PoseState.() -> PoseState) -> Unit) { + // TODO: API 연동 후 실제 데이터 로딩 로직 추가 reduce { copy() } }feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/RandomPoseViewModel.kt (3)
18-21: 네비게이션 인자 키가 매직 스트링으로 사용되고 있습니다.
"peopleCount"문자열이 하드코딩되어 있습니다. 네비게이션 키를 상수로 정의하면 타이포 방지와 유지보수성이 향상됩니다.♻️ 권장 수정안
// companion object 또는 별도 파일에 정의 companion object { const val ARG_PEOPLE_COUNT = "peopleCount" } private val peopleCount: PeopleCount = savedStateHandle .get<String>(ARG_PEOPLE_COUNT) ?.let { PeopleCount.valueOf(it) } ?: PeopleCount.ONE
44-50: 일부 Intent가 no-op으로 구현되어 있습니다.
EnterRandomPoseScreen,ClickLeftSwipe,ClickRightSwipe가Unit을 반환하며 아무 동작도 하지 않습니다. 튜토리얼 스와이프 동작이 추후 구현 예정이라면 TODO 주석을 추가하는 것이 좋겠습니다.♻️ 제안
when (intent) { - RandomPoseIntent.EnterRandomPoseScreen -> Unit + RandomPoseIntent.EnterRandomPoseScreen -> Unit // TODO: 초기 데이터 로딩 // 튜토리얼 - RandomPoseIntent.ClickLeftSwipe -> Unit - RandomPoseIntent.ClickRightSwipe -> Unit + RandomPoseIntent.ClickLeftSwipe -> Unit // TODO: 스와이프 애니메이션/힌트 + RandomPoseIntent.ClickRightSwipe -> Unit // TODO: 스와이프 애니메이션/힌트 RandomPoseIntent.ClickStartRandomPose -> reduce { copy(isShowTutorial = false) }
23-27: 더미 데이터가 ViewModel에 직접 정의되어 있습니다.
dummyPoseList가 프로덕션 코드에 포함되어 있습니다. API 연동 전 플레이스홀더로 사용되는 것으로 보이지만, 추후 Repository에서 데이터를 가져오도록 변경이 필요합니다.feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/component/PeopleCountBottomSheet.kt (1)
68-68:PeopleCount.entries.drop(1)로 1인 옵션을 명시적으로 제외합니다.이 시트에서는
drop(1)로ONE(1인)을 제외하고 2인부터 표시하는 반면,RandomPosePeopleCountBottomSheet는PeopleCount.entries.toImmutableList()로 모든 옵션을 표시합니다. 두 시트의 목적이 다르다면,drop(1)을 사용하는 이유를 코드 주석으로 명시하면 향후 유지보수에 도움이 될 것입니다.feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album/AllAlbumScreen.kt (1)
148-159: 기본값이 있으므로 null 처리는 불필요하나, UX 개선을 위해 버튼 비활성화 고려
selectedDeleteOption은AlbumDeleteOption.DELETE_WITH_PHOTOS로 기본값이 설정되어 있어 null이 될 수 없습니다. 다만 사용자가 명시적으로 옵션을 선택할 때까지 확인 버튼을 비활성화하는 것이 더 나은 UX입니다.DoubleButtonOptionBottomSheet는 이미buttonEnabled파라미터를 지원하므로, 사용자 선택 후에만 버튼을 활성화하도록 개선하는 것을 검토해 주세요.개선 예시
DoubleButtonOptionBottomSheet( title = "앨범을 삭제하시겠어요?", options = AlbumDeleteOption.entries.toImmutableList(), primaryButtonText = "삭제하기", secondaryButtonText = "취소", selectedOption = uiState.selectedDeleteOption, onDismissRequest = { onIntent(AllAlbumIntent.DismissDeleteAlbumBottomSheet) }, onClickCancel = { onIntent(AllAlbumIntent.DismissDeleteAlbumBottomSheet) }, onClickActionButton = { onIntent(AllAlbumIntent.ClickDeleteConfirmButton) }, onOptionSelect = { onIntent(AllAlbumIntent.SelectDeleteOption(it)) }, + buttonEnabled = false, )
`dev.chrisbanes.haze` 라이브러리를 `core:designsystem` 모듈에 추가하여 UI에 흐림 효과(blur effect)를 적용할 수 있도록 합니다.
Haze 라이브러리를 사용하여 블러 효과 배경을 적용하는 `backgroundHazeBlur` Modifier 확장 함수를 추가합니다. 이 함수는 `HazeState`를 받아 블러 효과를 관리하며, `enabled` 파라미터를 통해 블러 효과를 켜고 끌 수 있습니다.
스크랩 아이콘의 상하 패딩을 1dp에서 8dp로 수정하여 다른 아이콘과 동일하게 맞춥니다.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/PoseScreen.kt`:
- Around line 93-107: The two bottom sheets (PeopleCountBottomSheet and
RandomPosePeopleCountBottomSheet) can both render if
uiState.isShowPeopleCountBottomSheet and
uiState.isShowRandomPosePeopleCountBottomSheet are true; make their visibility
mutually exclusive by checking the flags in an if/else-if or by ensuring the
state updates in the intent handlers (onIntent with PoseIntent.Dismiss*/Click*
variants) never set both true at once; update the Compose branch around the
PeopleCountBottomSheet / RandomPosePeopleCountBottomSheet usage (referencing
uiState.isShowPeopleCountBottomSheet,
uiState.isShowRandomPosePeopleCountBottomSheet, PeopleCountBottomSheet,
RandomPosePeopleCountBottomSheet, and the respective PoseIntent events) so only
one sheet can be shown at a time.
In `@gradle/libs.versions.toml`:
- Line 36: The haze entry (haze = "1.7.1") is mixing JetBrains Compose
Multiplatform artifacts with an AndroidX Compose BOM (Compose BOM 2025.08.01)
causing version mismatch; either remove the AndroidX Compose BOM from dependency
management and centrally manage JetBrains Compose Multiplatform versions (keep
haze and haze-materials at 1.7.1 and ensure compose_multiplatform version is the
source of truth), or upgrade the AndroidX Compose BOM to a newer release that
aligns with Compose Multiplatform 1.9.3; update gradle/libs.versions.toml and
any BOM import configuration accordingly, and verify Kotlin 2.1.0 remains
compatible.
♻️ Duplicate comments (1)
feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/RandomPoseScreen.kt (1)
90-94: 튜토리얼 오버레이가 터치를 차단하지 않습니다.현재 오버레이는 시각 요소만 그려서 하위
RandomPoseFloatingBarContent버튼이 그대로 터치될 수 있습니다. 오버레이 영역에서 터치를 소비하도록 스크림(클릭 방지 레이어)을 추가해 주세요.🛠️ 터치 소비 레이어 추가 예시
+import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember ... internal fun RandomPoseScreen( uiState: RandomPoseUiState = RandomPoseUiState(), onIntent: (RandomPoseIntent) -> Unit = {}, ) { val hazeState = rememberHazeState() + val overlayInteraction = remember { MutableInteractionSource() } ... if (uiState.isShowTutorial) { - RandomPoseTutorialOverlay( - hazeState = hazeState, - onClickStart = { onIntent(RandomPoseIntent.ClickStartRandomPose) }, - ) + Box(Modifier.fillMaxSize()) { + Box( + Modifier + .fillMaxSize() + .clickable( + interactionSource = overlayInteraction, + indication = null, + ) {}, + ) + RandomPoseTutorialOverlay( + hazeState = hazeState, + onClickStart = { onIntent(RandomPoseIntent.ClickStartRandomPose) }, + ) + } } }
🧹 Nitpick comments (1)
core/designsystem/build.gradle.kts (1)
12-13: designsystem에서 haze를 API로 노출할 필요 여부 재확인공개 API에서 Haze 타입을 직접 노출하지 않는다면
api대신implementation으로 낮춰 의존성 표면을 줄이는 편이 안전합니다.🔧 제안 변경(공개 API 미노출 시)
- api(libs.haze) - api(libs.haze.materials) + implementation(libs.haze) + implementation(libs.haze.materials)
`PoseNavKey`의 `Main`과 `Detail`을 각각 `PoseMain`, `PoseDetail`로 변경하여 역할을 더 명확하게 나타냅니다. 변경된 `PoseNavKey`를 `PoseEntryProvider`와 `TopLevelNavItem`에 적용하여 내비게이션 경로를 일관성 있게 수정합니다.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@feature/pose/api/src/main/java/com/neki/android/feature/pose/api/PoseNavKey.kt`:
- Around line 15-18: The enum PeopleCount used by RandomPose is missing
kotlinx.serialization support; add the `@Serializable` annotation to the
PeopleCount enum declaration (so RandomPose can be serialized for backstack
save/restore), and import kotlinx.serialization.Serializable where PeopleCount
is declared; verify existing PoseDetail/Pose remain annotated to keep
consistency with PoseNavKey-based sealed/data classes.
In
`@feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/navigation/PoseEntryProvider.kt`:
- Around line 52-57: The RandomPose entry does not capture the navigation key so
its peopleCount may not be propagated to SavedStateHandle; change the
entry<PoseNavKey.RandomPose> block to accept the key parameter (same pattern as
PoseDetail) and pass key.peopleCount into the route/state so RandomPoseViewModel
can read it from SavedStateHandle; update the entry for PoseNavKey.RandomPose
surrounding RandomPoseRoute (which uses navigator::goBack and
navigator::navigateToPoseDetail) to take a lambda parameter like { key -> ... }
and ensure the key is used or forwarded to the view model/state.
…Count` 전달 `RandomPoseViewModel`에 `@AssistedInject`와 `@AssistedFactory`를 적용하여 `peopleCount`를 `SavedStateHandle`이 아닌 생성자를 통해 직접 주입받도록 수정합니다. 이를 통해 뷰모델 생성 시 동적인 파라미터를 타입-세이프하게 전달할 수 있게 되었습니다.
Ojongseok
left a comment
There was a problem hiding this comment.
블러 효과를 구현하는데 저런 라이브러리가 있군요 고생 많으셨습니다!
Color가 하드코딩된 부분들이 군데군데 있어 Color(0xFFxxxxxx) 해당 부분만 NekiTheme의 컬러로 변경해주시면 될 것 같습니다.
화면 전환 시에 enum class를 이용해서 파라미터로 전달하고 수신하는 부분 잘 보았습니다. 감사합니다!
`DoubleButtonOptionBottomSheet` 컴포저블의 `onClickCancel`과 `onClickActionButton` 파라미터명을 각각 `onClickSecondaryButton`과 `onClickPrimaryButton`으로 변경하여 역할을 더 명확하게 나타냅니다. 해당 컴포저블을 사용하는 모든 부분에 변경된 파라미터명을 일괄 적용했습니다.
인원 수 필터 선택 바텀시트에서 `drop(1)`을 제거하여 '전체'에 해당하는 첫 번째 아이템이 표시되도록 수정합니다.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🔗 관련 이슈
📙 작업 설명
📷 스크린샷
Screen_recording_20260125_035031.mp4
💬 추가 설명 or 리뷰 포인트 (선택)
Haze 백그라운드 블러 적용 용도
피그마 BackgroundBlur 효과를 컴포즈 내부적으로 구현 방법을 찾지 못해 적용한 라이브러리입니다.
1가지 State와 2가지 모디파이어 함수를 사용해서 백그라운드 블러를 적용 가능합니다.
val hazeState = rememberHazeState()Modifier.hazeEffect(hazeState, ...): 백그라운드 블러를 사용하는 컴포넌트에 적용Modifier.hazeSource(hazeState): 백그라운드 블러에 의해 가려지는 컴포넌트에 적용이 사진의 경우에선, 흐려진 배경을 가지는 컴포넌트가
hazeEffect()를 사용하고 있고,뒤에 가려진 침착맨 등을 포함한 컴포넌트가
hazeSource()(블러의 근원지) 가 됩니다.Summary by CodeRabbit
새로운 기능
개선 사항
디자인
데이터
✏️ Tip: You can customize this high-level summary in your review settings.