diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphDetail.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/detail/AfternoteDetailRoute.kt similarity index 77% rename from feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphDetail.kt rename to feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/detail/AfternoteDetailRoute.kt index dc77582842..08bc3fef0b 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphDetail.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/detail/AfternoteDetailRoute.kt @@ -1,4 +1,4 @@ -package com.afternote.feature.afternote.presentation.author.navigation +package com.afternote.feature.afternote.presentation.author.detail import androidx.annotation.StringRes import androidx.compose.foundation.background @@ -41,88 +41,11 @@ import com.afternote.core.ui.theme.AfternoteDesign import com.afternote.core.ui.topbar.DetailTopBar import com.afternote.feature.afternote.domain.AfternoteType import com.afternote.feature.afternote.presentation.R -import com.afternote.feature.afternote.presentation.author.detail.AfternoteDetailDeleteResult -import com.afternote.feature.afternote.presentation.author.detail.AfternoteDetailUiState -import com.afternote.feature.afternote.presentation.author.detail.AfternoteDetailViewModel -import com.afternote.feature.afternote.presentation.author.detail.DetailContentUiModel -import com.afternote.feature.afternote.presentation.author.detail.GalleryDetailScreen -import com.afternote.feature.afternote.presentation.author.detail.MemorialDetailScreen import com.afternote.feature.afternote.presentation.author.detail.account.AccountDetailScreen +import com.afternote.feature.afternote.presentation.shared.detail.DesignPendingDetailContent +import com.afternote.feature.afternote.presentation.shared.detail.DetailLoadErrorContent import kotlinx.coroutines.launch -@Composable -fun DesignPendingDetailContent(onBackClick: () -> Unit) { - Scaffold( - modifier = Modifier.fillMaxSize(), - containerColor = Color.Transparent, - topBar = { - DetailTopBar(title = "", onBackClick = onBackClick) - }, - ) { paddingValues -> - Box( - modifier = - Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center, - ) { - Text(text = stringResource(R.string.design_pending)) - } - } -} - -/** - * 상세 데이터 로드 실패 화면. - * - * [com.afternote.feature.afternote.presentation.author.detail.AfternoteDetailUiState.Error] 계약대로 - * [messageRes] 를 [stringResource] 로 변환해 표시한다 (없으면 [R.string.afternote_detail_load_error] 폴백). - * 예외 원문은 받지 않는다 — 서버 5xx 본문·역직렬화 예외 메시지에 내부 SQL·응답 원문 발췌가 섞여 오기 때문. - * - * 표시 방식 통일(#446) 결론이 나오면 이 컴포저블의 본문 표현만 교체한다 — Route 의 Error 분기 배선은 유지. - * [DesignPendingDetailContent] 는 ESTATE 등 아직 구현되지 않은 상세 타입의 폴백으로만 유지한다. - * - * @param messageRes 앱에 박힌 문자열 리소스 ID(`R.string.*`). `@StringRes` 는 이 Int 가 임의 정수가 아니라 - * string 리소스 id 임을 Lint 에 알리는 표식이며, [stringResource] 로 실제 텍스트로 변환한다. - * @param onRetryClick 재조회 진입점. `null` 이면 재시도 버튼을 그리지 않는다 — 잘못된 항목 ID 처럼 - * 같은 요청을 다시 보내도 결과가 달라지지 않는 실패에 쓴다. - */ -@Composable -fun DetailLoadErrorContent( - @StringRes messageRes: Int?, - onBackClick: () -> Unit, - onRetryClick: (() -> Unit)? = null, -) { - Scaffold( - modifier = Modifier.fillMaxSize(), - containerColor = Color.Transparent, - topBar = { - DetailTopBar(title = "", onBackClick = onBackClick) - }, - ) { paddingValues -> - Box( - modifier = - Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center, - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = stringResource(messageRes ?: R.string.afternote_detail_load_error)) - if (onRetryClick != null) { - Spacer(modifier = Modifier.height(16.dp)) - TextButton(onClick = onRetryClick) { - Text( - text = stringResource(R.string.afternote_detail_retry), - style = AfternoteDesign.typography.captionLargeB, - color = AfternoteDesign.colors.gray9, - ) - } - } - } - } - } -} - /** * 삭제 진행([com.afternote.feature.afternote.presentation.author.detail.AfternoteDetailUiState.Success.isDeleting]) * 동안 상세 화면 위에 겹쳐 그리는 오버레이. diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphEditor.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorRoute.kt similarity index 65% rename from feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphEditor.kt rename to feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorRoute.kt index 618347d8e0..8a16bf3c4a 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphEditor.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorRoute.kt @@ -1,6 +1,5 @@ -package com.afternote.feature.afternote.presentation.author.navigation +package com.afternote.feature.afternote.presentation.author.editor -import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -10,95 +9,12 @@ import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import com.afternote.feature.afternote.presentation.R -import com.afternote.feature.afternote.presentation.author.editor.AfternoteEditorBody -import com.afternote.feature.afternote.presentation.author.editor.AfternoteEditorScreen -import com.afternote.feature.afternote.presentation.author.editor.AfternoteEditorViewModel -import com.afternote.feature.afternote.presentation.author.editor.SaveAfternoteMemorialMedia -import com.afternote.feature.afternote.presentation.author.editor.SaveAfternotePayloadBuilder import com.afternote.feature.afternote.presentation.author.editor.processing.AfternoteProcessingMethodDefaults import com.afternote.feature.afternote.presentation.author.editor.state.AfternoteEditorError import com.afternote.feature.afternote.presentation.author.editor.state.AfternoteEditorState import com.afternote.feature.afternote.presentation.author.editor.state.rememberAfternoteEditorState import com.afternote.feature.afternote.presentation.author.navigation.model.SELECTED_RECEIVER_ID_KEY -@StringRes -internal fun AfternoteEditorError.messageResId(): Int = - when (this) { - is AfternoteEditorError.Validation -> { - reason.messageResId - } - - AfternoteEditorError.Network, - AfternoteEditorError.Server, - -> { - R.string.afternote_editor_save_failed_generic - } - - AfternoteEditorError.ReceiverSelectionUnavailable -> { - R.string.afternote_editor_receiver_selection_unavailable - } - - is AfternoteEditorError.Upload -> { - when (target) { - AfternoteEditorError.Upload.Target.THUMBNAIL -> R.string.afternote_editor_thumbnail_upload_failed - AfternoteEditorError.Upload.Target.SAVE_MEDIA -> R.string.afternote_editor_save_failed_generic - } - } - } - -/** - * 수신자 선택 화면이 남긴 id 를 폼에 반영한다. - * - * 목록 로드 실패로 id 를 해석할 수 없으면 [AfternoteEditorViewModel.resolveSelectedReceiver] 가 재조회 후 - * 오류 이벤트를 세운다 — 선택이 조용히 사라지지 않는다 (#1405). - */ -internal suspend fun tryApplyReceiverSelectionFromSavedState( - backStackEntry: NavBackStackEntry, - viewModel: AfternoteEditorViewModel, - state: AfternoteEditorState, -) { - val id = backStackEntry.savedStateHandle[SELECTED_RECEIVER_ID_KEY] as? Long ?: return - backStackEntry.savedStateHandle.remove(SELECTED_RECEIVER_ID_KEY) - val receiver = viewModel.resolveSelectedReceiver(id) ?: return - state.addReceiverById(id, receiver.name, receiver.label) -} - -internal fun buildOnRegisterClick( - editViewModel: AfternoteEditorViewModel, - state: AfternoteEditorState, -): () -> Unit = - { - // 폼 스냅샷은 한 번만 읽는다 — 필드마다 다시 읽으면 조립 도중 갱신이 끼어 서로 다른 시점의 값이 섞인다. - val form = state.currentForm() - val payload = - SaveAfternotePayloadBuilder.build( - form = form, - messageBlocks = state.currentEditorMessageBlocks(), - accountId = - state.idState.text - .toString(), - password = - state.passwordState.text - .toString(), - ) - editViewModel.saveAfternote( - payload = payload, - selectedReceiverIds = form.afternoteEditReceivers.map { it.id }, - memorialMedia = - SaveAfternoteMemorialMedia( - memorialVideoUrl = form.memorialVideoUrl, - memorialThumbnailUrl = form.memorialThumbnailUrl, - memorialPhotoUrl = form.memorialPhotoUrl, - pickedMemorialPhotoUri = form.pickedMemorialPhotoUri, - ), - ) - } - -internal fun shouldDeferEditorBaselineCapture( - isPrefillLoading: Boolean, - isProcessingMethodDefaultsInitializing: Boolean, -): Boolean = isPrefillLoading || isProcessingMethodDefaultsInitializing - /** * 작성자 에디터 화면: type-safe editor flow + 단방향 이벤트. * diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorRouteHelpers.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorRouteHelpers.kt new file mode 100644 index 0000000000..5c2e5db792 --- /dev/null +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorRouteHelpers.kt @@ -0,0 +1,88 @@ +package com.afternote.feature.afternote.presentation.author.editor + +import androidx.annotation.StringRes +import androidx.navigation.NavBackStackEntry +import com.afternote.feature.afternote.presentation.R +import com.afternote.feature.afternote.presentation.author.editor.state.AfternoteEditorError +import com.afternote.feature.afternote.presentation.author.editor.state.AfternoteEditorState +import com.afternote.feature.afternote.presentation.author.navigation.model.SELECTED_RECEIVER_ID_KEY + +// 에디터 조립부가 쓰는 순수 헬퍼들이다. Route 파일에는 조립만 남긴다 (#1514). + +@StringRes +internal fun AfternoteEditorError.messageResId(): Int = + when (this) { + is AfternoteEditorError.Validation -> { + reason.messageResId + } + + AfternoteEditorError.Network, + AfternoteEditorError.Server, + -> { + R.string.afternote_editor_save_failed_generic + } + + AfternoteEditorError.ReceiverSelectionUnavailable -> { + R.string.afternote_editor_receiver_selection_unavailable + } + + is AfternoteEditorError.Upload -> { + when (target) { + AfternoteEditorError.Upload.Target.THUMBNAIL -> R.string.afternote_editor_thumbnail_upload_failed + AfternoteEditorError.Upload.Target.SAVE_MEDIA -> R.string.afternote_editor_save_failed_generic + } + } + } + +/** + * 수신자 선택 화면이 남긴 id 를 폼에 반영한다. + * + * 목록 로드 실패로 id 를 해석할 수 없으면 [AfternoteEditorViewModel.resolveSelectedReceiver] 가 재조회 후 + * 오류 이벤트를 세운다 — 선택이 조용히 사라지지 않는다 (#1405). + */ +internal suspend fun tryApplyReceiverSelectionFromSavedState( + backStackEntry: NavBackStackEntry, + viewModel: AfternoteEditorViewModel, + state: AfternoteEditorState, +) { + val id = backStackEntry.savedStateHandle[SELECTED_RECEIVER_ID_KEY] as? Long ?: return + backStackEntry.savedStateHandle.remove(SELECTED_RECEIVER_ID_KEY) + val receiver = viewModel.resolveSelectedReceiver(id) ?: return + state.addReceiverById(id, receiver.name, receiver.label) +} + +internal fun buildOnRegisterClick( + editViewModel: AfternoteEditorViewModel, + state: AfternoteEditorState, +): () -> Unit = + { + // 폼 스냅샷은 한 번만 읽는다 — 필드마다 다시 읽으면 조립 도중 갱신이 끼어 서로 다른 시점의 값이 섞인다. + val form = state.currentForm() + val payload = + SaveAfternotePayloadBuilder.build( + form = form, + messageBlocks = state.currentEditorMessageBlocks(), + accountId = + state.idState.text + .toString(), + password = + state.passwordState.text + .toString(), + ) + editViewModel.saveAfternote( + payload = payload, + selectedReceiverIds = form.afternoteEditReceivers.map { it.id }, + memorialMedia = + SaveAfternoteMemorialMedia( + memorialVideoUrl = form.memorialVideoUrl, + memorialThumbnailUrl = form.memorialThumbnailUrl, + memorialPhotoUrl = form.memorialPhotoUrl, + pickedMemorialPhotoUri = form.pickedMemorialPhotoUri, + ), + ) + } + +internal fun shouldDeferEditorBaselineCapture( + isPrefillLoading: Boolean, + isProcessingMethodDefaultsInitializing: Boolean, +): Boolean = isPrefillLoading || isProcessingMethodDefaultsInitializing diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/editor/memorial/playlist/AddSongRoute.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/editor/memorial/playlist/AddSongRoute.kt new file mode 100644 index 0000000000..64e4eb98e5 --- /dev/null +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/editor/memorial/playlist/AddSongRoute.kt @@ -0,0 +1,16 @@ +package com.afternote.feature.afternote.presentation.author.editor.memorial.playlist + +import androidx.compose.runtime.Composable + +@Composable +internal fun AfternoteAddSongNavigation( + onPopBackStack: () -> Unit, + onSongsAdded: (List) -> Unit, + viewModel: AddSongViewModel, +) { + AddSongEntry( + viewModel = viewModel, + onBackClick = onPopBackStack, + onSongsAdded = onSongsAdded, + ) +} diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphSelectReceiver.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/editor/receiver/select/SelectReceiverRoute.kt similarity index 79% rename from feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphSelectReceiver.kt rename to feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/editor/receiver/select/SelectReceiverRoute.kt index 05bef4b11c..6727f5e534 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphSelectReceiver.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/editor/receiver/select/SelectReceiverRoute.kt @@ -1,11 +1,9 @@ -package com.afternote.feature.afternote.presentation.author.navigation +package com.afternote.feature.afternote.presentation.author.editor.receiver.select import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.afternote.feature.afternote.presentation.author.editor.receiver.select.SelectReceiverScreen -import com.afternote.feature.afternote.presentation.author.editor.receiver.select.SelectReceiverViewModel /** * 수신자 선택 화면 진입점 (#540). diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphHome.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/home/AfternoteHomeRoute.kt similarity index 75% rename from feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphHome.kt rename to feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/home/AfternoteHomeRoute.kt index 0b5a6c12e3..2ace1523ac 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphHome.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/home/AfternoteHomeRoute.kt @@ -1,8 +1,7 @@ -package com.afternote.feature.afternote.presentation.author.navigation +package com.afternote.feature.afternote.presentation.author.home import androidx.compose.runtime.Composable import com.afternote.feature.afternote.domain.AfternoteType -import com.afternote.feature.afternote.presentation.author.home.AfternoteHomeEntry @Composable internal fun AfternoteHomeNavigation( diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraph.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraph.kt index 02346e5e6d..a8513a4f32 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraph.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraph.kt @@ -10,10 +10,16 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation import com.afternote.core.ui.Route import com.afternote.feature.afternote.presentation.AfternoteHostViewModel +import com.afternote.feature.afternote.presentation.author.detail.AfternoteDetailNavigation +import com.afternote.feature.afternote.presentation.author.editor.AfternoteEditorNavigation import com.afternote.feature.afternote.presentation.author.editor.AfternoteEditorViewModel import com.afternote.feature.afternote.presentation.author.editor.memorial.playlist.AddSongViewModel +import com.afternote.feature.afternote.presentation.author.editor.memorial.playlist.AfternoteAddSongNavigation import com.afternote.feature.afternote.presentation.author.editor.memorial.playlist.MemorialPlaylistEntry +import com.afternote.feature.afternote.presentation.author.editor.receiver.select.AfternoteSelectReceiverNavigation +import com.afternote.feature.afternote.presentation.author.home.AfternoteHomeNavigation import com.afternote.feature.afternote.presentation.author.navigation.model.AfternoteRoute +import com.afternote.feature.afternote.presentation.shared.fingerprint.AfternoteFingerprintLoginNavigation /** * Afternote 피처의 네비게이션 그래프. diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphAddSong.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphAddSong.kt deleted file mode 100644 index 2d94ea21a0..0000000000 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphAddSong.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.afternote.feature.afternote.presentation.author.navigation - -import androidx.compose.runtime.Composable -import com.afternote.feature.afternote.presentation.author.editor.memorial.playlist.AddSongEntry -import com.afternote.feature.afternote.presentation.author.editor.memorial.playlist.AddSongViewModel -import com.afternote.feature.afternote.presentation.author.editor.memorial.playlist.Song - -@Composable -internal fun AfternoteAddSongNavigation( - onPopBackStack: () -> Unit, - onSongsAdded: (List) -> Unit, - viewModel: AddSongViewModel, -) { - AddSongEntry( - viewModel = viewModel, - onBackClick = onPopBackStack, - onSongsAdded = onSongsAdded, - ) -} diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/detail/ReceivedAfternoteDetailRoute.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/detail/ReceivedAfternoteDetailRoute.kt index 0465e60326..a007997ef8 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/detail/ReceivedAfternoteDetailRoute.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/detail/ReceivedAfternoteDetailRoute.kt @@ -7,8 +7,8 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.afternote.core.ui.loading.LoadingBody -import com.afternote.feature.afternote.presentation.author.navigation.DesignPendingDetailContent -import com.afternote.feature.afternote.presentation.author.navigation.DetailLoadErrorContent +import com.afternote.feature.afternote.presentation.shared.detail.DesignPendingDetailContent +import com.afternote.feature.afternote.presentation.shared.detail.DetailLoadErrorContent /** * 수신 애프터노트 상세 Stateful Route. diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteNavGraph.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteNavGraph.kt index 2abd6b3b4c..c70c566ff0 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteNavGraph.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/receiver/navigation/ReceivedAfternoteNavGraph.kt @@ -7,7 +7,6 @@ import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import com.afternote.core.ui.loading.LoadingBody -import com.afternote.feature.afternote.presentation.author.navigation.DetailLoadErrorContent import com.afternote.feature.afternote.presentation.author.navigation.afternoteComposable import com.afternote.feature.afternote.presentation.receiver.afternotelist.ReceiverAfternoteHomeEntry import com.afternote.feature.afternote.presentation.receiver.detail.ReceivedAfternoteDetailRoute @@ -15,6 +14,7 @@ import com.afternote.feature.afternote.presentation.receiver.navigation.model.Re import com.afternote.feature.afternote.presentation.receiver.playlist.MemorialPlaylistScreen import com.afternote.feature.afternote.presentation.receiver.playlist.ReceiverMemorialPlaylistUiState import com.afternote.feature.afternote.presentation.receiver.playlist.ReceiverMemorialPlaylistViewModel +import com.afternote.feature.afternote.presentation.shared.detail.DetailLoadErrorContent /** * 수신 애프터노트 목록·상세·추억 플레이리스트를 루트 NavHost 에 등록한다. diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/shared/detail/DetailStateContents.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/shared/detail/DetailStateContents.kt new file mode 100644 index 0000000000..a48df983c8 --- /dev/null +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/shared/detail/DetailStateContents.kt @@ -0,0 +1,97 @@ +package com.afternote.feature.afternote.presentation.shared.detail + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.afternote.core.ui.theme.AfternoteDesign +import com.afternote.core.ui.topbar.DetailTopBar +import com.afternote.feature.afternote.presentation.R + +// 작성자·수신자 상세가 함께 쓰는 «내용 대신 상태를 보여 주는» 화면들이다. 한쪽 기능 폴더에 +// 두면 다른 쪽이 남의 폴더를 import 해야 한다 (#1514). + +@Composable +fun DesignPendingDetailContent(onBackClick: () -> Unit) { + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = Color.Transparent, + topBar = { + DetailTopBar(title = "", onBackClick = onBackClick) + }, + ) { paddingValues -> + Box( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center, + ) { + Text(text = stringResource(R.string.design_pending)) + } + } +} + +/** + * 상세 데이터 로드 실패 화면. + * + * [com.afternote.feature.afternote.presentation.author.detail.AfternoteDetailUiState.Error] 계약대로 + * [messageRes] 를 [stringResource] 로 변환해 표시한다 (없으면 [R.string.afternote_detail_load_error] 폴백). + * 예외 원문은 받지 않는다 — 서버 5xx 본문·역직렬화 예외 메시지에 내부 SQL·응답 원문 발췌가 섞여 오기 때문. + * + * 표시 방식 통일(#446) 결론이 나오면 이 컴포저블의 본문 표현만 교체한다 — Route 의 Error 분기 배선은 유지. + * [DesignPendingDetailContent] 는 ESTATE 등 아직 구현되지 않은 상세 타입의 폴백으로만 유지한다. + * + * @param messageRes 앱에 박힌 문자열 리소스 ID(`R.string.*`). `@StringRes` 는 이 Int 가 임의 정수가 아니라 + * string 리소스 id 임을 Lint 에 알리는 표식이며, [stringResource] 로 실제 텍스트로 변환한다. + * @param onRetryClick 재조회 진입점. `null` 이면 재시도 버튼을 그리지 않는다 — 잘못된 항목 ID 처럼 + * 같은 요청을 다시 보내도 결과가 달라지지 않는 실패에 쓴다. + */ +@Composable +fun DetailLoadErrorContent( + @StringRes messageRes: Int?, + onBackClick: () -> Unit, + onRetryClick: (() -> Unit)? = null, +) { + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = Color.Transparent, + topBar = { + DetailTopBar(title = "", onBackClick = onBackClick) + }, + ) { paddingValues -> + Box( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = stringResource(messageRes ?: R.string.afternote_detail_load_error)) + if (onRetryClick != null) { + Spacer(modifier = Modifier.height(16.dp)) + TextButton(onClick = onRetryClick) { + Text( + text = stringResource(R.string.afternote_detail_retry), + style = AfternoteDesign.typography.captionLargeB, + color = AfternoteDesign.colors.gray9, + ) + } + } + } + } + } +} diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphFingerprint.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/shared/fingerprint/FingerprintLoginRoute.kt similarity index 88% rename from feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphFingerprint.kt rename to feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/shared/fingerprint/FingerprintLoginRoute.kt index d9d42042bd..f93182553f 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/author/navigation/AfternoteNavGraphFingerprint.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/shared/fingerprint/FingerprintLoginRoute.kt @@ -1,4 +1,4 @@ -package com.afternote.feature.afternote.presentation.author.navigation +package com.afternote.feature.afternote.presentation.shared.fingerprint import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -12,10 +12,6 @@ import androidx.compose.ui.res.stringResource import androidx.fragment.app.FragmentActivity import com.afternote.core.ui.findActivity import com.afternote.feature.afternote.presentation.R -import com.afternote.feature.afternote.presentation.shared.fingerprint.BiometricAuthResult -import com.afternote.feature.afternote.presentation.shared.fingerprint.BiometricMessages -import com.afternote.feature.afternote.presentation.shared.fingerprint.FingerprintLoginScreen -import com.afternote.feature.afternote.presentation.shared.fingerprint.authenticateBiometric import kotlinx.coroutines.launch @Composable diff --git a/feature/afternote/presentation/src/screenshotTest/kotlin/com/afternote/feature/afternote/presentation/author/navigation/DeleteInProgressOverlayScreenshotTest.kt b/feature/afternote/presentation/src/screenshotTest/kotlin/com/afternote/feature/afternote/presentation/author/detail/DeleteInProgressOverlayScreenshotTest.kt similarity index 93% rename from feature/afternote/presentation/src/screenshotTest/kotlin/com/afternote/feature/afternote/presentation/author/navigation/DeleteInProgressOverlayScreenshotTest.kt rename to feature/afternote/presentation/src/screenshotTest/kotlin/com/afternote/feature/afternote/presentation/author/detail/DeleteInProgressOverlayScreenshotTest.kt index 29e81e029c..b0764e9d1d 100644 --- a/feature/afternote/presentation/src/screenshotTest/kotlin/com/afternote/feature/afternote/presentation/author/navigation/DeleteInProgressOverlayScreenshotTest.kt +++ b/feature/afternote/presentation/src/screenshotTest/kotlin/com/afternote/feature/afternote/presentation/author/detail/DeleteInProgressOverlayScreenshotTest.kt @@ -1,4 +1,4 @@ -package com.afternote.feature.afternote.presentation.author.navigation +package com.afternote.feature.afternote.presentation.author.detail import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize diff --git a/feature/afternote/presentation/src/screenshotTest/kotlin/com/afternote/feature/afternote/presentation/author/navigation/DetailLoadErrorContentScreenshotTest.kt b/feature/afternote/presentation/src/screenshotTest/kotlin/com/afternote/feature/afternote/presentation/shared/detail/DetailLoadErrorContentScreenshotTest.kt similarity index 95% rename from feature/afternote/presentation/src/screenshotTest/kotlin/com/afternote/feature/afternote/presentation/author/navigation/DetailLoadErrorContentScreenshotTest.kt rename to feature/afternote/presentation/src/screenshotTest/kotlin/com/afternote/feature/afternote/presentation/shared/detail/DetailLoadErrorContentScreenshotTest.kt index e1b73cb44a..518207b9c7 100644 --- a/feature/afternote/presentation/src/screenshotTest/kotlin/com/afternote/feature/afternote/presentation/author/navigation/DetailLoadErrorContentScreenshotTest.kt +++ b/feature/afternote/presentation/src/screenshotTest/kotlin/com/afternote/feature/afternote/presentation/shared/detail/DetailLoadErrorContentScreenshotTest.kt @@ -1,4 +1,4 @@ -package com.afternote.feature.afternote.presentation.author.navigation +package com.afternote.feature.afternote.presentation.shared.detail import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview diff --git a/feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/author/navigation/DeleteInProgressOverlayScreenshotTestKt/deleteInProgressOverlayScreenshot_748aa731_0.png b/feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/author/detail/DeleteInProgressOverlayScreenshotTestKt/deleteInProgressOverlayScreenshot_748aa731_0.png similarity index 100% rename from feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/author/navigation/DeleteInProgressOverlayScreenshotTestKt/deleteInProgressOverlayScreenshot_748aa731_0.png rename to feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/author/detail/DeleteInProgressOverlayScreenshotTestKt/deleteInProgressOverlayScreenshot_748aa731_0.png diff --git a/feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/author/navigation/DetailLoadErrorContentScreenshotTestKt/detailLoadErrorContentDefaultFallbackScreenshot_748aa731_0.png b/feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/shared/detail/DetailLoadErrorContentScreenshotTestKt/detailLoadErrorContentDefaultFallbackScreenshot_748aa731_0.png similarity index 100% rename from feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/author/navigation/DetailLoadErrorContentScreenshotTestKt/detailLoadErrorContentDefaultFallbackScreenshot_748aa731_0.png rename to feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/shared/detail/DetailLoadErrorContentScreenshotTestKt/detailLoadErrorContentDefaultFallbackScreenshot_748aa731_0.png diff --git a/feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/author/navigation/DetailLoadErrorContentScreenshotTestKt/detailLoadErrorContentMessageResScreenshot_748aa731_0.png b/feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/shared/detail/DetailLoadErrorContentScreenshotTestKt/detailLoadErrorContentMessageResScreenshot_748aa731_0.png similarity index 100% rename from feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/author/navigation/DetailLoadErrorContentScreenshotTestKt/detailLoadErrorContentMessageResScreenshot_748aa731_0.png rename to feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/shared/detail/DetailLoadErrorContentScreenshotTestKt/detailLoadErrorContentMessageResScreenshot_748aa731_0.png diff --git a/feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/author/navigation/DetailLoadErrorContentScreenshotTestKt/detailLoadErrorContentRetryScreenshot_748aa731_0.png b/feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/shared/detail/DetailLoadErrorContentScreenshotTestKt/detailLoadErrorContentRetryScreenshot_748aa731_0.png similarity index 100% rename from feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/author/navigation/DetailLoadErrorContentScreenshotTestKt/detailLoadErrorContentRetryScreenshot_748aa731_0.png rename to feature/afternote/presentation/src/screenshotTestDebug/reference/com/afternote/feature/afternote/presentation/shared/detail/DetailLoadErrorContentScreenshotTestKt/detailLoadErrorContentRetryScreenshot_748aa731_0.png diff --git a/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorProcessingMethodDefaultsInteractionTest.kt b/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorProcessingMethodDefaultsInteractionTest.kt index 1a5cfbc800..51b63c029c 100644 --- a/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorProcessingMethodDefaultsInteractionTest.kt +++ b/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorProcessingMethodDefaultsInteractionTest.kt @@ -11,7 +11,6 @@ import com.afternote.core.ui.theme.AfternoteTheme import com.afternote.feature.afternote.presentation.author.editor.processing.model.ProcessingMethodItem import com.afternote.feature.afternote.presentation.author.editor.state.AfternoteTypeForm import com.afternote.feature.afternote.presentation.author.editor.state.EditorFormState -import com.afternote.feature.afternote.presentation.author.navigation.shouldDeferEditorBaselineCapture import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test diff --git a/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorReceiverSelectionRecoveryTest.kt b/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorReceiverSelectionRecoveryTest.kt index 2990dd6725..dce8d927c0 100644 --- a/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorReceiverSelectionRecoveryTest.kt +++ b/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/author/editor/AfternoteEditorReceiverSelectionRecoveryTest.kt @@ -11,7 +11,6 @@ import com.afternote.feature.afternote.domain.repository.author.MemorialThumbnai import com.afternote.feature.afternote.domain.usecase.editor.ResolveMemorialMediaForSaveUseCase import com.afternote.feature.afternote.presentation.R import com.afternote.feature.afternote.presentation.author.editor.state.AfternoteEditorError -import com.afternote.feature.afternote.presentation.author.navigation.messageResId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch