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
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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])
* 동안 상세 화면 위에 겹쳐 그리는 오버레이.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Long>(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 + 단방향 이벤트.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Long>(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
Original file line number Diff line number Diff line change
@@ -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<Song>) -> Unit,
viewModel: AddSongViewModel,
) {
AddSongEntry(
viewModel = viewModel,
onBackClick = onPopBackStack,
onSongsAdded = onSongsAdded,
)
}
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 피처의 네비게이션 그래프.
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ 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
import com.afternote.feature.afternote.presentation.receiver.navigation.model.ReceivedAfternoteRoute
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 에 등록한다.
Expand Down
Loading
Loading