Skip to content

[feature/#43] root 및 메인 네비게이션, 네비게이션 UI구현#50

Open
choi-day wants to merge 28 commits intodevfrom
feature/#43-navigation
Open

[feature/#43] root 및 메인 네비게이션, 네비게이션 UI구현#50
choi-day wants to merge 28 commits intodevfrom
feature/#43-navigation

Conversation

@choi-day
Copy link
Collaborator

@choi-day choi-day commented Mar 8, 2026

✨ 작업 내용

전체 네비게이션, 메인 네비게이션 및 UI구현했습니다


📄 상세 내용

  • BottomNavBar.kt: Bottom Navigation UI 컴포넌트를 구현한 파일입니다.
    주요 역할
    • 하단 네비게이션 UI 구성
    • 현재 선택된 Route에 따라 UI 상태 변경
    • Navigation 클릭 이벤트 전달
  • BottomNavItem.kt: Bottom Navigation에 사용되는 UI 모델 클래스입니다.
  • BottomNavRoute.kt: Bottom Navigation 화면을 정의하는 Route 타입입니다.
    특징
    • sealed interface를 사용
    • 문자열 route 대신 타입 기반 navigation 사용
  • RootNavRoute.kt: 앱 전체 Navigation 흐름을 정의하는 Route입니다.
  • RootNavigation.kt: 앱 전체 Navigation을 구성하는 파일입니다.
  • RootViewModel.kt: Navigation 상태 및 Root 화면 관련 로직을 관리하는 ViewModel입니다.

👀 To Reviewer

커다란 흐름(로그인 -> WelcomeScreen -> 회원가입 -> 홈화면)만 root로 뺴고 나머지는 자체 상세 네비게이션을 구현할 예정입니다.
Navigation 구조 설계나 네이밍에 대해 개선 의견이 있다면 피드백 부탁드립니다.


📷 스크린샷 / 영상

  • 배경 리스트는 네비게이션바의 반투명정도를 보여주기 위해 따로 구현한 더미데이터입니다. 커밋되지 않았습니다.
Screen_recording_20260308_205043.mp4

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 하단 네비게이션 바 추가 (홈, 매칭, 마이페이지, 채팅, 게시판)
    • 새로운 환영 화면 및 회원가입 플로우 추가
    • 시각 효과 개선 (하이즈 효과 적용)
  • 리팩토링

    • 네비게이션 아키텍처 개선
    • 로그인 프로세스 간소화
  • 종속성

    • AndroidX 네비게이션 라이브러리 업데이트

@choi-day choi-day requested review from dlwl224 and hyeminililo March 8, 2026 12:22
@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 2026

Walkthrough

이 PR은 Navigation3 라이브러리와 하이즈 라이브러리를 추가하고, 루트 네비게이션 시스템을 구축하며, 바텀 네비게이션 UI 컴포넌트를 도입합니다. MainActivity는 단순화되고 LoginViewModel의 의존성을 제거하며, 새로운 라우팅 인프라가 인증 상태에 따라 흐름을 제어합니다.

Changes

Cohort / File(s) Summary
의존성 및 빌드 구성
app/build.gradle.kts, gradle/libs.versions.toml
Navigation3 런타임, UI 모듈, lifecycle-viewmodel-navigation3, 하이즈 라이브러리 추가. 플러그인 선언(kotlin-compose, kotlin-serialization, ksp, dagger-hilt 등) 추가.
루트 네비게이션 시스템
app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootNavigation.kt, RootViewModel.kt, RootNavRoute.kt
RootNavigation 컴포저블은 인증 상태에 따라 로그인, 환영, 등록, 메인 화면으로 라우팅. RootViewModel은 TokenLocalDataSource를 통해 시작 목적지 결정. RootNavRoute는 네비게이션 경로 정의.
바텀 네비게이션 컴포넌트
app/src/main/java/com/gildongmu/dduru/presentation/navigation/BottomNavBar.kt, BottomNavItem.kt, BottomNavRoute.kt
BottomNavBar 컴포저블은 선택 상태 및 상호작용 처리. BottomNavItem은 경로, 제목, 아이콘을 담은 데이터 클래스. BottomNavRoute는 홈, 매칭, 마이페이지, 채팅, 게시판 경로 정의.
메인 화면 및 상태 관리
app/src/main/java/com/gildongmu/dduru/presentation/feature/main/MainScreen.kt, MainState.kt
MainScreen은 NavDisplay와 바텀 네비게이션 바를 포함. MainState는 네비게이션 백스택을 관리하고 현재 경로를 추적.
로그인 기능 리팩토링
app/src/main/java/com/gildongmu/dduru/presentation/feature/login/LoginBinder.kt, LoginViewModel.kt
LoginRoute에서 LoginBinder로 이름 변경 및 패키지 이동. LoginViewModel에서 AuthRepository, TokenLocalDataSource, 시작 화면 로직 제거.
메인 액티비티 단순화
app/src/main/java/com/gildongmu/dduru/presentation/feature/main/MainActivity.kt
내부 상태 관리 및 네비게이션 로직 제거. RootNavigation() 컴포저블만 렌더링하도록 단순화.
바텀 네비게이션 아이콘 리소스
app/src/main/res/drawable/ic_bottom_nav_*.xml
5개의 새로운 벡터 드로어블: 보드, 채팅, 메인, 매칭, 마이페이지 아이콘.

Sequence Diagram(s)

sequenceDiagram
    participant App as MainActivity
    participant RootNav as RootNavigation
    participant RootVM as RootViewModel
    participant TokenDS as TokenLocalDataSource
    participant NavBS as NavBackStack
    
    App->>RootNav: Render RootNavigation()
    RootNav->>RootVM: Initialize (hiltViewModel)
    RootVM->>TokenDS: Check access token
    TokenDS-->>RootVM: Return token status
    alt Token Valid
        RootVM-->>RootNav: Emit MainNavRoute
        RootNav->>NavBS: Initialize with MainNavRoute
        RootNav-->>App: Show MainScreen
    else Token Invalid/Empty
        RootVM-->>RootNav: Emit LoginNavRoute
        RootNav->>NavBS: Initialize with LoginNavRoute
        RootNav-->>App: Show LoginBinder
    end
Loading
sequenceDiagram
    participant MainScreen as MainScreen
    participant BottomNav as BottomNavBar
    participant MainState as MainState
    participant NavBS as NavBackStack
    
    MainScreen->>MainState: Get currentRoute
    MainState->>NavBS: Query last element
    NavBS-->>MainState: Return BottomNavRoute
    MainState-->>MainScreen: Provide currentRoute
    MainScreen->>BottomNav: Render with currentRoute
    BottomNav->>BottomNav: Map items to selection state
    BottomNav-->>MainScreen: Render nav items
    MainScreen->>MainState: navigate(route)
    MainState->>NavBS: Clear & repopulate stack
    NavBS-->>MainState: Update back stack
    MainState-->>MainScreen: Notify route change
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • dlwl224
🚥 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 제목은 PR의 주요 변경 사항(root 및 메인 네비게이션, 네비게이션 UI 구현)을 명확하게 요약하고 있으며, 실제 커밋 내용과 일치합니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#43-navigation

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.

@choi-day choi-day self-assigned this Mar 8, 2026
@choi-day choi-day added feature ✨ 새로운 기능 추가, 신규 화면, API 연동 ui 🎨 Compose UI 구현, 레이아웃, 컴포넌트 작업 (LazyColumn, Modifier, Animation, Color/Typo 적용) 다연 👻 다연씨 전용 라벨 labels Mar 8, 2026
@choi-day choi-day linked an issue Mar 8, 2026 that may be closed by this pull request
5 tasks
@choi-day choi-day removed a link to an issue Mar 8, 2026
5 tasks
@choi-day choi-day linked an issue Mar 8, 2026 that may be closed by this pull request
4 tasks
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/gildongmu/dduru/presentation/feature/login/LoginBinder.kt (1)

20-29: ⚠️ Potential issue | 🟠 Major

단발성 결과를 uiState로 소비하면 재실행될 수 있습니다.

Success/Error가 상태에 남아 있는 동안 재구성, 화면 회전, 화면 재진입 시 LaunchedEffect(uiState)가 다시 실행되어 중복 네비게이션이나 토스트가 발생할 수 있습니다. 이 부분은 SharedFlow/Channel 같은 effect 스트림으로 분리하는 쪽이 안전합니다.

As per coding guidelines, 단발성 이벤트(네비게이션/토스트/스낵바)는 State와 분리되어 (예: SharedFlow/Channel) 중복 실행·화면 회전 시 재처리 문제가 없는지 확인.

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

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/login/LoginBinder.kt`
around lines 20 - 29, 현재 LoginBinder에서 LaunchedEffect(uiState)로
LoginUiState.Success / LoginUiState.Error를 소비하여 재구성·회전 시 토스트/네비게이션이 중복 발생할 수
있습니다; 이를 고치려면 단발성 이벤트를 ViewModel 쪽으로 분리해 SharedFlow/Channel 형태의 이벤트 스트림(예:
LoginViewModel.loginEvent: SharedFlow<LoginEvent>)으로 노출하고 LoginUiState는 순수한 화면
상태만 보관하도록 변경한 뒤, LoginBinder에서는 LaunchedEffect(Unit) 또는 lifecycle-aware 수집기로 그
이벤트 흐름을 collect하여 onLoginSuccess(state.isNewUser) 호출과 Toast 표시를 처리하도록 수정하세요; 관련
식별자: LoginBinder, uiState, LoginUiState.Success, LoginUiState.Error,
onLoginSuccess, LoginViewModel.loginEvent (또는 유사한 event Flow 이름).
🧹 Nitpick comments (4)
app/build.gradle.kts (1)

123-134: 하드코딩된 의존성을 버전 카탈로그로 통합하는 것을 권장합니다.

새로 추가된 navigation3 및 haze 의존성은 버전 카탈로그(libs.xxx)를 사용하고 있어 좋습니다. 그러나 Lines 123-128의 기존 의존성들(credentials, googleid, kakao sdk, okhttp logging-interceptor)은 버전이 하드코딩되어 있어 일관성이 떨어집니다.

향후 유지보수 편의를 위해 모든 의존성을 libs.versions.toml로 통합하는 것을 고려해 주세요.

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

In `@app/build.gradle.kts` around lines 123 - 134, The hardcoded dependency
versions for androidx.credentials:credentials,
androidx.credentials:credentials-play-services-auth,
com.google.android.libraries.identity.googleid:googleid, com.kakao.sdk:v2-user,
and com.squareup.okhttp3:logging-interceptor should be migrated to the version
catalog; replace the explicit coordinates in app/build.gradle.kts with
corresponding implementation(libs.<alias>) entries and add matching aliases and
versions in libs.versions.toml (e.g., entries for androidx-credentials,
credentials-play-services-auth, googleid, kakao-v2-user,
okhttp-logging-interceptor) so all dependencies use the centralized version
catalog for consistency and easier maintenance.
app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootViewModel.kt (1)

15-17: RootViewModel이 data layer 세부 구현을 직접 알고 있습니다.

TokenLocalDataSource를 바로 주입하면 루트 진입 결정 로직이 data.local에 묶입니다. 여기서는 GetStartDestinationUseCaseObserveSessionUseCase 같은 domain 경계의 추상화로 올려 두는 편이 테스트와 교체에 더 유리합니다.

As per coding guidelines "레이어 분리 준수: presentation(ViewModel/Ui) ↔ data(repository/datasource) 의존 방향이 단방향인지 확인" 및 "ViewModel이 Android/IO 디테일에 과하게 의존하지 않는지 확인".

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

In
`@app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootViewModel.kt`
around lines 15 - 17, RootViewModel currently depends directly on
TokenLocalDataSource which leaks data-layer details into presentation; replace
the direct injection with a domain-level abstraction (e.g., inject
GetStartDestinationUseCase or ObserveSessionUseCase) and have those use-cases
encapsulate TokenLocalDataSource inside the domain/repository layer; update
RootViewModel constructor to accept the chosen use-case(s) (e.g.,
GetStartDestinationUseCase or ObserveSessionUseCase), move decision logic that
reads tokens into those use-case implementations, and adjust callers/tests to
use the new abstraction so the ViewModel depends only on domain interfaces.
app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootNavRoute.kt (1)

6-13: Route 네이밍 규칙을 지금 통일해 두는 편이 좋습니다.

같은 계층의 route인데 RootNavRoute.LoginNavRoute는 suffix가 중복되고, BottomNavRoute.Home은 그렇지 않아 호출부가 조금씩 흔들립니다. sealed interface 안에서는 Login/Welcome/Register/Main처럼 줄이거나, 반대로 모든 route 타입에 역할 suffix를 맞춰 두면 이후 중첩 네비게이션 추가 때 더 읽기 쉬워집니다.

As per coding guidelines "네이밍 컨벤션, 가독성, Google 권장 Android 아키텍처 준수 여부".

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

In
`@app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootNavRoute.kt`
around lines 6 - 13, Unify route naming inside the RootNavRoute sealed interface
to follow a consistent convention: either shorten all entries to simple names
(Login, Welcome, Register, Main) or append a uniform suffix to all (e.g.,
LoginRoute, WelcomeRoute, RegisterRoute, MainRoute); update the declarations
(currently LoginNavRoute, WelcomeNavRoute, RegisterNavRoute, MainNavRoute) to
the chosen form and adjust all call sites that reference these symbols (also
consider aligning with BottomNavRoute.Home naming) so references like
RootNavRoute.LoginNavRoute are consistently RootNavRoute.Login (or
RootNavRoute.LoginRoute) across the codebase.
app/src/main/java/com/gildongmu/dduru/presentation/feature/main/MainState.kt (1)

10-29: 이 state holder는 이름과 확장 방향을 조금 더 명확히 두는 편이 좋겠습니다.

rememberMainAppState/MainState는 실제로 app 전체가 아니라 bottom tab stack만 관리합니다. 같은 back stack에 탭 내부 상세 화면을 붙이기 시작하면 currentRoute = lastOrNull() 때문에 선택 탭이 바로 null이 되고, 지금의 clear() + add() 전략상 탭별 하위 스택도 보존되지 않습니다. BottomNavState처럼 책임을 좁히고 selected tab/탭별 stack 전략을 분리해두면 이후 확장이 더 수월합니다.

As per coding guidelines, 네이밍 컨벤션, 가독성, Google 권장 Android 아키텍처 준수 여부.

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

In `@app/src/main/java/com/gildongmu/dduru/presentation/feature/main/MainState.kt`
around lines 10 - 29, The holder currently named rememberMainAppState/MainState
only manages the bottom tab back stack and should be narrowed and renamed (e.g.,
BottomNavState/rememberBottomNavState); change currentRoute logic (avoid casting
lastOrNull() to BottomNavRoute) and stop using clear()+add() in navigate(route)
which discards per-tab stacks—introduce explicit selectedTab property and a
Map/registry of per-tab NavBackStack<NavKey> to preserve each tab's stack,
update navigate(route) to switch selectedTab and push/pop on the appropriate
per-tab stack, and update all references from MainState/rememberMainAppState to
the new BottomNavState/rememberBottomNavState names.
🤖 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/gildongmu/dduru/presentation/navigation/BottomNavBar.kt`:
- Around line 36-49: BottomNavBar's Row should be marked as a selectable group
and each tab item must be selectable so accessibility services recognize the
exclusive selection: add modifier.selectableGroup() to the Row in BottomNavBar
(where items.forEach is iterating and isSelected computed from currentRoute) and
update the item composable (BottomNavBarItem or the Column inside it) to include
selectable(selected = isSelected, role = Role.Tab) instead of a plain clickable;
keep onItemClick(item.key) as the selection action so semantics and focus/role
are properly exposed to screen readers.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootNavigation.kt`:
- Around line 64-73: The RegisterNavRoute destination is currently an empty
NavEntry which breaks the signup flow; replace the empty NavEntry for
RootNavRoute.RegisterNavRoute with the actual registration UI component (e.g.,
RegisterScreen) or at minimum a temporary placeholder screen so WelcomeScreen's
onNextClick (which pushes RootNavRoute.RegisterNavRoute) leads to a usable
destination. Update the NavEntry for RootNavRoute.RegisterNavRoute to invoke
RegisterScreen(...) or a simple PlaceholderRegisterScreen that exposes the next
actions, and ensure any required callbacks (e.g., onComplete/onBack) match the
existing navigation stack usage.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootViewModel.kt`:
- Around line 19-20: accessToken.first() 예외 발생 시 _startDestination이 null로 남아 루트
네비게이션이 멈추는 문제가 있습니다; accessToken.first() 호출(또는 이를 사용하는 함수/블록)을 try/catch로 감싸고 예외
발생 시 _startDestination에 LoginNavRoute를 할당하거나(startDestination 흐름을 로그인 경로로 폴백) 대신
로딩/에러 상태를 표현할 수 있는 별도 UiState(예: RootUiState.Loading/RootUiState.Error)를 내보내도록
수정하세요; 관련 심볼: _startDestination, startDestination, accessToken.first(),
LoginNavRoute, RootNavRoute.

In `@gradle/libs.versions.toml`:
- Around line 69-72: Update the lifecycleViewmodelNav3 version constant from
"2.10.0" to the latest stable release "2.9.3": locate the lifecycleViewmodelNav3
entry in the libs.versions.toml and change its value to "2.9.3" (leave
navigation3 and haze unchanged).

---

Outside diff comments:
In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/login/LoginBinder.kt`:
- Around line 20-29: 현재 LoginBinder에서 LaunchedEffect(uiState)로
LoginUiState.Success / LoginUiState.Error를 소비하여 재구성·회전 시 토스트/네비게이션이 중복 발생할 수
있습니다; 이를 고치려면 단발성 이벤트를 ViewModel 쪽으로 분리해 SharedFlow/Channel 형태의 이벤트 스트림(예:
LoginViewModel.loginEvent: SharedFlow<LoginEvent>)으로 노출하고 LoginUiState는 순수한 화면
상태만 보관하도록 변경한 뒤, LoginBinder에서는 LaunchedEffect(Unit) 또는 lifecycle-aware 수집기로 그
이벤트 흐름을 collect하여 onLoginSuccess(state.isNewUser) 호출과 Toast 표시를 처리하도록 수정하세요; 관련
식별자: LoginBinder, uiState, LoginUiState.Success, LoginUiState.Error,
onLoginSuccess, LoginViewModel.loginEvent (또는 유사한 event Flow 이름).

---

Nitpick comments:
In `@app/build.gradle.kts`:
- Around line 123-134: The hardcoded dependency versions for
androidx.credentials:credentials,
androidx.credentials:credentials-play-services-auth,
com.google.android.libraries.identity.googleid:googleid, com.kakao.sdk:v2-user,
and com.squareup.okhttp3:logging-interceptor should be migrated to the version
catalog; replace the explicit coordinates in app/build.gradle.kts with
corresponding implementation(libs.<alias>) entries and add matching aliases and
versions in libs.versions.toml (e.g., entries for androidx-credentials,
credentials-play-services-auth, googleid, kakao-v2-user,
okhttp-logging-interceptor) so all dependencies use the centralized version
catalog for consistency and easier maintenance.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/feature/main/MainState.kt`:
- Around line 10-29: The holder currently named rememberMainAppState/MainState
only manages the bottom tab back stack and should be narrowed and renamed (e.g.,
BottomNavState/rememberBottomNavState); change currentRoute logic (avoid casting
lastOrNull() to BottomNavRoute) and stop using clear()+add() in navigate(route)
which discards per-tab stacks—introduce explicit selectedTab property and a
Map/registry of per-tab NavBackStack<NavKey> to preserve each tab's stack,
update navigate(route) to switch selectedTab and push/pop on the appropriate
per-tab stack, and update all references from MainState/rememberMainAppState to
the new BottomNavState/rememberBottomNavState names.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootNavRoute.kt`:
- Around line 6-13: Unify route naming inside the RootNavRoute sealed interface
to follow a consistent convention: either shorten all entries to simple names
(Login, Welcome, Register, Main) or append a uniform suffix to all (e.g.,
LoginRoute, WelcomeRoute, RegisterRoute, MainRoute); update the declarations
(currently LoginNavRoute, WelcomeNavRoute, RegisterNavRoute, MainNavRoute) to
the chosen form and adjust all call sites that reference these symbols (also
consider aligning with BottomNavRoute.Home naming) so references like
RootNavRoute.LoginNavRoute are consistently RootNavRoute.Login (or
RootNavRoute.LoginRoute) across the codebase.

In
`@app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootViewModel.kt`:
- Around line 15-17: RootViewModel currently depends directly on
TokenLocalDataSource which leaks data-layer details into presentation; replace
the direct injection with a domain-level abstraction (e.g., inject
GetStartDestinationUseCase or ObserveSessionUseCase) and have those use-cases
encapsulate TokenLocalDataSource inside the domain/repository layer; update
RootViewModel constructor to accept the chosen use-case(s) (e.g.,
GetStartDestinationUseCase or ObserveSessionUseCase), move decision logic that
reads tokens into those use-case implementations, and adjust callers/tests to
use the new abstraction so the ViewModel depends only on domain interfaces.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7c1dab47-9b32-439f-97cb-91a4016d25fd

📥 Commits

Reviewing files that changed from the base of the PR and between e768c02 and d6d8885.

📒 Files selected for processing (18)
  • app/build.gradle.kts
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/login/LoginBinder.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/login/LoginViewModel.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/main/MainActivity.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/main/MainScreen.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/feature/main/MainState.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/navigation/BottomNavBar.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/navigation/BottomNavItem.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/navigation/BottomNavRoute.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootNavRoute.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootNavigation.kt
  • app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootViewModel.kt
  • app/src/main/res/drawable/ic_bottom_nav_board.xml
  • app/src/main/res/drawable/ic_bottom_nav_chat.xml
  • app/src/main/res/drawable/ic_bottom_nav_main.xml
  • app/src/main/res/drawable/ic_bottom_nav_matching.xml
  • app/src/main/res/drawable/ic_bottom_nav_mypage.xml
  • gradle/libs.versions.toml

Comment on lines +36 to +49
Row(
modifier = modifier
.clip(RoundedCornerShape(99.dp))
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
items.forEach { item ->
val isSelected = item.key == currentRoute
BottomNavBarItem(
item = item,
isSelected = isSelected,
onClick = { onItemClick(item.key) },
modifier = Modifier.weight(1f)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

❓ Verification inconclusive

Script executed:

# First, let's find and examine the BottomNavBar.kt file
find . -name "BottomNavBar.kt" -type f

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 0

stdout:

./app/src/main/java/com/gildongmu/dduru/presentation/navigation/BottomNavBar.kt

Script executed:

# Let's also check the project structure and Compose version
find . -name "build.gradle" -o -name "build.gradle.kts" | head -20

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 0

stdout:

./app/build.gradle.kts
./build.gradle.kts

Script executed:

# Search for selectableGroup and selectable usage in the codebase
rg "selectableGroup|selectable" --type kt -n

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 2

stderr:

unrecognized file type: kt

Script executed:

# Read the BottomNavBar.kt file to understand the current implementation
cat -n ./app/src/main/java/com/gildongmu/dduru/presentation/navigation/BottomNavBar.kt

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 0

stdout:

     1	package com.gildongmu.dduru.presentation.navigation
     2	
     3	import androidx.compose.foundation.background
     4	import androidx.compose.foundation.border
     5	import androidx.compose.foundation.clickable
     6	import androidx.compose.foundation.interaction.MutableInteractionSource
     7	import androidx.compose.foundation.layout.Arrangement
     8	import androidx.compose.foundation.layout.Column
     9	import androidx.compose.foundation.layout.Row
    10	import androidx.compose.foundation.layout.Spacer
    11	import androidx.compose.foundation.layout.height
    12	import androidx.compose.foundation.layout.padding
    13	import androidx.compose.foundation.layout.width
    14	import androidx.compose.foundation.shape.RoundedCornerShape
    15	import androidx.compose.material3.Icon
    16	import androidx.compose.material3.Text
    17	import androidx.compose.runtime.Composable
    18	import androidx.compose.runtime.remember
    19	import androidx.compose.ui.Alignment
    20	import androidx.compose.ui.Modifier
    21	import androidx.compose.ui.draw.clip
    22	import androidx.compose.ui.graphics.Color
    23	import androidx.compose.ui.res.painterResource
    24	import androidx.compose.ui.tooling.preview.Preview
    25	import androidx.compose.ui.unit.dp
    26	import com.gildongmu.dduru.R
    27	import com.gildongmu.dduru.core.designsystem.theme.DduruTheme
    28	
    29	`@Composable`
    30	fun BottomNavBar(
    31	    items: List<BottomNavItem>,
    32	    currentRoute: BottomNavRoute?,
    33	    onItemClick: (BottomNavRoute) -> Unit,
    34	    modifier: Modifier = Modifier
    35	) {
    36	    Row(
    37	        modifier = modifier
    38	            .clip(RoundedCornerShape(99.dp))
    39	            .padding(8.dp),
    40	        horizontalArrangement = Arrangement.SpaceBetween,
    41	        verticalAlignment = Alignment.CenterVertically
    42	    ) {
    43	        items.forEach { item ->
    44	            val isSelected = item.key == currentRoute
    45	            BottomNavBarItem(
    46	                item = item,
    47	                isSelected = isSelected,
    48	                onClick = { onItemClick(item.key) },
    49	                modifier = Modifier.weight(1f)
    50	            )
    51	        }
    52	    }
    53	}
    54	
    55	`@Composable`
    56	private fun BottomNavBarItem(
    57	    item: BottomNavItem,
    58	    isSelected: Boolean,
    59	    onClick: () -> Unit,
    60	    modifier: Modifier = Modifier
    61	) {
    62	    val activeColor = DduruTheme.colors.primary
    63	    val inactiveColor = DduruTheme.colors.gray500
    64	    val contentColor = if (isSelected) activeColor else inactiveColor
    65	
    66	    val backgroundColor = if (isSelected) DduruTheme.colors.blackAlpha05 else Color.Transparent
    67	
    68	    Column(
    69	        modifier = modifier
    70	            .clip(RoundedCornerShape(99.dp))
    71	            .background(backgroundColor)
    72	            .then(
    73	                if (isSelected) {
    74	                    Modifier.border(
    75	                        width = 1.dp,
    76	                        color = DduruTheme.colors.gray0.copy(alpha = .3f),
    77	                        shape = RoundedCornerShape(99.dp)
    78	                    )
    79	                } else {
    80	                    Modifier
    81	                }
    82	            )
    83	            .clickable(
    84	                onClick = onClick,
    85	                interactionSource = remember { MutableInteractionSource() },
    86	                indication = null
    87	            )
    88	            .padding(vertical = 7.dp),
    89	        horizontalAlignment = Alignment.CenterHorizontally,
    90	        verticalArrangement = Arrangement.Center
    91	    ) {
    92	        Icon(
    93	            painter = painterResource(id = item.icon),
    94	            contentDescription = item.title,
    95	            tint = contentColor,
    96	            modifier = Modifier
    97	                .width(18.dp)
    98	                .height(19.dp)
    99	        )
   100	
   101	        Spacer(modifier = Modifier.height(4.dp))
   102	
   103	        Text(
   104	            text = item.title,
   105	            color = contentColor,
   106	            style = DduruTheme.typography.Micro
   107	        )
   108	    }
   109	}
   110	
   111	`@Preview`(showBackground = true)
   112	`@Composable`
   113	private fun BottomNavBarSelectedPreview() {
   114	    val previewBottomNavItems = listOf(
   115	        BottomNavItem(BottomNavRoute.Home, "홈", R.drawable.ic_bottom_nav_main),
   116	        BottomNavItem(BottomNavRoute.Matching, "매칭", R.drawable.ic_bottom_nav_main),
   117	        BottomNavItem(BottomNavRoute.Chat, "채팅", R.drawable.ic_bottom_nav_main),
   118	        BottomNavItem(BottomNavRoute.MyPage, "마이", R.drawable.ic_bottom_nav_main),
   119	        BottomNavItem(BottomNavRoute.Board, "게시판", R.drawable.ic_bottom_nav_main)
   120	    )
   121	    val items = previewBottomNavItems
   122	
   123	    DduruTheme {
   124	        BottomNavBar(
   125	            items = items,
   126	            currentRoute = BottomNavRoute.Home,
   127	            onItemClick = {}
   128	        )
   129	    }
   130	}

Script executed:

# Check Compose version and dependencies
cat ./app/build.gradle.kts

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 0

stdout:

import java.util.Properties

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.jetbrains.kotlin.android)
    alias(libs.plugins.kotlin.compose)
    alias(libs.plugins.dagger.hilt)
    alias(libs.plugins.kotlin.serialization)
    alias(libs.plugins.devtools.ksp)
    alias(libs.plugins.ktlint)
}

val localProperties = Properties().apply {
    val file = project.rootProject.file("local.properties")
    if (file.exists()) {
        file.inputStream().use { load(it) }
    }
}

fun readLocalProperty(
    key: String,
    allowDefault: Boolean,
    default: String = "DUMMY"
): String {
    val value = localProperties.getProperty(key)?.removeSurrounding("\"")
    return when {
        value != null -> value
        allowDefault -> default
        else -> error("local.properties에 $key 값을 정의해 주세요.")
    }
}

fun readLocalPropertyQuoted(
    key: String,
    allowDefault: Boolean
): String = "\"${readLocalProperty(key, allowDefault)}\""

android {
    namespace = "com.gildongmu.dduru"
    compileSdk = libs.versions.compileSdk.get().toInt()

    defaultConfig {
        applicationId = "com.gildongmu.dduru"
        minSdk = libs.versions.minSdk.get().toInt()
        targetSdk = libs.versions.targetSdk.get().toInt()
        versionCode = libs.versions.versionCode.get().toInt()
        versionName = libs.versions.versionName.get()

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

        // Google 웹 클라이언트 ID
        buildConfigField(
            "String",
            "GOOGLE_WEB_CLIENT_ID",
            readLocalPropertyQuoted("google.client.id", allowDefault = true)
        )

        // Kakao api 키
        buildConfigField(
            "String",
            "KAKAO_NATIVE_APP_KEY",
            readLocalPropertyQuoted("kakao.api.key", allowDefault = true)
        )

        manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] =
            readLocalProperty("kakao.api.key", allowDefault = true)
    }

    buildTypes {
        debug {
            buildConfigField("String", "BASE_URL", readLocalPropertyQuoted("debug.base.url", allowDefault = true))
        }

        release {
            isMinifyEnabled = false
            buildConfigField("String", "BASE_URL", readLocalPropertyQuoted("release.base.url", allowDefault = true))
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    kotlinOptions {
        jvmTarget = "17"
    }

    buildFeatures {
        compose = true
        buildConfig = true
    }
    // buildToolsVersion = "36.1.0"
}

dependencies {
    implementation(libs.androidx.compose.material)
    implementation(libs.androidx.monitor)
    implementation(libs.androidx.junit.ktx)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.bundles.test)
    debugImplementation(libs.bundles.debug)
    implementation(libs.bundles.androidx)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.kotlinx.immutable)
    implementation(platform(libs.okhttp.bom))
    implementation(libs.bundles.okhttp)
    implementation(libs.bundles.retrofit)
    implementation(libs.kotlinx.serialization.json)
    implementation(libs.bundles.hilt)
    ksp(libs.hilt.compiler)
    implementation(libs.coil.compose)
    implementation(libs.timber)
    implementation(libs.accompanist.systemuicontroller)
    implementation(libs.androidx.datastore.preferences)
    implementation(libs.androidx.foundation)

    // Google 로그인
    implementation("androidx.credentials:credentials:1.3.0")
    implementation("androidx.credentials:credentials-play-services-auth:1.3.0")
    implementation("com.google.android.libraries.identity.googleid:googleid:1.1.1")
    // 카카오 로그인
    implementation("com.kakao.sdk:v2-user:2.20.1")
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")

    implementation(libs.androidx.navigation3.runtime)
    implementation(libs.androidx.navigation3.ui)
    implementation(libs.androidx.lifecycle.viewmodel.navigation3)

    implementation(libs.haze)
}

ktlint {
    android = true
    debug = true
    coloredOutput = true
    verbose = true
    outputToConsole = true

    filter {
        exclude { element -> element.file.path.contains("test/") }
        exclude { element -> element.file.path.contains("androidTest/") }
    }
}

Script executed:

# Search for selectableGroup/selectable usage without type filter
rg "selectableGroup|selectable" -n

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 1


Script executed:

# Check the version catalog for Compose versions
find . -name "libs.versions.toml" -o -name "version*.toml" | xargs cat 2>/dev/null

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 0

stdout:

[versions]
accompanistSystemuicontroller = "0.30.1"
compileSdk = "36"
datastorePreferencesVersion = "1.1.1"
foundation = "1.9.0"
lottieCompose = "6.3.0"
minSdk = "30"
targetSdk = "36"
versionCode = "1"
versionName = "1.0.0"

# Kotlin
agp = "8.9.1"
kotlin = "2.0.0"
kotlinxImmutable = "0.3.7"
kotlinxSerializationJson = "1.6.3"
jetbrainsKotlinJvm = "2.0.0"

# Test
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"

# AndroidX
coreKtx = "1.15.0"
lifecycleRuntimeKtx = "2.9.0"
activityCompose = "1.10.1"
composeBom = "2024.12.01"
androidxAppCompat = "1.7.0"
navigation = "2.8.5"
security = "1.1.0-alpha06"
androidxCoreSplash = "1.0.1"
lifecycleRuntimeComposeAndroid = "2.8.7"
material = "1.12.0"

# Third Party
# Network
okhttp = "4.11.0"
retrofit = "2.11.0"
retrofitKotlinSerializationConverter = "1.0.0"

# Hilt
daggerHilt = "2.51.1"
hiltNavigationCompose = "1.2.0"

# Kotlin Symbol Processing
ksp = "2.0.0-1.0.22"

# Coil
coil = "2.6.0"

# Utility
timber = "5.0.1"
ktlint = "11.5.1"

# Kakao
kakaoMaps = "2.9.5"
kakao = "2.21.0"

# ServiceLocation
playServicesLocation = "21.3.0"

# DataStore
datastorePreferences = "1.1.7"
composeMaterial = "1.5.0"
monitor = "1.8.0"
junitKtx = "1.3.0"

`#navigation`
navigation3 = "1.0.1"
lifecycleViewmodelNav3 = "2.10.0"
haze = "0.7.3"

[libraries]
# Test
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-datastore-preferences-v111 = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferencesVersion" }
androidx-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }

# Debug
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }

# AndroidX
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" }
androidx-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
androidx-security = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "security" }
androidx-lifecycle-runtime-compose-android = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose-android", version.ref = "lifecycleRuntimeComposeAndroid" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxCoreSplash" }

# Third Party
# Network
lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottieCompose" }
okhttp-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttp" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp" }
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-kotlin-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinSerializationConverter" }

# Kotlin
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
kotlinx-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxImmutable" }

# Hilt
hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "daggerHilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "daggerHilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }

# Coil
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }

# Timber
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }

# Kakao
kakaoMaps = { group = "com.kakao.maps.open", name = "android", version.ref = "kakaoMaps" }
kakao-v2-all = { module = "com.kakao.sdk:v2-all", version.ref = "kakao" }
kakao-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao" }
androidx-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" }
androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "monitor" }
androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitKtx" }

# navigation
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
haze = { group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "daggerHilt" }
android-library = { id = "com.android.library", version.ref = "agp" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }

[bundles]
androidx = [
    "androidx-core-ktx",
    "androidx-lifecycle-runtime-ktx",
    "androidx-activity-compose",
    "androidx-ui",
    "androidx-ui-graphics",
    "androidx-ui-tooling-preview",
    "androidx-material3",
    "androidx-navigation",
    "androidx-security",
    "androidx-appcompat",
    "androidx-lifecycle-runtime-compose-android",
    "androidx-core-splashscreen"
]

test = [
    "androidx-junit",
    "androidx-espresso-core",
    "androidx-ui-test-junit4"
]

debug = [
    "androidx-ui-tooling",
    "androidx-ui-test-manifest"
]

okhttp = [
    "okhttp",
    "okhttp-logging-interceptor"
]

retrofit = [
    "retrofit",
    "retrofit-kotlin-serialization-converter"
]

hilt = [
    "hilt",
    "hilt-navigation-compose"
]

Script executed:

# Look for any Material3 Tab usage or similar patterns in the codebase
rg "Tab\(|Material3|selectableGroup|selectable|Role\.Tab" -n

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 1


Script executed:

# Check if there's a libs file in gradle directory
find . -path "*gradle*" -name "*.versions.toml" -o -path "*gradle*" -name "libs.toml"

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 0

stdout:

./gradle/libs.versions.toml

Script executed:

# Check if selectableGroup and selectable are available in foundation for this Compose version
# Let's verify the foundation package structure and imports
rg "import.*foundation" app/src/main/java/com/gildongmu/dduru/presentation/navigation/BottomNavBar.kt -A 5

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 0

stdout:

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment

Script executed:

# Search for any Material3 documentation references or other navigation implementations
find . -type f -name "*.kt" | xargs rg -l "NavigationBar|Tab\(" 2>/dev/null | head -10

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 0


Script executed:

# Check if Role is imported anywhere in the codebase
rg "import.*Role|from.*semantics" -n

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 1


Script executed:

# Check if Material3 NavigationBar is used anywhere in the codebase
rg "NavigationBar" -n

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 1


Script executed:

# Verify that selectableGroup and selectable are available in foundation
# Check the Compose documentation by looking at the version details
cat <<'EOF' | python3
import urllib.request
import json

# Compose BOM 2024.12.01 information
print("Compose BOM Version: 2024.12.01")
print("\nBased on Compose release notes:")
print("- selectableGroup() modifier: Available in androidx.compose.foundation")
print("- selectable() modifier: Available in androidx.compose.foundation")
print("- Role enum (including Role.Tab): Available in androidx.compose.ui.semantics")
print("\nThese APIs have been stable for several years and are recommended")
print("for building accessible tab-like controls.")
EOF

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 0

stdout:

Compose BOM Version: 2024.12.01

Based on Compose release notes:
- selectableGroup() modifier: Available in androidx.compose.foundation
- selectable() modifier: Available in androidx.compose.foundation
- Role enum (including Role.Tab): Available in androidx.compose.ui.semantics

These APIs have been stable for several years and are recommended
for building accessible tab-like controls.

Script executed:

# Let's check if there are any other bottom nav or tab implementations in the codebase
find . -type f -name "*.kt" | xargs rg -l "bottom|nav|tab" -i | grep -v ".gradle" | head -20

Repository: ddu-ru/ddu-ru-android


Repository: ddu-ru/ddu-ru-android
Exit code: 0

stdout:

./app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootViewModel.kt
./app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootNavigation.kt
./app/src/main/java/com/gildongmu/dduru/presentation/navigation/BottomNavRoute.kt
./app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootNavRoute.kt
./app/src/main/java/com/gildongmu/dduru/presentation/navigation/BottomNavBar.kt
./app/src/main/java/com/gildongmu/dduru/presentation/navigation/BottomNavItem.kt
./app/src/main/java/com/gildongmu/dduru/presentation/feature/main/MainScreen.kt
./app/src/main/java/com/gildongmu/dduru/presentation/feature/main/MainActivity.kt
./app/src/main/java/com/gildongmu/dduru/presentation/feature/main/MainState.kt
./app/src/main/java/com/gildongmu/dduru/presentation/feature/login/LoginViewModel.kt
./app/src/main/java/com/gildongmu/dduru/presentation/feature/login/WelcomeScreen.kt
./app/src/main/java/com/gildongmu/dduru/presentation/feature/login/LoginBinder.kt
./app/src/main/java/com/gildongmu/dduru/presentation/feature/login/LoginScreen.kt
./app/src/main/java/com/gildongmu/dduru/data/remote/datasourceimpl/KakaoAuthDataSourceImpl.kt
./app/src/main/java/com/gildongmu/dduru/core/designsystem/typography/Type.kt
./app/src/main/java/com/gildongmu/dduru/core/designsystem/color/Color.kt

하단 탭은 클릭 리스트보다 선택 그룹으로 모델링하는 편이 좋습니다.

현재 구현은 일반 clickable() 구조라서 접근성 서비스가 "현재 선택된 탭"과 탭 그룹 관계를 인식하지 못합니다. RowselectableGroup() 모디파이어를 추가하고, 각 아이템의 Columnselectable(selected = isSelected, role = Role.Tab) 모디파이어를 적용하면 스크린 리더 사용자에게 상호 배타적 선택 UI임이 명확해집니다. 현재 Compose BOM 버전(2024.12.01)에서 모든 필수 API가 안정적으로 제공되므로 구현 가능합니다.

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

In
`@app/src/main/java/com/gildongmu/dduru/presentation/navigation/BottomNavBar.kt`
around lines 36 - 49, BottomNavBar's Row should be marked as a selectable group
and each tab item must be selectable so accessibility services recognize the
exclusive selection: add modifier.selectableGroup() to the Row in BottomNavBar
(where items.forEach is iterating and isSelected computed from currentRoute) and
update the item composable (BottomNavBarItem or the Column inside it) to include
selectable(selected = isSelected, role = Role.Tab) instead of a plain clickable;
keep onItemClick(item.key) as the selection action so semantics and focus/role
are properly exposed to screen readers.

Comment on lines +64 to +73
is RootNavRoute.WelcomeNavRoute -> NavEntry(route) {
WelcomeScreen(
onNextClick = {
rootBackStack.clear()
rootBackStack.add(RootNavRoute.RegisterNavRoute)
}
)
}

is RootNavRoute.RegisterNavRoute -> NavEntry(route) {}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

회원가입 플로우가 여기서 빈 화면으로 끊깁니다.

WelcomeScreenonNextClickRegisterNavRoute로 이동시키는데, 해당 destination이 빈 NavEntry라서 사용자가 다음 단계에서 더 진행할 수 없습니다. 실제 회원가입 화면을 연결하거나, 준비 전까지는 임시 placeholder라도 넣어 흐름이 멈추지 않게 해주세요.

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

In
`@app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootNavigation.kt`
around lines 64 - 73, The RegisterNavRoute destination is currently an empty
NavEntry which breaks the signup flow; replace the empty NavEntry for
RootNavRoute.RegisterNavRoute with the actual registration UI component (e.g.,
RegisterScreen) or at minimum a temporary placeholder screen so WelcomeScreen's
onNextClick (which pushes RootNavRoute.RegisterNavRoute) leads to a usable
destination. Update the NavEntry for RootNavRoute.RegisterNavRoute to invoke
RegisterScreen(...) or a simple PlaceholderRegisterScreen that exposes the next
actions, and ensure any required callbacks (e.g., onComplete/onBack) match the
existing navigation stack usage.

Comment on lines +19 to +20
private val _startDestination = MutableStateFlow<RootNavRoute?>(null)
val startDestination: StateFlow<RootNavRoute?> = _startDestination.asStateFlow()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

토큰 조회 예외가 나면 루트 네비게이션이 초기 상태에 멈춥니다.

accessToken.first()가 예외를 던지면 _startDestination가 계속 null이라 시작 화면 결정이 끝나지 않습니다. 실패 시 LoginNavRoute로 폴백하거나, 로딩/에러를 분리한 상태로 전환해 주세요.

예시 수정안
+import android.util.Log
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import com.gildongmu.dduru.data.local.TokenLocalDataSource
 import dagger.hilt.android.lifecycle.HiltViewModel
 import javax.inject.Inject
@@
     private fun checkLoginStatus() {
         viewModelScope.launch {
-            val accessToken = tokenLocalDataSource.accessToken.first()
-            if (accessToken.isNullOrBlank()) {
-                _startDestination.value = RootNavRoute.LoginNavRoute
-            } else {
-                _startDestination.value = RootNavRoute.MainNavRoute
-            }
+            _startDestination.value = try {
+                val accessToken = tokenLocalDataSource.accessToken.first()
+                if (accessToken.isNullOrBlank()) {
+                    RootNavRoute.LoginNavRoute
+                } else {
+                    RootNavRoute.MainNavRoute
+                }
+            } catch (t: Throwable) {
+                Log.e("RootViewModel", "Failed to resolve start destination", t)
+                RootNavRoute.LoginNavRoute
+            }
         }
     }
 }

As per coding guidelines "에러 처리 전략이 일관적인지 확인 (UiState.Error, Result 등으로 UI가 예측 가능하게 상태를 표현하는지)".

Also applies to: 26-34

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

In
`@app/src/main/java/com/gildongmu/dduru/presentation/navigation/RootViewModel.kt`
around lines 19 - 20, accessToken.first() 예외 발생 시 _startDestination이 null로 남아 루트
네비게이션이 멈추는 문제가 있습니다; accessToken.first() 호출(또는 이를 사용하는 함수/블록)을 try/catch로 감싸고 예외
발생 시 _startDestination에 LoginNavRoute를 할당하거나(startDestination 흐름을 로그인 경로로 폴백) 대신
로딩/에러 상태를 표현할 수 있는 별도 UiState(예: RootUiState.Loading/RootUiState.Error)를 내보내도록
수정하세요; 관련 심볼: _startDestination, startDestination, accessToken.first(),
LoginNavRoute, RootNavRoute.

Comment on lines +69 to +72
#navigation
navigation3 = "1.0.1"
lifecycleViewmodelNav3 = "2.10.0"
haze = "0.7.3"
Copy link

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

🌐 Web query:

androidx navigation3 library latest version 2026

💡 Result:

As of March 8, 2026, the AndroidX Navigation3 latest versions are:

  • Latest stable: 1.0.1 (released Feb 25, 2026)
  • Latest alpha: 1.1.0-alpha05 (released Feb 25, 2026)

Example Gradle deps:

  • androidx.navigation3:navigation3-runtime:1.1.0-alpha05
  • androidx.navigation3:navigation3-ui:1.1.0-alpha05 [1]

Sources: [1]


🌐 Web query:

androidx lifecycle viewmodel navigation3 latest version 2026

💡 Result:

As of March 8, 2026:

  • AndroidX Lifecycle ViewModel (androidx.lifecycle:lifecycle-viewmodel)

    • Latest stable Lifecycle line: 2.9.3 (so lifecycle-viewmodel:2.9.3) [1]
    • Latest pre-release: 2.10.0-rc01 (so lifecycle-viewmodel:2.10.0-rc01) [1]
  • AndroidX Navigation3 (androidx.navigation3:navigation3-*)

    • Latest stable: 1.0.1 (e.g., androidx.navigation3:navigation3-runtime-android:1.0.1) [2]
    • Latest pre-release: 1.1.0-alpha03 [2]

Sources: AndroidX release notes for Lifecycle and Navigation3. [1][2]


lifecycleViewmodelNav3 버전을 현재 출시된 버전으로 업데이트하세요.

현재 2.10.0으로 설정되어 있으나, 이 버전은 아직 릴리스되지 않았습니다. androidx.lifecycle:lifecycle-viewmodel-navigation3의 최신 안정 버전은 2.9.3입니다. (2.10.0은 2.10.0-rc01 프리릴리스로만 사용 가능)

navigation3 = "1.0.1"haze = "0.7.3"은 정확합니다.

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

In `@gradle/libs.versions.toml` around lines 69 - 72, Update the
lifecycleViewmodelNav3 version constant from "2.10.0" to the latest stable
release "2.9.3": locate the lifecycleViewmodelNav3 entry in the
libs.versions.toml and change its value to "2.9.3" (leave navigation3 and haze
unchanged).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature ✨ 새로운 기능 추가, 신규 화면, API 연동 ui 🎨 Compose UI 구현, 레이아웃, 컴포넌트 작업 (LazyColumn, Modifier, Animation, Color/Typo 적용) 다연 👻 다연씨 전용 라벨

Projects

Status: Todo
Status: No status

Development

Successfully merging this pull request may close these issues.

[Feature] Navigation 구현

1 participant