diff --git a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeScreen.kt b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeScreen.kt index 62b39a15b9..9b184faa75 100644 --- a/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeScreen.kt +++ b/feature/receiver/presentation/src/main/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeScreen.kt @@ -1,6 +1,5 @@ package com.afternote.feature.receiver.presentation.home -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,17 +11,18 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.afternote.core.ui.button.AfternoteButton @@ -51,6 +51,7 @@ fun ReceiverHomeScreen( actions: ReceiverHomeActions, modifier: Modifier = Modifier, ) { + val snackbarHostState = remember { SnackbarHostState() } Scaffold( modifier = modifier, containerColor = Color.Transparent, @@ -60,6 +61,7 @@ fun ReceiverHomeScreen( onSettingClick = actions.onSettingClick, ) }, + snackbarHost = { SnackbarHost(snackbarHostState) }, ) { innerPadding -> when (uiState) { ReceiverHomeUiState.Loading -> { } @@ -80,6 +82,7 @@ fun ReceiverHomeScreen( ) DownloadDialogHost( state = uiState.download, + snackbarHostState = snackbarHostState, onEvent = onEvent, ) } @@ -141,13 +144,20 @@ private fun SuccessContent( } } +/** + * 내려받기 확인 팝업 + 결과 처리. + * + * 실패 안내는 잠정 Snackbar 다 — 서버 작업 실패의 정본은 #446 공통 에러 팝업이라 컴포넌트가 + * 나오면 그쪽으로 재정렬된다 (#1391, #713 전례). `showSnackbar` 는 표출이 끝날 때까지 suspend + * 하므로 소비 이벤트는 표출 뒤에 보낸다 — 표출 중에는 [state] 가 Failed 로 유지돼 effect 가 + * 재시작되지 않고, 소비로 Idle 이 되면 이미 완료된 effect 만 정리된다 (#664 AddSongScreen 컨벤션). + */ @Composable private fun DownloadDialogHost( state: ReceiverDownloadState, + snackbarHostState: SnackbarHostState, onEvent: (ReceiverHomeEvent) -> Unit, ) { - val context = LocalContext.current - val resources = LocalResources.current val currentOnEvent by rememberUpdatedState(onEvent) val showDialog = @@ -162,19 +172,22 @@ private fun DownloadDialogHost( ) } - LaunchedEffect(state) { - when (state) { - is ReceiverDownloadState.Failed -> { - Toast.makeText(context, resources.getString(state.messageRes), Toast.LENGTH_SHORT).show() + when (state) { + is ReceiverDownloadState.Failed -> { + val message = stringResource(state.messageRes) + LaunchedEffect(state) { + snackbarHostState.showSnackbar(message = message, withDismissAction = true) currentOnEvent(ReceiverHomeEvent.ConsumeDownloadResult) } + } - ReceiverDownloadState.Done -> { + ReceiverDownloadState.Done -> { + LaunchedEffect(state) { currentOnEvent(ReceiverHomeEvent.ConsumeDownloadResult) } - - else -> { } } + + else -> { } } } diff --git a/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeDownloadSnackbarTest.kt b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeDownloadSnackbarTest.kt new file mode 100644 index 0000000000..ab94cb0335 --- /dev/null +++ b/feature/receiver/presentation/src/test/kotlin/com/afternote/feature/receiver/presentation/home/ReceiverHomeDownloadSnackbarTest.kt @@ -0,0 +1,65 @@ +package com.afternote.feature.receiver.presentation.home + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import com.afternote.core.ui.theme.AfternoteTheme +import com.afternote.feature.receiver.presentation.R +import com.afternote.feature.receiver.presentation.home.model.ReceiverDownloadState +import com.afternote.feature.receiver.presentation.home.model.ReceiverHomeUiState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +/** + * 수신자 홈 내려받기 실패 안내의 Snackbar 표출 가드 (#1391). + * + * 종전 Toast 는 Compose semantics 에 잡히지 않아 어떤 테스트로도 단언할 수 없었다 — 이 단언이 + * 가능해진 것 자체가 전환 목적의 일부다. 문구는 리소스 원문으로 대조한다 (모듈 테스트 관례, + * [com.afternote.feature.receiver.presentation.deliveryverification.ReceiverVerificationTest] 참조). + */ +@RunWith(RobolectricTestRunner::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(sdk = [35]) +class ReceiverHomeDownloadSnackbarTest { + @get:Rule + val composeRule = createComposeRule() + + @Test + fun `내려받기 실패 상태는 스낵바로 표출된다`() { + setHomeContent(ReceiverDownloadState.Failed(R.string.receiver_home_download_all_failed)) + + composeRule.onNodeWithText("모든 기록 내려받기에 실패했습니다.").assertIsDisplayed() + } + + @Test + fun `파일 저장 실패 상태도 자기 문구로 표출된다`() { + setHomeContent(ReceiverDownloadState.Failed(R.string.receiver_home_download_all_save_failed)) + + composeRule.onNodeWithText("파일 저장에 실패했습니다.").assertIsDisplayed() + } + + private fun setHomeContent(download: ReceiverDownloadState) { + composeRule.setContent { + AfternoteTheme { + ReceiverHomeScreen( + uiState = + ReceiverHomeUiState.Success( + senderName = "박서연", + senderMessage = null, + mindRecord = null, + timeLetterTotalCount = null, + afternoteTotalCount = null, + afternoteIcons = emptyList(), + download = download, + ), + onEvent = { }, + actions = ReceiverHomeActions.Noop, + ) + } + } + } +}