Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ class TimeLetterMindRecordCompletionAndroidTest {
composeRule.runOnIdle {
writeViewModel =
DailyQuestionWriteViewModel(
savedStateHandle = SavedStateHandle(emptyMap()),
repository = repository,
photoUploadRepository = CompletionPhotoUploadRepository,
draftLoader = MindRecordDraftLoader(CompletionDiaryRepository(mutableListOf()), repository),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ fun rememberMindRecordNavActions(navController: NavController): MindRecordNavAct
}

override fun onWriteDailyQuestion() {
navController.navigate(MindRecordRoute.DailyQuestionWriteRoute)
navController.navigate(MindRecordRoute.DailyQuestionWriteRoute())
}

override fun onWriteDiary() {
Expand All @@ -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),
)
}
}
Expand Down Expand Up @@ -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() {
Expand Down
6 changes: 6 additions & 0 deletions feature/mindrecord/presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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,
)
}
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] 등록 묶음.
Expand All @@ -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<Route.MemorySpace> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" 영역에서 진입하는 임시저장 목록 화면. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Long?>(null) }

pendingDeleteId?.let { id ->
DeleteConfirmDialog(
onConfirm = {
pendingDeleteId = null
viewModel.delete(id)
},
onDismiss = { pendingDeleteId = null },
)
}

when (val state = uiState) {
DailyQuestionListUiState.Loading -> {
Expand Down Expand Up @@ -77,7 +90,9 @@ fun DailyQuestionAnswerListScreen(
isListView = isListView,
todayQuestion = state.todayQuestion,
answers = state.answers,
onDelete = viewModel::delete,
onEdit = onEditClick,
// 삭제는 되돌릴 수 없다 — 종전에는 메뉴를 누르는 즉시 실행됐다 (#582).
onDelete = { pendingDeleteId = it },
)
}
}
Expand All @@ -90,6 +105,7 @@ private fun DailyQuestionListContent(
answers: List<DailyQuestion>,
modifier: Modifier = Modifier,
todayQuestion: TodayQuestionUi? = null,
onEdit: (Long) -> Unit = {},
onDelete: (Long) -> Unit = {},
) {
var yearMonth by remember { mutableStateOf(YearMonth.now()) }
Expand Down Expand Up @@ -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) },
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -89,6 +94,7 @@ fun DiaryScreen(
yearMonth = state.yearMonth,
monthDiaryCount = state.monthDiaryCount,
weeklyMoodEmoji = state.weeklyDominantMood?.toEmoji(),
onEdit = onEditClick,
onDelete = viewModel::delete,
onYearMonthChanged = viewModel::selectYearMonth,
)
Expand All @@ -98,7 +104,7 @@ fun DiaryScreen(
}

@Composable
private fun DiaryListContent(
internal fun DiaryListContent(
isListView: Boolean,
diaries: List<DailyDiary>,
// 조회 중인 월은 VM 이 들고 있다 — 자동 갱신이 같은 월을 다시 조회해야 하고,
Expand All @@ -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 = {},
) {
Expand Down Expand Up @@ -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) },
Expand All @@ -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) },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 =
Expand Down Expand Up @@ -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()
}
}
}
}
Expand Down
Loading
Loading