Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.daedan.festabook.presentation.common.component

import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarData
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.daedan.festabook.R
import com.daedan.festabook.data.util.ApiResultException
import com.daedan.festabook.presentation.theme.FestabookColor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.reflect.KClass

@Composable
fun FestabookSnackbar(
data: SnackbarData,
modifier: Modifier = Modifier,
) {
Snackbar(
modifier = modifier,
snackbarData = data,
actionColor = FestabookColor.accentBlue,
)
}

class SnackbarManager(
val hostState: SnackbarHostState,
val scope: CoroutineScope,
private val actionLabel: String,
private val errorMessages: Map<KClass<out ApiResultException>, String>,
private val defaultErrorMessage: String,
) {
fun show(message: String) {
hostState.currentSnackbarData?.dismiss()
scope.launch {
hostState.showSnackbar(
message = message,
duration = SnackbarDuration.Short,
actionLabel = actionLabel,
)
}
}

fun showError(throwable: Throwable) {
val message = errorMessages[throwable::class] ?: defaultErrorMessage
show(message)
}
}

@Composable
fun rememberAppSnackbarManager(
snackbarHostState: SnackbarHostState,
scope: CoroutineScope = rememberCoroutineScope(),
): SnackbarManager {
val clientErrorMessage = stringResource(R.string.error_client_exception)
val serverErrorMessage = stringResource(R.string.error_server_exception)
val networkErrorMessage = stringResource(R.string.error_network_exception)
val unknownErrorMessage = stringResource(R.string.error_unknown_exception)
val actionLabel = stringResource(R.string.fail_snackbar_confirm)

val errorMessages =
remember {
mapOf(
ApiResultException.ClientException::class to clientErrorMessage,
ApiResultException.ServerException::class to serverErrorMessage,
ApiResultException.NetworkException::class to networkErrorMessage,
ApiResultException.UnknownException::class to unknownErrorMessage,
)
}

return remember(snackbarHostState, scope) {
SnackbarManager(snackbarHostState, scope, actionLabel, errorMessages, unknownErrorMessage)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
Expand All @@ -37,9 +39,22 @@ fun HomeScreen(
viewModel: HomeViewModel,
onNavigateToExplore: () -> Unit,
modifier: Modifier = Modifier,
onShowErrorSnackbar: (Throwable) -> Unit = {}, // TODO Fragment 제거 시 필수 파라미터로 변경
) {
val festivalUiState by viewModel.festivalUiState.collectAsStateWithLifecycle()
val lineupUiState by viewModel.lineupUiState.collectAsStateWithLifecycle()
val currentOnShowErrorSnackbar by rememberUpdatedState(onShowErrorSnackbar)
LaunchedEffect(festivalUiState) {
when (val state = festivalUiState) {
is FestivalUiState.Error -> {
currentOnShowErrorSnackbar(state.throwable)
}

else -> {
Unit
}
}
Comment on lines +47 to +56
Copy link
Contributor

Choose a reason for hiding this comment

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

when문을 사용하신 이유는 error 일때 Snackbar말고 다른 상태에 대한 snackbar도 사용할 경우도 있어서 확장성을 고려하신걸까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

더 확장해서 festivalUiState의 사이드 이펙트를 이 LaunchedEffect에서 작성하도록 하고 싶었습니다.

}

when (val state = festivalUiState) {
is FestivalUiState.Loading -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.daedan.festabook.presentation.main.component.FirstVisitDialog
fun NavGraphBuilder.homeNavGraph(
viewModel: HomeViewModel,
mainViewModel: MainViewModel,
onShowErrorSnackbar: (Throwable) -> Unit,
onSubscriptionConfirm: () -> Unit,
onNavigateToExplore: () -> Unit,
) {
Expand All @@ -26,6 +27,7 @@ fun NavGraphBuilder.homeNavGraph(
}
HomeScreen(
viewModel = viewModel,
onShowErrorSnackbar = onShowErrorSnackbar,
onNavigateToExplore = onNavigateToExplore,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@ package com.daedan.festabook.presentation.main.component
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import com.daedan.festabook.R
import com.daedan.festabook.logging.DefaultFirebaseLogger
import com.daedan.festabook.presentation.NotificationPermissionManager
import com.daedan.festabook.presentation.common.ObserveAsEvents
import com.daedan.festabook.presentation.common.component.FestabookSnackbar
import com.daedan.festabook.presentation.common.component.SnackbarManager
import com.daedan.festabook.presentation.common.component.rememberAppSnackbarManager
import com.daedan.festabook.presentation.home.HomeViewModel
import com.daedan.festabook.presentation.home.navigation.homeNavGraph
import com.daedan.festabook.presentation.main.FestabookMainTab
Expand Down Expand Up @@ -52,6 +60,10 @@ fun MainScreen(
settingViewModel: SettingViewModel = viewModel(),
) {
val navigator = rememberFestabookNavigator()
val snackbarHostState = remember { SnackbarHostState() }
val snackbarManager = rememberAppSnackbarManager(snackbarHostState)
val backPressExitMessage = stringResource(R.string.back_press_exit_message)
val noticeEnabledMessage = stringResource(R.string.setting_notice_enabled)
Comment on lines +63 to +66
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

사용되지 않는 변수 확인 필요

noticeEnabledMessage가 선언되었지만 이 파일 내에서 사용되지 않습니다. 향후 구현을 위해 의도적으로 추가한 것인지, 아니면 제거해야 하는지 확인해 주세요.

🔧 사용하지 않는 경우 제거 제안
 val snackbarHostState = remember { SnackbarHostState() }
 val snackbarManager = rememberAppSnackbarManager(snackbarHostState)
 val backPressExitMessage = stringResource(R.string.back_press_exit_message)
-val noticeEnabledMessage = stringResource(R.string.setting_notice_enabled)
📝 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
val snackbarHostState = remember { SnackbarHostState() }
val snackbarManager = rememberAppSnackbarManager(snackbarHostState)
val backPressExitMessage = stringResource(R.string.back_press_exit_message)
val noticeEnabledMessage = stringResource(R.string.setting_notice_enabled)
val snackbarHostState = remember { SnackbarHostState() }
val snackbarManager = rememberAppSnackbarManager(snackbarHostState)
val backPressExitMessage = stringResource(R.string.back_press_exit_message)
🤖 Prompt for AI Agents
In
`@app/src/main/java/com/daedan/festabook/presentation/main/component/MainScreen.kt`
around lines 63 - 66, The local val noticeEnabledMessage declared via
noticeEnabledMessage = stringResource(R.string.setting_notice_enabled) inside
the MainScreen composable is unused; either remove that declaration (and any
now-unneeded imports) or actually use it where intended (e.g., pass it to a
Snackbar, dialog, or UI text) — if it’s meant as a placeholder for future work,
replace the declaration with a short TODO comment referencing its intended use;
locate the val noticeEnabledMessage in MainScreen.kt to apply the change.


ObserveAsEvents(flow = mainViewModel.navigateNewsEvent) {
navigator.navigateToMainTab(FestabookMainTab.NEWS)
Expand All @@ -60,23 +72,22 @@ fun MainScreen(
if (isDoublePress) {
onAppFinish()
} else {
// TODO: SnackBarHost로 변경
// showToast(getString(R.string.back_press_exit_message))
snackbarManager.show(backPressExitMessage)
}
}
ObserveAsEvents(flow = homeViewModel.navigateToScheduleEvent) {
navigator.navigateToMainTab(FestabookMainTab.SCHEDULE)
}
ObserveAsEvents(flow = settingViewModel.success) {
// TODO: SnackBarHost로 변경
// showSnackBar(getString(R.string.setting_notice_enabled))
}

BackHandler {
mainViewModel.onBackPressed()
}
Scaffold(
// TODO: 스낵바 구현 및 하위 프래그먼트에 해당 SnackBar 적용
snackbarHost = {
SnackbarHost(hostState = snackbarHostState) { data ->
FestabookSnackbar(data)
}
},
bottomBar = {
if (navigator.shouldShowBottomBar) {
FestabookBottomNavigationBar(
Expand Down Expand Up @@ -124,6 +135,7 @@ fun MainScreen(
placeMapViewModel = placeMapViewModel,
locationSource = locationSource,
logger = logger,
onShowErrorSnackBar = snackbarManager::showError,
onStartPlaceDetail = {
navigator.navigate(
FestabookRoute.PlaceDetail(
Expand All @@ -144,6 +156,7 @@ fun MainScreen(
notificationPermissionManager = notificationPermissionManager,
onNavigateToExplore = onNavigateToExplore,
onSubscriptionConfirm = onSubscriptionConfirm,
snackbarManager = snackbarManager,
)
}
}
Expand All @@ -160,6 +173,7 @@ private fun FestabookNavHost(
notificationPermissionManager: NotificationPermissionManager,
onNavigateToExplore: () -> Unit,
onSubscriptionConfirm: () -> Unit,
snackbarManager: SnackbarManager,
modifier: Modifier = Modifier,
) {
NavHost(
Expand All @@ -172,23 +186,27 @@ private fun FestabookNavHost(
mainViewModel = mainViewModel,
onNavigateToExplore = onNavigateToExplore,
onSubscriptionConfirm = onSubscriptionConfirm,
onShowErrorSnackbar = snackbarManager::showError,
)
scheduleNavGraph(
viewModel = scheduleViewModel,
onShowErrorSnackbar = snackbarManager::showError,
)
placeMapNavGraph(
placeDetailViewModelFactory = placeDetailViewModelFactory,
onBackToPreviousClick = { navigator.popBackStack() },
onShowErrorSnackbar = snackbarManager::showError,
)
newsNavGraph(
viewModel = newsViewModel,
onShowErrorSnackbar = snackbarManager::showError,
)
settingNavGraph(
homeViewModel = homeViewModel,
settingViewModel = settingViewModel,
notificationPermissionManager = notificationPermissionManager,
onShowSnackBar = { },
onShowErrorSnackBar = { },
onShowSnackBar = snackbarManager::show,
onShowErrorSnackBar = snackbarManager::showError,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,74 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.daedan.festabook.R
import com.daedan.festabook.presentation.common.component.FestabookTopAppBar
import com.daedan.festabook.presentation.news.NewsTab
import com.daedan.festabook.presentation.news.NewsViewModel
import com.daedan.festabook.presentation.news.faq.FAQUiState
import com.daedan.festabook.presentation.news.lost.LostUiState
import com.daedan.festabook.presentation.news.notice.NoticeUiState

@Composable
fun NewsScreen(
newsViewModel: NewsViewModel,
modifier: Modifier = Modifier,
onShowErrorSnackbar: (Throwable) -> Unit = {}, // TODO Fragment 제거 시 필수 파라미터로 변경
) {
val pageState = rememberPagerState { NewsTab.entries.size }
val scope = rememberCoroutineScope()

val noticeUiState by newsViewModel.noticeUiState.collectAsStateWithLifecycle()
val lostUiState by newsViewModel.lostUiState.collectAsStateWithLifecycle()
val faqUiState by newsViewModel.faqUiState.collectAsStateWithLifecycle()
val currentOnShowErrorSnackbar by rememberUpdatedState(onShowErrorSnackbar)

val isNoticeRefreshing = noticeUiState is NoticeUiState.Refreshing
val isLostItemRefreshing = lostUiState is LostUiState.Refreshing

LaunchedEffect(noticeUiState) {
if (noticeUiState is NoticeUiState.Success) {
pageState.animateScrollToPage(NewsTab.NOTICE.ordinal)
when (val uiState = noticeUiState) {
is NoticeUiState.Success -> {
pageState.animateScrollToPage(NewsTab.NOTICE.ordinal)
}

is NoticeUiState.Error -> {
currentOnShowErrorSnackbar(uiState.throwable)
}

else -> {
Unit
}
}
}
LaunchedEffect(lostUiState) {
when (val uiState = lostUiState) {
is LostUiState.Error -> {
currentOnShowErrorSnackbar(uiState.throwable)
}

else -> {
Unit
}
}
}

LaunchedEffect(faqUiState) {
when (val uiState = faqUiState) {
is FAQUiState.Error -> {
currentOnShowErrorSnackbar(uiState.throwable)
}

else -> {
Unit
}
}
}

Scaffold(
topBar = { FestabookTopAppBar(title = stringResource(R.string.news_title)) },
modifier = modifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import com.daedan.festabook.presentation.main.MainTabRoute
import com.daedan.festabook.presentation.news.NewsViewModel
import com.daedan.festabook.presentation.news.component.NewsScreen

fun NavGraphBuilder.newsNavGraph(viewModel: NewsViewModel) {
fun NavGraphBuilder.newsNavGraph(
viewModel: NewsViewModel,
onShowErrorSnackbar: (Throwable) -> Unit,
) {
composable<MainTabRoute.News> {
NewsScreen(
newsViewModel = viewModel,
onShowErrorSnackbar = onShowErrorSnackbar,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,15 @@ import com.daedan.festabook.presentation.theme.festabookSpacing
fun PlaceDetailRoute(
viewModel: PlaceDetailViewModel,
onBackToPreviousClick: () -> Unit,
onShowErrorSnackbar: (Throwable) -> Unit,
modifier: Modifier = Modifier,
) {
val placeDetailUiState by viewModel.placeDetail.collectAsStateWithLifecycle()
PlaceDetailScreen(
modifier = modifier,
uiState = placeDetailUiState,
onBackToPreviousClick = onBackToPreviousClick,
onShowErrorSnackbar = onShowErrorSnackbar,
)
}

Expand All @@ -85,13 +87,28 @@ fun PlaceDetailScreen(
uiState: PlaceDetailUiState,
onBackToPreviousClick: () -> Unit,
modifier: Modifier = Modifier,
onShowErrorSnackbar: (Throwable) -> Unit = {}, // TODO Fragment 제거 시 필수 파라미터로 변경
) {
val scrollState = rememberScrollState()
val currentOnShowErrorSnackbar by rememberUpdatedState(onShowErrorSnackbar)
var isDialogOpen by remember { mutableStateOf(false) }

BackHandler(enabled = !isDialogOpen) {
onBackToPreviousClick()
}

LaunchedEffect(uiState) {
when (uiState) {
is PlaceDetailUiState.Error -> {
currentOnShowErrorSnackbar(uiState.throwable)
}

else -> {
Unit
}
}
}

when (uiState) {
is PlaceDetailUiState.Success -> {
val pagerState =
Expand Down Expand Up @@ -410,6 +427,7 @@ private fun PlaceDetailScreenPreview() {
FestabookTheme {
PlaceDetailScreen(
onBackToPreviousClick = {},
onShowErrorSnackbar = {},
uiState =
PlaceDetailUiState.Success(
placeDetail =
Expand Down
Loading