From 9d91c717c8de7144bb9b58a225d5d61c9019ed29 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 28 Jan 2026 15:30:44 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat(main):=20=EC=8A=A4=EB=82=B5=EB=B0=94?= =?UTF-8?q?=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20`MainScreen`=EC=97=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존에 `Toast`로 처리되거나 주석 처리되어 있던 사용자 알림 기능을 `Snackbar` 기반으로 통합하고, 이를 중앙에서 관리할 수 있는 `SnackbarManager`를 도입했습니다. 이로 인해 앱 전체에서 일관된 스타일의 알림을 제공하고, 에러 처리 로직을 개선했습니다. - **`common/component/SnackBar.kt` 추가:** - `FestabookSnackbar` 컴포저블을 추가하여 앱의 디자인 시스템에 맞는 스낵바를 구현했습니다. - `SnackbarManager` 클래스를 도입하여 스낵바 메시지 표시(`show`) 및 에러 메시지 처리(`showError`) 로직을 중앙에서 관리하도록 했습니다. - `rememberAppSnackbarManager` 함수를 추가하여 `SnackbarManager`를 Compose 환경에서 쉽게 생성하고 재사용할 수 있도록 했습니다. - **`main/component/MainScreen.kt` 수정:** - `Scaffold`에 `SnackbarHost`를 추가하고, `FestabookSnackbar`를 사용하도록 설정했습니다. - `rememberAppSnackbarManager`를 이용해 `SnackbarManager` 인스턴스를 생성했습니다. - 기존 `Toast` 알림(뒤로가기 종료 알림)과 주석 처리되었던 알림(알림 설정 성공)을 `snackbarManager.show()` 호출로 대체했습니다. - `homeNavGraph`에 `snackbarManager`를 전달하여 하위 화면(Home, Setting 등)에서 스낵바를 사용할 수 있도록 `onShowSnackBar`와 `onShowErrorSnackBar` 콜백을 구현했습니다. --- .../presentation/common/component/SnackBar.kt | 79 +++++++++++++++++++ .../presentation/main/component/MainScreen.kt | 30 +++++-- 2 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/common/component/SnackBar.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/common/component/SnackBar.kt b/app/src/main/java/com/daedan/festabook/presentation/common/component/SnackBar.kt new file mode 100644 index 00000000..c0287395 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/common/component/SnackBar.kt @@ -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, String>, + private val defaultErrorMessage: String, +) { + fun show(message: String) { + scope.launch { + hostState.showSnackbar( + message = message, + withDismissAction = true, + duration = SnackbarDuration.Short, + actionLabel = actionLabel, + ) + } + } + + fun showError(throwable: Throwable) { + val message = errorMessages[throwable::class] ?: throwable.message ?: 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) + } +} 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 952c828f..7f9327dd 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 @@ -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 @@ -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) ObserveAsEvents(flow = mainViewModel.navigateNewsEvent) { navigator.navigateToMainTab(FestabookMainTab.NEWS) @@ -60,23 +72,25 @@ 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)) + snackbarManager.show(noticeEnabledMessage) } BackHandler { mainViewModel.onBackPressed() } Scaffold( - // TODO: 스낵바 구현 및 하위 프래그먼트에 해당 SnackBar 적용 + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) { data -> + FestabookSnackbar(data) + } + }, bottomBar = { if (navigator.shouldShowBottomBar) { FestabookBottomNavigationBar( @@ -144,6 +158,7 @@ fun MainScreen( notificationPermissionManager = notificationPermissionManager, onNavigateToExplore = onNavigateToExplore, onSubscriptionConfirm = onSubscriptionConfirm, + snackbarManager = snackbarManager, ) } } @@ -160,6 +175,7 @@ private fun FestabookNavHost( notificationPermissionManager: NotificationPermissionManager, onNavigateToExplore: () -> Unit, onSubscriptionConfirm: () -> Unit, + snackbarManager: SnackbarManager, modifier: Modifier = Modifier, ) { NavHost( @@ -187,8 +203,8 @@ private fun FestabookNavHost( homeViewModel = homeViewModel, settingViewModel = settingViewModel, notificationPermissionManager = notificationPermissionManager, - onShowSnackBar = { }, - onShowErrorSnackBar = { }, + onShowSnackBar = snackbarManager::show, + onShowErrorSnackBar = snackbarManager::showError, ) } } From 7da6f47d2f76156de6e449829ef0abc15976d499 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 28 Jan 2026 15:48:14 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat(PlaceDetail):=20=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=20=EC=8B=9C=20=EC=8A=A4=EB=82=B5=EB=B0=94=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 장소 상세 정보를 불러오는 데 실패했을 때 사용자에게 스낵바를 통해 에러를 알려주는 기능을 추가했습니다. - **`PlaceDetailScreen.kt` 수정:** - `PlaceDetailRoute`와 `PlaceDetailScreen`에 `onShowErrorSnackbar` 콜백 파라미터를 추가했습니다. - `LaunchedEffect`를 사용하여 `placeDetailUiState`가 `Error` 상태일 때, `onShowErrorSnackbar` 콜백을 호출하도록 구현했습니다. - **`PlaceMapNavigation.kt` 및 `MainScreen.kt` 수정:** - `placeMapNavGraph`를 통해 `MainScreen`의 `snackbarManager::showError` 함수를 `PlaceDetailRoute`까지 전달하여 스낵바가 표시되도록 연결했습니다. --- .../presentation/main/component/MainScreen.kt | 1 + .../placeDetail/component/PlaceDetailScreen.kt | 18 ++++++++++++++++++ .../placeMap/navigation/PlaceMapNavigation.kt | 4 ++-- 3 files changed, 21 insertions(+), 2 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 7f9327dd..7b2430ab 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 @@ -195,6 +195,7 @@ private fun FestabookNavHost( placeMapNavGraph( placeDetailViewModelFactory = placeDetailViewModelFactory, onBackToPreviousClick = { navigator.popBackStack() }, + onShowErrorSnackbar = snackbarManager::showError, ) newsNavGraph( viewModel = newsViewModel, diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt index 9ec75b47..0db4d71f 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt @@ -70,6 +70,7 @@ 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() @@ -77,21 +78,37 @@ fun PlaceDetailRoute( modifier = modifier, uiState = placeDetailUiState, onBackToPreviousClick = onBackToPreviousClick, + onShowErrorSnackbar = onShowErrorSnackbar, ) } @Composable fun PlaceDetailScreen( uiState: PlaceDetailUiState, + onShowErrorSnackbar: (Throwable) -> Unit, onBackToPreviousClick: () -> Unit, modifier: Modifier = Modifier, ) { 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 = @@ -410,6 +427,7 @@ private fun PlaceDetailScreenPreview() { FestabookTheme { PlaceDetailScreen( onBackToPreviousClick = {}, + onShowErrorSnackbar = {}, uiState = PlaceDetailUiState.Success( placeDetail = diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/navigation/PlaceMapNavigation.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/navigation/PlaceMapNavigation.kt index ac9b9a7b..fac91a7b 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/navigation/PlaceMapNavigation.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/navigation/PlaceMapNavigation.kt @@ -26,6 +26,7 @@ import kotlin.reflect.typeOf fun NavGraphBuilder.placeMapNavGraph( onBackToPreviousClick: () -> Unit, placeDetailViewModelFactory: PlaceDetailViewModel.Factory, + onShowErrorSnackbar: (Throwable) -> Unit, ) { composable { } @@ -56,13 +57,12 @@ fun NavGraphBuilder.placeMapNavGraph( PlaceDetailRoute( modifier = Modifier.graphicsLayer( - // compositingStrategy를 사용해 자식들을 하나의 레이어로 강제 통합 compositingStrategy = CompositingStrategy.Offscreen, - // 하드웨어 가속을 통해 애니메이션 도중 레이어가 쪼개지는 것을 방지 clip = true, ), viewModel = viewModel, onBackToPreviousClick = onBackToPreviousClick, + onShowErrorSnackbar = onShowErrorSnackbar, ) } } From 77e1d8f8d02e37c4f5377184aa1e9b89ff7bbf4e Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 28 Jan 2026 16:08:17 +0900 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20Composable=EC=9D=98=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=8A=A4=EB=82=B5?= =?UTF-8?q?=EB=B0=94=EB=A1=9C=20=ED=91=9C=EC=8B=9C=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 각 화면(`Home`, `Schedule`, `News`)의 ViewModel에서 발생하는 `Error` 상태를 감지하여 스낵바로 사용자에게 에러 메시지를 표시하는 기능을 추가했습니다. `MainScreen`에서 스낵바 표시 로직을 중앙에서 관리하고, 각 화면에는 에러 발생 시 호출할 콜백(`onShowErrorSnackbar`)을 전달하는 방식으로 구현했습니다. - **`MainScreen.kt` 수정:** - `homeNavGraph`, `scheduleNavGraph`, `newsNavGraph`에 `snackbarManager::showError`를 `onShowErrorSnackbar` 콜백으로 전달하여 에러 처리를 위임했습니다. - **`HomeScreen.kt`, `ScheduleScreen.kt`, `NewsScreen.kt` 수정:** - `onShowErrorSnackbar: (Throwable) -> Unit` 파라미터를 추가했습니다. - `LaunchedEffect`를 사용하여 각 ViewModel의 UI 상태(`festivalUiState`, `scheduleUiState`, `noticeUiState` 등)가 `Error`일 경우, `onShowErrorSnackbar` 콜백을 호출하도록 구현했습니다. - `rememberUpdatedState`를 사용하여 항상 최신의 콜백을 참조하도록 하여 안정성을 높였습니다. - **관련 `NavGraph` 파일들 수정:** - `homeNavGraph`, `scheduleNavGraph`, `newsNavGraph` 함수에 `onShowErrorSnackbar` 파라미터를 추가하고, 이를 각 화면 컴포저블로 전달하도록 수정했습니다. --- .../presentation/home/component/HomeScreen.kt | 15 +++++++ .../home/navigation/HomeNavigation.kt | 2 + .../presentation/main/component/MainScreen.kt | 3 ++ .../presentation/news/component/NewsScreen.kt | 42 ++++++++++++++++++- .../news/navigation/NewsNavigation.kt | 6 ++- .../component/PlaceDetailScreen.kt | 2 +- .../schedule/component/ScheduleScreen.kt | 15 +++++++ .../schedule/navigation/ScheduleNavigation.kt | 6 ++- 8 files changed, 86 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/home/component/HomeScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/home/component/HomeScreen.kt index 0214a9c3..73d6a093 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/home/component/HomeScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/home/component/HomeScreen.kt @@ -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 @@ -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 + } + } + } when (val state = festivalUiState) { is FestivalUiState.Loading -> { 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 588682c5..5d97d185 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 @@ -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, ) { @@ -26,6 +27,7 @@ fun NavGraphBuilder.homeNavGraph( } HomeScreen( viewModel = viewModel, + onShowErrorSnackbar = onShowErrorSnackbar, onNavigateToExplore = onNavigateToExplore, ) } 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 7b2430ab..200c27fb 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 @@ -188,9 +188,11 @@ private fun FestabookNavHost( mainViewModel = mainViewModel, onNavigateToExplore = onNavigateToExplore, onSubscriptionConfirm = onSubscriptionConfirm, + onShowErrorSnackbar = snackbarManager::showError, ) scheduleNavGraph( viewModel = scheduleViewModel, + onShowErrorSnackbar = snackbarManager::showError, ) placeMapNavGraph( placeDetailViewModelFactory = placeDetailViewModelFactory, @@ -199,6 +201,7 @@ private fun FestabookNavHost( ) newsNavGraph( viewModel = newsViewModel, + onShowErrorSnackbar = snackbarManager::showError, ) settingNavGraph( homeViewModel = homeViewModel, diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt index e420ba76..fe382d64 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/component/NewsScreen.kt @@ -9,6 +9,7 @@ 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 @@ -16,6 +17,7 @@ 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 @@ -23,6 +25,7 @@ import com.daedan.festabook.presentation.news.notice.NoticeUiState fun NewsScreen( newsViewModel: NewsViewModel, modifier: Modifier = Modifier, + onShowErrorSnackbar: (Throwable) -> Unit = {}, // TODO Fragment 제거 시 필수 파라미터로 변경 ) { val pageState = rememberPagerState { NewsTab.entries.size } val scope = rememberCoroutineScope() @@ -30,15 +33,50 @@ fun NewsScreen( 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, diff --git a/app/src/main/java/com/daedan/festabook/presentation/news/navigation/NewsNavigation.kt b/app/src/main/java/com/daedan/festabook/presentation/news/navigation/NewsNavigation.kt index 7e286e41..ee34b28f 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/news/navigation/NewsNavigation.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/news/navigation/NewsNavigation.kt @@ -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 { NewsScreen( newsViewModel = viewModel, + onShowErrorSnackbar = onShowErrorSnackbar, ) } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt index 0db4d71f..e4250823 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt @@ -85,9 +85,9 @@ fun PlaceDetailRoute( @Composable fun PlaceDetailScreen( uiState: PlaceDetailUiState, - onShowErrorSnackbar: (Throwable) -> Unit, onBackToPreviousClick: () -> Unit, modifier: Modifier = Modifier, + onShowErrorSnackbar: (Throwable) -> Unit = {}, // TODO Fragment 제거 시 필수 파라미터로 변경 ) { val scrollState = rememberScrollState() val currentOnShowErrorSnackbar by rememberUpdatedState(onShowErrorSnackbar) diff --git a/app/src/main/java/com/daedan/festabook/presentation/schedule/component/ScheduleScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/schedule/component/ScheduleScreen.kt index adeae6fa..588a2322 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/schedule/component/ScheduleScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/schedule/component/ScheduleScreen.kt @@ -11,6 +11,7 @@ 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.compose.ui.unit.dp @@ -29,8 +30,10 @@ import timber.log.Timber fun ScheduleScreen( scheduleViewModel: ScheduleViewModel, modifier: Modifier = Modifier, + onShowErrorSnackbar: (Throwable) -> Unit = {}, // TODO Fragment 제거 시 필수 파라미터로 변경 ) { val scheduleUiState by scheduleViewModel.scheduleUiState.collectAsStateWithLifecycle() + val currentOnShowErrorSnackbar by rememberUpdatedState(onShowErrorSnackbar) val currentState = when (scheduleUiState) { is ScheduleUiState.Refreshing -> (scheduleUiState as ScheduleUiState.Refreshing).lastSuccessState @@ -38,6 +41,18 @@ fun ScheduleScreen( else -> scheduleUiState } + LaunchedEffect(currentState) { + when (currentState) { + is ScheduleUiState.Error -> { + currentOnShowErrorSnackbar(currentState.throwable) + } + + else -> { + Unit + } + } + } + Scaffold( topBar = { FestabookTopAppBar(title = stringResource(R.string.schedule_title)) }, modifier = modifier, diff --git a/app/src/main/java/com/daedan/festabook/presentation/schedule/navigation/ScheduleNavigation.kt b/app/src/main/java/com/daedan/festabook/presentation/schedule/navigation/ScheduleNavigation.kt index 1d011fdb..6caad10f 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/schedule/navigation/ScheduleNavigation.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/schedule/navigation/ScheduleNavigation.kt @@ -6,10 +6,14 @@ import com.daedan.festabook.presentation.main.MainTabRoute import com.daedan.festabook.presentation.schedule.ScheduleViewModel import com.daedan.festabook.presentation.schedule.component.ScheduleScreen -fun NavGraphBuilder.scheduleNavGraph(viewModel: ScheduleViewModel) { +fun NavGraphBuilder.scheduleNavGraph( + viewModel: ScheduleViewModel, + onShowErrorSnackbar: (Throwable) -> Unit, +) { composable { ScheduleScreen( scheduleViewModel = viewModel, + onShowErrorSnackbar = onShowErrorSnackbar, ) } } From 3aae98fd12eed2f52ed1dd3c336839ab3003e937 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 28 Jan 2026 16:25:39 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor(SnackBar):=20=EC=8A=A4=EB=82=B5?= =?UTF-8?q?=EB=B0=94=20=EC=A4=91=EB=B3=B5=20=ED=91=9C=EC=8B=9C=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20=EB=B0=8F=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 스낵바가 표시된 상태에서 새로운 스낵바가 호출될 경우, 이전 스낵바를 즉시 닫고 새로운 스낵바가 표시되도록 `show` 함수의 동작을 수정했습니다. 또한, 불필요한 `Dismiss` 버튼을 제거하여 UI를 간소화했습니다. - **`SnackBar.kt` 수정:** - `show` 함수 호출 시, `hostState.currentSnackbarData?.dismiss()`를 먼저 실행하여 현재 표시 중인 스낵바를 닫는 로직을 추가했습니다. - `showSnackbar`의 `withDismissAction` 파라미터를 제거하여, 스낵바에 별도의 '닫기' 버튼이 나타나지 않도록 변경했습니다. - **`MainScreen.kt` 수정:** - 설정 화면에서 알림 활성화 성공 시 스낵바를 표시하던 `ObserveAsEvents` 로직을 제거했습니다. 이는 새로운 정책에 따라 더 이상 필요하지 않은 기능입니다. --- .../daedan/festabook/presentation/common/component/SnackBar.kt | 2 +- .../daedan/festabook/presentation/main/component/MainScreen.kt | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/common/component/SnackBar.kt b/app/src/main/java/com/daedan/festabook/presentation/common/component/SnackBar.kt index c0287395..07a4cb89 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/common/component/SnackBar.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/common/component/SnackBar.kt @@ -36,10 +36,10 @@ class SnackbarManager( private val defaultErrorMessage: String, ) { fun show(message: String) { + hostState.currentSnackbarData?.dismiss() scope.launch { hostState.showSnackbar( message = message, - withDismissAction = true, duration = SnackbarDuration.Short, actionLabel = actionLabel, ) 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 200c27fb..7e359a50 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 @@ -78,9 +78,6 @@ fun MainScreen( ObserveAsEvents(flow = homeViewModel.navigateToScheduleEvent) { navigator.navigateToMainTab(FestabookMainTab.SCHEDULE) } - ObserveAsEvents(flow = settingViewModel.success) { - snackbarManager.show(noticeEnabledMessage) - } BackHandler { mainViewModel.onBackPressed() From 3adc1d4b12b271b8b100e441c94bf244ce11b0f4 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 28 Jan 2026 16:51:28 +0900 Subject: [PATCH 5/6] =?UTF-8?q?refactor(PlaceMap):=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=98=A4=EB=A5=98=20=EB=B0=9C=EC=83=9D=20?= =?UTF-8?q?=EC=8B=9C=20=EC=8A=A4=EB=82=B5=EB=B0=94=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PlaceMapRoute`에서 발생하는 오류를 상위 컴포저블(`MainScreen`)로 전달하여 스낵바를 통해 사용자에게 표시할 수 있도록 구조를 개선했습니다. - **`PlaceMapScreen.kt` 수정:** - `PlaceMapRoute` 컴포저블에 `onShowErrorSnackBar: (Throwable) -> Unit` 콜백 파라미터를 추가했습니다. - `PlaceMapSideEffectHandler` 내에서 `ShowErrorSnackBar` 이펙트가 발생했을 때, `onShowErrorSnackBar` 콜백을 호출하여 `Throwable`을 전달하도록 수정했습니다. - **`MainScreen.kt` 수정:** - `PlaceMapRoute`를 호출할 때 `onShowErrorSnackBar` 파라미터에 `snackbarManager::showError`를 전달하여, 지도 화면에서 오류가 발생하면 `MainScreen`의 스낵바 매니저가 오류 메시지를 표시하도록 연결했습니다. --- .../daedan/festabook/presentation/main/component/MainScreen.kt | 1 + .../presentation/placeMap/component/PlaceMapScreen.kt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) 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 7e359a50..005c4c3e 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 @@ -135,6 +135,7 @@ fun MainScreen( placeMapViewModel = placeMapViewModel, locationSource = locationSource, logger = logger, + onShowErrorSnackBar = snackbarManager::showError, onStartPlaceDetail = { navigator.navigate( FestabookRoute.PlaceDetail( diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt index 0608f076..c2b70acc 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt @@ -54,6 +54,7 @@ import timber.log.Timber fun PlaceMapRoute( placeMapViewModel: PlaceMapViewModel, onStartPlaceDetail: (PlaceMapSideEffect.StartPlaceDetail) -> Unit, + onShowErrorSnackBar: (Throwable) -> Unit, locationSource: FusedLocationSource, logger: DefaultFirebaseLogger, modifier: Modifier = Modifier, @@ -92,7 +93,7 @@ fun PlaceMapRoute( places = it.places, ) }, - onShowErrorSnackBar = { }, + onShowErrorSnackBar = { onShowErrorSnackBar(it.error.throwable) }, ) } From 15eaf8bf04ab04f5cdd9486829aa6beb4707afc8 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Fri, 30 Jan 2026 14:45:46 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix(common):=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EB=A9=94=EC=8B=9C=EC=A7=80=EA=B0=80=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=8A=A4?= =?UTF-8?q?=EB=82=B5=EB=B0=94=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FestabookSnackBar`의 `showError` 함수에서 오류 메시지를 결정하는 로직을 수정했습니다. 기존에는 `throwable.message`가 `null`일 경우에만 기본 오류 메시지(`defaultErrorMessage`)가 표시되었습니다. 이로 인해 `throwable.message`에 사용자에게 직접 보여주기 어려운 기술적인 내용이 담겨 있을 때, 해당 내용이 그대로 노출되는 문제가 있었습니다. - **`SnackBar.kt` 수정:** - `showError` 함수에서 `throwable.message`를 확인하는 부분을 제거했습니다. - 이제 미리 정의된 `errorMessages`에 해당하는 예외가 아닐 경우, 항상 `defaultErrorMessage`가 표시되도록 변경하여 사용자에게 일관되고 이해하기 쉬운 오류 피드백을 제공합니다. --- .../daedan/festabook/presentation/common/component/SnackBar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/common/component/SnackBar.kt b/app/src/main/java/com/daedan/festabook/presentation/common/component/SnackBar.kt index 07a4cb89..59caf1b3 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/common/component/SnackBar.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/common/component/SnackBar.kt @@ -47,7 +47,7 @@ class SnackbarManager( } fun showError(throwable: Throwable) { - val message = errorMessages[throwable::class] ?: throwable.message ?: defaultErrorMessage + val message = errorMessages[throwable::class] ?: defaultErrorMessage show(message) } }