Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.daedan.festabook.presentation.home.navigation
Copy link
Contributor

Choose a reason for hiding this comment

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

HomeNavigation과 FestabookNavigator의 패키지 위치를 지금처럼 설정한 이유가 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

navGraph의 경우 각 패키지 별로 설정이 다 다르기 때문에 한 곳에서 모아서 관리하는 것보다, 각 패키지 별로 관리하는 것이 더 가독성이 높다고 판단했습니다.


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.MainTabRoute

fun NavGraphBuilder.homeNavGraph(
padding: PaddingValues,
viewModel: HomeViewModel,
onNavigateToExplore: () -> Unit,
) {
composable<MainTabRoute.Home> {
HomeScreen(
modifier = Modifier.padding(padding),
viewModel = viewModel,
onNavigateToExplore = onNavigateToExplore,
)
}
}
Original file line number Diff line number Diff line change
@@ -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(
@param:DrawableRes val iconResId: Int,
@param:StringRes val labelResId: Int,
val route: MainTabRoute,
) {
HOME(
iconResId = R.drawable.ic_home,
labelResId = R.string.menu_home_title,
route = MainTabRoute.Home,
),
SCHEDULE(
iconResId = R.drawable.ic_schedule,
labelResId = R.string.menu_schedule_title,
route = MainTabRoute.Schedule,
),
PLACE_MAP(
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 메뉴는 MAP으로 네이밍해도 괜찮지 않을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

저는 패키지 네이밍을 따라가는 것이 더 직관적이라고 생각했습니다..!

iconResId = R.drawable.ic_map,
labelResId = R.string.menu_map_title,
route = MainTabRoute.PlaceMap,
),
NEWS(
iconResId = R.drawable.ic_news,
labelResId = R.string.menu_news_title,
route = MainTabRoute.News,
),
SETTING(
iconResId = R.drawable.ic_setting,
labelResId = R.string.menu_setting_title,
route = MainTabRoute.Setting,
),
;

companion object Defaults {
@Composable
fun find(predicate: @Composable (FestabookRoute) -> Boolean) =
entries.find {
predicate(it.route)
}

val selectedColor
@Composable
@ReadOnlyComposable
get() = FestabookColor.black

val unselectedColor
@Composable
@ReadOnlyComposable
get() = FestabookColor.gray400
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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
}
Comment on lines +24 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 네비게이션 옵션을 val 로 설정한 이유가 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

공통적으로 쓰일 확률이 높아 옵션을 재사용하기 위해 이렇게 구성했습니다.


val currentTab
@Composable
get() =
FestabookMainTab.find {
currentDestination?.hasRoute(it::class) ?: false
}

val shouldShowBottomBar
@Composable
get() =
FestabookMainTab.find {
currentDestination?.hasRoute(it::class) ?: false
} != null

val startRoute = MainTabRoute.Home // TODO: Splash와 Explore 연동 시 변경

fun navigateToMainTab(route: FestabookRoute) {
navController.navigate(
route,
defaultNavOptions,
)
}

fun navigate(
route: FestabookRoute,
navOptions: NavOptions? = null,
) {
navController.navigate(
route,
navOptions,
)
}

fun popBackStack() {
navController.popBackStack()
}

fun popBackStack(
route: FestabookRoute,
inclusive: Boolean = false,
) {
navController.popBackStack(route, inclusive)
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

뒤로가기 기능을 하는 함수도 만들어두면 좋을 것 같아요 ~

Copy link
Contributor Author

@oungsi2000 oungsi2000 Jan 19, 2026

Choose a reason for hiding this comment

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

@Composable
fun rememberFestabookNavigator(): FestabookNavigator {
val navController = rememberNavController()
return remember {
FestabookNavigator(navController = navController)
}
}
Comment on lines +78 to +84
Copy link
Contributor

Choose a reason for hiding this comment

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

함수로 따로 빼신 이유가 궁금합니다 !

Copy link
Contributor Author

Choose a reason for hiding this comment

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

navController와 생명주기를 동일하게 가져가도록 보장하고 싶었기 때문입니다 !

Original file line number Diff line number Diff line change
@@ -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 MainTabRoute : FestabookRoute {
@Serializable
data object Home : MainTabRoute

@Serializable
data object Schedule : MainTabRoute

@Serializable
data object PlaceMap : MainTabRoute

@Serializable
data object News : MainTabRoute

@Serializable
data object Setting : MainTabRoute
}
Original file line number Diff line number Diff line change
@@ -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 = stringResource(tab.labelResId),
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 = remember { MutableInteractionSource() },
),
painter = painterResource(id = R.drawable.btn_fab_manu),
contentDescription = stringResource(FestabookMainTab.PLACE_MAP.labelResId),
)
}

@Preview(showBackground = true)
@Composable
private fun FestabookBottomNavigationBarPreview() {
FestabookTheme {
var currentTabState by remember { mutableStateOf(FestabookMainTab.HOME) }
Scaffold(
bottomBar = {
FestabookBottomNavigationBar(
currentTab = currentTabState,
onTabSelect = { currentTabState = it },
)
},
) { it }
}
}
Loading