From 4f79f526ad1818487c4d116f7b9f045b57d9e88f Mon Sep 17 00:00:00 2001 From: hyhyh Date: Wed, 18 Feb 2026 20:21:46 +0900 Subject: [PATCH] dailyquestion --- .../data/api/DailyRecordApiService.kt | 11 +- .../dailyrecord/data/dto/DailyRecordDto.kt | 41 ++- .../data/repository/MindRecordRepository.kt | 10 +- .../repository/MindRecordRepositoryImpl.kt | 25 +- .../domain/model/DailyQuestionData.kt | 13 + .../domain/model/MindRecordsResult.kt | 11 + .../domain/usecase/CreateMindRecordUseCase.kt | 10 +- .../domain/usecase/GetDailyQuestionUseCase.kt | 18 ++ .../domain/usecase/GetMindRecordsUseCase.kt | 17 +- .../component/RecordCalendarSort.kt | 27 +- .../component/RecordDiaryContentItem.kt | 60 ++++- .../RecordDiaryQuestionContentItem.kt | 5 +- .../presentation/component/RecordListItem.kt | 4 +- .../presentation/component/RecordListSort.kt | 6 +- .../navigiation/RecordNavgraph.kt | 75 ++++-- .../presentation/navigiation/RecordRoute.kt | 2 +- .../screen/RecordDailyQuestionListScreen.kt | 14 +- .../screen/RecordDeepMindScreen.kt | 51 ++-- .../presentation/screen/RecordDiaryScreen.kt | 21 +- .../screen/RecordFirstDiaryListScreen.kt | 36 ++- .../screen/RecordQuestionScreen.kt | 73 ++++-- .../presentation/uimodel/MindRecordUiState.kt | 9 +- .../viewmodel/MindRecordViewModel.kt | 242 +++++++++++++----- .../viewmodel/MindRecordWriterViewModel.kt | 65 ++++- .../home/presentation/screen/HomeScreen.kt | 2 +- 25 files changed, 622 insertions(+), 226 deletions(-) create mode 100644 app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/model/DailyQuestionData.kt create mode 100644 app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/model/MindRecordsResult.kt create mode 100644 app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/GetDailyQuestionUseCase.kt diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/api/DailyRecordApiService.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/api/DailyRecordApiService.kt index 319364e7..0ad99466 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/api/DailyRecordApiService.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/api/DailyRecordApiService.kt @@ -9,9 +9,16 @@ import retrofit2.http.* */ interface DailyRecordApiService { - // 전체 기록 목록 조회 + @GET("daily-question") + suspend fun getDailyQuestion(): DailyQuestionResponse + @GET("mind-records") - suspend fun getMindRecords(): MindRecordListResponse + suspend fun getMindRecords( + @Query("type") type: String, + @Query("view") view: String = "LIST", + @Query("year") year: Int? = null, + @Query("month") month: Int? = null + ): MindRecordListResponse // 기록 작성 @POST("mind-records") diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/dto/DailyRecordDto.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/dto/DailyRecordDto.kt index 09da57d9..a4dca258 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/dto/DailyRecordDto.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/dto/DailyRecordDto.kt @@ -1,7 +1,8 @@ package com.kuit.afternote.feature.dailyrecord.data.dto +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable - +import kotlinx.serialization.json.JsonNames /** * 마음의 기록 API DTOs. (스웨거 기준) @@ -27,7 +28,7 @@ data class MindRecordListResponse( */ @Serializable data class MindRecordListData( - /** 기록 요약 리스트 */ + /** 기록 요약 리스트 (각 항목에 content 포함) */ val records: List, /** 표시된 날짜들 (달력 하이라이트용) */ val markedDates: List? = emptyList() @@ -35,19 +36,23 @@ data class MindRecordListData( /** * 기록 요약 정보 + * DAILY_QUESTION 타입은 API에서 question 필드로 질문 내용을 반환할 수 있음. */ +@OptIn(ExperimentalSerializationApi::class) @Serializable data class MindRecordSummary( /** 기록 고유 ID */ val recordId: Long, /** 기록 타입 (예: DIARY, LETTER 등) */ val type: String, - /** 기록 제목 */ - val title: String, + /** 기록 제목 (API: title 또는 question) */ + @JsonNames("question", "title") val title: String? = null, /** 기록 날짜 (yyyy-MM-dd) */ val date: String, /** 임시 저장 여부 */ - val isDraft: Boolean + val isDraft: Boolean, + /** 기록 내용 (API: content 또는 answer). DAILY_QUESTION은 answer로 반환될 수 있음. */ + @JsonNames("answer", "content") val content: String? = null ) /** @@ -79,6 +84,17 @@ data class MindRecordDetail( val category: String? ) +/** + * 데일리 질문 조회 응답 (GET /daily-question) + * 오늘 날짜 기준으로 서버가 1개의 질문을 반환합니다. + * API 스펙: { questionId, content } + */ +@Serializable +data class DailyQuestionResponse( + val questionId: Long, + val content: String +) + /** * 기록 작성 요청 DTO (POST /mind-records) */ @@ -118,12 +134,19 @@ data class PostMindRecordData( @Serializable data class CreateMindRecordRequest( val type: String, - val title: String?, // nullable 허용 - val content: String?, // nullable 허용 - val date: String?, // nullable 허용 + val title: String?, + val content: String?, + val date: String?, val isDraft: Boolean, val questionId: Long? = null, - val category: String? = null + val category: String? = null, + val imageList: List? = emptyList() // 추가 +) + +@Serializable +data class MindRecordImageItem( + val mediaType: String, + val imageUrl: String ) diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/repository/MindRecordRepository.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/repository/MindRecordRepository.kt index af98b683..e704fbee 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/repository/MindRecordRepository.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/repository/MindRecordRepository.kt @@ -5,9 +5,17 @@ import com.kuit.afternote.feature.dailyrecord.data.dto.MindRecordDetailResponse import com.kuit.afternote.feature.dailyrecord.data.dto.MindRecordListResponse import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordRequest import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordResponse +import com.kuit.afternote.feature.dailyrecord.domain.model.DailyQuestionData interface MindRecordRepository { - suspend fun getMindRecords(): MindRecordListResponse + suspend fun getDailyQuestion(): DailyQuestionData? + + suspend fun getMindRecords( + type: String, + view: String = "LIST", + year: Int? = null, + month: Int? = null + ): MindRecordListResponse suspend fun createMindRecord(request: CreateMindRecordRequest): PostMindRecordResponse diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/repository/MindRecordRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/repository/MindRecordRepositoryImpl.kt index 7cc62386..44cac9ea 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/repository/MindRecordRepositoryImpl.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/repository/MindRecordRepositoryImpl.kt @@ -6,13 +6,27 @@ import com.kuit.afternote.feature.dailyrecord.data.dto.MindRecordDetailResponse import com.kuit.afternote.feature.dailyrecord.data.dto.MindRecordListResponse import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordRequest import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordResponse +import com.kuit.afternote.feature.dailyrecord.domain.model.DailyQuestionData +import retrofit2.HttpException import javax.inject.Inject class MindRecordRepositoryImpl @Inject constructor( private val apiService: DailyRecordApiService -) : MindRecordRepository{ - override suspend fun getMindRecords(): MindRecordListResponse { - return apiService.getMindRecords() +) : MindRecordRepository { + override suspend fun getDailyQuestion(): DailyQuestionData? = try { + val response = apiService.getDailyQuestion() + DailyQuestionData(questionId = response.questionId, content = response.content) + } catch (e: Exception) { + null + } + + override suspend fun getMindRecords( + type: String, + view: String, + year: Int?, + month: Int? + ): MindRecordListResponse { + return apiService.getMindRecords(type = type, view = view, year = year, month = month) } override suspend fun createMindRecord(request: CreateMindRecordRequest): PostMindRecordResponse { @@ -20,7 +34,10 @@ class MindRecordRepositoryImpl @Inject constructor( } override suspend fun deleteMindRecord(recordId: Long) { - apiService.deleteMindRecord(recordId) + val response = apiService.deleteMindRecord(recordId) + if (!response.isSuccessful) { + throw HttpException(response) + } } override suspend fun getMindRecord(recordId: Long): MindRecordDetailResponse { val response = apiService.getMindRecord(recordId) diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/model/DailyQuestionData.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/model/DailyQuestionData.kt new file mode 100644 index 00000000..5b18b8ec --- /dev/null +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/model/DailyQuestionData.kt @@ -0,0 +1,13 @@ +package com.kuit.afternote.feature.dailyrecord.domain.model + +/** + * 오늘의 데일리 질문 정보. + * GET /daily-question API 응답을 도메인 모델로 변환한 결과. + * + * @param questionId 데일리 질문 ID (POST /mind-records 시 필수) + * @param content 질문 내용 (UI 표시용) + */ +data class DailyQuestionData( + val questionId: Long, + val content: String +) diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/model/MindRecordsResult.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/model/MindRecordsResult.kt new file mode 100644 index 00000000..99390280 --- /dev/null +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/model/MindRecordsResult.kt @@ -0,0 +1,11 @@ +package com.kuit.afternote.feature.dailyrecord.domain.model + +import com.kuit.afternote.feature.dailyrecord.data.dto.MindRecordSummary + +/** + * 마음의 기록 목록 조회 결과 (records + markedDates) + */ +data class MindRecordsResult( + val records: List, + val markedDates: List = emptyList() +) diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/CreateMindRecordUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/CreateMindRecordUseCase.kt index 79817df9..53a8ccb8 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/CreateMindRecordUseCase.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/CreateMindRecordUseCase.kt @@ -27,7 +27,15 @@ class CreateMindRecordUseCase @Inject constructor( category: String? = null ): Result { return try { - val request = CreateMindRecordRequest(type, title, content, date, isDraft, questionId, category) + val request = CreateMindRecordRequest( + type = type, + title = title, + content = content ?: "", + date = date, + isDraft = isDraft, + questionId = questionId, + category = category + ) val response = repository.createMindRecord(request) Result.success(response) } catch (e: Exception) { diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/GetDailyQuestionUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/GetDailyQuestionUseCase.kt new file mode 100644 index 00000000..f0e6c9b3 --- /dev/null +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/GetDailyQuestionUseCase.kt @@ -0,0 +1,18 @@ +package com.kuit.afternote.feature.dailyrecord.domain.usecase + +import com.kuit.afternote.feature.dailyrecord.data.repository.MindRecordRepository +import com.kuit.afternote.feature.dailyrecord.domain.model.DailyQuestionData +import javax.inject.Inject + +/** + * 오늘의 데일리 질문을 조회합니다. + * DAILY_QUESTION 타입 기록 작성 시 questionId가 필수이므로 등록 전에 호출합니다. + * content는 UI에 질문 텍스트를 표시할 때 사용합니다. + * + * @return 오늘의 질문 (questionId, content), 실패 시 null + */ +class GetDailyQuestionUseCase @Inject constructor( + private val repository: MindRecordRepository +) { + suspend operator fun invoke(): DailyQuestionData? = repository.getDailyQuestion() +} diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/GetMindRecordsUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/GetMindRecordsUseCase.kt index 69dc1554..e3cdb62e 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/GetMindRecordsUseCase.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/GetMindRecordsUseCase.kt @@ -1,15 +1,24 @@ package com.kuit.afternote.feature.dailyrecord.domain.usecase -import com.kuit.afternote.feature.dailyrecord.data.dto.MindRecordSummary import com.kuit.afternote.feature.dailyrecord.data.repository.MindRecordRepositoryImpl +import com.kuit.afternote.feature.dailyrecord.domain.model.MindRecordsResult import javax.inject.Inject class GetMindRecordsUseCase @Inject constructor( private val repository: MindRecordRepositoryImpl ) { - suspend operator fun invoke(): List { - val response = repository.getMindRecords() - return response.data?.records ?: emptyList() + suspend operator fun invoke( + type: String, + view: String, + year: Int?, + month: Int? + ): MindRecordsResult { + val response = repository.getMindRecords(type, view, year, month) + val data = response.data + return MindRecordsResult( + records = data?.records ?: emptyList(), + markedDates = data?.markedDates ?: emptyList() + ) } } diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordCalendarSort.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordCalendarSort.kt index 3244a0db..d02b1651 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordCalendarSort.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordCalendarSort.kt @@ -19,6 +19,10 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -43,13 +47,24 @@ import java.util.Locale @Composable fun RecordCalendarSort( modifier: Modifier = Modifier, - today: LocalDate + today: LocalDate, + markedDates: Set = emptySet() ) { val year = today.year val month = today.monthValue val formatter = DateTimeFormatter.ofPattern("yyyy년 mm월", Locale.KOREA) - val markedDates = listOf(1, 6, 8, 11, 13, 15, 16, 17, 25) - var selectedDate = today.dayOfMonth + val markedDaysInMonth = markedDates.mapNotNull { dateStr -> + runCatching { + val parts = dateStr.split("-") + if (parts.size == 3 && + parts[0].toIntOrNull() == year && + parts[1].toIntOrNull() == month + ) { + parts[2].toIntOrNull() + } else null + }.getOrNull() + }.filterNotNull().toSet() + var selectedDate by remember { mutableStateOf(today.dayOfMonth) } Box( modifier = Modifier @@ -125,10 +140,10 @@ fun RecordCalendarSort( day == selectedDate -> Color(0xFF328BFF) else -> Color.Transparent } - ).then( - if (day in markedDates) { + ) .then( + if (day in markedDaysInMonth) { Modifier.border( - width = 1.dp, + width = 2.dp, color = Color(0xFF328BFF), shape = CircleShape ) diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordDiaryContentItem.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordDiaryContentItem.kt index 081b2dc5..c1cd5c21 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordDiaryContentItem.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordDiaryContentItem.kt @@ -17,7 +17,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Divider import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -37,6 +38,7 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import com.kuit.afternote.R @@ -65,8 +67,13 @@ fun RecordDiaryContentItem( showDatePicker: Boolean = false, onDatePickerDismiss: () -> Unit, - - ) { + // 깊은 생각 기록 시 카테고리 (기록 주제) + selectedCategory: String = "나의 가치관", + onCategoryChange: (String) -> Unit = {}, + showCategoryDropdown: Boolean = false, + onCategoryClick: () -> Unit = {}, + onCategoryDropdownDismiss: () -> Unit = {} +) { // 오늘 날짜 기준으로 받아오기 val today = LocalDate.now() val formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") @@ -106,11 +113,39 @@ fun RecordDiaryContentItem( modifier = Modifier.padding(vertical = 8.dp) ) } else if (s == "기록 주제") { - Text( - text = "나의 가치관", - fontSize = 12.sp, - modifier = Modifier.padding(vertical = 8.dp) - ) + Box { + Text( + text = selectedCategory, + fontSize = 12.sp, + modifier = Modifier.padding(vertical = 8.dp) + ) + DropdownMenu( + expanded = showCategoryDropdown, + onDismissRequest = onCategoryDropdownDismiss, + offset = DpOffset(x = 0.dp, y = 0.dp), + modifier = Modifier.width(350.dp) + ) { + listOf( + "나의 가치관", + "오늘 떠올린 생각", + "인생을 되돌아 보며" + ).forEach { option -> + DropdownMenuItem( + text = { + Text( + text = option, + fontSize = 14.sp, + fontFamily = Sansneo + ) + }, + onClick = { + onCategoryChange(option) + onCategoryDropdownDismiss() + } + ) + } + } + } } Image( @@ -118,7 +153,9 @@ fun RecordDiaryContentItem( contentDescription = "밑 화살표", modifier = Modifier .size(24.dp) - .clickable { onDateClick() } + .clickable { + if (s == "기록 주제") onCategoryClick() else onDateClick() + } ) if (sendDate.isNotEmpty()) { val sendDate = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") @@ -176,10 +213,7 @@ fun RecordDiaryContentItem( DateWheelPicker( modifier = Modifier.width(DateWheelPickerDefaults.ContainerWidth), currentDate = selectedDate, - onDateChanged = { date -> - selectedDate = date - onDateSelected(date.year, date.monthValue, date.dayOfMonth) - }, + onDateChanged = { date -> selectedDate = date }, minDate = LocalDate.now() ) } diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordDiaryQuestionContentItem.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordDiaryQuestionContentItem.kt index 38fc4451..32b97b36 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordDiaryQuestionContentItem.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordDiaryQuestionContentItem.kt @@ -20,6 +20,7 @@ import com.kuit.afternote.ui.theme.Sansneo @Composable fun RecordDiaryQuestionContentItem( modifier: Modifier = Modifier, + questionText: String, title: String, onTitleChange: (String) -> Unit, content: String, @@ -33,7 +34,7 @@ fun RecordDiaryQuestionContentItem( .padding(start = 8.dp, bottom = 8.dp) ) { Text( - text = "오늘 하루, 누구에게 가장 고마웠나요?", + text = questionText.ifBlank { "질문을 불러오는 중..." }, color = Black, fontSize = 18.sp, fontWeight = FontWeight.Normal, @@ -41,6 +42,7 @@ fun RecordDiaryQuestionContentItem( ) } HorizontalDivider(color = Color.LightGray, thickness = 0.8.dp) + BasicTextField( value = title, onValueChange = onTitleChange, @@ -69,7 +71,6 @@ fun RecordDiaryQuestionContentItem( BasicTextField( value = content, onValueChange = onContentChange, - singleLine = true, modifier = Modifier .fillMaxWidth() .padding(top = 40.dp), diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordListItem.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordListItem.kt index 41ce84dd..f872e015 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordListItem.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordListItem.kt @@ -42,8 +42,8 @@ fun RecordListItem( var expanded by remember { mutableStateOf(false) } RecordQnAListItem( - question = record.title, - answer = record.draftLabel, + question = record.title.ifBlank { "-" }, + answer = (record.content ?: record.title).ifBlank { "-" }, dateText = record.formattedDate, trailing = { Row( diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordListSort.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordListSort.kt index df14d186..a8349cd2 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordListSort.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordListSort.kt @@ -41,7 +41,8 @@ import java.util.Locale @Composable fun RecordListSort( modifier: Modifier = Modifier, - today: LocalDate + today: LocalDate, + markedDates: Set = emptySet() ) { var isClicked by remember { mutableStateOf(true) } val year = today.year @@ -104,7 +105,8 @@ fun RecordListSort( Spacer(Modifier.height(20.dp)) if (!isClicked) { RecordCalendarSort( - today = today + today = today, + markedDates = markedDates ) } } diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/navigiation/RecordNavgraph.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/navigiation/RecordNavgraph.kt index 69d64ed0..f35bf9a2 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/navigiation/RecordNavgraph.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/navigiation/RecordNavgraph.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import androidx.navigation.toRoute import com.kuit.afternote.core.ui.component.navigation.BottomNavItem import com.kuit.afternote.feature.dailyrecord.presentation.navigiation.RecordRoute import com.kuit.afternote.feature.dailyrecord.presentation.screen.RecordDailyQuestionListScreen @@ -22,6 +23,8 @@ import com.kuit.afternote.feature.dailyrecord.presentation.screen.RecordWeekendR import com.kuit.afternote.feature.dailyrecord.presentation.viewmodel.MindRecordViewModel import com.kuit.afternote.feature.dailyrecord.presentation.viewmodel.MindRecordWriterViewModel import com.kuit.afternote.feature.timeletter.presentation.viewmodel.TimeLetterWriterViewModel +import java.time.LocalDate +import java.time.format.DateTimeFormatter @RequiresApi(Build.VERSION_CODES.O) fun NavGraphBuilder.recordNavGraph( @@ -32,28 +35,39 @@ fun NavGraphBuilder.recordNavGraph( composable("record_main") { RecordMainScreen( - onDiaryClick = { navController.navigate(RecordRoute.DiaryRoute) }, - onQuestionClick = { navController.navigate(RecordRoute.QuestionRoute) }, - onDeepMindClick = { navController.navigate(RecordRoute.DeepMindRoute) }, + onDiaryClick = { navController.navigate(RecordRoute.ListRoute("DIARY")) }, + onQuestionClick = { navController.navigate(RecordRoute.QuestionRouteList) }, + onDeepMindClick = { navController.navigate(RecordRoute.ListRoute("DEEP_THOUGHT")) }, onWeekendReportClick = { navController.navigate(RecordRoute.WeekendReportRoute) }, onBottomNavTabSelected = onBottomNavTabSelected ) } - composable { + composable { backStackEntry -> + val route = backStackEntry.toRoute() + val listMode = route.listMode val viewModel: MindRecordViewModel = hiltViewModel() RecordFirstDiaryListScreen( + listMode = listMode, onBackClick = { navController.popBackStack() }, - onPlusRecordClick = { navController.navigate(RecordRoute.DiaryRoute) }, + onPlusRecordClick = { + when (listMode) { + "DEEP_THOUGHT" -> navController.navigate(RecordRoute.DeepMindRoute) + else -> navController.navigate(RecordRoute.DiaryRoute) + } + }, onEditClick = { recordId -> navController.navigate(RecordRoute.EditRoute(recordId)) }, onBottomNavTabSelected = onBottomNavTabSelected, viewModel = viewModel ) - LaunchedEffect(Unit) { - viewModel.loadRecords() + LaunchedEffect(listMode) { + if (listMode == "DEEP_THOUGHT") { + viewModel.loadRecords("DEEP_THOUGHT") + } else { + viewModel.loadRecordsForDiaryList() + } } - } composable { val viewModel: MindRecordViewModel = hiltViewModel() @@ -67,9 +81,8 @@ fun NavGraphBuilder.recordNavGraph( viewModel = viewModel ) LaunchedEffect(Unit) { - viewModel.loadRecords() + viewModel.loadRecords("DAILY_QUESTION") } - } composable { val viewModel: MindRecordWriterViewModel = hiltViewModel() @@ -81,18 +94,17 @@ fun NavGraphBuilder.recordNavGraph( content = uiState.content, onTitleChange = viewModel::updateTitle, onContentChange = viewModel::updateContent, - sendDate = uiState.sendDate, showDatePicker = uiState.showDatePicker, onDateClick = viewModel::showDatePicker, onDatePickerDismiss = viewModel::hideDatePicker, - - onRegisterClick = { - viewModel.registerWithPopUpThenSave { - navController.popBackStack() - } + onDateSelected = { year, month, day -> + val formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") + viewModel.updateSendDate(LocalDate.of(year, month, day).format(formatter)) }, - + onRegisterClick = { + viewModel.registerWithPopUpThenSave(onSuccess = { navController.popBackStack() }) + } ) } composable { @@ -112,11 +124,26 @@ fun NavGraphBuilder.recordNavGraph( RecordDeepMindScreen( onLeftClick = { navController.popBackStack() }, recordId = null, + title = uiState.title, + content = uiState.content, + onTitleChange = viewModel::updateTitle, + onContentChange = viewModel::updateContent, sendDate = uiState.sendDate, showDatePicker = uiState.showDatePicker, onDateClick = viewModel::showDatePicker, - onDatePickerDismiss = viewModel::hideDatePicker - + onDatePickerDismiss = viewModel::hideDatePicker, + selectedCategory = uiState.selectedCategory, + onCategoryChange = viewModel::updateCategory, + showCategoryDropdown = uiState.showCategoryDropdown, + onCategoryClick = viewModel::showCategoryDropdown, + onCategoryDropdownDismiss = viewModel::hideCategoryDropdown, + onRegisterClick = { + viewModel.registerWithPopUpThenSave( + recordType = "DEEP_THOUGHT", + category = uiState.selectedCategory, + onSuccess = { navController.popBackStack() } + ) + } ) } @@ -126,16 +153,20 @@ fun NavGraphBuilder.recordNavGraph( onBottomNavTabSelected = onBottomNavTabSelected ) } - composable { _ -> + composable { backStackEntry -> + val route = backStackEntry.toRoute() val viewModel: MindRecordViewModel = hiltViewModel() val record by viewModel.selectedRecord.collectAsState() + LaunchedEffect(route.recordId) { + viewModel.loadRecord(route.recordId) + } + RecordQuestionScreen( - record = record, // 수정 시 기존 데이터 전달 + record = record, onLeftClick = { navController.popBackStack() }, viewModel = viewModel ) - } } diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/navigiation/RecordRoute.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/navigiation/RecordRoute.kt index 847fe5da..a80b9ea7 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/navigiation/RecordRoute.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/navigiation/RecordRoute.kt @@ -19,7 +19,7 @@ sealed interface RecordRoute { data object QuestionRouteList : RecordRoute @Serializable - data object ListRoute : RecordRoute + data class ListRoute(val listMode: String = "DIARY") : RecordRoute @Serializable data object WeekendReportRoute : RecordRoute diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordDailyQuestionListScreen.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordDailyQuestionListScreen.kt index 1249e13a..b2c1ef95 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordDailyQuestionListScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordDailyQuestionListScreen.kt @@ -41,7 +41,7 @@ fun RecordDailyQuestionListScreen( val records by viewModel.records.collectAsState() // UIModel 구독 LaunchedEffect(Unit) { - viewModel.loadRecords() + viewModel.loadRecords("DAILY_QUESTION") } Scaffold( @@ -82,12 +82,14 @@ fun RecordDailyQuestionListScreen( RecordListItem( record = record, onDeleteClick = { - viewModel.deleteRecord(record.id){ - viewModel.loadRecords() - } + viewModel.deleteRecord( + recordId = record.id, + recordType = record.type ?: "DAILY_QUESTION", + onReload = { viewModel.loadRecords("DAILY_QUESTION") } + ) }, - onEditClick = { record-> - onEditClick(record) + onEditClick = { recordId -> + onEditClick(recordId) } ) // 이제 UIModel을 그대로 넘김 } diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordDeepMindScreen.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordDeepMindScreen.kt index d76ab49f..65d0f208 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordDeepMindScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordDeepMindScreen.kt @@ -8,39 +8,33 @@ import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.kuit.afternote.feature.dailyrecord.presentation.viewmodel.MindRecordViewModel import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordDiaryContentItem import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordSubTopbar -import java.time.LocalDate @RequiresApi(Build.VERSION_CODES.O) @Composable fun RecordDeepMindScreen( modifier: Modifier = Modifier, onLeftClick: () -> Unit, - //viewModel: MindRecordViewModel, recordId: Long?, + title: String, + content: String, + onTitleChange: (String) -> Unit, + onContentChange: (String) -> Unit, onDateClick: () -> Unit, sendDate: String, showDatePicker: Boolean, - onDatePickerDismiss: () -> Unit - + onDatePickerDismiss: () -> Unit, + selectedCategory: String, + onCategoryChange: (String) -> Unit, + showCategoryDropdown: Boolean, + onCategoryClick: () -> Unit, + onCategoryDropdownDismiss: () -> Unit, + onRegisterClick: () -> Unit ) { - var title by remember { mutableStateOf("") } - var content by remember { mutableStateOf("") } - - // val uiState by viewModel.uiState.collectAsState() - Scaffold { paddingValues -> LazyColumn( modifier = Modifier @@ -51,19 +45,7 @@ fun RecordDeepMindScreen( RecordSubTopbar( text = "깊은 생각 기록하기", onLeftClock = onLeftClick, - onRightClick = { -// viewModel.onCreateRecord( -// type = "DIARY", -// title = title, -// content = content, -// date = LocalDate.now().toString(), -// isDraft = false -// ) { -// viewModel.loadRecords() // 등록 성공 후 조회 실행 -// onLeftClick() // 성공 시 화면 닫기 -// } - } - + onRightClick = onRegisterClick ) } @@ -72,13 +54,18 @@ fun RecordDeepMindScreen( standard = "깊은 생각 기록하기", onDateSelected = { _, _, _ -> }, title = title, - onTitleChange = { title = it }, + onTitleChange = onTitleChange, content = content, - onContentChange = { content = it }, + onContentChange = onContentChange, onDateClick = {}, sendDate = sendDate, showDatePicker = showDatePicker, onDatePickerDismiss = onDatePickerDismiss, + selectedCategory = selectedCategory, + onCategoryChange = onCategoryChange, + showCategoryDropdown = showCategoryDropdown, + onCategoryClick = onCategoryClick, + onCategoryDropdownDismiss = onCategoryDropdownDismiss ) } } diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordDiaryScreen.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordDiaryScreen.kt index e9a02491..9521c28f 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordDiaryScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordDiaryScreen.kt @@ -9,16 +9,10 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordDiaryContentItem import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordSubTopbar -import java.time.LocalDate -import java.time.format.DateTimeFormatter /** * 애프터노트 일기 기록하기 화면 @@ -47,12 +41,8 @@ fun RecordDiaryScreen( //등록 버튼 onRegisterClick: () -> Unit, + onDateSelected: (Int, Int, Int) -> Unit = { _, _, _ -> }, ) { -// var title by remember { mutableStateOf("") } -// var content by remember { mutableStateOf("") } - var sendDate by remember { mutableStateOf("") } - var showDatePicker by remember { mutableStateOf(false) } - val formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") Scaffold { paddingValues -> LazyColumn( modifier = Modifier @@ -71,15 +61,14 @@ fun RecordDiaryScreen( RecordDiaryContentItem( standard = "일기 기록하기", onDateSelected = { year, month, day -> - val selectedDate = LocalDate.of(year, month, day) - sendDate = selectedDate.format(formatter) // 원하는 형식으로 변환 - showDatePicker = false - } , + onDateSelected(year, month, day) + }, title = title, onTitleChange = onTitleChange, content = content, onContentChange = onContentChange, - onDateClick = { showDatePicker = true }, sendDate = sendDate, + onDateClick = onDateClick, + sendDate = sendDate, showDatePicker = showDatePicker, onDatePickerDismiss = onDatePickerDismiss, ) diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordFirstDiaryListScreen.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordFirstDiaryListScreen.kt index f4802a85..772f9881 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordFirstDiaryListScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordFirstDiaryListScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -45,17 +46,23 @@ import java.time.LocalDate @Composable fun RecordFirstDiaryListScreen( modifier: Modifier = Modifier, + listMode: String = "DIARY", onBackClick: () -> Unit, onPlusRecordClick: () -> Unit, onEditClick: (Long) -> Unit, onBottomNavTabSelected: (BottomNavItem) -> Unit = {}, - viewModel: MindRecordViewModel // ViewModel 주입 + viewModel: MindRecordViewModel ) { val today = LocalDate.now() - val records by viewModel.records.collectAsState() // UIModel 구독 + val records by viewModel.records.collectAsState() + val markedDates by viewModel.markedDates.collectAsStateWithLifecycle() - LaunchedEffect(key1 = Unit) { - viewModel.loadRecords() + LaunchedEffect(key1 = listMode) { + if (listMode == "DEEP_THOUGHT") { + viewModel.loadRecords("DEEP_THOUGHT") + } else { + viewModel.loadRecordsForDiaryList() + } } Scaffold( modifier = modifier.fillMaxWidth(), @@ -100,18 +107,31 @@ fun RecordFirstDiaryListScreen( modifier = Modifier.padding(paddingValues) ) { TopBar( - title = "일기", + title = if (listMode == "DEEP_THOUGHT") "깊은 생각" else "일기", onBackClick = onBackClick, ) LazyColumn { item { - RecordListSort(today = today) + RecordListSort( + today = today, + markedDates = markedDates + ) } items(records) { record -> RecordListItem( record = record, - onDeleteClick ={ - viewModel.deleteRecord(record.id) + onDeleteClick = { + viewModel.deleteRecord( + recordId = record.id, + recordType = record.type ?: listMode, + onReload = { + if (listMode == "DEEP_THOUGHT") { + viewModel.loadRecords("DEEP_THOUGHT") + } else { + viewModel.loadRecordsForDiaryList() + } + } + ) }, onEditClick = { recordId -> onEditClick(recordId) diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordQuestionScreen.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordQuestionScreen.kt index 1bf84b66..9d2eadb4 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordQuestionScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordQuestionScreen.kt @@ -2,17 +2,11 @@ package com.kuit.afternote.feature.dailyrecord.presentation.screen import android.os.Build import androidx.annotation.RequiresApi -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -22,20 +16,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import com.kuit.afternote.core.ui.component.button.AddFloatingActionButton -import com.kuit.afternote.core.ui.component.navigation.BottomNavItem -import com.kuit.afternote.core.ui.component.navigation.BottomNavigationBar -import com.kuit.afternote.core.ui.component.navigation.TopBar -import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordDiaryContentItem import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordDiaryQuestionContentItem -import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordListItem -import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordListSort import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordSubTopbar import com.kuit.afternote.feature.dailyrecord.presentation.uimodel.MindRecordUiModel +import com.kuit.afternote.feature.dailyrecord.presentation.viewmodel.CreateRecordParams +import com.kuit.afternote.feature.dailyrecord.presentation.viewmodel.EditRecordParams import com.kuit.afternote.feature.dailyrecord.presentation.viewmodel.MindRecordViewModel -import com.kuit.afternote.ui.theme.Gray1 import java.time.LocalDate @RequiresApi(Build.VERSION_CODES.O) @@ -44,26 +30,71 @@ fun RecordQuestionScreen( record: MindRecordUiModel?, modifier: Modifier = Modifier, onLeftClick: () -> Unit, - viewModel: MindRecordViewModel // ViewModel 주입 + viewModel: MindRecordViewModel ) { - var title by remember { mutableStateOf("") } - var content by remember { mutableStateOf("") } + val uiState by viewModel.uiState.collectAsState() + val questionText = uiState.dailyQuestionText ?: "" + var title by remember { mutableStateOf(record?.title ?: "") } + var content by remember { mutableStateOf(record?.content ?: "") } + + LaunchedEffect(record) { + if (record == null) { + viewModel.loadDailyQuestion() + } else { + title = record.title ?: "" + content = record.content ?: "" + } + } + + LaunchedEffect(questionText) { + if (record == null && questionText.isNotBlank() && title.isEmpty()) { + title = questionText + } + } + Scaffold { paddingValues -> LazyColumn( modifier = Modifier .fillMaxWidth() - .windowInsetsPadding(WindowInsets.statusBars) // 상태바 만큼 패딩을 줘서 겹치지 않도록 + .windowInsetsPadding(WindowInsets.statusBars) ) { item { RecordSubTopbar( text = "데일리 질문 답변", onLeftClock = onLeftClick, - onRightClick = {} + onRightClick = { + if (record != null) { + viewModel.editRecord( + params = EditRecordParams( + recordId = record.id, + title = title.ifBlank { questionText }, + content = content, + date = record.originalDate, + type = record.type ?: "DAILY_QUESTION", + category = record.category, + isDraft = false + ), + onSuccess = onLeftClick + ) + } else { + viewModel.onCreateRecord( + params = CreateRecordParams( + type = "DAILY_QUESTION", + title = title.ifBlank { questionText }, + content = content, + date = LocalDate.now().toString(), + isDraft = false + ), + onSuccess = onLeftClick + ) + } + } ) } item { RecordDiaryQuestionContentItem( + questionText = if (record != null) (record.title.ifBlank { questionText }) else questionText, title = title, onTitleChange = { title = it }, content = content, diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/uimodel/MindRecordUiState.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/uimodel/MindRecordUiState.kt index 91c13990..43be0517 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/uimodel/MindRecordUiState.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/uimodel/MindRecordUiState.kt @@ -1,6 +1,10 @@ package com.kuit.afternote.feature.dailyrecord.presentation.uimodel data class MindRecordUiState( + /** 오늘의 데일리 질문 텍스트 (GET /daily-question content). null이면 로딩 중 또는 실패. */ + val dailyQuestionText: String? = null, + /** 기록 생성 실패 시 에러 메시지. null이면 에러 없음. */ + val createErrorMessage: String? = null, val title: String = "", val content: String = "", val sendDate: String = "", @@ -14,5 +18,8 @@ data class MindRecordUiState( val showRegisteredPopUp: Boolean = false, val showDraftSavePopUp: Boolean = false, val showWaitingAgainPopUp: Boolean = false, - val draftId: Long? = null + val draftId: Long? = null, + /** 깊은 생각 카테고리 (나의 가치관, 오늘 떠올린 생각, 인생을 되돌아 보며) */ + val selectedCategory: String = "나의 가치관", + val showCategoryDropdown: Boolean = false ) diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/viewmodel/MindRecordViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/viewmodel/MindRecordViewModel.kt index cf1d7ecf..d0603ed1 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/viewmodel/MindRecordViewModel.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/viewmodel/MindRecordViewModel.kt @@ -5,10 +5,10 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordRequest -import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordResponse import com.kuit.afternote.feature.dailyrecord.domain.usecase.CreateMindRecordUseCase import com.kuit.afternote.feature.dailyrecord.domain.usecase.DeleteMindRecordUseCase import com.kuit.afternote.feature.dailyrecord.domain.usecase.EditMindRecordUseCase +import com.kuit.afternote.feature.dailyrecord.domain.usecase.GetDailyQuestionUseCase import com.kuit.afternote.feature.dailyrecord.domain.usecase.GetMindRecordUseCase import com.kuit.afternote.feature.dailyrecord.domain.usecase.GetMindRecordsUseCase import com.kuit.afternote.feature.dailyrecord.presentation.uimodel.MindRecordUiModel @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import retrofit2.HttpException import java.time.DayOfWeek import java.time.LocalDate import java.time.format.TextStyle @@ -30,18 +31,45 @@ import java.time.temporal.TemporalAdjusters import java.util.Locale import javax.inject.Inject +private const val TAG = "MindRecordViewModel" + +/** Params for [MindRecordViewModel.onCreateRecord] to satisfy S107 (max 7 params). */ +data class CreateRecordParams( + val type: String, + val title: String, + val content: String, + val date: String, + val isDraft: Boolean, + val questionId: Long? = null, + val category: String? = null +) + +/** Params for [MindRecordViewModel.editRecord] to satisfy S107 (max 7 params). */ +data class EditRecordParams( + val recordId: Long, + val title: String, + val content: String, + val date: String, + val type: String, + val category: String?, + val isDraft: Boolean +) + @HiltViewModel class MindRecordViewModel @Inject constructor( private val getMindRecordsUseCase: GetMindRecordsUseCase, private val createMindRecordUseCase: CreateMindRecordUseCase, + private val getDailyQuestionUseCase: GetDailyQuestionUseCase, private val deleteMindRecordUseCase: DeleteMindRecordUseCase, private val getMindRecordUseCase: GetMindRecordUseCase, private val editMindRecordUseCase: EditMindRecordUseCase - - ) : ViewModel() { +) : ViewModel() { private val _records = MutableStateFlow>(emptyList()) val records: StateFlow> = _records + + private val _markedDates = MutableStateFlow>(emptySet()) + val markedDates: StateFlow> = _markedDates private val weekDates: List = run { val now = LocalDate.now() val monday = now.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) @@ -81,26 +109,73 @@ class MindRecordViewModel @Inject constructor( val parts = date.split("-") return "${parts[1].toInt()}월 ${parts[2].toInt()}일" } - fun loadRecords() { + fun loadRecords( + type: String, + view: String = "LIST", + year: Int? = null, + month: Int? = null + ) { viewModelScope.launch { try { - val response = getMindRecordsUseCase() - val domainRecords = response ?: emptyList() // null 방어 - + val result = getMindRecordsUseCase(type, view, year, month) + val domainRecords = result.records + val apiMarked = result.markedDates.toSet() + val fallbackFromRecords = domainRecords.map { it.date }.toSet() + _markedDates.value = + if (apiMarked.isNotEmpty()) apiMarked else fallbackFromRecords _records.value = domainRecords.map { summary -> MindRecordUiModel( id = summary.recordId, - title = summary.title, + title = summary.title ?: "", formattedDate = runCatching { formatDate(summary.date) } - .getOrElse { summary.date }, // 포맷 실패 시 원본 날짜 사용 + .getOrElse { summary.date }, draftLabel = if (summary.isDraft) "임시저장" else "완료", - originalDate = summary.date + content = summary.content, + originalDate = summary.date, + type = summary.type ) } } catch (e: Exception) { - // 예외 로그 찍기 Log.e("MindRecordViewModel", "loadRecords 실패", e) _records.value = emptyList() + _markedDates.value = emptySet() + } + } + } + + /** + * 일기(DIARY)와 깊은 생각(DEEP_THOUGHT)을 모두 조회하여 합칩니다. + * RecordFirstDiaryListScreen에서 사용합니다. + */ + fun loadRecordsForDiaryList() { + viewModelScope.launch { + try { + val diaryResult = getMindRecordsUseCase("DIARY", "LIST", null, null) + val deepResult = getMindRecordsUseCase("DEEP_THOUGHT", "LIST", null, null) + val combined = + diaryResult.records + deepResult.records + val sorted = combined.sortedByDescending { it.date } + val apiMarkedDates = + (diaryResult.markedDates + deepResult.markedDates).toSet() + val fallbackFromRecords = sorted.map { it.date }.toSet() + _markedDates.value = + if (apiMarkedDates.isNotEmpty()) apiMarkedDates else fallbackFromRecords + _records.value = sorted.map { summary -> + MindRecordUiModel( + id = summary.recordId, + title = summary.title ?: "", + formattedDate = runCatching { formatDate(summary.date) } + .getOrElse { summary.date }, + draftLabel = if (summary.isDraft) "임시저장" else "완료", + content = summary.content, + originalDate = summary.date, + type = summary.type + ) + } + } catch (e: Exception) { + Log.e("MindRecordViewModel", "loadRecordsForDiaryList 실패", e) + _records.value = emptyList() + _markedDates.value = emptySet() } } } @@ -109,45 +184,95 @@ class MindRecordViewModel @Inject constructor( private val _uiState = MutableStateFlow(MindRecordUiState()) val uiState: StateFlow = _uiState - fun onCreateRecord( - type: String, - title: String, - content: String, - date: String, - isDraft: Boolean, - questionId: Long? = null, - category: String? = null, - onSuccess: () -> Unit = {} - ) { + /** + * 오늘의 데일리 질문을 조회하여 UI에 표시합니다. + * RecordQuestionScreen 진입 시(record == null) 호출합니다. + */ + fun loadDailyQuestion() { viewModelScope.launch { - try { - val result = createMindRecordUseCase( - type = type, - title = title, - content = content, - date = date, - isDraft = isDraft, - questionId = questionId, - category = category - ) - //_uiState.update { it.copy(isCreated = true, newRecord = result) } - loadRecords() - onSuccess() // 성공 시 콜백 실행 - } catch (e: Exception) { - // _uiState.update { it.copy(errorMessage = e.message) } - } + val data = getDailyQuestionUseCase() + _uiState.update { it.copy(dailyQuestionText = data?.content) } } } - fun deleteRecord(recordId: Long, onSuccess: () -> Unit = {}) { + fun onCreateRecord(params: CreateRecordParams, onSuccess: () -> Unit = {}) { viewModelScope.launch { - try { - deleteMindRecordUseCase(recordId) - loadRecords() - onSuccess() - } catch (e: Exception) { - Log.e("MindRecordViewModel", "deleteRecord 실패", e) + val resolvedQuestionId = when { + params.type == "DAILY_QUESTION" && params.questionId == null -> { + val data = getDailyQuestionUseCase() + if (data == null) { + _uiState.update { + it.copy(createErrorMessage = "오늘의 질문을 불러올 수 없습니다.") + } + return@launch + } + data.questionId + } + else -> params.questionId } + + createMindRecordUseCase( + type = params.type, + title = params.title.ifBlank { null }, + content = params.content, + date = params.date, + isDraft = params.isDraft, + questionId = resolvedQuestionId, + category = params.category + ).fold( + onSuccess = { + _uiState.update { it.copy(createErrorMessage = null) } + loadRecords(params.type) + onSuccess() + }, + onFailure = { e -> + Log.e("MindRecordViewModel", "onCreateRecord 실패", e) + val message = when (e) { + is HttpException -> when (e.code()) { + 401 -> "인증이 만료되었습니다. 다시 로그인해 주세요." + else -> e.message() ?: "등록에 실패했습니다." + } + else -> e.message ?: "등록에 실패했습니다." + } + _uiState.update { it.copy(createErrorMessage = message) } + } + ) + } + } + + fun clearCreateError() { + _uiState.update { it.copy(createErrorMessage = null) } + } + + /** + * 기록을 삭제하고 목록을 갱신합니다. + * + * @param recordId 삭제할 기록 ID + * @param recordType 기록 유형 (갱신 시 loadRecords에 사용) + * @param onReload 삭제 성공 후 목록 갱신 콜백. null이면 loadRecords(recordType) 호출. + * 일기 목록(DIARY+DEEP_THOUGHT 통합)에서는 loadRecordsForDiaryList 전달. + * @param onSuccess 삭제 성공 시 콜백 + */ + fun deleteRecord( + recordId: Long, + recordType: String, + onReload: (() -> Unit)? = null, + onSuccess: () -> Unit = {} + ) { + viewModelScope.launch { + deleteMindRecordUseCase(recordId).fold( + onSuccess = { + if (onReload != null) { + onReload() + } else { + loadRecords(recordType) + } + onSuccess() + }, + onFailure = { e -> + Log.e(TAG, "deleteRecord failed recordId=$recordId", e) + } + ) } } @@ -177,30 +302,29 @@ class MindRecordViewModel @Inject constructor( _selectedRecord.value = null } } catch (e: Exception) { - Log.e("MindRecordViewModel", "loadRecord 실패", e) + Log.e(TAG, "loadRecord failed recordId=$recordId", e) _selectedRecord.value = null } } } - fun editRecord( - recordId: Long, - title: String, - content: String, - date: String, - type: String, - category: String?, - isDraft: Boolean, - onSuccess: () -> Unit = {} - ) { + fun editRecord(params: EditRecordParams, onSuccess: () -> Unit = {}) { viewModelScope.launch { try { - val request = PostMindRecordRequest(title, content, date, type, isDraft, questionId = null,category) - val result = editMindRecordUseCase(recordId, request).getOrThrow() - loadRecords() + val request = PostMindRecordRequest( + params.title, + params.content, + params.date, + params.type, + params.isDraft, + questionId = null, + params.category + ) + editMindRecordUseCase(params.recordId, request).getOrThrow() + loadRecords(params.type) onSuccess() } catch (e: Exception) { - Log.e("MindRecordViewModel", "editRecord 실패", e) + Log.e(TAG, "editRecord failed recordId=${params.recordId}", e) } } } diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/viewmodel/MindRecordWriterViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/viewmodel/MindRecordWriterViewModel.kt index 86c2213f..0befc036 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/viewmodel/MindRecordWriterViewModel.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/viewmodel/MindRecordWriterViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope import com.kuit.afternote.feature.dailyrecord.domain.usecase.CreateMindRecordUseCase import com.kuit.afternote.feature.dailyrecord.domain.usecase.DeleteMindRecordUseCase import com.kuit.afternote.feature.dailyrecord.domain.usecase.EditMindRecordUseCase +import com.kuit.afternote.feature.dailyrecord.domain.usecase.GetMindRecordUseCase import com.kuit.afternote.feature.dailyrecord.domain.usecase.GetMindRecordsUseCase import com.kuit.afternote.feature.dailyrecord.presentation.uimodel.MindRecordUiState import dagger.hilt.android.lifecycle.HiltViewModel @@ -18,6 +19,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.time.LocalDate +import java.time.format.DateTimeFormatter import javax.inject.Inject @HiltViewModel class MindRecordWriterViewModel @@ -28,7 +31,7 @@ constructor( private val deleteMindRecordUseCase: DeleteMindRecordUseCase, private val editMindRecordUseCase: EditMindRecordUseCase, private val getMindRecordsUseCase: GetMindRecordsUseCase, - private val getMindRecordUseCase: EditMindRecordUseCase, + private val getMindRecordUseCase: GetMindRecordUseCase, ) : ViewModel(){ private val _uiState = MutableStateFlow(MindRecordUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -54,6 +57,23 @@ constructor( validateSaveEnabled() } + /** + * 깊은 생각 카테고리 업데이트 + * + * @param category 나의 가치관, 오늘 떠올린 생각, 인생을 되돌아 보며 + */ + fun updateCategory(category: String) { + _uiState.update { it.copy(selectedCategory = category) } + } + + fun showCategoryDropdown() { + _uiState.update { it.copy(showCategoryDropdown = true) } + } + + fun hideCategoryDropdown() { + _uiState.update { it.copy(showCategoryDropdown = false) } + } + /** * 발송 날짜 업데이트 * @@ -96,11 +116,23 @@ constructor( * * @param onSuccess 저장 성공 시 콜백 */ - fun registerWithPopUpThenSave(onSuccess: () -> Unit) { - saveMindRecord(onSuccess = onSuccess) + fun registerWithPopUpThenSave( + recordType: String = "DIARY", + category: String? = null, + onSuccess: () -> Unit + ) { + saveMindRecord(recordType = recordType, category = category, onSuccess = onSuccess) } + /** + * 마음의 기록 저장 (일기 또는 깊은 생각). + * + * @param recordType "DIARY" 또는 "DEEP_THOUGHT" + * @param category DEEP_THOUGHT일 때 카테고리 (예: 나의 가치관) + */ fun saveMindRecord( + recordType: String = "DIARY", + category: String? = null, showPopUpAfterSuccess: Boolean = true, onSuccess: () -> Unit ) { @@ -109,11 +141,12 @@ constructor( val state = _uiState.value val sendAt = buildSendAt(state.sendDate) val result = createMindRecordUseCase( - type = "DIARY", // 혹은 "MIND_RECORD" 등 서버에서 요구하는 타입 + type = recordType, title = state.title.ifBlank { null }, - content = state.content.ifBlank { null }, - date = sendAt, - isDraft = false + content = state.content.ifBlank { "" }, + date = sendAt ?: LocalDate.now().toString(), + isDraft = false, + category = category ) _uiState.update { it.copy(isLoading = false) } @@ -136,13 +169,19 @@ constructor( } } /** - * sendAt 문자열 생성 (yyyy. MM. dd + HH:mm -> yyyy-MM-ddTHH:mm:00) + * sendDate를 API 형식(yyyy-MM-dd)으로 변환. + * "yyyy년 MM월 dd일" 또는 "yyyy. MM. dd" 형식 지원. */ - private fun buildSendAt( - sendDate: String, - ): String? { + private fun buildSendAt(sendDate: String): String? { if (sendDate.isBlank()) return null - val normalizedDate = sendDate.replace(". ", "-").trim() - return "$normalizedDate" + return runCatching { + val formatter = DateTimeFormatter.ofPattern("yyyy년 M월 d일") + LocalDate.parse(sendDate.trim(), formatter).toString() + }.getOrElse { + runCatching { + val altFormatter = DateTimeFormatter.ofPattern("yyyy. MM. dd") + LocalDate.parse(sendDate.trim(), altFormatter).toString() + }.getOrNull() + } } } diff --git a/app/src/main/java/com/kuit/afternote/feature/home/presentation/screen/HomeScreen.kt b/app/src/main/java/com/kuit/afternote/feature/home/presentation/screen/HomeScreen.kt index 9de223a5..e20b35d3 100644 --- a/app/src/main/java/com/kuit/afternote/feature/home/presentation/screen/HomeScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/home/presentation/screen/HomeScreen.kt @@ -78,7 +78,7 @@ fun HomeScreen( LaunchedEffect(Unit) { profileViewModel.loadProfile() - recordViewModel.loadRecords() + recordViewModel.loadRecordsForDiaryList() content = content.copy(