Conversation
* `Badge`: 뱃지 타입과 획득 여부를 관리하는 데이터 클래스 추가 * `BadgeType`: 뱃지 종류를 정의하는 열거형(Enum) 추가 (`FIRST_PRESENTATION`, `SECOND_ANALYSIS`, `FIRST_PRACTICE`, `RETROSPECT_COMPLETED`, `PERFECT_SCORE`, `TEN_ANALYSIS`)
My 페이지의 상태 관리를 위해 MVI(Intent, State, Effect) 패턴을 적용하고 관련 모델을 추가했습니다.
* **feat: My 페이지 Contract 정의**
* `MyUiIntent`: 데이터 요청을 위한 `FetchData` 정의
* `MyUiState`: 로딩 상태, 프로필 이미지, 닉네임, 배지 리스트(`ImmutableList`)를 포함하는 상태 구조 정의
* `MyUiEffect`: 메시지 노출(`ShowMessage`)을 위한 사이드 이펙트 정의
* **refactor: MyUiState 위치 및 구조 변경**
* 기존의 단순 `sealed interface`였던 `MyUiState`를 삭제하고, `com.team.prezel.feature.my.impl.contract` 패키지로 이동하여 데이터 클래스 형태로 재정의했습니다.
* **feat: UI 메시지 모델 추가**
* 유저 정보 및 배지 조회 실패 시 사용할 `MyUiMessage` enum 클래스를 추가했습니다.
* feat: PrezelBadge 컴포넌트 구현
* 뱃지 이미지와 타이틀을 표시하는 `PrezelBadge` 컴포저블을 추가했습니다.
* `isAchieved` 상태에 따라 획득 미달성 시 자물쇠 아이콘과 스크림(Scrim) 배경이 노출되도록 구현했습니다.
* 컴포넌트 동작 확인을 위한 `PrezelBadgeAchievedPreview`, `PrezelBadgeNotAchievedPreview`를 추가했습니다.
* feat: 뱃지 타입별 리소스 및 매핑 로직 추가
* `BadgeType` 모델을 기반으로 한 타이틀 문자열 및 드로어블 리소스 매핑 확장 함수(`title()`, `drawableResId()`)를 구현했습니다.
* 뱃지 관련 문자열 리소스 6종(시작이 반, 감 잡는 중, 워밍업 등)을 추가했습니다.
* chore: core:ui 모듈 의존성 추가
* `BadgeType` 모델 사용을 위해 `core:model` 프로젝트 의존성을 추가했습니다.
* feat: 사용자 뱃지 목록 조회를 위한 UseCase 추가 임시 데이터를 활용하여 사용자의 뱃지 획득 현황을 가져오는 `FetchUserBadgesUseCase`를 구현했습니다. * `BadgeType`의 각 항목을 기반으로 뱃지 리스트를 생성합니다. * `Random.nextBoolean()`을 사용하여 임시로 달성 여부(`isAchieved`)를 설정하도록 구현되었습니다.
* `badge_start.xml`: 시작 단계의 뱃지를 나타내는 벡터 그래픽 리소스를 추가하였습니다. (140x140dp, 파란색 배경 및 흰색 아이콘 조합)
* `feature:my` 모듈 내 UI 레이어에서 사용할 `BadgeUiModel` 클래스를 정의했습니다. * 도메인 모델인 `Badge`를 `BadgeUiModel`로 변환하기 위한 `toUiModel` 확장 함수를 추가했습니다.
* **feat: MyPage 프로필 및 상단 바 컴포넌트 추가**
* `ProfileSection`: 사용자의 프로필 이미지, 닉네임 표시 및 '프로필 편집' 버튼 제공
* `MyTopAppBar`: 설정 아이콘을 포함한 MyPage 전용 상단 바 구현
* 디자인 시스템의 `PrezelAvatar`, `PrezelButton`, `PrezelTopAppBar` 적용
* **feat: 뱃지 섹션(`BadgeSection`) 구현**
* 사용자가 획득한 뱃지 목록을 가로 스크롤(`LazyRow`) 형태로 보여주는 섹션 추가
* `BadgeUiModel` 및 `BadgeType`을 연동하여 뱃지 이미지, 타이틀, 획득 여부(활성화 상태) 렌더링
* 화면 너비에 맞춰 뱃지 아이템 크기를 동적으로 계산하는 로직 추가
* **chore: 빌드 의존성 추가**
* `feature:my:impl` 모듈에 `coreDomain`, `coreModel`, `featureProfileApi` 및 `kotlinx-collections-immutable` 의존성 추가
`PrezelList` 컴포넌트의 제목 색상을 외부에서 유연하게 설정할 수 있도록 기능을 개선했습니다. * `PrezelList` 파라미터에 `titleTextColor`를 추가하고 기본값을 `LocalContentColor.current`로 설정하여 하위 호환성을 유지했습니다. * 내부 `PrezelListTitle` 컴포저블이 하드코딩된 색상 대신 전달받은 `textColor`를 사용하도록 수정했습니다.
* **feat: MyViewModel 내 사용자 정보 및 배지 조회 로직 구현**
* `FetchUserInfoUseCase` 및 `FetchUserBadgesUseCase`를 통해 프로필 정보와 배지 목록을 비동기적으로 가져오는 `fetchData` 로직을 추가했습니다.
* `BaseViewModel`을 상속받아 MVI 구조(`MyUiState`, `MyUiIntent`, `MyUiEffect`)를 적용했습니다.
* 데이터 로드 실패 시 `ShowMessage` 이벤트를 통해 에러 스낵바를 노출하도록 처리했습니다.
* **feat: MyScreen UI 구현 및 상태 연동**
* `MyTopAppBar`, `ProfileSection`, `BadgeSection` 컴포너트를 조합하여 마이페이지 전체 레이아웃을 구성했습니다.
* `LaunchedEffect`를 통해 화면 진입 시 데이터를 요청하고, `uiEffect`를 수집하여 실패 메시지를 스낵바(`showPrezelSnackbar`)로 표시하도록 구현했습니다.
* `MyScreenPreview`를 추가하여 다양한 배지 상태에 따른 미리보기를 지원합니다.
* **feat: MyEntryBuilder 내비게이션 연결**
* `MyScreen`에서 프로필 수정 화면으로 이동할 수 있도록 `navigateToEditProfile` 콜백에 `ProfileNavKey.Edit` 경로를 연결했습니다.
* 설정 및 배지 화면 이동을 위한 콜백 파라미터를 추가했습니다.
* **refactor: MyViewModel 접근 제어자 수정**
* `MyViewModel`을 내부 모듈에서만 접근 가능하도록 `internal`로 변경했습니다.
`MyScreen`의 `MyTopAppBar`와 `ProfileSection` 사이의 간격을 조정하기 위해 `V16` 크기의 `Spacer`를 추가했습니다.
`PrezelNavigationBarScaffold`에서 내비게이션 바가 사라질 때의 애니메이션을 명시적으로 설정하였습니다. * `AnimatedVisibility`의 `exit` transition을 `ExitTransition.None`으로 설정하여 바텀바가 사라질 때의 전환 효과를 제거하였습니다.
`BadgeSection` 컴포넌트 내 `ChevronRight` 아이콘의 색상을 디자인 시스템 토큰인 `PrezelTheme.colors.iconRegular`로 지정하였습니다.
`ProfileSection` 컴포넌트가 UI 상태 객체(`MyUiState`) 전체에 의존하지 않고 필요한 데이터만 주입받도록 개선했습니다.
* **refactor: ProfileSection 파라미터 분리**
* `uiState: MyUiState` 파라미터를 제거하고 `nickname: String`과 `profileImageUrl: String?`으로 분리하여 의존성을 낮췄습니다.
* 내부 `PrezelAvatar` 및 `Text` 컴포넌트에서 변경된 파라미터를 사용하도록 수정했습니다.
* **refactor: MyScreen 및 Preview 코드 반영**
* `MyScreen`에서 `ProfileSection` 호출 시 `uiState`의 필드를 개별적으로 전달하도록 수정했습니다.
* `ProfileSection`의 Preview 코드에서 상태 객체 생성 대신 직접 값을 전달하도록 업데이트했습니다.
`UserRepositoryImpl`의 더미 데이터에서 빈 문자열로 설정되어 있던 `nickname` 값을 "닉네임"으로 수정하였습니다.
📝 WalkthroughWalkthrough프로필 화면을 구현합니다. 배지 모델 및 사용 사례 추가, 배지 UI 컴포넌트 개발, 프로필 및 배지 섹션 구성, 상태 관리 아키텍처 적용으로 사용자 정보와 배지를 표시합니다. Changes
Possibly related PRs
🚥 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. 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: 4
🧹 Nitpick comments (9)
Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/navigation/MyEntryBuilder.kt (1)
21-22: 주석 처리된 네비게이션 스텁은 명시적 TODO로 대체해 주세요.
navigateToSetting/navigateToBadge가 주석 처리된 코드만 남아 있어 후속 작업 추적이 어렵습니다. 또한Prezel/feature/profile/api/.../ProfileNavKey.kt에는 현재Create/Edit만 존재하여 주석 내ProfileNavKey.Setting/ProfileNavKey.Badge는 실제로 정의되어 있지 않습니다. 설정/뱃지 상세가 프로필 피처에 속하는지(별도 피처 NavKey가 필요한지)도 함께 결정해 주세요.♻️ 예시 변경안
- navigateToSetting = { /* navigator.navigate(ProfileNavKey.Setting) */ }, - navigateToBadge = { /* navigator.navigate(ProfileNavKey.Badge) */ }, + navigateToSetting = { + // TODO(#<issue>): 설정 화면 NavKey 정의 후 연결 + }, + navigateToBadge = { + // TODO(#<issue>): 뱃지 상세 화면 NavKey 정의 후 연결 + },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/navigation/MyEntryBuilder.kt` around lines 21 - 22, Replace the commented navigator stubs for navigateToSetting and navigateToBadge with explicit TODO lambdas that make the missing work visible and actionable (e.g., a lambda that logs or throws a NotImplemented/UnsupportedOperation with a short TODO message), reference the missing symbols ProfileNavKey.Setting and ProfileNavKey.Badge by name in the TODO so the reviewer knows these NavKeys are absent, and update the TODO to state whether these screens should be added to the profile feature (add ProfileNavKey entries) or moved to a separate feature (create new NavKeys) so follow-up work is tracked.Prezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/user/FetchUserBadgesUseCase.kt (1)
8-20: 목업 임시 구현임을 명시적으로 표시해 주세요.현재 구현은 결정적 로직을
runCatching으로 감싸고 있어Result.failure경로로 진입할 가능성이 없습니다. 실제 레포지토리 연동으로 대체될 예정이라면, 후속 작업이 누락되지 않도록TODO/FIXME주석과 트래킹 이슈 번호를 남겨두시는 걸 권장합니다. 또한 도메인 모듈에kotlin.random.Random이 직접 포함되어 있어 실 구현 전환 시 의존성 정리도 함께 검토 부탁드립니다.♻️ 예시 변경안
class FetchUserBadgesUseCase `@Inject` constructor() { + // TODO(#<issue>): 목업 구현 — 뱃지 조회 Repository 연동 후 교체 suspend operator fun invoke(): Result<List<Badge>> = runCatching { listOf(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/user/FetchUserBadgesUseCase.kt` around lines 8 - 20, The FetchUserBadgesUseCase.invoke currently contains a temporary mock that deterministically cannot fail (wrapping pure data in runCatching) and uses kotlin.random.Random directly; mark this as a mock by adding a clear TODO/FIXME comment with your issue/track ID, remove or avoid misleading use of runCatching around non-throwing code (or explicitly throw a MockNotImplementedException if you want failure path exercised), and add a note to remove direct kotlin.random.Random from the domain module when wiring the real repository; reference FetchUserBadgesUseCase.invoke, the runCatching block, and Random usage so the implementer replaces the body with real repository calls later.Prezel/core/ui/src/main/res/values/strings.xml (1)
3-9: 리소스 키 prefix가 실제 모듈 경로와 어긋납니다.이 파일은
Prezel/core/ui모듈(단일 모듈,impl서브모듈 없음)에 위치합니다. 반면 키는core_ui_impl_badge_*형태로impl이 붙어 있어 다른 모듈의 리소스 네이밍 컨벤션(예:core_designsystem_*은core/designsystem단일 모듈,feature_history_impl_*은feature/history/impl서브모듈과 일치)과 어긋납니다. 후속 혼동을 막기 위해core_ui_badge_*로 정리하는 것을 권장합니다.♻️ 예시 변경안
- <string name="core_ui_impl_badge_first_presentation">시작이 반</string> - <string name="core_ui_impl_badge_second_analysis">감 잡는 중</string> - <string name="core_ui_impl_badge_first_practice">워밍업</string> - <string name="core_ui_impl_badge_retrospect_completed">끝까지 해냄</string> - <string name="core_ui_impl_badge_perfect_score">컨디션 최고</string> - <string name="core_ui_impl_badge_ten_analysis">감 잡았다</string> + <string name="core_ui_badge_first_presentation">시작이 반</string> + <string name="core_ui_badge_second_analysis">감 잡는 중</string> + <string name="core_ui_badge_first_practice">워밍업</string> + <string name="core_ui_badge_retrospect_completed">끝까지 해냄</string> + <string name="core_ui_badge_perfect_score">컨디션 최고</string> + <string name="core_ui_badge_ten_analysis">감 잡았다</string>
PrezelBadge.kt의BadgeType.title()함수(라인 57-62)에서 참조하는 리소스 ID도 함께 변경해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/ui/src/main/res/values/strings.xml` around lines 3 - 9, Resource keys in strings.xml use the incorrect "core_ui_impl_badge_*" prefix; rename them to "core_ui_badge_*" to match the module (Prezel/core/ui) and consistent naming conventions, e.g. change core_ui_impl_badge_first_presentation -> core_ui_badge_first_presentation, etc. After renaming each string resource, update all callers to the new IDs, specifically update BadgeType.title() in PrezelBadge.kt (the function that references those resources around lines 57-62) to use the new core_ui_badge_* identifiers. Ensure no remaining references to the old core_ui_impl_badge_* keys remain in the codebase or resource XMLs.Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PrezelBadge.kt (1)
66-75:@Composable어노테이션 제거 권장.
BadgeType.drawableResId()는 내부에서stringResource등 Composable-only API를 사용하지 않고 단순 when 매핑만 수행하므로@Composable일 필요가 없습니다. 일반 확장 함수로 두면 non-Composable 컨텍스트(예: 테스트, 유스케이스)에서도 재사용 가능합니다.♻️ 제안 diff
-// todo: 뱃지 디자인 업데이트 완료 후 반영할 예정 -@Composable -fun BadgeType.drawableResId(): Int = +// todo: 뱃지 디자인 업데이트 완료 후 반영할 예정 +@DrawableRes +fun BadgeType.drawableResId(): Int = when (this) { BadgeType.FIRST_PRESENTATION -> R.drawable.badge_start🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PrezelBadge.kt` around lines 66 - 75, Remove the unnecessary `@Composable` annotation from the BadgeType.drawableResId() extension since it only performs a plain when-to-resource-id mapping and does not call any Compose-only APIs; change it to a regular function so BadgeType.drawableResId() can be used from non-Compose contexts (tests, viewmodels, use-cases) and keep the existing when branches and returned R.drawable.* constants unchanged.Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/BadgeSection.kt (2)
58-73: 하드코딩된 문자열("나의 뱃지","뱃지")을 string 리소스로 분리하세요.
title과contentDescription모두 하드코딩되어 있어 i18n/접근성 관점에서 리소스화를 권장합니다.feature_my_impl_*네임스페이스로strings.xml에 추가하는 컨벤션을 이미 따르고 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/BadgeSection.kt` around lines 58 - 73, Replace the hardcoded Korean strings in BadgeListTitle with string resources: move "나의 뱃지" and "뱃지" into strings.xml under the feature_my_impl_* namespace (e.g., feature_my_impl_badge_title and feature_my_impl_badge_content_description) and load them via stringResource(...) where PrezelList's title and Icon's contentDescription are set; update the BadgeListTitle function to call stringResource for the title and contentDescription while keeping the existing PrezelList/Icon usage (PrezelList title, Icon painterResource(PrezelIcons.ChevronRight)) so i18n and accessibility conventions are followed.
40-55: 섹션 전체 클릭 동작을 재검토해 주세요.
Column에clickable(indication = null)을 걸어 “나의 뱃지” 영역 어디를 터치해도onClickBadge가 호출되는 구조입니다. 디자인 의도가 “뱃지 리스트 타이틀(우측 ChevronRight)만 탭해서 상세로 이동”이라면, 해당 클릭 영역을BadgeListTitle쪽으로 한정하는 편이 사용자 기대치에 더 부합합니다. 또한 추후 “개별 뱃지 탭 → 뱃지 상세” 요구가 생기면 현재 구조에서 이벤트 충돌이 발생할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/BadgeSection.kt` around lines 40 - 55, The Column currently applies clickable(...) causing onClickBadge to fire for the entire section; remove the clickable modifier from the Column and instead apply it only to the title component so only BadgeListTitle triggers onClickBadge. Concretely, drop clickable(...) from the Column modifier, wrap BadgeListTitle (or the chevron area inside it) with clickable(onClick = onClickBadge, interactionSource = null, indication = null), and keep BadgeList as-is so individual badge items can host their own click handlers later to avoid event conflicts.Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/ProfileSection.kt (1)
33-56: 하드코딩된 UI 문자열을 string 리소스로 분리하는 것을 권장합니다.
contentDescription = "프로필 이미지"와text = "프로필 편집"이 하드코딩되어 있어 i18n/접근성 일관성이 떨어집니다.MyTopAppBar에서는 이미stringResource(R.string.feature_my_impl_title)방식으로 분리하고 있어 동일한 컨벤션을 따르는 것이 좋아 보입니다.♻️ 제안 diff
+import androidx.compose.ui.res.stringResource PrezelAvatar( imageUrl = profileImageUrl, - contentDescription = "프로필 이미지", + contentDescription = stringResource(R.string.feature_my_impl_profile_image_description), size = PrezelAvatarSize.REGULAR, ) ... PrezelButton( - text = "프로필 편집", + text = stringResource(R.string.feature_my_impl_edit_profile), iconResId = PrezelIcons.Edit,그리고
feature/my/impl의strings.xml에 대응 리소스를 추가해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/ProfileSection.kt` around lines 33 - 56, 현재 ProfileSection.kt에서 PrezelAvatar의 contentDescription과 PrezelButton의 text가 하드코딩("프로필 이미지", "프로필 편집")되어 있으므로 string 리소스로 분리하세요: 변경할 위치는 PrezelAvatar(contentDescription = ...)와 PrezelButton(text = ... , onClick = onClickEditProfile) 호출부이며 각각을 stringResource(R.string.feature_my_profile_image_description)와 stringResource(R.string.feature_my_edit_profile)로 교체하고, 대응하는 문자열 리소스 항목을 feature/my/impl의 strings.xml에 추가해 주세요.Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.kt (2)
44-59:LaunchedEffect분리 및repeatOnLifecycle적용 권장.현재 하나의
LaunchedEffect(Unit)블록에서 1회성 intent 디스패치와uiEffect무기한 수집을 함께 수행합니다. 두 개로 분리하고, effect 수집은 백그라운드에서 불필요하게 동작하지 않도록repeatOnLifecycle(Lifecycle.State.STARTED)로 감싸는 것을 권장합니다. 스낵바가 비활성 상태에서 소진되어 사용자가 놓치는 케이스를 방지할 수 있습니다.♻️ 제안 diff
- LaunchedEffect(Unit) { - viewModel.onIntent(MyUiIntent.FetchData) - - viewModel.uiEffect.collect { effect -> - when (effect) { - is MyUiEffect.ShowMessage -> { - val resId = when (effect.message) { - MyUiMessage.FETCH_USER_INFO_FAILED -> R.string.feature_my_impl_message_fetch_user_info_failed - MyUiMessage.FETCH_USER_BADGES_FAILED -> R.string.feature_my_impl_message_fetch_user_badges_failed - } - - snackbarHostState.showPrezelSnackbar(message = resources.getString(resId)) - } - } - } - } + LaunchedEffect(Unit) { + viewModel.onIntent(MyUiIntent.FetchData) + } + + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(viewModel.uiEffect, lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiEffect.collect { effect -> + when (effect) { + is MyUiEffect.ShowMessage -> { + val resId = when (effect.message) { + MyUiMessage.FETCH_USER_INFO_FAILED -> R.string.feature_my_impl_message_fetch_user_info_failed + MyUiMessage.FETCH_USER_BADGES_FAILED -> R.string.feature_my_impl_message_fetch_user_badges_failed + } + snackbarHostState.showPrezelSnackbar(message = resources.getString(resId)) + } + } + } + } + }저장소 내 다른 피처 스크린에서 동일한 수집 패턴이 이미 확립되어 있다면, 기존 컨벤션을 우선해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.kt` around lines 44 - 59, Split the single LaunchedEffect that both dispatches the one-shot intent and continuously collects uiEffect: keep one small LaunchedEffect or direct call that dispatches viewModel.onIntent(MyUiIntent.FetchData) once, and move the effect collector into a lifecycle-aware collector using repeatOnLifecycle(Lifecycle.State.STARTED) so viewModel.uiEffect is only collected while the UI is started; inside that block collect the effects and call snackbarHostState.showPrezelSnackbar(resources.getString(resId)) for MyUiEffect.ShowMessage as before. Ensure you reference the same viewModel.uiEffect, MyUiIntent.FetchData and snackbarHostState.showPrezelSnackbar symbols when moving code so behaviour is unchanged but collection is lifecycle-scoped.
31-31:@OptIn(ExperimentalMaterial3Api::class)제거 가능합니다.
MyScreen함수는 Material3 experimental API를 직접 사용하지 않으며,MyTopAppBar만 호출합니다.MyTopAppBar가 이미 자체적으로@OptIn(ExperimentalMaterial3Api::class)을 가지고 있으므로 호출부에서는 별도 OptIn이 필요 없습니다. 실제로 파일 내 privateMyScreen함수(line 71)에도 이 annotation이 없습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.kt` at line 31, Remove the unnecessary OptIn annotation from the caller: delete the top-level `@OptIn`(ExperimentalMaterial3Api::class) placed above the MyScreen declaration because MyScreen does not directly use experimental Material3 APIs and MyTopAppBar already opts in; ensure only the MyTopAppBar definition retains its `@OptIn` and that MyScreen (and the private MyScreen at line ~71) no longer carry the annotation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PrezelBadge.kt`:
- Around line 83-111: The overlay scrim Box inside PrezelBadge (the conditional
block guarded by isAchieved) is clipped with PrezelTheme.shapes.V16 while the
parent Box and the Image are not, causing the original badge image to show
through outside the rounded scrim (especially when bgScrim is translucent); to
fix, apply the same clip shape to the parent container or the Image (the outer
Box/modifier or the Image's modifier) so the entire badge (Image + scrim) uses
PrezelTheme.shapes.V16 and yields consistent rounded corners.
In `@Prezel/core/ui/src/main/res/drawable/badge_start.xml`:
- Around line 1-19: Rename the drawable resource file from badge_start.xml to
badge_star.xml to correct the typo, then update all references in PrezelBadge.kt
(specifically in the drawableResId() method where R.drawable.badge_start is used
for the badge cases FIRST_PRESENTATION, SECOND_ANALYSIS, FIRST_PRACTICE,
RETROSPECT_COMPLETED, PERFECT_SCORE, TEN_ANALYSIS) to R.drawable.badge_star so
the resource name matches the star icon and references resolve correctly.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/BadgeSection.kt`:
- Around line 75-94: Remove the unnecessary OptIn for ExperimentalMaterial3Api
on the BadgeList composable: open the BadgeList declaration (the function named
BadgeList) and delete the `@OptIn`(ExperimentalMaterial3Api::class) annotation,
since only stable APIs (BoxWithConstraints, LazyRow, Arrangement.spacedBy, etc.)
are used; ensure no other references to ExperimentalMaterial3Api remain in that
file after removal.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/MyTopAppBar.kt`:
- Around line 19-26: Remove the `@OptIn`(ExperimentalMaterial3Api::class)
annotation from the MyTopAppBar composable and delete the
Modifier.background(PrezelTheme.colors.bgRegular) call on the PrezelTopAppBar
invocation; PrezelTopAppBar handles its own opt-in and manages background via
containerColor/scrollBehavior (so avoid forcing a background externally) and if
scroll-aware background is desired, forward a scrollBehavior parameter into
PrezelTopAppBar instead of applying background here.
---
Nitpick comments:
In
`@Prezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/user/FetchUserBadgesUseCase.kt`:
- Around line 8-20: The FetchUserBadgesUseCase.invoke currently contains a
temporary mock that deterministically cannot fail (wrapping pure data in
runCatching) and uses kotlin.random.Random directly; mark this as a mock by
adding a clear TODO/FIXME comment with your issue/track ID, remove or avoid
misleading use of runCatching around non-throwing code (or explicitly throw a
MockNotImplementedException if you want failure path exercised), and add a note
to remove direct kotlin.random.Random from the domain module when wiring the
real repository; reference FetchUserBadgesUseCase.invoke, the runCatching block,
and Random usage so the implementer replaces the body with real repository calls
later.
In
`@Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PrezelBadge.kt`:
- Around line 66-75: Remove the unnecessary `@Composable` annotation from the
BadgeType.drawableResId() extension since it only performs a plain
when-to-resource-id mapping and does not call any Compose-only APIs; change it
to a regular function so BadgeType.drawableResId() can be used from non-Compose
contexts (tests, viewmodels, use-cases) and keep the existing when branches and
returned R.drawable.* constants unchanged.
In `@Prezel/core/ui/src/main/res/values/strings.xml`:
- Around line 3-9: Resource keys in strings.xml use the incorrect
"core_ui_impl_badge_*" prefix; rename them to "core_ui_badge_*" to match the
module (Prezel/core/ui) and consistent naming conventions, e.g. change
core_ui_impl_badge_first_presentation -> core_ui_badge_first_presentation, etc.
After renaming each string resource, update all callers to the new IDs,
specifically update BadgeType.title() in PrezelBadge.kt (the function that
references those resources around lines 57-62) to use the new core_ui_badge_*
identifiers. Ensure no remaining references to the old core_ui_impl_badge_* keys
remain in the codebase or resource XMLs.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/BadgeSection.kt`:
- Around line 58-73: Replace the hardcoded Korean strings in BadgeListTitle with
string resources: move "나의 뱃지" and "뱃지" into strings.xml under the
feature_my_impl_* namespace (e.g., feature_my_impl_badge_title and
feature_my_impl_badge_content_description) and load them via stringResource(...)
where PrezelList's title and Icon's contentDescription are set; update the
BadgeListTitle function to call stringResource for the title and
contentDescription while keeping the existing PrezelList/Icon usage (PrezelList
title, Icon painterResource(PrezelIcons.ChevronRight)) so i18n and accessibility
conventions are followed.
- Around line 40-55: The Column currently applies clickable(...) causing
onClickBadge to fire for the entire section; remove the clickable modifier from
the Column and instead apply it only to the title component so only
BadgeListTitle triggers onClickBadge. Concretely, drop clickable(...) from the
Column modifier, wrap BadgeListTitle (or the chevron area inside it) with
clickable(onClick = onClickBadge, interactionSource = null, indication = null),
and keep BadgeList as-is so individual badge items can host their own click
handlers later to avoid event conflicts.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/ProfileSection.kt`:
- Around line 33-56: 현재 ProfileSection.kt에서 PrezelAvatar의 contentDescription과
PrezelButton의 text가 하드코딩("프로필 이미지", "프로필 편집")되어 있으므로 string 리소스로 분리하세요: 변경할 위치는
PrezelAvatar(contentDescription = ...)와 PrezelButton(text = ... , onClick =
onClickEditProfile) 호출부이며 각각을
stringResource(R.string.feature_my_profile_image_description)와
stringResource(R.string.feature_my_edit_profile)로 교체하고, 대응하는 문자열 리소스 항목을
feature/my/impl의 strings.xml에 추가해 주세요.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.kt`:
- Around line 44-59: Split the single LaunchedEffect that both dispatches the
one-shot intent and continuously collects uiEffect: keep one small
LaunchedEffect or direct call that dispatches
viewModel.onIntent(MyUiIntent.FetchData) once, and move the effect collector
into a lifecycle-aware collector using
repeatOnLifecycle(Lifecycle.State.STARTED) so viewModel.uiEffect is only
collected while the UI is started; inside that block collect the effects and
call snackbarHostState.showPrezelSnackbar(resources.getString(resId)) for
MyUiEffect.ShowMessage as before. Ensure you reference the same
viewModel.uiEffect, MyUiIntent.FetchData and
snackbarHostState.showPrezelSnackbar symbols when moving code so behaviour is
unchanged but collection is lifecycle-scoped.
- Line 31: Remove the unnecessary OptIn annotation from the caller: delete the
top-level `@OptIn`(ExperimentalMaterial3Api::class) placed above the MyScreen
declaration because MyScreen does not directly use experimental Material3 APIs
and MyTopAppBar already opts in; ensure only the MyTopAppBar definition retains
its `@OptIn` and that MyScreen (and the private MyScreen at line ~71) no longer
carry the annotation.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/navigation/MyEntryBuilder.kt`:
- Around line 21-22: Replace the commented navigator stubs for navigateToSetting
and navigateToBadge with explicit TODO lambdas that make the missing work
visible and actionable (e.g., a lambda that logs or throws a
NotImplemented/UnsupportedOperation with a short TODO message), reference the
missing symbols ProfileNavKey.Setting and ProfileNavKey.Badge by name in the
TODO so the reviewer knows these NavKeys are absent, and update the TODO to
state whether these screens should be added to the profile feature (add
ProfileNavKey entries) or moved to a separate feature (create new NavKeys) so
follow-up work is tracked.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4d41ba6f-24d1-412e-af3b-b1dfb5ef5d4f
📒 Files selected for processing (23)
Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/UserRepositoryImpl.ktPrezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelNavigationBar.ktPrezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/list/PrezelList.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/user/FetchUserBadgesUseCase.ktPrezel/core/model/src/main/java/com/team/prezel/core/model/badge/Badge.ktPrezel/core/ui/build.gradle.ktsPrezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PrezelBadge.ktPrezel/core/ui/src/main/res/drawable/badge_start.xmlPrezel/core/ui/src/main/res/values/strings.xmlPrezel/feature/my/impl/build.gradle.ktsPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyUiState.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyViewModel.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/BadgeSection.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/MyTopAppBar.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/ProfileSection.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/contract/MyUiEffect.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/contract/MyUiIntent.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/contract/MyUiState.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/model/BadgeUiModel.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/model/MyUiMessage.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/navigation/MyEntryBuilder.ktPrezel/feature/my/impl/src/main/res/values/strings.xml
💤 Files with no reviewable changes (1)
- Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyUiState.kt
| Box( | ||
| modifier = modifier | ||
| .fillMaxWidth() | ||
| .aspectRatio(1f), | ||
| ) { | ||
| Image( | ||
| painter = painterResource(resId), | ||
| contentDescription = null, | ||
| contentScale = ContentScale.Crop, | ||
| modifier = Modifier.fillMaxSize(), | ||
| ) | ||
|
|
||
| if (!isAchieved) { | ||
| Box( | ||
| modifier = Modifier | ||
| .fillMaxSize() | ||
| .clip(shape = PrezelTheme.shapes.V16) | ||
| .background(color = PrezelTheme.colors.bgScrim), | ||
| contentAlignment = Alignment.Center, | ||
| ) { | ||
| Icon( | ||
| painter = painterResource(PrezelIcons.Lock), | ||
| contentDescription = null, | ||
| tint = PrezelTheme.colors.solidWhite, | ||
| modifier = Modifier.fillMaxSize(0.24f), | ||
| ) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
획득 전 상태의 이미지 클립 범위가 Scrim과 다릅니다.
내부 scrim Box만 clip(shape = PrezelTheme.shapes.V16)으로 모서리를 둥글게 하는 반면, 바깥 Box와 Image는 클립이 적용되지 않아, 뱃지 원본 이미지의 사각 모서리가 둥근 scrim 바깥으로 비칠 수 있습니다(특히 bgScrim이 반투명일 때). 의도가 “뱃지 전체를 둥글게 보이게”라면 부모 Box 또는 Image에도 동일 shape로 clip을 적용하는 것이 일관적입니다.
🛡️ 제안 diff
Box(
modifier = modifier
.fillMaxWidth()
- .aspectRatio(1f),
+ .aspectRatio(1f)
+ .clip(shape = PrezelTheme.shapes.V16),
) {
Image(
painter = painterResource(resId),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
if (!isAchieved) {
Box(
modifier = Modifier
.fillMaxSize()
- .clip(shape = PrezelTheme.shapes.V16)
.background(color = PrezelTheme.colors.bgScrim),
contentAlignment = Alignment.Center,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PrezelBadge.kt`
around lines 83 - 111, The overlay scrim Box inside PrezelBadge (the conditional
block guarded by isAchieved) is clipped with PrezelTheme.shapes.V16 while the
parent Box and the Image are not, causing the original badge image to show
through outside the rounded scrim (especially when bgScrim is translucent); to
fix, apply the same clip shape to the parent container or the Image (the outer
Box/modifier or the Image's modifier) so the entire badge (Image + scrim) uses
PrezelTheme.shapes.V16 and yields consistent rounded corners.
| @OptIn(ExperimentalMaterial3Api::class) | ||
| @Composable | ||
| private fun BadgeList( | ||
| badges: ImmutableList<BadgeUiModel>, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| val horizontalPadding = PrezelTheme.spacing.V20 | ||
| val itemSpacing = PrezelTheme.spacing.V16 | ||
|
|
||
| BoxWithConstraints(modifier = modifier.fillMaxWidth()) { | ||
| val itemSize = remember(maxWidth, horizontalPadding, itemSpacing) { | ||
| (maxWidth - horizontalPadding * 2 - itemSpacing) / 2 | ||
| } | ||
|
|
||
| LazyRow( | ||
| modifier = Modifier.fillMaxWidth(), | ||
| contentPadding = PaddingValues(horizontal = horizontalPadding), | ||
| horizontalArrangement = Arrangement.spacedBy(itemSpacing), | ||
| overscrollEffect = null, | ||
| ) { |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Jetpack Compose 1.7+/1.8 Foundation, is LazyRow's overscrollEffectparameter marked@ExperimentalFoundationApiand doesBoxWithConstraints require any OptIn?
💡 Result:
No, LazyRow's overscrollEffect parameter is not marked @ExperimentalFoundationApi in Jetpack Compose Foundation 1.7+/1.8. BoxWithConstraints does not require any OptIn annotation.
Citations:
- 1: https://developer.android.com/jetpack/androidx/releases/compose-foundation
- 2: https://composables.com/docs/androidx.compose.foundation/foundation/1.11.0-beta02/composable-functions/LazyRow
- 3: https://composables.com/docs/androidx.compose.foundation/foundation/1.9.0-beta03/composable-functions/LazyRow
- 4: https://composables.com/jetpack-compose/androidx.compose.foundation/foundation-layout/composable-functions/BoxWithConstraints/api
- 5: https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/BoxWithConstraints.composable
- 6: https://linkedin.com/company/perspectif-io
불필요한 @OptIn(ExperimentalMaterial3Api::class) 제거
BadgeList 함수에서 사용하는 BoxWithConstraints, LazyRow, Arrangement.spacedBy 등 모든 API는 Foundation 패키지의 안정적인 API입니다. Material3 Experimental API를 실제로 사용하지 않으므로 이 OptIn 어노테이션은 필요하지 않습니다. 제거하기 바랍니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/BadgeSection.kt`
around lines 75 - 94, Remove the unnecessary OptIn for ExperimentalMaterial3Api
on the BadgeList composable: open the BadgeList declaration (the function named
BadgeList) and delete the `@OptIn`(ExperimentalMaterial3Api::class) annotation,
since only stable APIs (BoxWithConstraints, LazyRow, Arrangement.spacedBy, etc.)
are used; ensure no other references to ExperimentalMaterial3Api remain in that
file after removal.
| @OptIn(ExperimentalMaterial3Api::class) | ||
| @Composable | ||
| internal fun MyTopAppBar(onClickSetting: () -> Unit) { | ||
| PrezelTopAppBar( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .background(PrezelTheme.colors.bgRegular), | ||
| title = { Text(text = stringResource(R.string.feature_my_impl_title)) }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# PrezelTopAppBar 시그니처와 `@OptIn` 여부, 배경 관련 파라미터 확인
fd -a 'PrezelTopAppBar.kt'
ast-grep --pattern $'fun PrezelTopAppBar($$$) {
$$$
}'
rg -nP -C3 '\bExperimentalMaterial3Api\b' -g '**/PrezelTopAppBar.kt'
rg -nP -C3 '\b(containerColor|backgroundColor)\b' -g '**/PrezelTopAppBar.kt'Repository: Team-Prezel/Prezel-Android
Length of output: 368
🏁 Script executed:
#!/bin/bash
# 저장소 구조 탐색 및 PrezelTopAppBar 파일 찾기
echo "=== Finding PrezelTopAppBar ==="
find . -type f -name "*PrezelTopAppBar*" 2>/dev/null | head -20
echo -e "\n=== Searching for PrezelTopAppBar function definition ==="
rg -l "fun PrezelTopAppBar" 2>/dev/null | head -10
rg "fun PrezelTopAppBar" -A 15 2>/dev/null | head -50Repository: Team-Prezel/Prezel-Android
Length of output: 6338
🏁 Script executed:
#!/bin/bash
cat -n Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/TopAppBar.kt | head -60Repository: Team-Prezel/Prezel-Android
Length of output: 2741
🏁 Script executed:
#!/bin/bash
# MyTopAppBar.kt 전체 확인
cat -n Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/MyTopAppBar.ktRepository: Team-Prezel/Prezel-Android
Length of output: 2059
@OptIn 제거 및 배경 Modifier 수정 필요
-
@OptIn(ExperimentalMaterial3Api::class)는 제거하세요.PrezelTopAppBar이 이미 내부에서 opt-in을 처리하므로 호출부에서 반복할 필요 없습니다. -
Modifier.background(PrezelTheme.colors.bgRegular)을 제거하세요.PrezelTopAppBar는 기본적으로containerColor = Color.Transparent로 설정되어 있으며,scrollBehavior를 통해 스크롤 시 자동으로bgRegular로 변경됩니다. 외부에서 배경을 강제 적용하면 스크롤 상태에 관계없이 항상 배경이 표시되어 설계 의도와 맞지 않습니다. 스크롤 동작이 필요하면PrezelTopAppBar에scrollBehavior파라미터를 전달하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/MyTopAppBar.kt`
around lines 19 - 26, Remove the `@OptIn`(ExperimentalMaterial3Api::class)
annotation from the MyTopAppBar composable and delete the
Modifier.background(PrezelTheme.colors.bgRegular) call on the PrezelTopAppBar
invocation; PrezelTopAppBar handles its own opt-in and manages background via
containerColor/scrollBehavior (so avoid forcing a background externally) and if
scroll-aware background is desired, forward a scrollBehavior parameter into
PrezelTopAppBar instead of applying background here.
📌 작업 내용
MyViewModel과 MVI Contract를 추가했습니다.PrezelBadge컴포넌트와 뱃지 모델/유즈케이스를 추가했습니다.PrezelList제목 색상 커스터마이징을 지원하도록 확장했습니다.PrezelNavigationBar종료 애니메이션과MyScreen상단 여백 등 UI 디테일을 보완했습니다.🧩 관련 이슈
📸 스크린샷
📢 전달 사항
FetchUserBadgesUseCase에서 목업 형태로 내려주고 있습니다.Summary by CodeRabbit
릴리즈 노트
새 기능
버그 수정
UI/UX 개선