From e9de57c408ae28d52f8041922d9d2e41b020cace Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Fri, 16 Jan 2026 14:47:54 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat(dependency):=20Navigation=20Compose?= =?UTF-8?q?=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Navigation Compose 라이브러리를 프로젝트에 추가하여 Jetpack Compose 환경에서의 화면 전환 및 탐색 기능을 구현할 수 있도록 준비했습니다. - **`gradle/libs.versions.toml` 수정:** - `navigationCompose` 버전 변수(`2.9.6`)를 추가했습니다. - `androidx.navigation:navigation-compose` 라이브러리를 `androidx-navigation-compose`라는 이름으로 등록했습니다. - **`app/build.gradle.kts` 수정:** - `dependencies` 블록에 `implementation(libs.androidx.navigation.compose)`를 추가하여 Navigation Compose 라이브러리를 모듈에 적용했습니다. --- app/build.gradle.kts | 1 + gradle/libs.versions.toml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9416bb39..954d3e29 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -150,6 +150,7 @@ android { dependencies { ktlintRuleset(libs.ktlint) implementation(libs.map.sdk) + implementation(libs.androidx.navigation.compose) implementation(libs.play.services.location) implementation(libs.androidx.core.ktx) implementation(libs.androidx.fragment.ktx) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2f9f357f..f5aebc83 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,7 @@ material = "1.12.0" activity = "1.10.1" constraintlayout = "2.2.1" mockk = "1.14.5" +navigationCompose = "2.9.6" photoviewDialog = "1.0.3" playServicesLocation = "21.3.0" retrofit = "3.0.0" @@ -48,6 +49,7 @@ activityCompose = "1.11.0" androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTesting" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" } app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" } assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertjCore" } From 007ef661b98fd78ae126eed4062215b603e076ac Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Fri, 16 Jan 2026 14:50:02 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat(main):=20=ED=95=98=EB=8B=A8=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=B0=94=20?= =?UTF-8?q?=EB=B0=8F=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 메인 화면의 하단 네비게이션 바 UI와 관련된 컴포저블 및 라우팅 구조를 추가했습니다. - **`FestabookMainTab.kt` 추가:** - `HOME`, `SCHEDULE`, `PLACE_MAP`, `NEWS`, `SETTING` 5개의 탭을 정의하는 `enum class`를 추가했습니다. - 각 탭은 아이콘, 라벨, 라우트 정보 등을 포함합니다. - **`component/FestabookBottomNavigationBar.kt` 추가:** - `FestabookBottomNavigationBar` 컴포저블을 구현하여 하단 탭 UI를 구성했습니다. - 중앙의 `PLACE_MAP` 탭은 Floating Action Button(FAB) 스타일로 돌출되도록 별도 처리했습니다. - **`FestabookRoute.kt` 추가:** - 앱 전반의 화면 전환을 관리하기 위한 `FestabookRoute`와 메인 탭에 해당하는 `FestabookMainRoute` sealed interface를 정의했습니다. - `kotlinx.serialization`을 사용하여 각 라우트를 직렬화 가능하도록 설정했습니다. --- .../presentation/main/FestabookMainTab.kt | 59 +++++++ .../presentation/main/FestabookRoute.kt | 34 ++++ .../component/FestabookBottomNavigationBar.kt | 150 ++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt create mode 100644 app/src/main/java/com/daedan/festabook/presentation/main/FestabookRoute.kt create mode 100644 app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt new file mode 100644 index 00000000..5e4bf222 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt @@ -0,0 +1,59 @@ +package com.daedan.festabook.presentation.main + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import com.daedan.festabook.R +import com.daedan.festabook.presentation.theme.FestabookColor + +enum class FestabookMainTab( + @DrawableRes val iconResId: Int, + @StringRes val labelResId: Int, + val contentDescription: String, + val route: FestabookMainRoute, +) { + HOME( + iconResId = R.drawable.ic_home, + labelResId = R.string.menu_home_title, + contentDescription = "홈", + route = FestabookMainRoute.Home, + ), + SCHEDULE( + iconResId = R.drawable.ic_schedule, + labelResId = R.string.menu_schedule_title, + contentDescription = "일정", + route = FestabookMainRoute.Schedule, + ), + PLACE_MAP( + iconResId = R.drawable.ic_map, + labelResId = R.string.menu_map_title, + contentDescription = "지도", + route = FestabookMainRoute.PlaceMap, + ), + NEWS( + iconResId = R.drawable.ic_news, + labelResId = R.string.menu_news_title, + contentDescription = "뉴스", + route = FestabookMainRoute.News, + ), + SETTING( + iconResId = R.drawable.ic_setting, + labelResId = R.string.menu_setting_title, + contentDescription = "설정", + route = FestabookMainRoute.Setting, + ), + ; + + companion object Defaults { + val selectedColor + @Composable + @ReadOnlyComposable + get() = FestabookColor.black + + val unselectedColor + @Composable + @ReadOnlyComposable + get() = FestabookColor.gray400 + } +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookRoute.kt b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookRoute.kt new file mode 100644 index 00000000..8e6cf326 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookRoute.kt @@ -0,0 +1,34 @@ +package com.daedan.festabook.presentation.main + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface FestabookRoute { + @Serializable + data object Splash : FestabookRoute + + // TODO: PlaceUiModel, PlaceDetailUiModel 생성자에 추가 후 UiModel에 @Serializable 어노테이션 필요 + @Serializable + data object PlaceDetail : FestabookRoute + + @Serializable + data object Explore : FestabookRoute +} + +@Serializable +sealed interface FestabookMainRoute : FestabookRoute { + @Serializable + data object Home : FestabookMainRoute + + @Serializable + data object Schedule : FestabookMainRoute + + @Serializable + data object PlaceMap : FestabookMainRoute + + @Serializable + data object News : FestabookMainRoute + + @Serializable + data object Setting : FestabookMainRoute +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt b/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt new file mode 100644 index 00000000..0f17050a --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt @@ -0,0 +1,150 @@ +package com.daedan.festabook.presentation.main.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.selection.selectable +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.daedan.festabook.R +import com.daedan.festabook.presentation.main.FestabookMainTab +import com.daedan.festabook.presentation.main.FestabookMainTab.Defaults.selectedColor +import com.daedan.festabook.presentation.main.FestabookMainTab.Defaults.unselectedColor +import com.daedan.festabook.presentation.theme.FestabookColor +import com.daedan.festabook.presentation.theme.FestabookTheme +import com.daedan.festabook.presentation.theme.FestabookTypography +import com.daedan.festabook.presentation.theme.festabookSpacing + +@Composable +fun FestabookBottomNavigationBar( + currentTab: FestabookMainTab, + onTabSelect: (FestabookMainTab) -> Unit, + modifier: Modifier = Modifier, +) { + Box( + contentAlignment = Alignment.BottomCenter, + modifier = modifier, + ) { + Row( + modifier = + Modifier + .background(color = FestabookColor.white) + .fillMaxWidth() + .height(70.dp), + ) { + FestabookMainTab.entries.forEach { item -> + when (item) { + FestabookMainTab.PLACE_MAP -> Spacer(modifier = Modifier.weight(1f)) + else -> { + FestabookNavigationItem( + tab = item, + selected = item == currentTab, + onClick = onTabSelect, + ) + } + } + } + } + PlaceMapNavigationItem(onClick = onTabSelect) + } +} + +@Composable +private fun RowScope.FestabookNavigationItem( + onClick: (FestabookMainTab) -> Unit, + tab: FestabookMainTab, + selected: Boolean, + modifier: Modifier = Modifier, +) { + Box( + modifier = + modifier + .weight(1f) + .fillMaxHeight() + .background(color = FestabookColor.white) + .selectable( + selected = selected, + onClick = { onClick(tab) }, + interactionSource = remember { MutableInteractionSource() }, + indication = null, // 리플(Ripple) 완벽 제거 + ), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + painter = painterResource(id = tab.iconResId), + contentDescription = tab.contentDescription, + tint = if (selected) selectedColor else unselectedColor, + ) + Spacer(modifier = Modifier.height(festabookSpacing.paddingBody1)) + Text( + text = stringResource(tab.labelResId), + style = FestabookTypography.labelMedium, + color = if (selected) selectedColor else unselectedColor, + ) + } + } +} + +@Composable +private fun PlaceMapNavigationItem( + onClick: (FestabookMainTab) -> Unit, + modifier: Modifier = Modifier, +) { + Image( + modifier = + modifier + .offset(y = -festabookSpacing.paddingBody4) + .clickable( + onClick = { + onClick(FestabookMainTab.PLACE_MAP) + }, + indication = null, + interactionSource = null, + ), + painter = painterResource(id = R.drawable.btn_fab_manu), + contentDescription = null, + ) +} + +@Preview(showBackground = true) +@Composable +private fun FestabookBottomNavigationBarPreview() { + FestabookTheme { + var currentTabState by remember { mutableStateOf(FestabookMainTab.HOME) } + Scaffold( + bottomBar = { + FestabookBottomNavigationBar( + currentTab = currentTabState, + onTabSelect = { currentTabState = it }, + ) + }, + ) { it } + } +} From 75d76559d5f464078f1e213087ecbf11a6e913c4 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Fri, 16 Jan 2026 16:57:58 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat(Main):=20Compose=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=EC=9D=98=20=EB=A9=94=EC=9D=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=B0=8F=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compose `NavHost`를 기반으로 한 메인 화면의 네비게이션 구조를 구현하고, 하단 네비게이션 바와 각 탭의 화면을 연결했습니다. `FestabookNavigator` 클래스를 도입하여 화면 간 이동 로직을 중앙에서 관리하도록 했습니다. - **`FestabookNavigator.kt` 추가:** - `NavHostController`를 감싸는 `FestabookNavigator` 클래스를 추가하여 네비게이션 로직을 캡슐화했습니다. - 현재 경로에 따라 현재 탭(`currentTab`)과 하단 바 표시 여부(`shouldShowBottomBar`)를 결정하는 로직을 구현했습니다. - `rememberFestabookNavigator` 컴포저블을 추가하여 네비게이터 인스턴스를 쉽게 생성하고 기억할 수 있도록 했습니다. - **`MainScreen.kt` 추가:** - `Scaffold`와 `NavHost`를 사용하여 메인 화면의 전체적인 레이아웃을 구성했습니다. - `FestabookBottomNavigationBar`와 `NavHost`를 `FestabookNavigator`와 연동하여 탭 선택 시 화면이 전환되도록 구현했습니다. - **`homeNavGraph.kt` 추가:** - 홈 화면(`HomeScreen`)에 대한 `NavGraph` 확장 함수를 추가하여, 메인 `NavHost`에서 홈 그래프를 모듈화하여 관리할 수 있도록 했습니다. - **`FestabookMainTab.kt` 수정:** - `find` 컴패니언 함수를 추가하여, 주어진 조건에 맞는 `FestabookMainTab`을 찾을 수 있도록 유틸리티 기능을 제공했습니다. - 이 함수는 `FestabookNavigator`에서 현재 활성화된 탭을 찾는 데 사용됩니다. --- .../home/navigation/HomeNavigation.kt | 24 ++++++ .../presentation/main/FestabookMainTab.kt | 6 ++ .../presentation/main/FestabookNavigator.kt | 73 +++++++++++++++++++ .../component/FestabookBottomNavigationBar.kt | 2 +- .../presentation/main/component/MainScreen.kt | 46 ++++++++++++ 5 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/home/navigation/HomeNavigation.kt create mode 100644 app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt create mode 100644 app/src/main/java/com/daedan/festabook/presentation/main/component/MainScreen.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/home/navigation/HomeNavigation.kt b/app/src/main/java/com/daedan/festabook/presentation/home/navigation/HomeNavigation.kt new file mode 100644 index 00000000..49762f77 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/home/navigation/HomeNavigation.kt @@ -0,0 +1,24 @@ +package com.daedan.festabook.presentation.home.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.daedan.festabook.presentation.home.HomeViewModel +import com.daedan.festabook.presentation.home.component.HomeScreen +import com.daedan.festabook.presentation.main.FestabookMainRoute + +fun NavGraphBuilder.homeNavGraph( + padding: PaddingValues, + viewModel: HomeViewModel, + onNavigateToExplore: () -> Unit, +) { + composable { + HomeScreen( + modifier = Modifier.padding(padding), + viewModel = viewModel, + onNavigateToExplore = onNavigateToExplore, + ) + } +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt index 5e4bf222..368e9602 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt @@ -46,6 +46,12 @@ enum class FestabookMainTab( ; companion object Defaults { + @Composable + fun find(predicate: @Composable (FestabookRoute) -> Boolean) = + entries.find { + predicate(it.route) + } + val selectedColor @Composable @ReadOnlyComposable diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt new file mode 100644 index 00000000..3a3811ee --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt @@ -0,0 +1,73 @@ +package com.daedan.festabook.presentation.main + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.NavOptions +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navOptions + +class FestabookNavigator( + val navController: NavHostController, +) { + private val currentDestination + @Composable + get() = + navController + .currentBackStackEntryAsState() + .value + ?.destination + + private val defaultNavOptions = + navOptions { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + + val currentTab + @Composable + get() = + FestabookMainTab.find { + currentDestination?.hasRoute(it::class) == true + } + + val shouldShowBottomBar + @Composable + get() = + FestabookMainTab.find { + currentDestination?.hasRoute(it::class) == true + } != null + + val startRoute = FestabookMainRoute.Home // TODO: Splash와 Explore 연동 시 변경 + + fun navigateToMainTab(route: FestabookRoute) { + navController.navigate( + route, + defaultNavOptions, + ) + } + + fun navigate( + route: FestabookRoute, + navOptions: NavOptions? = null, + ) { + navController.navigate( + route, + navOptions, + ) + } +} + +@Composable +fun rememberFestabookNavigator(): FestabookNavigator { + val navController = rememberNavController() + return remember { + FestabookNavigator(navController = navController) + } +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt b/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt index 0f17050a..47dcfcbd 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt @@ -40,7 +40,7 @@ import com.daedan.festabook.presentation.theme.festabookSpacing @Composable fun FestabookBottomNavigationBar( - currentTab: FestabookMainTab, + currentTab: FestabookMainTab?, onTabSelect: (FestabookMainTab) -> Unit, modifier: Modifier = Modifier, ) { diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/component/MainScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/main/component/MainScreen.kt new file mode 100644 index 00000000..a5df00f1 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/main/component/MainScreen.kt @@ -0,0 +1,46 @@ +package com.daedan.festabook.presentation.main.component + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import com.daedan.festabook.presentation.home.HomeViewModel +import com.daedan.festabook.presentation.home.navigation.homeNavGraph +import com.daedan.festabook.presentation.main.FestabookRoute +import com.daedan.festabook.presentation.main.rememberFestabookNavigator + +@Composable +fun MainScreen( + homeViewModel: HomeViewModel, + modifier: Modifier = Modifier, +) { + val navigator = rememberFestabookNavigator() + + Scaffold( + // TODO: 스낵바 구현 및 하위 프래그먼트에 해당 SnackBar 적용 + bottomBar = { + FestabookBottomNavigationBar( + currentTab = navigator.currentTab, + onTabSelect = { navigator.navigateToMainTab(it.route) }, + ) + }, + modifier = modifier, + ) { innerPadding -> + NavHost( + modifier = Modifier.fillMaxSize(), + startDestination = navigator.startRoute, + navController = navigator.navController, + ) { + homeNavGraph( + padding = innerPadding, + viewModel = homeViewModel, + onNavigateToExplore = { navigator.navigate(FestabookRoute.Explore) }, + ) + + // TODO: 각 화면에서 나머지 graph들 정의 + // 만약 Fragment에서 Screen에 넣어줄 부가적인 작업 시 각 화면에서 Route 컴포저블로 감싸서 전달 요망 + // 참고:https://github.com/Project-Unifest/unifest-android/blob/develop/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt + } + } +} From 180304f516386d3a45b27e7fd38daba142399a62 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Fri, 16 Jan 2026 21:09:18 +0900 Subject: [PATCH 04/10] =?UTF-8?q?test:=20FlowExtension=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compose `NavHost`를 기반으로 한 메인 화면의 네비게이션 구조를 구현하고, 하단 네비게이션 바와 각 탭의 화면을 연결했습니다. `FestabookNavigator` 클래스를 도입하여 화면 간 이동 로직을 중앙에서 관리하도록 했습니다. - **`FestabookNavigator.kt` 추가:** - `NavHostController`를 감싸는 `FestabookNavigator` 클래스를 추가하여 네비게이션 로직을 캡슐화했습니다. - 현재 경로에 따라 현재 탭(`currentTab`)과 하단 바 표시 여부(`shouldShowBottomBar`)를 결정하는 로직을 구현했습니다. - `rememberFestabookNavigator` 컴포저블을 추가하여 네비게이터 인스턴스를 쉽게 생성하고 기억할 수 있도록 했습니다. - **`MainScreen.kt` 추가:** - `Scaffold`와 `NavHost`를 사용하여 메인 화면의 전체적인 레이아웃을 구성했습니다. - `FestabookBottomNavigationBar`와 `NavHost`를 `FestabookNavigator`와 연동하여 탭 선택 시 화면이 전환되도록 구현했습니다. - **`homeNavGraph.kt` 추가:** - 홈 화면(`HomeScreen`)에 대한 `NavGraph` 확장 함수를 추가하여, 메인 `NavHost`에서 홈 그래프를 모듈화하여 관리할 수 있도록 했습니다. - **`FestabookMainTab.kt` 수정:** - `find` 컴패니언 함수를 추가하여, 주어진 조건에 맞는 `FestabookMainTab`을 찾을 수 있도록 유틸리티 기능을 제공했습니다. - 이 함수는 `FestabookNavigator`에서 현재 활성화된 탭을 찾는 데 사용됩니다. --- .../com/daedan/festabook/FlowExtension.kt | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 app/src/test/java/com/daedan/festabook/FlowExtension.kt diff --git a/app/src/test/java/com/daedan/festabook/FlowExtension.kt b/app/src/test/java/com/daedan/festabook/FlowExtension.kt deleted file mode 100644 index 16f9d7bf..00000000 --- a/app/src/test/java/com/daedan/festabook/FlowExtension.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.daedan.festabook - -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.timeout -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.withTimeout -import kotlin.time.Duration.Companion.seconds - -@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) -fun TestScope.observeEvent(flow: Flow): Deferred { - val event = - backgroundScope.async { - withTimeout(3000) { - flow.first() - } - } - advanceUntilIdle() - return event -} - -@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) -fun TestScope.observeMultipleEvent( - flow: Flow, - result: MutableList, -) { - backgroundScope.launch(UnconfinedTestDispatcher()) { - flow - .timeout(3.seconds) - .collect { - result.add(it) - } - } -} From fef77a744d4ac09f297cba195563eb3e0c11f60b Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Fri, 16 Jan 2026 21:46:00 +0900 Subject: [PATCH 05/10] =?UTF-8?q?refactor(Main):=20FAB=EC=9D=98=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=84=B1=20=EB=B0=8F=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EB=9E=99=EC=85=98=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FestabookBottomNavigationBar`의 플로팅 액션 버튼(FAB)의 접근성과 사용자 인터랙션을 개선했습니다. - **`FestabookBottomNavigationBar.kt` 수정:** - `interactionSource`에 `null` 대신 `MutableInteractionSource`를 명시적으로 전달하여 클릭 시 시각적 피드백(리플 효과)이 표시되도록 수정했습니다. - 접근성 향상을 위해 `contentDescription`에 `null` 대신 `PLACE_MAP` 탭에 정의된 설명을 사용하도록 변경했습니다. --- .../main/component/FestabookBottomNavigationBar.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt b/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt index 47dcfcbd..b352be96 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt @@ -126,10 +126,10 @@ private fun PlaceMapNavigationItem( onClick(FestabookMainTab.PLACE_MAP) }, indication = null, - interactionSource = null, + interactionSource = remember { MutableInteractionSource() }, ), painter = painterResource(id = R.drawable.btn_fab_manu), - contentDescription = null, + contentDescription = FestabookMainTab.PLACE_MAP.contentDescription, ) } From 5d62e39c7732e5e1667282023a57a18f9f0834aa Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Fri, 16 Jan 2026 21:49:52 +0900 Subject: [PATCH 06/10] =?UTF-8?q?refactor(Main):=20=EB=B0=94=ED=85=80?= =?UTF-8?q?=EB=82=B4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B0=80?= =?UTF-8?q?=EC=8B=9C=EC=84=B1=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FestabookBottomNavigationBar`의 플로팅 액션 버튼(FAB)의 접근성과 사용자 인터랙션을 개선했습니다. - **`FestabookBottomNavigationBar.kt` 수정:** - `interactionSource`에 `null` 대신 `MutableInteractionSource`를 명시적으로 전달하여 클릭 시 시각적 피드백(리플 효과)이 표시되도록 수정했습니다. - 접근성 향상을 위해 `contentDescription`에 `null` 대신 `PLACE_MAP` 탭에 정의된 설명을 사용하도록 변경했습니다. --- .../presentation/main/component/MainScreen.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/component/MainScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/main/component/MainScreen.kt index a5df00f1..8c3c7869 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/component/MainScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/component/MainScreen.kt @@ -20,10 +20,12 @@ fun MainScreen( Scaffold( // TODO: 스낵바 구현 및 하위 프래그먼트에 해당 SnackBar 적용 bottomBar = { - FestabookBottomNavigationBar( - currentTab = navigator.currentTab, - onTabSelect = { navigator.navigateToMainTab(it.route) }, - ) + if (navigator.shouldShowBottomBar) { + FestabookBottomNavigationBar( + currentTab = navigator.currentTab, + onTabSelect = { navigator.navigateToMainTab(it.route) }, + ) + } }, modifier = modifier, ) { innerPadding -> From 9da1eb029a9575358e31e0d27224e034209079e5 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Mon, 19 Jan 2026 16:45:24 +0900 Subject: [PATCH 07/10] =?UTF-8?q?refactor(Main):=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=EC=84=B1=20=EA=B0=95=ED=99=94=EB=A5=BC=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?contentDescription=20=EC=86=8D=EC=84=B1=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 하단 네비게이션 바 컴포넌트의 접근성을 개선했습니다. - **`FestabookMainTab.kt` 수정:** - 각 탭에 하드코딩되어 있던 `contentDescription` 문자열 속성을 제거했습니다. - **`FestabookBottomNavigationBar.kt` 수정:** - 각 탭 아이콘과 플로팅 액션 버튼(FAB)의 `contentDescription`에 기존 문자열 대신 `stringResource`를 사용하도록 변경했습니다. 이를 통해 다국어 지원 및 접근성 관리가 용이해졌습니다. --- .../festabook/presentation/main/FestabookMainTab.kt | 10 ++-------- .../main/component/FestabookBottomNavigationBar.kt | 4 ++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt index 368e9602..4d5f8e5c 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt @@ -8,39 +8,33 @@ import com.daedan.festabook.R import com.daedan.festabook.presentation.theme.FestabookColor enum class FestabookMainTab( - @DrawableRes val iconResId: Int, - @StringRes val labelResId: Int, - val contentDescription: String, + @param:DrawableRes val iconResId: Int, + @param:StringRes val labelResId: Int, val route: FestabookMainRoute, ) { HOME( iconResId = R.drawable.ic_home, labelResId = R.string.menu_home_title, - contentDescription = "홈", route = FestabookMainRoute.Home, ), SCHEDULE( iconResId = R.drawable.ic_schedule, labelResId = R.string.menu_schedule_title, - contentDescription = "일정", route = FestabookMainRoute.Schedule, ), PLACE_MAP( iconResId = R.drawable.ic_map, labelResId = R.string.menu_map_title, - contentDescription = "지도", route = FestabookMainRoute.PlaceMap, ), NEWS( iconResId = R.drawable.ic_news, labelResId = R.string.menu_news_title, - contentDescription = "뉴스", route = FestabookMainRoute.News, ), SETTING( iconResId = R.drawable.ic_setting, labelResId = R.string.menu_setting_title, - contentDescription = "설정", route = FestabookMainRoute.Setting, ), ; diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt b/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt index b352be96..740e5aa0 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/component/FestabookBottomNavigationBar.kt @@ -99,7 +99,7 @@ private fun RowScope.FestabookNavigationItem( ) { Icon( painter = painterResource(id = tab.iconResId), - contentDescription = tab.contentDescription, + contentDescription = stringResource(tab.labelResId), tint = if (selected) selectedColor else unselectedColor, ) Spacer(modifier = Modifier.height(festabookSpacing.paddingBody1)) @@ -129,7 +129,7 @@ private fun PlaceMapNavigationItem( interactionSource = remember { MutableInteractionSource() }, ), painter = painterResource(id = R.drawable.btn_fab_manu), - contentDescription = FestabookMainTab.PLACE_MAP.contentDescription, + contentDescription = stringResource(FestabookMainTab.PLACE_MAP.labelResId), ) } From b99cdadf64c148e85700eb16330cf7c4117b098f Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Mon, 19 Jan 2026 16:50:41 +0900 Subject: [PATCH 08/10] =?UTF-8?q?refactor(Main):=20`FestabookNavigator`?= =?UTF-8?q?=EC=9D=98=20=EC=A4=91=EB=B3=B5=EB=90=9C=20boolean=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FestabookNavigator`에서 `currentDestination`이 `null`일 경우 발생할 수 있는 잠재적 `NullPointerException`을 방지하기 위해 코드 안정성을 개선했습니다. - **`FestabookNavigator.kt` 수정:** - `currentTab`과 `shouldShowBottomBar`의 `get()` 메서드 내에서 `currentDestination?.hasRoute(...)`의 결과가 `null`일 경우 `false`를 반환하도록 엘비스 연산자(`?: false`)를 추가했습니다. --- .../daedan/festabook/presentation/main/FestabookNavigator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt index 3a3811ee..7db4b3ff 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt @@ -34,14 +34,14 @@ class FestabookNavigator( @Composable get() = FestabookMainTab.find { - currentDestination?.hasRoute(it::class) == true + currentDestination?.hasRoute(it::class) ?: false } val shouldShowBottomBar @Composable get() = FestabookMainTab.find { - currentDestination?.hasRoute(it::class) == true + currentDestination?.hasRoute(it::class) ?: false } != null val startRoute = FestabookMainRoute.Home // TODO: Splash와 Explore 연동 시 변경 From 4cf0aa2879d19f0ad11b8e619cc96c05154ae694 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Mon, 19 Jan 2026 16:57:12 +0900 Subject: [PATCH 09/10] =?UTF-8?q?refactor(Main):=20FestabookNavigator?= =?UTF-8?q?=EC=97=90=20popBackStack=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FestabookNavigator` 클래스에 `popBackStack` 메서드를 추가하여 외부에서 화면 스택을 조작할 수 있도록 기능을 확장했습니다. - **`FestabookNavigator.kt` 수정:** - `navController.popBackStack()`를 호출하는 `popBackStack()` 함수를 추가했습니다. - 특정 라우트까지 스택을 제거할 수 있도록 `route`와 `inclusive` 파라미터를 받는 오버로딩된 `popBackStack()` 함수를 추가했습니다. --- .../festabook/presentation/main/FestabookNavigator.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt index 7db4b3ff..ca2a96b0 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt @@ -62,6 +62,17 @@ class FestabookNavigator( navOptions, ) } + + fun popBackStack() { + navController.popBackStack() + } + + fun popBackStack( + route: FestabookRoute, + inclusive: Boolean = false, + ) { + navController.popBackStack(route, inclusive) + } } @Composable From 48a97e7a5fc324187fe0b22a0aee20706724d4a1 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Mon, 19 Jan 2026 16:58:25 +0900 Subject: [PATCH 10/10] =?UTF-8?q?refactor(main):=20FestabookMainRoute=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9D=84=20MainTabRoute=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FestabookMainRoute`라는 `sealed interface`의 이름을 `MainTabRoute`로 변경하여, 메인 화면의 탭 라우트를 나타내는 역할임을 더 명확하게 표현했습니다. 이 변경사항을 관련된 모든 파일에 적용했습니다. - **`FestabookRoute.kt` 수정:** - `FestabookMainRoute`를 `MainTabRoute`로 이름 변경했습니다. - **`FestabookMainTab.kt` 수정:** - 각 탭(`enum`)이 참조하는 라우트 타입을 `MainTabRoute`로 업데이트했습니다. - **`HomeNavigation.kt` 수정:** - `homeNavGraph`에서 사용하는 라우트 타입을 `MainTabRoute.Home`으로 변경했습니다. - **`FestabookNavigator.kt` 수정:** - `startRoute`의 타입을 `MainTabRoute.Home`으로 업데이트했습니다. --- .../presentation/home/navigation/HomeNavigation.kt | 4 ++-- .../festabook/presentation/main/FestabookMainTab.kt | 12 ++++++------ .../presentation/main/FestabookNavigator.kt | 2 +- .../festabook/presentation/main/FestabookRoute.kt | 12 ++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/home/navigation/HomeNavigation.kt b/app/src/main/java/com/daedan/festabook/presentation/home/navigation/HomeNavigation.kt index 49762f77..13bdf5fb 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/home/navigation/HomeNavigation.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/home/navigation/HomeNavigation.kt @@ -7,14 +7,14 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.daedan.festabook.presentation.home.HomeViewModel import com.daedan.festabook.presentation.home.component.HomeScreen -import com.daedan.festabook.presentation.main.FestabookMainRoute +import com.daedan.festabook.presentation.main.MainTabRoute fun NavGraphBuilder.homeNavGraph( padding: PaddingValues, viewModel: HomeViewModel, onNavigateToExplore: () -> Unit, ) { - composable { + composable { HomeScreen( modifier = Modifier.padding(padding), viewModel = viewModel, diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt index 4d5f8e5c..77ab8b65 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookMainTab.kt @@ -10,32 +10,32 @@ import com.daedan.festabook.presentation.theme.FestabookColor enum class FestabookMainTab( @param:DrawableRes val iconResId: Int, @param:StringRes val labelResId: Int, - val route: FestabookMainRoute, + val route: MainTabRoute, ) { HOME( iconResId = R.drawable.ic_home, labelResId = R.string.menu_home_title, - route = FestabookMainRoute.Home, + route = MainTabRoute.Home, ), SCHEDULE( iconResId = R.drawable.ic_schedule, labelResId = R.string.menu_schedule_title, - route = FestabookMainRoute.Schedule, + route = MainTabRoute.Schedule, ), PLACE_MAP( iconResId = R.drawable.ic_map, labelResId = R.string.menu_map_title, - route = FestabookMainRoute.PlaceMap, + route = MainTabRoute.PlaceMap, ), NEWS( iconResId = R.drawable.ic_news, labelResId = R.string.menu_news_title, - route = FestabookMainRoute.News, + route = MainTabRoute.News, ), SETTING( iconResId = R.drawable.ic_setting, labelResId = R.string.menu_setting_title, - route = FestabookMainRoute.Setting, + route = MainTabRoute.Setting, ), ; diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt index ca2a96b0..7e5101a7 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookNavigator.kt @@ -44,7 +44,7 @@ class FestabookNavigator( currentDestination?.hasRoute(it::class) ?: false } != null - val startRoute = FestabookMainRoute.Home // TODO: Splash와 Explore 연동 시 변경 + val startRoute = MainTabRoute.Home // TODO: Splash와 Explore 연동 시 변경 fun navigateToMainTab(route: FestabookRoute) { navController.navigate( diff --git a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookRoute.kt b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookRoute.kt index 8e6cf326..e9eac87f 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/main/FestabookRoute.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/main/FestabookRoute.kt @@ -16,19 +16,19 @@ sealed interface FestabookRoute { } @Serializable -sealed interface FestabookMainRoute : FestabookRoute { +sealed interface MainTabRoute : FestabookRoute { @Serializable - data object Home : FestabookMainRoute + data object Home : MainTabRoute @Serializable - data object Schedule : FestabookMainRoute + data object Schedule : MainTabRoute @Serializable - data object PlaceMap : FestabookMainRoute + data object PlaceMap : MainTabRoute @Serializable - data object News : FestabookMainRoute + data object News : MainTabRoute @Serializable - data object Setting : FestabookMainRoute + data object Setting : MainTabRoute }