Skip to content

[NDGL-71] 네비게이션 바 디자인 제작#16

Merged
mj010504 merged 4 commits intodevelopfrom
design/NDGL-71
Feb 14, 2026
Merged

[NDGL-71] 네비게이션 바 디자인 제작#16
mj010504 merged 4 commits intodevelopfrom
design/NDGL-71

Conversation

@mj010504
Copy link
Copy Markdown
Contributor

@mj010504 mj010504 commented Feb 5, 2026

개요

  • 네비게이션 바 디자인 제작
디자인 스크린샷
image image

변경사항

  • 불필요한 아이콘 삭제
  • DropShadow 유틸 함수 추가
  • 네비게이션 바 디자인 제작

테스트 체크 리스트

  • Bottom Navigation간 전환

Summary by CodeRabbit

  • 새로운 기능

    • 하단 네비게이션에 애니메이션 탭 인디케이터 추가
    • 드롭 섀도우 적용을 위한 유틸리티 함수 추가
    • 하단 바의 노출/숨김을 제어하는 애니메이션 래핑 추가
  • 스타일

    • 하단 네비게이션 레이아웃 및 시각적 구성 재설계(라운드 배경, 그림자, 선택 시 레이블 표시)
    • 네비게이션 아이콘 리소스 교체 및 아이콘 세트 업데이트

@mj010504 mj010504 requested a review from jihee-dev February 5, 2026 13:20
@mj010504 mj010504 self-assigned this Feb 5, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 5, 2026

Walkthrough

BottomNavigationBar를 NavigationBar 기반에서 BoxWithConstraints 기반의 커스텀 레이아웃으로 교체하고, 네비게이션 아이콘 리소스 참조를 새 아이콘으로 변경했으며 Modifier용 dropShadow 유틸리티를 추가했습니다.

Changes

Cohort / File(s) Summary
바텀 네비게이션 아이콘 업데이트
app/src/main/java/com/yapp/ndgl/navigation/BottomNavTab.kt
TRAVEL_HELPER, HOME, TRAVEL 탭의 아이콘 리소스 참조를 각각 ic_24_tool, ic_24_home, ic_24_bag로 변경.
바텀 네비게이션 바 리팩토링
app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.kt, app/src/main/java/com/yapp/ndgl/ui/NDGLApp.kt
NavigationBar/NavigationBarItem 기반에서 BoxWithConstraints 기반 커스텀 바로 변경: 탭별 너비 계산, 애니메이션 표시기(animateDpAsState), 선택 시 라벨 표시(AnimatedVisibility), Scaffold 내 위치 및 가시성 처리 변경.
드롭 섀도우 유틸리티 추가
core/ui/src/main/java/com/yapp/ndgl/core/ui/util/DropShadow.kt
Modifier 확장 함수 dropShadow(shape, color, blur, offsetX, offsetY, spread) 추가.
기존 드로어블 리소스 삭제
core/ui/src/main/res/drawable/ic_nav_helper.xml, core/ui/src/main/res/drawable/ic_nav_home.xml, core/ui/src/main/res/drawable/ic_nav_travel.xml
이전 네비게이션 벡터 드로어블 파일들이 삭제됨(대체 아이콘 참조로 인한 제거).

Sequence Diagram(s)

sequenceDiagram
    participant 사용자 as 사용자
    participant UI as BottomNavigationBar
    participant 상태 as NavState
    participant 애니메이터 as SpringAnimator

    사용자->>UI: 탭 클릭
    UI->>상태: onTabSelected(newTab)
    상태-->>UI: currentTab 업데이트
    UI->>UI: 탭 너비 계산 및 targetOffset 결정
    UI->>애니메이터: animateTo(targetOffset)
    애니메이터-->>UI: 오프셋 애니메이션 프레임
    UI->>사용자: 표시기 위치/라벨 변경 렌더링
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 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 제목은 'NDGL-71 네비게이션 바 디자인 제작'으로, 네비게이션 바 디자인 작업이라는 주요 변경 사항을 명확하게 설명하고 있으며 변경 사항과 완전히 일치합니다.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch design/NDGL-71

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.

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: 1

🤖 Fix all issues with AI agents
In `@app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.kt`:
- Around line 50-51: selectedIndex can be -1 when no tab route matches
currentTab, which later leads the when branch to hit else and place the
indicator incorrectly; change the selectedIndex computation
(BottomNavTab.entries and selectedIndex) to defensively handle -1 by
clamping/normalizing it (e.g., use .indexOfFirst { it.route == currentTab }.let
{ if (it == -1) 0 else it } or .coerceIn(0, tabs.lastIndex)) so the subsequent
when that maps index -> indicator position always receives a valid index.
🧹 Nitpick comments (2)
app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.kt (2)

71-80: 탭 개수가 변경될 경우 확장성 부족

현재 indicator offset 계산이 정확히 3개의 탭에 맞춰 하드코딩되어 있습니다. 향후 탭이 추가되거나 변경될 경우 이 로직도 수동으로 수정해야 합니다.

♻️ 확장 가능한 계산 방식 제안
         val indicatorOffset by animateDpAsState(
-            targetValue = when (selectedIndex) {
-                0 -> 0.dp
-                1 -> unselectedWidth
-                else -> totalWidth - selectedWidth
-            },
+            targetValue = if (selectedIndex == 0) {
+                0.dp
+            } else {
+                unselectedWidth * selectedIndex
+            },
             animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow),
         )

134-138: Preview 함수 추가 👍

다양한 탭 상태를 확인할 수 있도록 추가 Preview를 고려해볼 수 있습니다.

♻️ 다양한 상태 Preview 제안
`@Preview`(showBackground = true, name = "Travel Helper Selected")
`@Composable`
private fun BottomNavigationBarTravelHelperPreview() {
    BottomNavigationBar(currentTab = BottomNavTab.TRAVEL_HELPER.route, onTabSelected = {})
}

`@Preview`(showBackground = true, name = "Travel Selected")
`@Composable`
private fun BottomNavigationBarTravelPreview() {
    BottomNavigationBar(currentTab = BottomNavTab.TRAVEL.route, onTabSelected = {})
}

Comment thread app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.kt
Comment thread app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.kt Outdated
Comment thread app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.kt
Comment thread app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.kt Outdated
Comment thread app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.kt`:
- Around line 91-127: 현재 탭 너비가 즉시 바뀌어 indicator의 spring 애니메이션과 시각적으로 어긋나므로,
tabs.forEach 블록 안에서 isSelected 기반으로 selectedWidth/unselectedWidth 값을
animateDpAsState로 래핑해 탭의 width에 적용하세요; 예를 들어 val tabWidth =
animateDpAsState(targetValue = if (isSelected) selectedWidth else
unselectedWidth, animationSpec = spring(...)) 형태로 생성하고 기존 Row의
Modifier.width(...)를 .width(tabWidth.value)로 교체해 indicator의 offset 애니메이션과 동기화되도록
만드세요 (참조: tabs.forEach, isSelected, selectedWidth, unselectedWidth,
onTabSelected, AnimatedVisibility).
🧹 Nitpick comments (1)
app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.kt (1)

74-81: when 블록이 탭 3개에 하드코딩되어 있음

현재 when(selectedIndex) 로직이 0, 1, else로 고정되어 있어, 탭이 추가/삭제되면 indicator offset 계산이 깨집니다. 일반화된 계산으로 대체하면 유지보수가 용이합니다.

♻️ 일반화 제안
         val indicatorOffset by animateDpAsState(
-            targetValue = when (selectedIndex) {
-                0 -> 0.dp
-                1 -> unselectedWidth
-                else -> totalWidth - selectedWidth
-            },
+            targetValue = unselectedWidth * selectedIndex,
             animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow),
         )

Comment on lines +91 to 127
Row(modifier = Modifier.fillMaxSize()) {
tabs.forEach { tab ->
val isSelected = currentTab == tab.route

Row(
modifier = Modifier
.width(if (isSelected) selectedWidth else unselectedWidth)
.fillMaxHeight()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) { onTabSelected(tab.route) },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
) {
Icon(
imageVector = ImageVector.vectorResource(id = topLevelRoute.icon),
contentDescription = topLevelRoute.label,
modifier = Modifier.size(24.dp),
imageVector = ImageVector.vectorResource(id = tab.icon),
contentDescription = tab.label,
tint = if (isSelected) NDGLTheme.colors.white else NDGLTheme.colors.black600,
)
},
label = { Text(text = topLevelRoute.label) },
)

AnimatedVisibility(
visible = isSelected,
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally(),
) {
Text(
text = tab.label,
color = NDGLTheme.colors.white,
style = NDGLTheme.typography.bodyLgMedium,
maxLines = 1,
)
}
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

탭 너비가 애니메이션 없이 즉시 변경됨 — indicator와 시각적 불일치 가능

Line 74-81에서 indicator의 offset은 spring 애니메이션으로 부드럽게 이동하지만, Line 97에서 각 탭의 widthif/else로 즉시 전환됩니다. 이로 인해 indicator는 슬라이딩 중인데 탭 콘텐츠(아이콘+라벨)는 갑자기 위치가 바뀌는 현상이 발생할 수 있습니다.

탭 너비도 animateDpAsState로 감싸면 indicator와 동기화됩니다:

♻️ 탭 너비 애니메이션 적용 제안
             tabs.forEach { tab ->
                 val isSelected = currentTab == tab.route
+                val tabWidth by animateDpAsState(
+                    targetValue = if (isSelected) selectedWidth else unselectedWidth,
+                    animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow),
+                )

                 Row(
                     modifier = Modifier
-                        .width(if (isSelected) selectedWidth else unselectedWidth)
+                        .width(tabWidth)
                         .fillMaxHeight()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Row(modifier = Modifier.fillMaxSize()) {
tabs.forEach { tab ->
val isSelected = currentTab == tab.route
Row(
modifier = Modifier
.width(if (isSelected) selectedWidth else unselectedWidth)
.fillMaxHeight()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) { onTabSelected(tab.route) },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
) {
Icon(
imageVector = ImageVector.vectorResource(id = topLevelRoute.icon),
contentDescription = topLevelRoute.label,
modifier = Modifier.size(24.dp),
imageVector = ImageVector.vectorResource(id = tab.icon),
contentDescription = tab.label,
tint = if (isSelected) NDGLTheme.colors.white else NDGLTheme.colors.black600,
)
},
label = { Text(text = topLevelRoute.label) },
)
AnimatedVisibility(
visible = isSelected,
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally(),
) {
Text(
text = tab.label,
color = NDGLTheme.colors.white,
style = NDGLTheme.typography.bodyLgMedium,
maxLines = 1,
)
}
}
}
}
Row(modifier = Modifier.fillMaxSize()) {
tabs.forEach { tab ->
val isSelected = currentTab == tab.route
val tabWidth by animateDpAsState(
targetValue = if (isSelected) selectedWidth else unselectedWidth,
animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow),
)
Row(
modifier = Modifier
.width(tabWidth)
.fillMaxHeight()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) { onTabSelected(tab.route) },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
) {
Icon(
modifier = Modifier.size(24.dp),
imageVector = ImageVector.vectorResource(id = tab.icon),
contentDescription = tab.label,
tint = if (isSelected) NDGLTheme.colors.white else NDGLTheme.colors.black600,
)
AnimatedVisibility(
visible = isSelected,
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally(),
) {
Text(
text = tab.label,
color = NDGLTheme.colors.white,
style = NDGLTheme.typography.bodyLgMedium,
maxLines = 1,
)
}
}
}
}
🤖 Prompt for AI Agents
In `@app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.kt` around lines 91 -
127, 현재 탭 너비가 즉시 바뀌어 indicator의 spring 애니메이션과 시각적으로 어긋나므로, tabs.forEach 블록 안에서
isSelected 기반으로 selectedWidth/unselectedWidth 값을 animateDpAsState로 래핑해 탭의 width에
적용하세요; 예를 들어 val tabWidth = animateDpAsState(targetValue = if (isSelected)
selectedWidth else unselectedWidth, animationSpec = spring(...)) 형태로 생성하고 기존
Row의 Modifier.width(...)를 .width(tabWidth.value)로 교체해 indicator의 offset 애니메이션과
동기화되도록 만드세요 (참조: tabs.forEach, isSelected, selectedWidth, unselectedWidth,
onTabSelected, AnimatedVisibility).

@mj010504
Copy link
Copy Markdown
Contributor Author

@jihee-dev
Scaffold의 BottomBar를 사용했을 때 기본 containerColor로 박스형태로 감싸는 현상이 있어서 Box를 사용해서 이슈 해결하였습니다.
확인 부탁드립니다!

image image

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: 2

🤖 Fix all issues with AI agents
In `@app/src/main/java/com/yapp/ndgl/ui/NDGLApp.kt`:
- Around line 57-59: Replace the unsafe cast of
navigationState.currentTopLevelKey to Route in the BottomNavigationBar call with
a safe cast and a fallback: use a safe cast (e.g. currentTopLevelKey as? Route)
and provide a default Route or null-handling before passing to
BottomNavigationBar so it never throws a ClassCastException during
AnimatedVisibility exit recompositions; update the expression used for
currentTab in the BottomNavigationBar invocation and ensure navigator.navigate
handling (onTabSelected) accepts the fallback case.
- Around line 40-51: Scaffold no longer supplies bottom inset because
BottomNavigationBar is overlaid via AnimatedVisibility, so screens rendered by
NavDisplay (used with entries = navigationState.toEntries(entryProvider) and
onBack = navigator::goBack) may be obscured; update the content padding passed
into those screens by adding a bottom padding equal to the navigation bar height
(68.dp + 20.dp) plus system navigationBarsPadding (or use
WindowInsets.navigationBars.asPaddingValues()) so Home, Travel, TravelHelper and
other entry composables receive Modifier.padding(innerPadding) extended with the
extra bottom inset to avoid content being hidden behind the overlaid
BottomNavigationBar.
🧹 Nitpick comments (2)
app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.kt (2)

72-79: Indicator offset 계산이 3개 탭에 하드코딩되어 있음

현재 when 블록은 인덱스 0, 1, else만 처리하므로 탭이 추가/삭제될 때마다 수정이 필요합니다. 선택된 탭 앞의 모든 탭이 unselectedWidth이므로, selectedIndex * unselectedWidth로 일반화할 수 있습니다.

♻️ 일반화된 offset 계산 제안
         val indicatorOffset by animateDpAsState(
-            targetValue = when (selectedIndex) {
-                0 -> 0.dp
-                1 -> unselectedWidth
-                else -> totalWidth - selectedWidth
-            },
+            targetValue = unselectedWidth * selectedIndex,
             animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow),
         )

53-67: 하드코딩된 dp 값들을 상수로 추출하는 것을 권장합니다

34.dp, 68.dp, 24.dp, 20.dp, 12.dp, 6.dp, 0.56f 등 여러 매직 넘버가 컴포저블 내부에 산재해 있습니다. 파일 상단이나 companion object에 상수로 추출하면 디자인 변경 시 일괄 수정이 용이하고 가독성도 향상됩니다.

Comment on lines +40 to +51
Box(modifier = Modifier.fillMaxSize()) {
Scaffold(
modifier = Modifier.fillMaxSize(),
) { innerPadding ->
NavDisplay(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
onBack = navigator::goBack,
entries = navigationState.toEntries(entryProvider),
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Searching for bottom padding handling in feature screens ==="
rg -n -C3 'bottomPadding\|navigationBarsPadding\|padding.*bottom\|contentPadding' --type=kotlin -g '*/feature/*' 2>/dev/null | head -100

echo ""
echo "=== Checking NDGLApp.kt file ==="
cat -n app/src/main/java/com/yapp/ndgl/ui/NDGLApp.kt | head -70

echo ""
echo "=== Searching for navigationState implementation ==="
fd -e kt -e java | xargs rg -l 'navigationState' | head -5

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 2947


🏁 Script executed:

#!/bin/bash
echo "=== Searching for BottomNavigationBar implementation ==="
rg -n 'fun BottomNavigationBar' --type=kotlin -A 15 | head -40

echo ""
echo "=== Searching for currentTopLevelKey implementation ==="
rg -n 'currentTopLevelKey' --type=kotlin -B 2 -A 2 | head -50

echo ""
echo "=== Checking Home feature screen ==="
fd -e kt -path '*/feature/home/*' | xargs cat -n | head -80

echo ""
echo "=== Checking if topLevelKeys contains Route objects ==="
rg -n 'topLevelKeys.*=' --type=kotlin -A 1 | head -30

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 5134


🏁 Script executed:

#!/bin/bash
echo "=== Finding Home feature screen files ==="
find app/src/main -path '*/feature/home/*' -name '*.kt' | head -10

echo ""
echo "=== Finding Travel feature screen files ==="
find app/src/main -path '*/feature/travel/*' -name '*.kt' | head -10

echo ""
echo "=== Searching for Home screen composable ==="
rg -n '@Composable.*Home\|fun Home' --type=kotlin -A 5 | head -40

echo ""
echo "=== Searching for any bottom padding in screens ==="
rg -n 'modifier.*padding.*bottom\|contentPadding' app/src/main --type=kotlin | head -20

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 264


🏁 Script executed:

#!/bin/bash
echo "=== Finding all Kotlin files in feature directories ==="
find app/src/main -type d -name 'feature' | head -5

echo ""
echo "=== Listing feature directory structure ==="
find app/src/main -path '*/feature/*' -name '*.kt' | head -20

echo ""
echo "=== Searching for Screen composables ==="
rg -n '@Composable.*Screen\|fun.*Screen' --type=kotlin | head -30

echo ""
echo "=== Getting full BottomNavigationBar.kt to understand padding ==="
cat -n app/src/main/java/com/yapp/ndgl/ui/BottomNavigationBar.kt | sed -n '46,70p'

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 1322


BottomNavigationBar 오버레이로 인한 콘텐츠 하단 가림 현상

Scaffold의 bottomBar 파라미터를 제거하고 AnimatedVisibility로 오버레이된 BottomNavigationBar로 변경했으므로, innerPadding에 더 이상 하단 네비게이션 바 높이(68.dp + 20.dp + navigationBarsPadding)가 반영되지 않습니다. 결과적으로 각 화면의 하단 콘텐츠가 네비게이션 바에 의해 가려질 가능성이 있습니다.

Home, Travel, TravelHelper 등의 화면들이 적절한 하단 padding을 적용하여 콘텐츠가 네비게이션 바 뒤에 숨겨지지 않도록 해야 합니다.

🤖 Prompt for AI Agents
In `@app/src/main/java/com/yapp/ndgl/ui/NDGLApp.kt` around lines 40 - 51, Scaffold
no longer supplies bottom inset because BottomNavigationBar is overlaid via
AnimatedVisibility, so screens rendered by NavDisplay (used with entries =
navigationState.toEntries(entryProvider) and onBack = navigator::goBack) may be
obscured; update the content padding passed into those screens by adding a
bottom padding equal to the navigation bar height (68.dp + 20.dp) plus system
navigationBarsPadding (or use WindowInsets.navigationBars.asPaddingValues()) so
Home, Travel, TravelHelper and other entry composables receive
Modifier.padding(innerPadding) extended with the extra bottom inset to avoid
content being hidden behind the overlaid BottomNavigationBar.

Comment thread app/src/main/java/com/yapp/ndgl/ui/NDGLApp.kt
@mj010504 mj010504 merged commit 81cd0cb into develop Feb 14, 2026
2 checks passed
@mj010504 mj010504 deleted the design/NDGL-71 branch February 14, 2026 01:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants