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` 값을 "닉네임"으로 수정하였습니다.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthrough사용자 프로필 및 약관 동의 기능을 구현하며, 네트워크 기반 데이터 로딩, 뱃지 시스템, 프로필 편집, 세션 관리 인프라를 추가합니다. 인증 흐름을 재구성하고 약관 기능을 독립 모듈로 분리했습니다. ChangesTerms Feature Module
User Profile & Badges
Auth & Session Management
Navigation & App Flow
Possibly related PRs
|
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
* **style: `PrezelBadge` UI 구조 및 클리핑 적용 위치 변경**
* 이미지와 오버레이를 포함하는 최상위 `Box` 컨테이너에 `clip(PrezelTheme.shapes.V16)` 수식어를 이동 적용했습니다.
* 내부 스크림(Scrim) 레이아웃에 중복으로 적용되어 있던 `clip` 수식어를 제거하여 구조를 단순화했습니다.
* **cleanup: `BadgeSection` 내 `BadgeList` 컴포저블 정리**
* 더 이상 사용되지 않는 `@OptIn(ExperimentalMaterial3Api::class)` 어노테이션을 삭제했습니다.
* **refactor: MyTopAppBar 컴포넌트 스타일 수정**
* `PrezelTopAppBar`의 `modifier`에서 중복되거나 불필요한 `.background(PrezelTheme.colors.bgRegular)` 설정을 삭제했습니다.
* **build: `core:datastore` 및 `core:network` 모듈 의존성 정리**
* 더 이상 사용되지 않는 `core:model` 프로젝트 의존성을 `core:datastore` 및 `core:network` 모듈에서 제거했습니다.
* **feat: 사용자 및 프로필 관리 기능 구현**
* `UserRepositoryImpl` 내에 `UserRemoteDataSource`를 주입하여 실제 API 연동을 완료했습니다.
* 프로필 수정(닉네임, 이미지)을 위한 `patchProfile` 로직과 `PatchUserProfileUseCase`를 추가했습니다. 이미지 업로드 시 Ktor `Multipart`를 사용하도록 구현했습니다.
* 닉네임 중복 확인 기능을 실제 네트워크 요청으로 교체했습니다.
* `GetUserResponse` 등의 네트워크 모델을 정의하고 도메인 모델(`User`)로의 매핑 로직을 추가했습니다.
* **feat: 약관 동의 관련 기능 및 도메인 레이어 추가**
* 약관 동의 요청을 위한 `TermsRepository`, `TermsRemoteDataSource`, `TermsService`를 구현했습니다.
* 선택한 약관 정보를 서버에 전달하는 `AgreeTermsUseCase`를 추가했습니다.
* **refactor: 인증 토큰(`AuthTokens`) 모델 구조 개선 및 레이어별 분리**
* 전역적으로 사용되던 `core:model`의 `AuthTokens`를 삭제하고, `TokenProvider`, `AuthLocalDataSource` 등 각 레이어의 인터페이스 내부에 전용 `AuthTokens` 클래스를 정의하여 결합도를 낮췄습니다.
* `TokenProviderImpl` 및 `AuthLocalDataSourceImpl`에서 변경된 토큰 모델을 참조하도록 수정했습니다.
* **refactor: 도메인 레이어 코드 정리 및 패키지 구조 변경**
* `AuthRepository`, `LoginUseCase` 등 인증 관련 주요 클래스들의 파일 경로를 `src/main/java`에서 `src/main/kotlin`으로 이동하고 코드를 정리했습니다.
* **build: 네트워크 및 데이터 모듈 의존성 설정 업데이트**
* `DataSourceModule`, `NetworkModule`, `RepositoryModule`에 새로 추가된 서비스 및 데이터 소스에 대한 Hilt DI 설정을 추가했습니다.
* **refactor: `UserRepository` 내 사용자 정보 조회 파라미터 제거**
* `fetchUserInfo` 메서드에서 강제 갱신 여부를 결정하던 `isRefresh` 파라미터를 제거했습니다.
* `UserRepositoryImpl`에서 `isRefresh` 조건 없이 캐시된 데이터(`cachedUserInfo`)가 존재하면 즉시 반환하도록 로직을 단순화했습니다.
* **refactor: `FetchUserInfoUseCase` 호출 구조 변경**
* Repository의 인터페이스 변경에 맞춰 `isRefresh` 파라미터 없이 `invoke` 하도록 수정했습니다.
* **refactor: `AuthRepository` 역할 축소 및 세션 관리 로직 개선**
* `AuthRepository`에서 사용자 정보(`User`)를 직접 반환하던 로직을 제거하고, 토큰 존재 여부 확인(`hasJwtToken`) 및 세션 초기화(`clearSession`) 기능을 추가했습니다.
* `login` 및 `logout` 메서드가 사용자 모델 대신 `Result<Unit>`을 반환하도록 변경하여 관심사를 분리했습니다.
* `AuthRepositoryImpl` 내의 `UserRemoteDataSource` 의존성을 제거하고 로컬 세션 삭제 로직을 `clearLocalSession`으로 공통화했습니다.
* **refactor: UseCase 기반의 인증 로직 오케스트레이션**
* **`CheckLoginStatusUseCase`**: 토큰 존재 여부를 먼저 확인한 후, `UserRepository`를 통해 최신 사용자 정보를 가져오도록 수정했습니다. 인증 실패(UNAUTHORIZED) 시 자동으로 세션을 초기화하는 예외 처리를 추가했습니다.
* **`LoginUseCase`**: 서버 로그인 성공 후 즉시 사용자 정보를 페치(fetch)하여 `Result<User>`를 반환하도록 로직을 통합했습니다.
* **feat: `AppError.UNAUTHORIZED` 상태 추가 및 에러 매핑 세분화**
* 도메인 에러 모델에 `UNAUTHORIZED`를 추가했습니다.
* `INVALID_TOKEN`, `TOKEN_STOLEN`, `USER_NOT_FOUND`, `INVALID_ID_TOKEN` 등 인증 관련 서버 에러 코드를 `AppError.UNAUTHORIZED`로 일괄 매핑하도록 수정했습니다.
* **refactor: UI 레이어 내 변경 사항 반영**
* `LoginViewModel`: `LoginUseCase`가 `User` 객체를 직접 반환함에 따라 불필요한 null 체크 로직을 제거했습니다.
* `TermsViewModel`: 약관 동의 실패 처리 시 `UNAUTHORIZED` 에러에 대한 메시지 매핑을 추가했습니다.
* **fix: `SplashViewModel` 내 실패 처리 로직 보완**
* 초기 데이터 로드 또는 인증 확인 실패 시(`onFailure`), 에러 메시지 표시와 더불어 로그인 화면으로 진입할 수 있도록 `SplashUiEffect.NavigateToLogin` 이벤트를 추가했습니다.
* **refactor: `UserRepositoryImpl`의 메모리 캐싱 로직 삭제**
* `cachedUserInfo` 필드를 제거하고, `fetchUserInfo` 호출 시 캐시 존재 여부와 관계없이 항상 `userRemoteDataSource`를 통해 최신 데이터를 가져오도록 수정했습니다.
* 프로필 정보 수정(`patchProfile`) 시 수행하던 캐시 초기화 로직을 제거했습니다.
* **feat: `ProfileUiIntent` 내 `ClearProfileImage` 추가 및 처리**
* 프로필 이미지 삭제 요청을 명확하게 처리하기 위해 전용 Intent인 `ClearProfileImage`를 추가했습니다.
* `ProfileViewModel`에서 해당 Intent 수신 시 이미지 URL과 파일을 초기화하도록 로직을 구현했습니다.
* **refactor: 프로필 이미지 캐시 파일 생성 로직 단순화**
* `copyProfileImageToCache` 함수에서 `ContentResolver`를 통해 파일명을 조회하던 복잡한 로직을 제거했습니다.
* `File.createTempFile`을 사용하여 캐시 디렉토리에 임시 파일을 생성하는 방식으로 변경하여 가독성과 효율성을 높였습니다.
* **refactor: 프로필 이미지 임시 파일 수명 주기 관리**
* `ProfileScreen`에 `DisposableEffect`를 추가하여, 화면이 dispose될 때 편집 중이던 임시 프로필 이미지 파일을 삭제하도록 개선했습니다.
* 이미지 삭제 UI 동작 시 `UpdateProfileImage`에 빈 값을 넘기는 대신 `ClearProfileImage` Intent를 사용하도록 수정했습니다.
* **refactor: 프로필 이미지 선택 및 처리 로직 분리**
* 이미지 선택을 위한 `ActivityResultLauncher` 초기화 로직을 `rememberProfileImagePicker` Composable 함수로 추출했습니다.
* 프로필 이미지 클릭 시의 분기 로직(이미지 선택기 실행 또는 프로필 제거)을 `handleProfileImageClick` 함수로 분리하여 `ProfileScreen` 본문의 복잡도를 낮췄습니다.
* **refactor: UI 메시지 리소스 매핑 방식 개선**
* `ProfileUiMessage`에 따른 문자열 리소스 ID 반환 로직을 `resId` 확장 프로퍼티로 정의했습니다.
* 이를 통해 `LaunchedEffect` 내에서 스낵바를 표시하는 코드를 간결하게 수정했습니다.
* **style: 코드 정리 및 포맷팅 수정**
* `ProfileScreen` 내에서 사용하지 않는 `LocalContext` 참조를 제거했습니다.
* 가독성 향상을 위해 불필요한 공백을 제거하고 인덴트 등 코드 포맷을 정리했습니다.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/MyTopAppBar.kt (1)
4-4:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
@OptIn(ExperimentalMaterial3Api::class)및 관련 import 제거 필요
PrezelTopAppBar가 이미 내부에서 opt-in을 처리하고 있으므로 호출부에서@OptIn(ExperimentalMaterial3Api::class)를 선언할 필요가 없습니다. 이 어노테이션과 함께 4번 줄의ExperimentalMaterial3Apiimport도 불필요해집니다.🛠️ Proposed fix
-import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text-@OptIn(ExperimentalMaterial3Api::class) `@Composable` internal fun MyTopAppBar(onClickSetting: () -> Unit) {Also applies to: 18-18
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/MyTopAppBar.kt` at line 4, Remove the redundant opt-in: delete the `@OptIn`(ExperimentalMaterial3Api::class) annotation from the MyTopAppBar call site and remove the corresponding import line "import androidx.compose.material3.ExperimentalMaterial3Api" (and the same redundant annotation/import at the other occurrence around line 18); PrezelTopAppBar already handles the experimental API opt-in, so simply call PrezelTopAppBar without the `@OptIn` annotation or the ExperimentalMaterial3Api import.
🧹 Nitpick comments (3)
Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.kt (1)
17-17: 💤 Low value
hasJwtToken()에서 Flow 접근 시 예외 처리 고려
authLocalDataSource.tokens.firstOrNull()호출 시 발생할 수 있는 예외가Result.success로 감싸지지 않습니다. DataStore Flow가 예외를 발생시키는 경우는 드물지만, 방어적 코딩을 위해runCatching으로 감싸는 것을 고려해 보세요.♻️ 방어적 코딩 제안
- override suspend fun hasJwtToken(): Result<Boolean> = Result.success(authLocalDataSource.tokens.firstOrNull() != null) + override suspend fun hasJwtToken(): Result<Boolean> = runCatching { + authLocalDataSource.tokens.firstOrNull() != null + }.mapDomainFailure()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.kt` at line 17, The call in hasJwtToken() uses authLocalDataSource.tokens.firstOrNull() without catching exceptions; wrap that call in runCatching and convert the result into a Result<Boolean> so exceptions produce Result.failure and normal execution returns Result.success(tokens.firstOrNull() != null). Update the implementation of hasJwtToken() to use runCatching { authLocalDataSource.tokens.firstOrNull() != null } or equivalent and map the caught value/exception into the Result returned from the function.Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/UserRemoteDataSourceImpl.kt (1)
31-31: 💤 Low value파일 확장자 기반 Content-Type 검증 고려
"image/${file.extension}"은 확장자가 비표준인 경우 유효하지 않은 MIME 타입을 생성할 수 있습니다 (예:image/jpeg대신image/jpg). 프로필 이미지는 일반적으로 표준 포맷이므로 큰 문제는 아니지만, 확장자 매핑을 고려해 볼 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/UserRemoteDataSourceImpl.kt` at line 31, The code in UserRemoteDataSourceImpl sets Content-Type using "image/${file.extension}", which can produce nonstandard MIME types (e.g., image/jpg); replace this with a proper MIME lookup: use a mapping or system utility (e.g., URLConnection.guessContentTypeFromName(file.name) or Android's MimeTypeMap) to resolve file.extension to a standard MIME type, fall back to "image/jpeg" or "application/octet-stream" if the lookup returns null, and then call append(HttpHeaders.ContentType, resolvedMime) instead of using file.extension directly.Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashViewModel.kt (1)
41-47: 💤 Low value
routeUser함수에 불필요한suspend수정자가 있습니다.
routeUser함수 내부에서 suspend 함수를 호출하지 않으므로suspend키워드가 불필요합니다. 제거하면 함수의 의도가 더 명확해집니다.♻️ 제안된 수정
- private suspend fun routeUser(user: User) { + private fun routeUser(user: User) { when { !user.isTermsAgreement -> sendEffect(SplashUiEffect.NavigateToTerms) !user.isProfileComplete -> sendEffect(SplashUiEffect.NavigateToCreateProfile) else -> sendEffect(SplashUiEffect.NavigateToHome) } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashViewModel.kt` around lines 41 - 47, The routeUser function is marked suspend but doesn't call any suspend functions; remove the unnecessary suspend modifier from the routeUser declaration to make intent clear, update any callers if they relied on it being suspend (they should be able to call it normally), and keep the existing logic that checks User.isTermsAgreement / User.isProfileComplete and dispatches effects via sendEffect(SplashUiEffect.*).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/client/HttpClientFactory.kt`:
- Around line 157-163: The session-invalidation check in
Throwable.isSessionInvalidForReissue() currently only matches INVALID_TOKEN,
TOKEN_STOLEN, and USER_NOT_FOUND; update this function to also include
ServerErrorCode.UNAUTHORIZED, ServerErrorCode.FORBIDDEN, and
ServerErrorCode.INVALID_ID_TOKEN so that responses mapped to
AppError.UNAUTHORIZED trigger the same invalidation flow and cause the
refresh-path to delete stored tokens (ensure the check in
isSessionInvalidForReissue() is expanded to include those three codes).
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/UserRemoteDataSourceImpl.kt`:
- Around line 23-36: The multipart form field names are mistakenly wrapped with
escaped quotes in the MultiPartFormDataContent builder (see formData {
append("\"nickname\"", ...) and append("\"profileImage\"", ...) in
UserRemoteDataSourceImpl); remove the escaped quotes so the calls use plain
field names ("nickname" and "profileImage") and keep the rest (file bytes and
Headers.build) unchanged to ensure correct Content-Disposition and server-side
parsing when profileImageFile is appended.
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.kt`:
- Around line 76-79: DisposableEffect currently uses Unit so it only runs once
and only deletes the final temp file; change it to key off the current temp file
so the effect re-runs and disposes the previous file whenever the selected image
changes. Specifically, replace DisposableEffect(Unit) with
DisposableEffect((uiState as?
ProfileUiState.Content)?.editing?.profileImageFile) and keep the onDispose logic
that calls (uiState as?
ProfileUiState.Content)?.editing?.profileImageFile?.delete() (guarding nulls) so
each previous temp file is deleted when a new one is selected.
- Around line 210-220: The copyProfileImageToCache function can throw
IO/security exceptions from File.createTempFile, outputStream(), or
input.copyTo(), risking crashes and leftover temp files; wrap the entire body of
Context.copyProfileImageToCache in a try/catch (catch
IOException/Exception/SecurityException as appropriate), log the error, ensure
any partially created targetFile is deleted if an exception occurs (check
targetFile.exists() then delete), and return null on failure so the caller can
handle the error safely; keep using input.use/output.use for stream closing but
move/create the targetFile reference such that it can be cleaned up in the catch
block.
In `@Prezel/feature/terms/impl/build.gradle.kts`:
- Around line 20-27: Remove the unused dependency on projects.featureLoginApi
from the dependencies block in build.gradle.kts for the terms:impl module;
confirm TermsEntryBuilder.kt only references ProfileNavKey and TermsNavKey (and
not LoginNavKey) and then delete the line
"implementation(projects.featureLoginApi)" from the dependencies section to
eliminate the unnecessary module coupling.
---
Duplicate comments:
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/MyTopAppBar.kt`:
- Line 4: Remove the redundant opt-in: delete the
`@OptIn`(ExperimentalMaterial3Api::class) annotation from the MyTopAppBar call
site and remove the corresponding import line "import
androidx.compose.material3.ExperimentalMaterial3Api" (and the same redundant
annotation/import at the other occurrence around line 18); PrezelTopAppBar
already handles the experimental API opt-in, so simply call PrezelTopAppBar
without the `@OptIn` annotation or the ExperimentalMaterial3Api import.
---
Nitpick comments:
In
`@Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.kt`:
- Line 17: The call in hasJwtToken() uses
authLocalDataSource.tokens.firstOrNull() without catching exceptions; wrap that
call in runCatching and convert the result into a Result<Boolean> so exceptions
produce Result.failure and normal execution returns
Result.success(tokens.firstOrNull() != null). Update the implementation of
hasJwtToken() to use runCatching { authLocalDataSource.tokens.firstOrNull() !=
null } or equivalent and map the caught value/exception into the Result returned
from the function.
In
`@Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/UserRemoteDataSourceImpl.kt`:
- Line 31: The code in UserRemoteDataSourceImpl sets Content-Type using
"image/${file.extension}", which can produce nonstandard MIME types (e.g.,
image/jpg); replace this with a proper MIME lookup: use a mapping or system
utility (e.g., URLConnection.guessContentTypeFromName(file.name) or Android's
MimeTypeMap) to resolve file.extension to a standard MIME type, fall back to
"image/jpeg" or "application/octet-stream" if the lookup returns null, and then
call append(HttpHeaders.ContentType, resolvedMime) instead of using
file.extension directly.
In
`@Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashViewModel.kt`:
- Around line 41-47: The routeUser function is marked suspend but doesn't call
any suspend functions; remove the unnecessary suspend modifier from the
routeUser declaration to make intent clear, update any callers if they relied on
it being suspend (they should be able to call it normally), and keep the
existing logic that checks User.isTermsAgreement / User.isProfileComplete and
dispatches effects via sendEffect(SplashUiEffect.*).
🪄 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: 0c4f6681-2125-42ca-858d-6ab767d4387d
📒 Files selected for processing (88)
Prezel/app/build.gradle.ktsPrezel/core/data/src/main/java/com/team/prezel/core/data/auth/TokenProviderImpl.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/error/AppErrorExt.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/repository/TermsRepositoryImpl.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/repository/UserRepositoryImpl.ktPrezel/core/datastore/build.gradle.ktsPrezel/core/datastore/src/main/java/com/team/prezel/core/datastore/auth/AuthLocalDataSource.ktPrezel/core/datastore/src/main/java/com/team/prezel/core/datastore/auth/AuthLocalDataSourceImpl.ktPrezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelNavigationBar.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/repository/auth/AuthRepository.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/repository/profile/UserRepository.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/repository/terms/TermsRepository.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/auth/CheckLoginStatusUseCase.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/auth/LoginUseCase.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/auth/LogoutUseCase.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/auth/WithdrawUseCase.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/terms/AgreeTermsUseCase.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/user/FetchUserInfoUseCase.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/user/PatchUserProfileUseCase.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/user/ValidateNicknameUseCase.ktPrezel/core/model/src/main/java/com/team/prezel/core/model/base/AppError.ktPrezel/core/model/src/main/java/com/team/prezel/core/model/base/AppException.ktPrezel/core/model/src/main/java/com/team/prezel/core/model/profile/User.ktPrezel/core/model/src/main/java/com/team/prezel/core/model/terms/TermsAgreement.ktPrezel/core/network/build.gradle.ktsPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthSessionCache.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/AuthSessionCacheImpl.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/auth/TokenProvider.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/client/HttpClientFactory.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSource.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/TermsRemoteDataSource.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/TermsRemoteDataSourceImpl.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/UserRemoteDataSource.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/datasource/UserRemoteDataSourceImpl.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/di/AuthSessionModule.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/di/DataSourceModule.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/di/NetworkModule.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/BaseResponse.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/terms/AgreeTermsRequest.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/model/user/GetUserResponse.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/service/TermsService.ktPrezel/core/network/src/main/java/com/team/prezel/core/network/service/UserService.ktPrezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PrezelBadge.ktPrezel/feature/login/impl/build.gradle.ktsPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginViewModel.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/contract/LoginUiEffect.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/contract/LoginUiIntent.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/contract/LoginUiState.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/model/LoginUiMessage.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiEffect.ktPrezel/feature/login/impl/src/main/res/values/strings.xmlPrezel/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/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/contract/ProfileUiIntent.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/contract/ProfileUiState.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/model/ProfileUiMessage.ktPrezel/feature/profile/impl/src/main/res/values/strings.xmlPrezel/feature/splash/impl/build.gradle.ktsPrezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashScreen.ktPrezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/SplashViewModel.ktPrezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/contract/SplashUiEffect.ktPrezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/navigation/SplashEntryBuilder.ktPrezel/feature/terms/api/build.gradle.ktsPrezel/feature/terms/api/consumer-rules.proPrezel/feature/terms/api/proguard-rules.proPrezel/feature/terms/api/src/main/java/com/team/prezel/feature/terms/api/TermsNavKey.ktPrezel/feature/terms/impl/build.gradle.ktsPrezel/feature/terms/impl/consumer-rules.proPrezel/feature/terms/impl/proguard-rules.proPrezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/TermsScreen.ktPrezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/TermsViewModel.ktPrezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/component/TermsDetailModal.ktPrezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/contract/TermsUiEffect.ktPrezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/contract/TermsUiIntent.ktPrezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/contract/TermsUiState.ktPrezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/model/TermsUiMessage.ktPrezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/navigation/TermsEntryBuilder.ktPrezel/feature/terms/impl/src/main/res/values/strings.xmlPrezel/settings.gradle.kts
💤 Files with no reviewable changes (6)
- Prezel/core/network/build.gradle.kts
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiEffect.kt
- Prezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/auth/WithdrawUseCase.kt
- Prezel/core/datastore/build.gradle.kts
- Prezel/feature/login/impl/src/main/res/values/strings.xml
- Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.kt
✅ Files skipped from review due to trivial changes (15)
- Prezel/core/network/src/main/java/com/team/prezel/core/network/model/terms/AgreeTermsRequest.kt
- Prezel/feature/terms/impl/consumer-rules.pro
- Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/contract/SplashUiEffect.kt
- Prezel/feature/terms/api/proguard-rules.pro
- Prezel/feature/profile/impl/src/main/res/values/strings.xml
- Prezel/feature/terms/api/consumer-rules.pro
- Prezel/feature/terms/impl/proguard-rules.pro
- Prezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/contract/TermsUiEffect.kt
- Prezel/app/build.gradle.kts
- Prezel/core/model/src/main/java/com/team/prezel/core/model/terms/TermsAgreement.kt
- Prezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/model/TermsUiMessage.kt
- Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/TermsRemoteDataSource.kt
- Prezel/feature/terms/api/build.gradle.kts
- Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/BadgeSection.kt
- Prezel/core/network/src/main/java/com/team/prezel/core/network/model/user/GetUserResponse.kt
🚧 Files skipped from review as they are similar to previous changes (2)
- Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyViewModel.kt
- Prezel/core/ui/src/main/java/com/team/prezel/core/ui/component/PrezelBadge.kt
* **refactor: `copyProfileImageToCache` 함수 내 캐시 파일 관리 로직 고도화**
* 임시 파일 생성 방식(`File.createTempFile`) 대신 고정된 이름 포맷(`profile_image.확장자`)을 사용하도록 변경했습니다.
* `MimeTypeMap`을 사용하여 URI로부터 파일 확장자를 동적으로 추출하는 로직을 추가했습니다.
* 새로운 캐시 파일을 생성하기 전, 기존에 존재하던 프로필 이미지 관련 캐시 파일들을 찾아 삭제하는 정리 로직을 구현했습니다.
* **cleanup: 불필요한 `DisposableEffect` 제거**
* 화면 이탈 시(onDispose) 캐시 파일을 삭제하던 로직을 제거했습니다. 파일 관리는 이제 이미지 선택 및 캐시 복사 시점에 수행됩니다.
* **refactor: `feature:terms:impl` 모듈 의존성 정리**
* `build.gradle.kts`에서 사용되지 않는 `featureLoginApi` 의존성을 제거했습니다.
* **refactor: 에러 관련 클래스 패키지 이동**
* 공통으로 사용되는 `AppError` 및 `AppException` 클래스를 `core:model` 모듈에서 `core:common` 모듈의 `com.team.prezel.core.common.error` 패키지로 이동했습니다.
* **build: 모듈 의존성 설정 업데이트**
* `AndroidFeatureImplConventionPlugin`에 `core-common` 의존성을 추가하여 Feature 모듈에서 사용할 수 있도록 구성했습니다.
* `core:domain` 모듈의 `build.gradle.kts`에 `core-common` 프로젝트 의존성을 추가했습니다.
* **refactor: 변경된 에러 클래스 참조 수정 및 코드 정리**
* `CheckLoginStatusUseCase`, `TermsViewModel`, `AppErrorExt` 등 전반적인 모듈에서 변경된 패키지 경로에 맞춰 import 문을 업데이트했습니다.
* `TermsViewModel` 및 `AppErrorExt` 내 `when` 식의 들여쓰기를 수정하여 가독성을 개선했습니다.
📌 작업 내용
MyViewModel과 MVI Contract를 추가했습니다.PrezelBadge컴포넌트와 뱃지 모델/유즈케이스를 추가했습니다.PrezelList제목 색상 커스터마이징을 지원하도록 확장했습니다.PrezelNavigationBar종료 애니메이션과MyScreen상단 여백 등 UI 디테일을 보완했습니다.🧩 관련 이슈
📸 스크린샷
📢 전달 사항
FetchUserBadgesUseCase에서 목업 형태로 내려주고 있습니다.Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항