diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/MindRecordFlowAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/MindRecordFlowAndroidTest.kt index 97d075dba4..71f6d13186 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/MindRecordFlowAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/MindRecordFlowAndroidTest.kt @@ -109,6 +109,7 @@ class MindRecordFlowAndroidTest { repository.createResults.addLast(Result.success(1L)) val viewModel = DailyQuestionWriteViewModel( + savedStateHandle = SavedStateHandle(emptyMap()), repository = repository, photoUploadRepository = PhotoUploadRepository { _, _ -> Result.success("https://cdn.test/question.jpg") }, draftLoader = MindRecordDraftLoader(FakeDiaryRepository(), repository), diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt index b92d97415e..fc5996f016 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt @@ -415,6 +415,7 @@ class TimeLetterMindRecordCompletionAndroidTest { composeRule.runOnIdle { writeViewModel = DailyQuestionWriteViewModel( + savedStateHandle = SavedStateHandle(emptyMap()), repository = repository, photoUploadRepository = CompletionPhotoUploadRepository, draftLoader = MindRecordDraftLoader(CompletionDiaryRepository(mutableListOf()), repository), diff --git a/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt b/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt index 9dc8e04c7a..366fc59449 100644 --- a/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt +++ b/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt @@ -105,7 +105,7 @@ fun rememberMindRecordNavActions(navController: NavController): MindRecordNavAct } override fun onWriteDailyQuestion() { - navController.navigate(MindRecordRoute.DailyQuestionWriteRoute) + navController.navigate(MindRecordRoute.DailyQuestionWriteRoute()) } override fun onWriteDiary() { @@ -128,12 +128,30 @@ fun rememberMindRecordNavActions(navController: NavController): MindRecordNavAct navController.popBackStack() } + override fun onEditDailyQuestion(answerId: Long) { + navController.navigate(MindRecordRoute.DailyQuestionWriteRoute(answerId = answerId)) + } + + override fun onEditDiary( + diaryId: Long, + yearMonth: String, + ) { + navController.navigate( + MindRecordRoute.DiaryWriteRoute( + recordId = diaryId, + yearMonth = yearMonth, + // 정식 기록이라 draft 목록이 아닌 전체 목록에서 찾는다 (#582). + isDraft = false, + ), + ) + } + override fun onEditDiaryDraft( draftId: Long, draftYearMonth: String, ) { navController.navigate( - MindRecordRoute.DiaryWriteRoute(draftId = draftId, draftYearMonth = draftYearMonth), + MindRecordRoute.DiaryWriteRoute(recordId = draftId, yearMonth = draftYearMonth), ) } } @@ -355,7 +373,9 @@ fun rememberHomeTabActions( override fun onAnswerClick() { // 카드 문구가 "데일리질문 답변하기" 라 답변 작성 화면으로 보낸다. // 기록 탭의 작성 진입(`MindRecordNavActions.onWriteDailyQuestion`)과 같은 목적지다. - appState.navController.navigate(MindRecordRoute.DailyQuestionWriteRoute) + // 라우트가 data class 라 **인스턴스**를 넘겨야 한다 — 클래스 참조로 넘기면 + // 이동이 조용히 무산되고 홈의 이 버튼이 눌리지 않는다 (#770). + appState.navController.navigate(MindRecordRoute.DailyQuestionWriteRoute()) } override fun onNextStepClick() { diff --git a/feature/mindrecord/presentation/build.gradle.kts b/feature/mindrecord/presentation/build.gradle.kts index fa204f17f4..94a78ba931 100644 --- a/feature/mindrecord/presentation/build.gradle.kts +++ b/feature/mindrecord/presentation/build.gradle.kts @@ -37,4 +37,10 @@ dependencies { testImplementation(libs.robolectric) testImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.test.manifest) + implementation(libs.compose.rich.editor) + + testImplementation(libs.coroutines.test) + + // 라우트 인자 해석이 Bundle 을 타므로 실제 Android 구현이 필요하다 (#582). + testImplementation(libs.robolectric) } diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/DeleteConfirmDialog.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/DeleteConfirmDialog.kt new file mode 100644 index 0000000000..869991d46d --- /dev/null +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/DeleteConfirmDialog.kt @@ -0,0 +1,59 @@ +package com.afternote.feature.mindrecord.presentation.component + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import com.afternote.core.ui.theme.AfternoteDesign +import com.afternote.feature.mindrecord.presentation.R + +/** + * 기록 삭제 확인 (#582). + * + * 삭제는 되돌릴 수 없는데 종전에는 메뉴를 누르는 **즉시** 실행됐다. 실수로 눌러도 + * 복구할 방법이 없으므로 한 단계를 둔다. + */ +@Composable +fun DeleteConfirmDialog( + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + containerColor = Color.White, + title = { + Text( + text = stringResource(R.string.mindrecord_delete_confirm_title), + style = AfternoteDesign.typography.bodyLargeB, + color = AfternoteDesign.colors.gray9, + ) + }, + text = { + Text( + text = stringResource(R.string.mindrecord_delete_confirm_message), + style = AfternoteDesign.typography.bodySmallR, + color = AfternoteDesign.colors.gray6, + ) + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text( + text = stringResource(R.string.mindrecord_delete_confirm), + style = AfternoteDesign.typography.bodySmallB, + color = AfternoteDesign.colors.gray9, + ) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text( + text = stringResource(R.string.mindrecord_delete_cancel), + style = AfternoteDesign.typography.bodySmallR, + color = AfternoteDesign.colors.gray6, + ) + } + }, + ) +} diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/navigation/MindRecordNavActions.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/navigation/MindRecordNavActions.kt index 6b7fefc2aa..22b0149adc 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/navigation/MindRecordNavActions.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/navigation/MindRecordNavActions.kt @@ -31,6 +31,15 @@ interface MindRecordNavActions { fun onDraftListBack() + /** 목록의 "수정하기" — 정식 데일리질문 답변을 프리필한 작성 화면으로 연다 (#582). */ + fun onEditDailyQuestion(answerId: Long) + + /** 목록의 "수정하기" — 정식 일기를 프리필한 작성 화면으로 연다 (#582). */ + fun onEditDiary( + diaryId: Long, + yearMonth: String, + ) + /** 임시저장 목록에서 일기 draft 를 탭 — 일기 작성 화면을 이어쓰기 모드로 연다. */ fun onEditDiaryDraft( draftId: Long, diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/navigation/MindRecordNavGraph.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/navigation/MindRecordNavGraph.kt index 6632640691..b69ed4eec3 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/navigation/MindRecordNavGraph.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/navigation/MindRecordNavGraph.kt @@ -10,6 +10,7 @@ import com.afternote.feature.mindrecord.presentation.screen.sender.DailyQuestion import com.afternote.feature.mindrecord.presentation.screen.sender.DiaryWriteScreen import com.afternote.feature.mindrecord.presentation.screen.sender.DraftListScreen import com.afternote.feature.mindrecord.presentation.screen.sender.HomeScreen +import java.time.YearMonth /** * 마인드레코드 피처의 루트 [NavHost] 등록 묶음. @@ -27,6 +28,11 @@ fun NavGraphBuilder.mindRecordNavGraph(actions: MindRecordNavActions) { MindRecordCategoryUi.WeeklyReport -> Unit } }, + onEditDailyQuestion = actions::onEditDailyQuestion, + // 목록은 보고 있는 달의 항목만 담으므로, 그 달을 함께 넘겨 프리필 조회 범위를 좁힌다. + // 목록이 보고 있던 달을 그대로 넘긴다. 이번 달로 고정하면 지난달 일기의 + // «수정하기» 가 프리필 없이 열리고, 그대로 저장하면 원본을 덮어쓴다 (#582 리뷰). + onEditDiary = { diaryId, yearMonth -> actions.onEditDiary(diaryId, yearMonth.toString()) }, ) } composable { diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/navigation/MindRecordRoute.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/navigation/MindRecordRoute.kt index e4766f0152..e84c881742 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/navigation/MindRecordRoute.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/navigation/MindRecordRoute.kt @@ -3,17 +3,29 @@ package com.afternote.feature.mindrecord.presentation.navigation import kotlinx.serialization.Serializable sealed interface MindRecordRoute { + /** + * 데일리질문 작성 화면. + * + * [answerId] 가 있으면 **정식 답변 수정** 모드 — 그 답변을 프리필하고 저장 시 PATCH 한다. + * 없으면 오늘 질문에 새로 답한다(임시저장이 있으면 이어쓰기). + */ @Serializable - data object DailyQuestionWriteRoute : MindRecordRoute + data class DailyQuestionWriteRoute( + val answerId: Long? = null, + ) : MindRecordRoute /** - * 일기 작성 화면. [draftId] 가 있으면 임시저장 이어쓰기 모드 — 해당 달([draftYearMonth], - * `yyyy-MM`)의 draft 목록에서 항목을 찾아 프리필하고, 저장 시 PATCH 로 수정한다. + * 일기 작성 화면. + * + * [recordId] 가 있으면 그 일기를 프리필하고 저장 시 PATCH 한다. [isDraft] 가 true 면 + * 임시저장 이어쓰기(등록 시 `isDraft=false` 로 전환), false 면 **정식 기록 수정**이다. + * [yearMonth](`yyyy-MM`) 는 해당 항목을 찾을 달이다. */ @Serializable data class DiaryWriteRoute( - val draftId: Long? = null, - val draftYearMonth: String? = null, + val recordId: Long? = null, + val yearMonth: String? = null, + val isDraft: Boolean = true, ) : MindRecordRoute /** 작성 화면 키보드 툴바의 "임시저장 N" 영역에서 진입하는 임시저장 목록 화면. */ diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DailyQuestionAnswerListScreen.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DailyQuestionAnswerListScreen.kt index a06a30cd50..e8d2b2683b 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DailyQuestionAnswerListScreen.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DailyQuestionAnswerListScreen.kt @@ -28,6 +28,7 @@ import com.afternote.core.ui.theme.AfternoteTheme import com.afternote.feature.mindrecord.presentation.component.DailyCalendar import com.afternote.feature.mindrecord.presentation.component.DailyQuestionBanner import com.afternote.feature.mindrecord.presentation.component.DailyQuestionListCard +import com.afternote.feature.mindrecord.presentation.component.DeleteConfirmDialog import com.afternote.feature.mindrecord.presentation.component.MindRecordEmptyState import com.afternote.feature.mindrecord.presentation.component.MindRecordErrorBox import com.afternote.feature.mindrecord.presentation.model.DailyQuestion @@ -41,9 +42,21 @@ import java.time.YearMonth fun DailyQuestionAnswerListScreen( modifier: Modifier = Modifier, isListView: Boolean = true, + onEditClick: (Long) -> Unit = {}, viewModel: DailyQuestionListViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var pendingDeleteId by remember { mutableStateOf(null) } + + pendingDeleteId?.let { id -> + DeleteConfirmDialog( + onConfirm = { + pendingDeleteId = null + viewModel.delete(id) + }, + onDismiss = { pendingDeleteId = null }, + ) + } when (val state = uiState) { DailyQuestionListUiState.Loading -> { @@ -77,7 +90,9 @@ fun DailyQuestionAnswerListScreen( isListView = isListView, todayQuestion = state.todayQuestion, answers = state.answers, - onDelete = viewModel::delete, + onEdit = onEditClick, + // 삭제는 되돌릴 수 없다 — 종전에는 메뉴를 누르는 즉시 실행됐다 (#582). + onDelete = { pendingDeleteId = it }, ) } } @@ -90,6 +105,7 @@ private fun DailyQuestionListContent( answers: List, modifier: Modifier = Modifier, todayQuestion: TodayQuestionUi? = null, + onEdit: (Long) -> Unit = {}, onDelete: (Long) -> Unit = {}, ) { var yearMonth by remember { mutableStateOf(YearMonth.now()) } @@ -147,7 +163,11 @@ private fun DailyQuestionListContent( } // Figma 2757:16116 — 리스트 형은 답변 카드만 나열 items(answers, key = { it.id }) { answer -> - DailyQuestionListCard(answer = answer, onDelete = { onDelete(answer.id) }) + DailyQuestionListCard( + answer = answer, + onEdit = { onEdit(answer.id) }, + onDelete = { onDelete(answer.id) }, + ) } } } diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DiaryScreen.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DiaryScreen.kt index 69d162e30a..843f3edd4b 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DiaryScreen.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/DiaryScreen.kt @@ -51,6 +51,11 @@ private val PreviewYearMonth = YearMonth.of(2026, 7) fun DiaryScreen( modifier: Modifier = Modifier, isListView: Boolean = true, + /** + * «수정하기» — 기록 ID 와 **보고 있는 달**. 달을 빼면 프리필이 이번 달 목록에서 그 + * 기록을 찾다 실패하고, 빈 화면에서 저장하면 원본을 덮어쓸 수 있다 (#582 리뷰). + */ + onEditClick: (Long, YearMonth) -> Unit = { _, _ -> }, viewModel: DiaryListViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -89,6 +94,7 @@ fun DiaryScreen( yearMonth = state.yearMonth, monthDiaryCount = state.monthDiaryCount, weeklyMoodEmoji = state.weeklyDominantMood?.toEmoji(), + onEdit = onEditClick, onDelete = viewModel::delete, onYearMonthChanged = viewModel::selectYearMonth, ) @@ -98,7 +104,7 @@ fun DiaryScreen( } @Composable -private fun DiaryListContent( +internal fun DiaryListContent( isListView: Boolean, diaries: List, // 조회 중인 월은 VM 이 들고 있다 — 자동 갱신이 같은 월을 다시 조회해야 하고, @@ -108,6 +114,8 @@ private fun DiaryListContent( modifier: Modifier = Modifier, monthDiaryCount: Int = 0, weeklyMoodEmoji: String? = null, + /** «수정하기» — 기록 ID 와 이 화면이 보고 있는 달. 달은 여기서만 알 수 있다. */ + onEdit: (Long, YearMonth) -> Unit = { _, _ -> }, onDelete: (Long) -> Unit = {}, onYearMonthChanged: (YearMonth) -> Unit = {}, ) { @@ -152,6 +160,7 @@ private fun DiaryListContent( items(diaries, key = { it.id }) { diary -> DiaryComponent( + onEdit = { onEdit(diary.id, yearMonth) }, diary = diary, modifier = Modifier.padding(vertical = 8.dp), onDelete = { onDelete(diary.id) }, @@ -175,6 +184,7 @@ private fun DiaryListContent( } gridItems(diaries, key = { it.id }) { diary -> DiaryCard( + onEdit = { onEdit(diary.id, yearMonth) }, diary = diary, onDelete = { onDelete(diary.id) }, ) diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/HomeScreen.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/HomeScreen.kt index 152b367c54..8111316ffd 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/HomeScreen.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/screen/sender/HomeScreen.kt @@ -38,6 +38,7 @@ import com.afternote.feature.mindrecord.presentation.viewmodel.DailyQuestionList import com.afternote.feature.mindrecord.presentation.viewmodel.DiaryListViewModel import com.afternote.feature.mindrecord.presentation.viewmodel.WeeklyReportViewModel import kotlinx.coroutines.flow.drop +import java.time.YearMonth import com.afternote.feature.mindrecord.presentation.R as MindRecordR @Composable @@ -51,6 +52,8 @@ fun HomeScreen( // 프리페치가 되므로 트레이드오프를 받아들인다. weeklyReportViewModel: WeeklyReportViewModel = hiltViewModel(), onWriteClick: (MindRecordCategoryUi) -> Unit = {}, + onEditDailyQuestion: (Long) -> Unit = {}, + onEditDiary: (Long, YearMonth) -> Unit = { _, _ -> }, ) { // Figma 2757:16116 — 마음의 기록 탭은 데일리 질문 / 일기 / 주간리포트 3개 val categories = @@ -175,9 +178,23 @@ fun HomeScreen( HorizontalPager(state = pagerState) { _ -> when (selectedCategory) { - MindRecordCategoryUi.DailyQuestion -> DailyQuestionAnswerListScreen(isListView = isListView) - MindRecordCategoryUi.Diary -> DiaryScreen(isListView = isListView) - else -> WeeklyReportScreen() + MindRecordCategoryUi.DailyQuestion -> { + DailyQuestionAnswerListScreen( + isListView = isListView, + onEditClick = onEditDailyQuestion, + ) + } + + MindRecordCategoryUi.Diary -> { + DiaryScreen( + isListView = isListView, + onEditClick = onEditDiary, + ) + } + + else -> { + WeeklyReportScreen() + } } } } diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteUiState.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteUiState.kt index ceec9a3000..33a096b459 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteUiState.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteUiState.kt @@ -7,7 +7,11 @@ data class DailyQuestionWriteUiState( /** "Day N" 배너 표기용 — 오늘의 질문이 서비스 기준 몇 일차인지. */ val questionDay: Int? = null, val questionContent: String = "", - /** 오늘 이미 임시저장된 답변 레코드 ID — null 이 아니면 제출 시 POST 대신 PATCH 로 전환한다. */ + /** + * 수정 대상 답변 레코드 ID — null 이 아니면 제출 시 POST 대신 PATCH 로 전환한다. + * + * 오늘의 임시저장 이어쓰기와 목록의 "수정하기" 가 같은 값을 쓴다 (#582). + */ val draftId: Long? = null, /** * 이어쓸 임시저장 본문이 도착했는지. @@ -17,6 +21,11 @@ data class DailyQuestionWriteUiState( */ val draftLoaded: Boolean = false, val answer: String = "", + /** + * 본문이 서버에서 채워졌는지. 리치 에디터는 [answer] 를 **초기 시드로만** 읽으므로, + * 비동기 프리필이 끝난 뒤 에디터를 다시 만들어야 내용이 보인다 (#582). + */ + val contentLoaded: Boolean = false, val isQuestionLoading: Boolean = true, val questionLoadError: UiText? = null, /** 이어쓸 임시저장 본문을 불러오는 중 (#923). */ @@ -37,7 +46,9 @@ data class DailyQuestionWriteUiState( val canSubmit: Boolean get() = answer.isNotBlank() && - !isQuestionLoading && + // 수정 모드는 오늘 질문을 부르지 않아 questionId 가 없다 — 대상 레코드가 + // 있으면 저장할 수 있다 (#582). 그래서 isQuestionLoading 대신 «둘 중 하나» 를 본다. + (questionId != null || draftId != null) && // 이어쓸 본문이 도착하기 전에 저장하면 draftId 가 아직 null 이라 POST 로 나가고, // 서버가 questionId upsert 라 기존 임시저장 본문이 덮인다 — #923 과 같은 유실이다. !isResumingDraft && diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModel.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModel.kt index 4bd97cba7d..9475326f8c 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModel.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModel.kt @@ -1,14 +1,17 @@ package com.afternote.feature.mindrecord.presentation.viewmodel import androidx.annotation.StringRes +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute import com.afternote.core.domain.repository.PhotoUploadRepository import com.afternote.core.ui.UiText import com.afternote.feature.mindrecord.domain.model.DailyQuestionCreatePayload import com.afternote.feature.mindrecord.domain.model.DailyQuestionUpdatePayload import com.afternote.feature.mindrecord.domain.repository.DailyQuestionRepository import com.afternote.feature.mindrecord.presentation.R +import com.afternote.feature.mindrecord.presentation.navigation.MindRecordRoute import com.afternote.feature.mindrecord.presentation.util.isHtmlBlank import com.afternote.feature.mindrecord.presentation.util.toUploadedFileKey import dagger.hilt.android.lifecycle.HiltViewModel @@ -24,6 +27,7 @@ import javax.inject.Inject class DailyQuestionWriteViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, private val repository: DailyQuestionRepository, private val photoUploadRepository: PhotoUploadRepository, private val draftLoader: MindRecordDraftLoader, @@ -31,8 +35,13 @@ class DailyQuestionWriteViewModel private val _uiState = MutableStateFlow(DailyQuestionWriteUiState()) val uiState: StateFlow = _uiState.asStateFlow() + /** 수정 대상 답변 ID. 목록의 "수정하기" 로 들어오면 채워진다 (#582). */ + private val editingAnswerId: Long? = + savedStateHandle.toRoute().answerId + init { - loadTodayQuestion() + // 수정 진입이면 대상 레코드를 프리필하고, 신규면 오늘 질문을 부른다 (#582). + if (editingAnswerId != null) loadAnswer(editingAnswerId) else loadTodayQuestion() loadDraftCount() } @@ -45,6 +54,43 @@ class DailyQuestionWriteViewModel } } + /** + * 정식 답변을 프리필한다 (#582). + * + * 오늘 질문을 다시 묻지 않는다 — 수정 대상은 이미 특정된 레코드이고, 저장은 + * `PATCH /daily-questions/{id}` 로 나간다. `questionId` 도 그래서 필요 없다. + */ + private fun loadAnswer(answerId: Long) { + viewModelScope.launch { + _uiState.update { it.copy(isQuestionLoading = true, questionLoadError = null) } + repository + .getList() + .mapCatching { list -> list.first { it.dailyQuestionId == answerId } } + .onSuccess { answer -> + _uiState.update { + it.copy( + draftId = answer.dailyQuestionId, + questionContent = answer.title, + answer = answer.content, + isQuestionLoading = false, + contentLoaded = true, + ) + } + }.onFailure { e -> + _uiState.update { + it.copy( + isQuestionLoading = false, + questionLoadError = + UiText.DynamicOrResource( + value = e.message, + fallbackResId = R.string.mindrecord_error_daily_question_today_failed, + ), + ) + } + } + } + } + private fun loadTodayQuestion() { viewModelScope.launch { _uiState.update { it.copy(isQuestionLoading = true, questionLoadError = null) } @@ -196,15 +242,19 @@ class DailyQuestionWriteViewModel return } + // 수정·이어쓰기는 오늘 질문을 부르지 않아 questionId 가 없다. PATCH 는 대상 레코드 + // ID 만 있으면 되고 명세에도 questionId 가 없다 (#582). 그래서 «둘 다 없을 때» 만 + // 막는다 — 신규 작성인데 오늘 질문 조회가 실패한 경우다. val questionId = state.questionId - if (questionId == null) { - // 오늘 질문이 없으면 서버가 답변을 어디에 붙일지 알 수 없어 요청 자체가 불가능하다. - // 사유를 알리고 조회를 다시 걸어 사용자가 재시도할 수 있게 한다. + if (questionId == null && state.draftId == null) { + // 서버가 답변을 어디에 붙일지 알 수 없어 요청 자체가 불가능하다. 사유를 + // 알리고 조회를 다시 걸어 사용자가 재시도할 수 있게 한다 (#565). failSubmit(R.string.mindrecord_error_daily_question_missing) // 이미 조회 중이면 그대로 둔다 — 연타로 같은 요청을 겹쳐 쌓지 않는다. if (!state.isQuestionLoading) loadTodayQuestion() return } + if (!state.canSubmit) return viewModelScope.launch { _uiState.update { it.copy(submitState = SubmitState.InProgress) } @@ -225,7 +275,8 @@ class DailyQuestionWriteViewModel DailyQuestionCreatePayload( content = state.answer.toWireContent(), isDraft = isDraft, - questionId = questionId, + // 생성 경로는 questionId 가 반드시 있다 — 위 가드가 null 을 걸렀다. + questionId = requireNotNull(questionId), ), ) } diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DiaryWriteViewModel.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DiaryWriteViewModel.kt index acd17dcf77..98d387de80 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DiaryWriteViewModel.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DiaryWriteViewModel.kt @@ -28,8 +28,9 @@ import javax.inject.Inject * 일기 작성/이어쓰기 ViewModel. * * - 신규 작성: `POST /diary` (+ 선택한 수신자 ID 를 `receiverIds` 로 함께 전송). - * - 이어쓰기: 라우트의 `draftId` 로 해당 달 draft 목록에서 항목을 찾아 프리필하고, - * 저장 시 `PATCH /diary/{diaryId}` 로 수정한다 (등록 시 isDraft=false 로 전환). + * - 수정: 라우트의 `recordId` 로 해당 달 목록에서 항목을 찾아 프리필하고, 저장 시 + * `PATCH /diary/{diaryId}` 로 수정한다. 임시저장 이어쓰기(`isDraft=true`)와 정식 기록 + * 수정(`isDraft=false`)이 같은 경로를 쓰며, 조회하는 목록의 `draftOnly` 만 다르다 (#582). */ @HiltViewModel class DiaryWriteViewModel @@ -42,18 +43,22 @@ class DiaryWriteViewModel private val draftLoader: MindRecordDraftLoader, ) : ViewModel() { private val route = savedStateHandle.toRoute() - private val editingDraftId: Long? = route.draftId + + /** 수정 대상 일기 ID. 임시저장 이어쓰기와 정식 기록 수정이 같은 값을 쓴다 (#582). */ + private val editingDiaryId: Long? = route.recordId private val _uiState = MutableStateFlow(DiaryWriteUiState()) val uiState: StateFlow = _uiState.asStateFlow() init { - if (editingDraftId != null) { + // 이어쓰기(임시저장)일 때만 «이어쓰는 중» 으로 표시한다 — 정식 기록 수정은 아니다 (#582). + if (editingDiaryId != null && route.isDraft) { _uiState.update { it.copy(isEditingDraft = true) } } loadReceivers() loadDraftCount() - editingDraftId?.let { loadDraft(it, route.draftYearMonth) } + // 라우트가 draftId/draftYearMonth → recordId/yearMonth/isDraft 로 일반화됐다 (#582). + editingDiaryId?.let { loadExisting(it, route.yearMonth, route.isDraft) } } /** 툴바 카운트는 화면 장식이라 실패해도 화면을 막지 않고 '모름' 으로 남긴다. */ @@ -127,9 +132,9 @@ class DiaryWriteViewModel viewModelScope.launch { _uiState.update { it.copy(submitState = SubmitState.InProgress) } val result = - if (editingDraftId != null) { + if (editingDiaryId != null) { repository.update( - id = editingDraftId, + id = editingDiaryId, payload = DiaryUpdatePayload( title = state.title, @@ -185,15 +190,25 @@ class DiaryWriteViewModel } // 단건 조회 엔드포인트가 없어 해당 달의 draft 목록에서 id 로 찾는다. - private fun loadDraft( - draftId: Long, + + /** + * 수정 대상 일기를 프리필한다. + * + * 임시저장과 정식 기록은 **같은 목록 API 의 `draftOnly` 만 다르다.** 종전에는 임시저장 + * 경로만 있어서 목록의 "수정하기" 가 갈 곳이 없었다 (#582). + */ + private fun loadExisting( + diaryId: Long, yearMonth: String?, + isDraft: Boolean, ) { viewModelScope.launch { _uiState.update { it.copy(isDraftLoading = true, draftLoadError = null) } repository - .getList(yearMonth = yearMonth ?: YearMonth.now().toString(), draftOnly = true) - .mapCatching { list -> list.diaries.first { it.diaryId == draftId } } + .getList( + yearMonth = yearMonth ?: YearMonth.now().toString(), + draftOnly = if (isDraft) true else null, + ).mapCatching { list -> list.diaries.first { it.diaryId == diaryId } } .onSuccess { draft -> _uiState.update { it.copy( diff --git a/feature/mindrecord/presentation/src/main/res/values/strings.xml b/feature/mindrecord/presentation/src/main/res/values/strings.xml index 1cfe725122..e67c22f8bb 100644 --- a/feature/mindrecord/presentation/src/main/res/values/strings.xml +++ b/feature/mindrecord/presentation/src/main/res/values/strings.xml @@ -26,6 +26,11 @@ 삭제하기 + + 이 기록을 삭제할까요? + 삭제한 기록은 되돌릴 수 없어요. + 삭제 + 취소 수정하기 더보기 메뉴 diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionEditTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionEditTest.kt new file mode 100644 index 0000000000..2ad73b35c2 --- /dev/null +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionEditTest.kt @@ -0,0 +1,182 @@ +package com.afternote.feature.mindrecord.presentation.viewmodel + +import androidx.lifecycle.SavedStateHandle +import com.afternote.core.domain.repository.PhotoUploadRepository +import com.afternote.feature.mindrecord.domain.model.DailyQuestion +import com.afternote.feature.mindrecord.domain.model.DailyQuestionCreatePayload +import com.afternote.feature.mindrecord.domain.model.DailyQuestionUpdatePayload +import com.afternote.feature.mindrecord.domain.model.DiaryCreatePayload +import com.afternote.feature.mindrecord.domain.model.DiaryList +import com.afternote.feature.mindrecord.domain.model.DiaryUpdatePayload +import com.afternote.feature.mindrecord.domain.model.TodayDailyQuestion +import com.afternote.feature.mindrecord.domain.repository.DailyQuestionRepository +import com.afternote.feature.mindrecord.domain.repository.DiaryRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * 데일리질문 "수정하기" 경로 가드 (#582). + * + * 종전에는 목록의 수정하기가 빈 콜백이라 팝업만 닫히고 아무 일도 일어나지 않았다. + * 편집 경로가 붙은 뒤에도 두 가지가 어긋나기 쉽다. + * + * - 수정 모드는 **오늘 질문을 부르지 않는다** — 대상은 이미 특정된 레코드다. + * - 저장은 새 답변을 만드는 POST 가 아니라 그 레코드를 고치는 PATCH 여야 한다. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class DailyQuestionEditTest { + @Before + fun setUp() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `수정 모드는 대상 답변을 프리필하고 오늘 질문을 부르지 않는다`() { + val repository = FakeRepository(answers = listOf(answer(id = 7L, content = "

원본

"))) + + val viewModel = editViewModel(repository, answerId = 7L) + + assertEquals("

원본

", viewModel.uiState.value.answer) + assertEquals(7L, viewModel.uiState.value.draftId) + assertEquals("오늘 질문은 부르지 않는다", 0, repository.todayCalls) + assertTrue("에디터를 다시 시드해야 본문이 보인다", viewModel.uiState.value.contentLoaded) + } + + @Test + fun `수정 모드 저장은 새로 만들지 않고 PATCH 로 나간다`() { + val repository = FakeRepository(answers = listOf(answer(id = 7L, content = "

원본

"))) + val viewModel = editViewModel(repository, answerId = 7L) + + viewModel.onAnswerChanged("

고친 본문

") + viewModel.submit() + + assertEquals(0, repository.createCalls) + assertEquals(listOf(7L), repository.updatedIds) + assertEquals("

고친 본문

", repository.updatedPayloads.single().content) + } + + @Test + fun `수정 모드는 questionId 가 없어도 저장할 수 있다`() { + // 명세의 PATCH 요청에는 questionId 가 없다 — 대상 레코드 ID 만 있으면 된다. + val repository = FakeRepository(answers = listOf(answer(id = 7L, content = "

원본

"))) + val viewModel = editViewModel(repository, answerId = 7L) + + assertNull(viewModel.uiState.value.questionId) + assertTrue("저장 버튼이 살아 있어야 한다", viewModel.uiState.value.canSubmit) + } + + @Test + fun `신규 작성은 종전대로 오늘 질문을 부른다`() { + val repository = FakeRepository(answers = emptyList()) + + val viewModel = editViewModel(repository, answerId = null) + + assertEquals(1, repository.todayCalls) + assertEquals("", viewModel.uiState.value.answer) + } + + // ── 테스트 도구 ─────────────────────────────────────────────────────────── + + private fun editViewModel( + repository: DailyQuestionRepository, + answerId: Long?, + ): DailyQuestionWriteViewModel { + val handle = + SavedStateHandle( + if (answerId == null) emptyMap() else mapOf("answerId" to answerId), + ) + return DailyQuestionWriteViewModel( + handle, + repository, + PhotoUploadRepository { _, _ -> error("업로드는 이 시나리오에서 호출되면 안 됨") }, + // 툴바 카운트는 이 시나리오의 관심사가 아니다 (#769). + MindRecordDraftLoader( + diaryRepository = EditTestEmptyDiaryRepository, + dailyQuestionRepository = repository, + ), + ) + } + + private fun answer( + id: Long, + content: String, + ) = DailyQuestion( + dailyQuestionId = id, + title = "오늘의 질문", + content = content, + createdAt = "2026.08.23 일", + ) + + private class FakeRepository( + private val answers: List, + ) : DailyQuestionRepository { + var todayCalls = 0 + var createCalls = 0 + val updatedIds = mutableListOf() + val updatedPayloads = mutableListOf() + + override suspend fun getList( + date: String?, + draftOnly: Boolean?, + ): Result> = Result.success(answers) + + override suspend fun getToday(): Result { + todayCalls++ + return Result.success( + TodayDailyQuestion(questionId = 1L, day = 1, content = "오늘의 질문", isAnswered = false), + ) + } + + override suspend fun create(payload: DailyQuestionCreatePayload): Result { + createCalls++ + return Result.success(1L) + } + + override suspend fun update( + id: Long, + payload: DailyQuestionUpdatePayload, + ): Result { + updatedIds += id + updatedPayloads += payload + return Result.success(id) + } + + override suspend fun delete(id: Long): Result = error("delete 는 이 시나리오에서 호출되면 안 됨") + } +} + +/** 툴바 카운트 조회만 받아 주는 빈 일기 저장소. */ +private object EditTestEmptyDiaryRepository : DiaryRepository { + override suspend fun getList( + yearMonth: String, + draftOnly: Boolean?, + ): Result = Result.success(DiaryList(diaries = emptyList(), monthDiaryCount = 0, weeklyDominantMood = null)) + + override suspend fun create(payload: DiaryCreatePayload): Result = error("호출되면 안 됨") + + override suspend fun update( + id: Long, + payload: DiaryUpdatePayload, + ): Result = error("호출되면 안 됨") + + override suspend fun delete(id: Long): Result = error("호출되면 안 됨") +} diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionResumeDraftTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionResumeDraftTest.kt index 2e18366b6d..35623ca010 100644 --- a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionResumeDraftTest.kt +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionResumeDraftTest.kt @@ -1,5 +1,6 @@ package com.afternote.feature.mindrecord.presentation.viewmodel +import androidx.lifecycle.SavedStateHandle import com.afternote.core.domain.repository.PhotoUploadRepository import com.afternote.feature.mindrecord.domain.model.DailyQuestion import com.afternote.feature.mindrecord.domain.model.DailyQuestionCreatePayload @@ -69,6 +70,7 @@ class DailyQuestionResumeDraftTest { val state = DailyQuestionWriteUiState( answer = "무언가", + questionId = 1L, isQuestionLoading = false, isResumingDraft = true, ) @@ -79,9 +81,12 @@ class DailyQuestionResumeDraftTest { @Test fun `불러오기가 끝나면 저장이 다시 열린다`() { // 차단만 걸고 복귀를 빠뜨리면 저장이 영구히 잠긴다. + // #582 로 canSubmit 이 «questionId 또는 draftId 중 하나» 도 요구한다 — 수정 모드는 + // 오늘 질문을 부르지 않아 questionId 가 없기 때문이다. 신규 작성 상태를 재현한다. val state = DailyQuestionWriteUiState( answer = "무언가", + questionId = 1L, isQuestionLoading = false, isResumingDraft = false, ) @@ -104,6 +109,7 @@ class DailyQuestionResumeDraftTest { val repository = fakeRepository() val viewModel = DailyQuestionWriteViewModel( + savedStateHandle = SavedStateHandle(emptyMap()), repository = repository, photoUploadRepository = PhotoUploadRepository { _, _ -> Result.success("") }, // 툴바 카운트는 이 테스트의 관심사가 아니다 — 같은 저장소를 넘겨 0건으로 둔다 (#769). @@ -143,6 +149,7 @@ class DailyQuestionResumeDraftTest { val repository = fakeRepository() val viewModel = DailyQuestionWriteViewModel( + savedStateHandle = SavedStateHandle(emptyMap()), repository = repository, photoUploadRepository = PhotoUploadRepository { _, _ -> Result.success("") }, // 툴바 카운트는 이 테스트의 관심사가 아니다 — 같은 저장소를 넘겨 0건으로 둔다 (#769). diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModelTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModelTest.kt index 0c848b6be8..7fc161c41d 100644 --- a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModelTest.kt +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModelTest.kt @@ -1,5 +1,6 @@ package com.afternote.feature.mindrecord.presentation.viewmodel +import androidx.lifecycle.SavedStateHandle import com.afternote.core.domain.repository.PhotoUploadRepository import com.afternote.feature.mindrecord.domain.model.DailyQuestion import com.afternote.feature.mindrecord.domain.model.DailyQuestionCreatePayload @@ -57,7 +58,13 @@ class DailyQuestionWriteViewModelTest { }, onGetList = { Result.success(listOf(draft(content = "서버에 남아 있던 옛 임시저장본"))) }, ) - val viewModel = DailyQuestionWriteViewModel(repository, NoopPhotoUploadRepository, noopDraftLoader()) + val viewModel = + DailyQuestionWriteViewModel( + SavedStateHandle(emptyMap()), + repository, + NoopPhotoUploadRepository, + noopDraftLoader(), + ) viewModel.onAnswerChanged("사용자가 방금 입력한 답변") viewModel.submit() @@ -76,7 +83,13 @@ class DailyQuestionWriteViewModelTest { onGetList = { Result.success(listOf(draft(content = "이어쓸 본문"))) }, ) - val viewModel = DailyQuestionWriteViewModel(repository, NoopPhotoUploadRepository, noopDraftLoader()) + val viewModel = + DailyQuestionWriteViewModel( + SavedStateHandle(emptyMap()), + repository, + NoopPhotoUploadRepository, + noopDraftLoader(), + ) assertEquals("이어쓸 본문", viewModel.uiState.value.answer) assertEquals(7L, viewModel.uiState.value.draftId) @@ -99,6 +112,7 @@ class DailyQuestionWriteViewModelTest { ) val viewModel = DailyQuestionWriteViewModel( + SavedStateHandle(emptyMap()), repository, PhotoUploadRepository { _, _ -> Result.success("https://cdn/just-picked.jpg") }, noopDraftLoader(), @@ -123,6 +137,7 @@ class DailyQuestionWriteViewModelTest { ) val viewModel = DailyQuestionWriteViewModel( + SavedStateHandle(emptyMap()), repository, PhotoUploadRepository { _, _ -> Result.success("https://cdn/picked.jpg") }, noopDraftLoader(), @@ -145,7 +160,13 @@ class DailyQuestionWriteViewModelTest { onGetList = { Result.success(listOf(draft(content = html))) }, ) - val viewModel = DailyQuestionWriteViewModel(repository, NoopPhotoUploadRepository, noopDraftLoader()) + val viewModel = + DailyQuestionWriteViewModel( + SavedStateHandle(emptyMap()), + repository, + NoopPhotoUploadRepository, + noopDraftLoader(), + ) assertEquals(html, viewModel.uiState.value.answer) } @@ -157,7 +178,13 @@ class DailyQuestionWriteViewModelTest { onGetToday = { Result.failure(IllegalStateException("네트워크 실패")) }, onGetList = { Result.success(emptyList()) }, ) - val viewModel = DailyQuestionWriteViewModel(repository, NoopPhotoUploadRepository, noopDraftLoader()) + val viewModel = + DailyQuestionWriteViewModel( + SavedStateHandle(emptyMap()), + repository, + NoopPhotoUploadRepository, + noopDraftLoader(), + ) viewModel.onAnswerChanged("답변") viewModel.submit() @@ -200,6 +227,7 @@ class DailyQuestionWriteViewModelTest { ) val viewModel = DailyQuestionWriteViewModel( + SavedStateHandle(emptyMap()), repository, PhotoUploadRepository { _, _ -> Result.success("https://cdn.example.net/mindrecords/staging/13/a.png") @@ -227,7 +255,7 @@ class DailyQuestionWriteViewModelTest { Result.success(Unit) }, ) - val viewModel = DailyQuestionWriteViewModel(repository, NoopPhotoUploadRepository, noopDraftLoader()) + val viewModel = DailyQuestionWriteViewModel(SavedStateHandle(emptyMap()), repository, NoopPhotoUploadRepository, noopDraftLoader()) viewModel.onAnswerChanged("

수정

") viewModel.submit() diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/MindRecordFailureRecoveryTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/MindRecordFailureRecoveryTest.kt index 11b97a3509..f6a70c5aa4 100644 --- a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/MindRecordFailureRecoveryTest.kt +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/MindRecordFailureRecoveryTest.kt @@ -1,5 +1,6 @@ package com.afternote.feature.mindrecord.presentation.viewmodel +import androidx.lifecycle.SavedStateHandle import com.afternote.core.domain.repository.PhotoUploadRepository import com.afternote.feature.mindrecord.domain.model.DailyQuestion import com.afternote.feature.mindrecord.domain.model.DailyQuestionCreatePayload @@ -110,6 +111,7 @@ class MindRecordFailureRecoveryTest { val repository = NeverSubmittingRepository() val viewModel = DailyQuestionWriteViewModel( + savedStateHandle = SavedStateHandle(emptyMap()), repository = repository, photoUploadRepository = PhotoUploadRepository { _, _ -> uploadGate.await() }, // 툴바 카운트는 이 테스트의 관심사가 아니다 — 빈 목록으로 고정한다 (#769).