Skip to content

Compose 테마 구축 및 마이페이지 Compose UI 전환#374

Merged
unam98 merged 3 commits intodevelopfrom
feature/wave2-compose-theme-migration
Apr 3, 2026
Merged

Compose 테마 구축 및 마이페이지 Compose UI 전환#374
unam98 merged 3 commits intodevelopfrom
feature/wave2-compose-theme-migration

Conversation

@unam98
Copy link
Copy Markdown
Collaborator

@unam98 unam98 commented Apr 2, 2026

작업 배경

  • Compose 마이그레이션 Wave 2: 앱 공용 Compose 테마 구축 + 첫 번째 화면(마이페이지) Compose 전환
  • 기존 XML + DataBinding UI를 ComposeView 호스트 패턴으로 교체하여 기존 Fragment 구조와 공존

변경 사항

구분 파일 내용
신규 ui/theme/Color.kt 앱 컬러 팔레트 정의 (M1M7, G1G6 등)
신규 ui/theme/Type.kt Pretendard 9개 웨이트 FontFamily + Material3 Typography 매핑
신규 ui/theme/Theme.kt RunnectTheme — Material3 lightColorScheme 기반 앱 테마
신규 MyPageScreen.kt 마이페이지 Compose UI (프로필, 레벨 프로그레스, 메뉴 리스트, 버전)
신규 VisitorModeScreen.kt 방문자 모드 Compose UI
수정 MyPageFragment.kt BaseVisitorFragmentFragment + ComposeView 호스트로 전환
수정 libs.versions.toml coil-compose 라이브러리 추가
수정 build.gradle coil-compose 의존성 추가

영향 범위

  • 마이페이지 탭 UI가 XML → Compose로 전환
  • 기존 ViewPager 내 Fragment 구조 유지 (ComposeView 호스트)
  • 다른 화면에 영향 없음

Test Plan

  • 디버그 빌드 성공 확인
  • 마이페이지 탭 진입 후 프로필 정보(닉네임, 레벨, 프로그레스바, 스탬프) 정상 표시
  • 메뉴 항목(러닝 기록, 목표 보상, 업로드한 코스, 설정, 카카오톡 문의) 클릭 시 정상 이동
  • 닉네임 수정 후 마이페이지 복귀 시 변경 반영
  • 방문자 모드에서 마이페이지 접근 시 visitor UI + 회원가입 버튼 동작

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Visitor mode screen for guests with a prominent sign-up action
    • Redesigned My Page built with Compose, including edit-profile, history, reward, upload, settings, and inquiry actions
  • Improvements

    • New app theme: cohesive color palette and typography
    • Improved profile image handling and loading UX
    • Snackbar-driven error feedback and explicit loading states
    • App dependency added for improved image loading in Compose

- RunnectTheme 생성 (Material3 lightColorScheme + Pretendard Typography)
- Color.kt: 앱 컬러 팔레트 정의 (M1~M7, G1~G6)
- Type.kt: Pretendard 9개 웨이트 + Material3 텍스트 스타일 매핑
- MyPageScreen: 프로필, 레벨 프로그레스, 메뉴, 버전 정보 Compose UI
- VisitorModeScreen: 방문자 모드 Compose UI
- MyPageFragment: BaseVisitorFragment → Fragment + ComposeView 호스트로 전환
- coil-compose 의존성 추가
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 2, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 44b885d8-4baf-4d01-99af-aab52ecdf06b

📥 Commits

Reviewing files that changed from the base of the PR and between 506d53e and fe76fa4.

📒 Files selected for processing (4)
  • app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt
  • app/src/main/java/com/runnect/runnect/presentation/mypage/VisitorModeScreen.kt
  • app/src/main/java/com/runnect/runnect/presentation/ui/theme/Theme.kt
  • app/src/main/java/com/runnect/runnect/presentation/ui/theme/Type.kt
✅ Files skipped from review due to trivial changes (2)
  • app/src/main/java/com/runnect/runnect/presentation/mypage/VisitorModeScreen.kt
  • app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/runnect/runnect/presentation/ui/theme/Theme.kt
  • app/src/main/java/com/runnect/runnect/presentation/ui/theme/Type.kt

📝 Walkthrough

Walkthrough

The MyPage feature was migrated from an XML/ViewBinding Fragment to Jetpack Compose with conditional visitor-mode rendering. New Compose screens (MyPageScreen, VisitorModeScreen), theme primitives (colors, typography, RunnectTheme), and a Coil Compose dependency were added. MyPageFragment now collects ViewModel state and navigates to Login for visitor sign-up.

Changes

Cohort / File(s) Summary
Dependencies
app/build.gradle, gradle/libs.versions.toml
Added coil-compose to the Gradle version catalog and implementation libs.coil.compose to app dependencies to enable Coil image loading in Compose.
MyPage Fragment
app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageFragment.kt
Replaced XML/ViewBinding BaseVisitorFragment with a Compose-based Fragment; injected VisitorModeManager; conditionally dispatches MyPageIntent.LoadUserInfo; collects viewModel.state and passes it to Compose; changed getStampResourceId to accept stampId; added navigateToLogin() for visitor signup.
Compose Screens
app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt, app/src/main/java/com/runnect/runnect/presentation/mypage/VisitorModeScreen.kt
Added MyPageScreen composable (state-driven UI, loading, snackbar, profile/level/menu callbacks) and VisitorModeScreen composable (visitor message and signup button).
Theme primitives
app/src/main/java/com/runnect/runnect/presentation/ui/theme/Color.kt, .../Theme.kt, .../Type.kt
Introduced Compose color tokens, RunnectTheme Material3 wrapper (color scheme + text styles provider), and Pretendard-based RunnectTextStyles (font family and many TextStyle presets).

Sequence Diagram

sequenceDiagram
    participant Fragment as MyPageFragment
    participant VisitorMgr as VisitorModeManager
    participant ViewModel as MyPageViewModel
    participant Compose as Compose\n(MyPageScreen / VisitorModeScreen)
    participant UI as UI Rendering

    Fragment->>VisitorMgr: check isVisitorMode
    alt Visitor Mode
        Fragment->>Compose: render VisitorModeScreen(onSignUpClick)
        Compose->>UI: display image, message, signup button
        UI->>Fragment: onSignUpClick
        Fragment->>Fragment: navigateToLogin()
    else Authenticated
        Fragment->>ViewModel: dispatch LoadUserInfo intent
        ViewModel->>ViewModel: fetch user data
        ViewModel-->>Fragment: emit MyPageUiState
        Fragment->>Compose: render MyPageScreen(state, callbacks)
        alt Loading
            Compose->>UI: show CircularProgressIndicator
        else Content
            Compose->>UI: render profile, level, menus
            UI->>Fragment: callback (edit/history/reward/upload/setting)
            Fragment->>Fragment: perform navigation / launch activity result
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hopped from XML to Compose so bright,

New colors, fonts, and screens in sight.
Visitor waits with a friendly sign,
Profile and progress now align.
Hooray—MyPage hops into the light! 🎨✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main changes: establishing a Compose theme and migrating MyPage to Compose UI, which aligns with the primary objectives and file changes.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/wave2-compose-theme-migration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ComposeView 호스트 패턴

기존 Fragment 구조(ViewPager)를 유지하면서 내부 UI만 Compose로 교체하는 방식입니다.

Fragment.onCreateView() → ComposeView 반환 → setContent { Composable }
  • ComposeView: Android View 계층 안에 Compose UI를 삽입하는 브릿지
  • setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed): Fragment의 View 생명주기에 맞춰 Compose를 자동 해제. ViewPager에서 Fragment가 재사용될 때 메모리 릭 방지
  • setContent { }: 이 블록 안에서 Composable 함수를 호출하면 Compose UI가 렌더링됨

ComposeView 공식 문서

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed) 이게 컴포즈가 아닌 xml 기반이었다고 하면 어떤 개념과 유사한지, 이 코드를 추가했을 때 사이드 이펙트 등 문제가 생길 여지는 없는지.

코드 짤 때 누락하면 안 되는 필수적인 부분으로 보이는데 그렇다면 이걸 프리셋으로 설정해놓은 베이스나 유틸 코드를 만들어 보는 건 어떤지

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

XML 대응 개념

setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)는 XML 기반에서 onDestroyView()에서 리소스 해제하는 것과 동일한 역할입니다.

Fragment에서 View가 파괴될 때(onDestroyView) Compose의 composition도 함께 해제하여:

  • 메모리 릭 방지 (Compose가 파괴된 View를 계속 참조하는 것 방지)
  • ViewPager에서 Fragment가 재생성될 때 깨끗한 상태로 시작

사이드 이펙트

없습니다. 오히려 빠뜨리면 메모리 릭이 생기는 필수 코드입니다.

베이스 유틸 제안에 대해

좋은 아이디어입니다. Fragment에서 ComposeView를 호스팅하는 패턴이 반복될 것이므로:

// 향후 만들 수 있는 베이스
abstract class ComposeFragment : Fragment() {
    override fun onCreateView(...): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
            setContent { RunnectTheme { Content() } }
        }
    }
    @Composable abstract fun Content()
}

다만 플랜 원칙에 따라 5~6개 화면 전환 후 패턴이 확정되면 그때 추출하는 게 안전합니다. 지금은 1개 화면이라 섣부른 추상화 위험.

ViewCompositionStrategy 공식 문서

if (visitorModeManager.isVisitorMode) {
VisitorModeScreen(
onSignUpClick = { navigateToLogin() }
)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

collectAsState()

ViewModel의 StateFlow<MyPageUiState>를 Compose가 관찰하는 핵심 연결점입니다.

val state by viewModel.state.collectAsState()
  • collectAsState(): StateFlow를 Compose의 State로 변환. 값이 바뀔 때마다 이 값을 사용하는 Composable만 자동으로 다시 그려짐 (recomposition)
  • by 위임: state.value 대신 state로 직접 접근 가능하게 해주는 Kotlin 문법
  • LiveData의 observe() + XML 바인딩을 이 한 줄이 대체함

collectAsState 공식 문서

@Composable
fun RunnectTheme(
content: @Composable () -> Unit
) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

RunnectTheme

모든 Compose 화면의 최상위에 감싸는 테마 Composable입니다.

RunnectTheme {
    MyPageScreen(...)  // 이 안의 모든 Composable이 테마 적용됨
}
  • MaterialTheme()에 앱 컬러(RunnectColorScheme)와 타이포그래피(RunnectTypography)를 주입
  • 하위 Composable에서 MaterialTheme.colorScheme.primary로 테마 컬러 접근 가능
  • XML의 styles.xml + themes.xml 역할을 코드로 대체

다크 모드는 현재 미지원 — 추후 darkColorScheme을 추가하면 됨

onRewardClick: () -> Unit,
onUploadClick: () -> Unit,
onSettingClick: () -> Unit,
onKakaoInquiryClick: () -> Unit,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Composable 함수 = UI 컴포넌트

Compose에서는 UI를 함수로 선언합니다. XML 레이아웃 파일 대신 Kotlin 함수가 UI를 정의.

@Composable
fun MyPageScreen(state: MyPageUiState, onEditProfileClick: () -> Unit, ...)

핵심 개념:

  • @Composable: 이 어노테이션이 붙은 함수만 UI를 그릴 수 있음
  • state 파라미터: 화면에 보여줄 데이터. state가 바뀌면 Compose가 자동으로 UI를 다시 그림 (recomposition)
  • 이벤트 콜백 람다 (onEditProfileClick 등): 사용자 클릭 → Fragment로 전달 → 네비게이션 처리. Compose는 UI만 담당하고, 네비게이션 같은 Android 프레임워크 작업은 Fragment에 위임

XML과 비교:

  • Column { } = LinearLayout(vertical)
  • Row { } = LinearLayout(horizontal)
  • Box { } = FrameLayout
  • Modifier = XML 속성 (layout_width, padding, background 등을 체이닝)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

q-1. recomposition 원리, 장단점
q-2. 가령 configuration change가 일어나서 화면이 재생성될 때 뷰모델에서 state를 가지고 있으면 그걸 그대로 불러와서 의도치 않게 액션(ex. 토스트 띄우기)이 한 번 더 실행되는 등의 이슈가 있을 수 있는데 컴포즈로 마이그레이션 하면서 이런 측면에서 검토해야 할 부분은 없는지

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

q-1. Recomposition 원리

Compose는 State가 바뀌면 해당 State를 읽는 Composable만 다시 호출합니다.

val state by viewModel.state.collectAsState()
// state.nickname이 바뀌면 → nickname을 읽는 Text만 다시 그려짐
// state.levelPercent가 안 바뀌었으면 → ProgressBar는 건드리지 않음

장점: XML에서는 notifyDataSetChanged()로 전체를 다시 그리지만, Compose는 변경된 부분만 업데이트
단점: remember 없이 매 recomposition마다 객체를 생성하면 성능 저하 가능

q-2. Configuration Change + 의도치 않은 액션 반복

맞는 우려입니다. 이 PR에서의 대응:

  • State 기반 에러(state.error): LaunchedEffect(state.error)state.error 값이 바뀔 때만 실행됨. config change 후 같은 error 값이면 재실행 안 됨
  • Effect 기반 일회성 이벤트: SharedFlow로 전달하므로 한 번 collect되면 소비됨. config change 시 replay 안 됨
  • ViewModel은 config change에서 살아남으므로 state가 유지되어 API 재호출 없이 UI 복원

검토 필요한 부분: 만약 에러 Snackbar가 config change 후에도 다시 뜨면 ViewModel에서 error를 null로 초기화하는 로직 추가 필요. 현재는 LaunchedEffect의 key가 동일하면 재실행 안 되므로 문제없음.

modifier = Modifier
.fillMaxWidth()
.height(85.dp)
.padding(horizontal = 23.dp),
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Modifier 체이닝

XML에서 속성을 여러 개 나열하는 것처럼, Compose에서는 Modifier를 체이닝합니다.

Modifier
    .fillMaxWidth()           // android:layout_width="match_parent"
    .height(85.dp)            // android:layout_height="85dp"
    .padding(horizontal = 23.dp)  // android:paddingHorizontal="23dp"

순서가 중요합니다:

  • Modifier.padding(10.dp).background(Color.Red) → 패딩 안쪽에 빨간 배경
  • Modifier.background(Color.Red).padding(10.dp) → 빨간 배경 안에 패딩

XML에서는 padding/margin이 별도 속성이지만, Compose에서는 Modifier 순서로 제어합니다.

AsyncImage(
model = profileImgResId,
contentDescription = null,
modifier = Modifier.size(63.dp)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

AsyncImage (Coil Compose)

기존 XML에서 ImageView.load(resId) (Coil)로 이미지를 로드했던 것의 Compose 버전입니다.

AsyncImage(
    model = profileImgResId,  // drawable 리소스 ID 또는 URL
    contentDescription = null,
    modifier = Modifier.size(63.dp)
)
  • coil-compose 라이브러리가 제공하는 Composable
  • model에 Int(리소스 ID), String(URL), Uri 등 다양한 타입을 넘길 수 있음
  • 네트워크 이미지일 경우 자동으로 비동기 로딩 + 캐싱 처리

fontWeight = FontWeight.Bold,
fontSize = 15.sp,
color = G1
)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

LinearProgressIndicator

XML의 <ProgressBar style="horizontal">에 대응하는 Material3 Compose 컴포넌트입니다.

LinearProgressIndicator(
    progress = { levelPercent / 100f },  // 0f~1f 범위 (100% = 1f)
    color = M1,         // 진행 바 색상
    trackColor = G4,    // 배경 트랙 색상
)
  • progress가 람다인 이유: 값이 바뀔 때마다 recomposition 없이 애니메이션만 업데이트하기 위한 성능 최적화
  • XML에서는 android:max="100"\ + android:progress="50"`으로 했지만, Compose는 0~1 비율로 표현

import com.runnect.runnect.R

val PretendardFontFamily = FontFamily(
Font(R.font.pretendard_thin, FontWeight.Thin),
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

FontFamily 정의

XML에서 res/font/runnect_font.xml로 폰트 패밀리를 정의했던 것의 Compose 버전입니다.

val PretendardFontFamily = FontFamily(
    Font(R.font.pretendard_regular, FontWeight.Normal),
    Font(R.font.pretendard_bold, FontWeight.Bold),
    // ...
)
  • 같은 res/font/ 리소스를 그대로 사용하되, Compose의 FontFamily로 래핑
  • FontWeight별로 매핑해두면 fontWeight = FontWeight.Bold 지정 시 자동으로 맞는 폰트 파일 선택
  • XML의 android:fontFamily="@font/pretendard_bold"fontFamily = PretendardFontFamily, fontWeight = FontWeight.Bold로 표현

@unam98
Copy link
Copy Markdown
Collaborator Author

unam98 commented Apr 3, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageFragment.kt (1)

66-66: Prefer lifecycle-aware state collection in this ComposeView.

collectAsState() keeps the flow active as long as the composition exists. In this Fragment/ViewPager host, collectAsStateWithLifecycle() is the safer default so off-screen pages stop collecting and recomposing when the view lifecycle drops below STARTED.

♻️ Suggested change
-import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@
-                        val state by viewModel.state.collectAsState()
+                        val state by viewModel.state.collectAsStateWithLifecycle()

If this module does not already include lifecycle-runtime-compose, add it alongside the other lifecycle artifacts.

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

In `@app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageFragment.kt`
at line 66, In MyPageFragment, replace the ComposeView's use of
viewModel.state.collectAsState() with the lifecycle-aware
collectAsStateWithLifecycle() so the Compose collection stops when the view
lifecycle is below STARTED; update the reference to the state (val state by
viewModel.state.collectAsStateWithLifecycle()) inside the ComposeView/Compost
content and ensure the module has lifecycle-runtime-compose dependency added if
missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageFragment.kt`:
- Around line 59-91: The fragment stopped consuming MyPageUiState.error after
migrating to Compose; restore the persistent Snackbar path by observing
state.error and showing a persistent Snackbar when non-null. In MyPageFragment
(inside setContent or just outside) collect viewModel.state (the same state from
viewModel.state.collectAsState()/collectLatest) and when state.error is present
call your Snackbar host (e.g., ScaffoldState.snackbarHostState.showSnackbar) or
the fragment-side persistent Snackbar helper, then clear or acknowledge the
error on the ViewModel; ensure this uses the existing MyPageUiState.error field
(not MyPageEffect) and update the ViewModel to clear the error after showing to
avoid repeated displays.
- Around line 105-121: The fragment never receives profile changes because
MyPageEditNameActivity's setResult only includes EXTRA_NICK_NAME and the
fragment's resultEditNameLauncher ignores EXTRA_PROFILE; update the activity
(MyPageEditNameActivity) to include the profile payload (EXTRA_PROFILE — same
format you pass in navigateToEditName) in the RESULT_OK Intent, and update the
fragment's registerForActivityResult callback in setResultEditNameLauncher to
read EXTRA_PROFILE from result.data (if present) and call
viewModel.intent(MyPageIntent.UpdateProfileImg(profileValue)) in addition to
MyPageIntent.UpdateNickname(name).

In `@app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt`:
- Around line 52-80: Wrap the variable content beneath MyPageToolbar in a
weighted, scrollable container so the toolbar remains fixed and the body can
scroll and center the loader correctly; specifically, keep MyPageToolbar as-is,
then replace the current conditional children with a single Column (or Box)
using Modifier.weight(1f).verticalScroll(rememberScrollState()) (or for
centering the loader use Modifier.weight(1f) + Box with fillMaxSize inside) and
move ProfileSection, LevelProgressSection, MenuSection, VersionSection and the
loading Box into that weighted scrollable container so the loading spinner
centers and the content becomes scrollable on small screens or large fonts.
- Around line 155-208: LevelProgressSection uses the raw levelPercent from the
API directly in LinearProgressIndicator and the label; clamp levelPercent to the
0..100 range and compute a single progressFloat = (clampedLevelPercent / 100f)
to use for LinearProgressIndicator(progress = progressFloat) and for the
displayed percent text (clampedLevelPercent.toString()), ensuring invalid API
values don't produce out-of-range progress values; update references to
levelPercent within LevelProgressSection accordingly.

---

Nitpick comments:
In `@app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageFragment.kt`:
- Line 66: In MyPageFragment, replace the ComposeView's use of
viewModel.state.collectAsState() with the lifecycle-aware
collectAsStateWithLifecycle() so the Compose collection stops when the view
lifecycle is below STARTED; update the reference to the state (val state by
viewModel.state.collectAsStateWithLifecycle()) inside the ComposeView/Compost
content and ensure the module has lifecycle-runtime-compose dependency added if
missing.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46cfa26b-3bb3-400a-8110-dec7a1e01ba5

📥 Commits

Reviewing files that changed from the base of the PR and between 9c62c23 and f2788c4.

📒 Files selected for processing (8)
  • app/build.gradle
  • app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageFragment.kt
  • app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt
  • app/src/main/java/com/runnect/runnect/presentation/mypage/VisitorModeScreen.kt
  • app/src/main/java/com/runnect/runnect/presentation/ui/theme/Color.kt
  • app/src/main/java/com/runnect/runnect/presentation/ui/theme/Theme.kt
  • app/src/main/java/com/runnect/runnect/presentation/ui/theme/Type.kt
  • gradle/libs.versions.toml

Comment on lines 105 to +121
private fun setResultEditNameLauncher() {
resultEditNameLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
if (result.resultCode == Activity.RESULT_OK) {
val name = result.data?.getStringExtra(EXTRA_NICK_NAME)
?: viewModel.currentState.nickname
viewModel.intent(MyPageIntent.UpdateNickname(name))
}
}
}

private fun addListener() {
with(binding) {
ivMyPageEditFrame.setOnClickListener {
val intent = Intent(requireContext(), MyPageEditNameActivity::class.java)
intent.putExtra(EXTRA_NICK_NAME, viewModel.currentState.nickname)
val stampResId = getStampResourceId()
intent.putExtra(EXTRA_PROFILE, stampResId)
resultEditNameLauncher.launch(intent)
}

viewMyPageMainRewardFrame.setOnClickListener {
Analytics.logClickedItemEvent(EVENT_CLICK_GOAL_REWARD)
navigateTo<MyRewardActivity>()
}
viewMyPageMainHistoryFrame.setOnClickListener {
Analytics.logClickedItemEvent(EVENT_CLICK_RUNNING_RECORD)
navigateTo<MyHistoryActivity>()
}

viewMyPageMainUploadFrame.setOnClickListener {
Analytics.logClickedItemEvent(EVENT_CLICK_UPLOADED_COURSE)
navigateTo<MyUploadActivity>()
}
viewMyPageMainSettingFrame.setOnClickListener {
moveToSettingFragment()
}
viewMyPageMainKakaoChannelInquiryFrame.setOnClickListener {
inquiryKakao()
}
}
private fun navigateToEditName() {
val intent = Intent(requireContext(), MyPageEditNameActivity::class.java)
intent.putExtra(EXTRA_NICK_NAME, viewModel.currentState.nickname)
val stampResId = getStampResourceId(viewModel.currentState.stampId)
intent.putExtra(EXTRA_PROFILE, stampResId)
resultEditNameLauncher.launch(intent)
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 3, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="$(fd 'MyPageEditNameActivity\.kt$' app/src/main/java | head -n1)"
if [[ -n "${file:-}" ]]; then
  echo "== $file =="
  sed -n '1,260p' "$file"
fi

echo
echo "== Result extras / profile update usages =="
rg -n -C3 'EXTRA_PROFILE|EXTRA_NICK_NAME|UpdateProfileImg|setResult\(|putExtra\(' app/src/main/java

Repository: Runnect/Runnect-Android

Length of output: 42919


Profile image changes in MyPageEditNameActivity are not returned to the caller.

The activity receives EXTRA_PROFILE during initialization and calls viewModel.setProfileImg(), but when it succeeds, setResult() only includes EXTRA_NICK_NAME. The fragment's launcher callback ignores EXTRA_PROFILE in the result and never invokes MyPageIntent.UpdateProfileImg(), so any avatar edits made in the activity are dropped and won't display until the next full reload.

Evidence from MyPageEditNameActivity.kt
// Lines 69-72: Result only includes nickname
setResult(
    RESULT_OK,
    Intent().putExtra(EXTRA_NICK_NAME, viewModel.nickName.value)
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageFragment.kt`
around lines 105 - 121, The fragment never receives profile changes because
MyPageEditNameActivity's setResult only includes EXTRA_NICK_NAME and the
fragment's resultEditNameLauncher ignores EXTRA_PROFILE; update the activity
(MyPageEditNameActivity) to include the profile payload (EXTRA_PROFILE — same
format you pass in navigateToEditName) in the RESULT_OK Intent, and update the
fragment's registerForActivityResult callback in setResultEditNameLauncher to
read EXTRA_PROFILE from result.data (if present) and call
viewModel.intent(MyPageIntent.UpdateProfileImg(profileValue)) in addition to
MyPageIntent.UpdateNickname(name).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

현재 코드 유지 — MyPageEditNameActivity의 setResult에 닉네임만 포함하는 것은 이 PR 이전부터 동일한 pre-existing 동작. getUserInfo() API 재호출로 동기화되는 기존 구조.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@unam98, 이해했습니다! MyPageEditNameActivitysetResult에 닉네임만 포함하는 것이 이 PR 이전부터 의도된 동작이며, 프로필 이미지 동기화는 getUserInfo() API 재호출로 처리되는 기존 구조임을 확인했습니다. 코드를 그대로 유지하는 것이 맞겠습니다.


✏️ Learnings added
Learnt from: unam98
Repo: Runnect/Runnect-Android PR: 374
File: app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageFragment.kt:105-121
Timestamp: 2026-04-03T01:14:47.736Z
Learning: In the Runnect-Android project, `MyPageEditNameActivity.setResult()` intentionally includes only `EXTRA_NICK_NAME` (not `EXTRA_PROFILE`) in the `RESULT_OK` Intent. Profile image synchronization after editing is handled by re-calling the `getUserInfo()` API rather than passing the updated profile image back through the activity result. This is a pre-existing design pattern predating the Compose migration (PR `#374`), not a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: unam98
Repo: Runnect/Runnect-Android PR: 373
File: app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageFragment.kt:102-108
Timestamp: 2026-04-02T17:59:56.729Z
Learning: In `MyPageFragment.kt` (Runnect-Android), the team deliberately uses `state.error` (in `MyPageUiState`) for persistent error display via Snackbar in `bindState()`, and reserves `MyPageEffect` (e.g., `ShowError`) for one-time actions such as navigation. This is an intentional MVI design decision: state for persistent UI errors, effects for one-time events.

Comment thread app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt Outdated
- Scaffold + SnackbarHost로 state.error 시 Snackbar 표시 복원
- LaunchedEffect(state.error)로 에러 발생 시 자동 트리거
- 콘텐츠 영역에 weight(1f) + verticalScroll 추가로 소형 화면 스크롤 지원
- 로딩 스피너를 weight(1f) Box로 감싸 정확한 중앙 배치
- levelPercent를 coerceIn(0, 100)으로 클램핑하여 API 이상값 방어
Copy link
Copy Markdown
Collaborator Author

@unam98 unam98 left a comment

Choose a reason for hiding this comment

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

리뷰 대응 완료 (506d53e)

onRewardClick: () -> Unit,
onUploadClick: () -> Unit,
onSettingClick: () -> Unit,
onKakaoInquiryClick: () -> Unit,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

q-1. recomposition 원리, 장단점
q-2. 가령 configuration change가 일어나서 화면이 재생성될 때 뷰모델에서 state를 가지고 있으면 그걸 그대로 불러와서 의도치 않게 액션(ex. 토스트 띄우기)이 한 번 더 실행되는 등의 이슈가 있을 수 있는데 컴포즈로 마이그레이션 하면서 이런 측면에서 검토해야 할 부분은 없는지

inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed) 이게 컴포즈가 아닌 xml 기반이었다고 하면 어떤 개념과 유사한지, 이 코드를 추가했을 때 사이드 이펙트 등 문제가 생길 여지는 없는지.

코드 짤 때 누락하면 안 되는 필수적인 부분으로 보이는데 그렇다면 이걸 프리셋으로 설정해놓은 베이스나 유틸 코드를 만들어 보는 건 어떤지

Comment thread app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt Outdated
Comment thread app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt Outdated
Comment thread app/src/main/java/com/runnect/runnect/presentation/ui/theme/Type.kt Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt (2)

122-127: 타이포그래피 하드코딩은 Theme 토큰 사용으로 통일하는 것을 권장합니다.

Text마다 fontFamily/fontWeight/fontSize를 직접 지정하는 패턴이 반복됩니다. 이미 Type.kt를 도입한 만큼 MaterialTheme.typography 기반으로 맞추면 일관성과 변경 용이성이 좋아집니다.

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

In `@app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt`
around lines 122 - 127, The Text in MyPageScreen (the Text call using
stringResource(R.string.my_page_title)) hardcodes typography properties; replace
these ad-hoc fontFamily/fontWeight/fontSize uses by applying the app's
typography tokens (MaterialTheme.typography) defined in Type.kt — e.g. pick the
appropriate TextStyle from MaterialTheme.typography (headline/subtitle/body as
defined) and use that style in the Text composable (remove direct
fontFamily/fontWeight/fontSize) so all typography is driven by the central
Type.kt tokens.

253-275: 메뉴 아이템 선언 중복은 리스트 기반 렌더링으로 줄이는 게 좋습니다.

현재 MenuItem(...) 호출이 반복되어 유지보수 시 수정 포인트가 늘어납니다. 메뉴 모델 리스트 + forEachIndexed로 선언하면 변경이 쉬워집니다.

Refactor 예시
 `@Composable`
 private fun MenuSection(
     onHistoryClick: () -> Unit,
     onRewardClick: () -> Unit,
     onUploadClick: () -> Unit,
     onSettingClick: () -> Unit,
     onKakaoInquiryClick: () -> Unit,
 ) {
-    Column {
-        MenuItem(
-            title = stringResource(R.string.my_page_history_title),
-            onClick = onHistoryClick
-        )
-        MenuItem(
-            title = stringResource(R.string.my_page_reward_title),
-            onClick = onRewardClick
-        )
-        MenuItem(
-            title = stringResource(R.string.my_page_upload_title),
-            onClick = onUploadClick
-        )
-        MenuItem(
-            title = stringResource(R.string.my_page_setting_title),
-            onClick = onSettingClick
-        )
-        MenuItem(
-            title = stringResource(R.string.my_page_kakao_channel_inquiry),
-            onClick = onKakaoInquiryClick,
-            showDivider = false
-        )
-    }
+    val menus = listOf(
+        stringResource(R.string.my_page_history_title) to onHistoryClick,
+        stringResource(R.string.my_page_reward_title) to onRewardClick,
+        stringResource(R.string.my_page_upload_title) to onUploadClick,
+        stringResource(R.string.my_page_setting_title) to onSettingClick,
+        stringResource(R.string.my_page_kakao_channel_inquiry) to onKakaoInquiryClick,
+    )
+
+    Column {
+        menus.forEachIndexed { index, (title, onClick) ->
+            MenuItem(
+                title = title,
+                onClick = onClick,
+                showDivider = index != menus.lastIndex
+            )
+        }
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt`
around lines 253 - 275, Replace the repeated MenuItem(...) calls in
MyPageScreen's Column with a single list-driven rendering: create a local list
of menu descriptors (e.g., data class or Triple holding titleResId, onClick
callback, optional showDivider flag) populated with entries referencing the
existing callbacks (onHistoryClick, onRewardClick, onUploadClick,
onSettingClick, onKakaoInquiryClick), then iterate over that list with
forEachIndexed and call MenuItem(...) inside the loop, computing showDivider
either from the descriptor or by checking the index (e.g., disable divider for
the last or specific item). This reduces duplication while keeping MenuItem
usage and behavior identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt`:
- Around line 122-127: The Text in MyPageScreen (the Text call using
stringResource(R.string.my_page_title)) hardcodes typography properties; replace
these ad-hoc fontFamily/fontWeight/fontSize uses by applying the app's
typography tokens (MaterialTheme.typography) defined in Type.kt — e.g. pick the
appropriate TextStyle from MaterialTheme.typography (headline/subtitle/body as
defined) and use that style in the Text composable (remove direct
fontFamily/fontWeight/fontSize) so all typography is driven by the central
Type.kt tokens.
- Around line 253-275: Replace the repeated MenuItem(...) calls in
MyPageScreen's Column with a single list-driven rendering: create a local list
of menu descriptors (e.g., data class or Triple holding titleResId, onClick
callback, optional showDivider flag) populated with entries referencing the
existing callbacks (onHistoryClick, onRewardClick, onUploadClick,
onSettingClick, onKakaoInquiryClick), then iterate over that list with
forEachIndexed and call MenuItem(...) inside the loop, computing showDivider
either from the descriptor or by checking the index (e.g., disable divider for
the last or specific item). This reduces duplication while keeping MenuItem
usage and behavior identical.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e00b453-d892-4df2-aea6-247b3537ebfa

📥 Commits

Reviewing files that changed from the base of the PR and between f2788c4 and 506d53e.

📒 Files selected for processing (1)
  • app/src/main/java/com/runnect/runnect/presentation/mypage/MyPageScreen.kt

- RunnectTextStyles 도입: fontFamily+fontWeight+fontSize를 하나의 스타일로 관리
- CompositionLocal로 RunnectTheme.textStyle 접근 제공
- sp → dp.value.sp로 시스템 폰트 크기 영향 제거
- 네이밍: small/medium/large → bold20, semiBold15 등 weight+size 기반
- MyPageScreen, VisitorModeScreen의 모든 Text를 style = textStyle.xxx로 통일
@unam98 unam98 self-assigned this Apr 3, 2026
@unam98 unam98 merged commit eeccb1c into develop Apr 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant