From f9291a06bbd3657fb919e247fd98f24502e2c8fe Mon Sep 17 00:00:00 2001 From: hyhyh Date: Tue, 17 Feb 2026 22:51:43 +0900 Subject: [PATCH 1/8] =?UTF-8?q?"api=20=EC=97=B0=EA=B2=B0=20=EC=A4=91"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/api/DailyRecordApiService.kt | 33 ++++++++-- .../data/repository/MindRecordRepository.kt | 7 +- .../repository/MindRecordRepositoryImpl.kt | 9 ++- .../domain/usecase/GetMindRecordsUseCase.kt | 9 ++- .../presentation/component/EmotionKeyword.kt | 8 +++ .../component/RecordweekendMindKeyword.kt | 65 +++++++++++++++---- .../navigiation/RecordNavgraph.kt | 24 +++---- .../screen/RecordDailyQuestionListScreen.kt | 17 +++-- .../screen/RecordFirstDiaryListScreen.kt | 13 ++-- .../screen/RecordQuestionScreen.kt | 55 ++++++++++------ .../screen/RecordWeekendReportScreen.kt | 6 ++ .../viewmodel/MindRecordViewModel.kt | 54 ++++++++------- .../home/presentation/screen/HomeScreen.kt | 2 +- 13 files changed, 210 insertions(+), 92 deletions(-) create mode 100644 app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/EmotionKeyword.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..cdd0e136 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 @@ -1,17 +1,42 @@ package com.kuit.afternote.feature.dailyrecord.data.api -import com.kuit.afternote.feature.dailyrecord.data.dto.* +import com.kuit.afternote.feature.dailyrecord.data.dto.CreateMindRecordRequest +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.data.dto.ReceiverEnabledRequest import retrofit2.Response -import retrofit2.http.* +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query /** * 마음의 기록 API 서비스 (Swagger 기준) + * + * */ interface DailyRecordApiService { - // 전체 기록 목록 조회 + /** + * 마음의 기록 목록 조회 (GET /mind-records) + * + * @param type 기록 유형 (DAILY_QUESTION, DIARY, DEEP_THOUGHT) + * @param view 조회 화면 타입 (LIST, CALENDAR) + * @param year 연도 (view=CALENDAR일 때) + * @param month 월 1~12 (view=CALENDAR일 때) + */ @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/repository/MindRecordRepository.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/data/repository/MindRecordRepository.kt index af98b683..7319526d 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 @@ -7,7 +7,12 @@ import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordRequest import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordResponse interface MindRecordRepository { - suspend fun getMindRecords(): MindRecordListResponse + 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..8d7813cd 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 @@ -11,8 +11,13 @@ import javax.inject.Inject class MindRecordRepositoryImpl @Inject constructor( private val apiService: DailyRecordApiService ) : MindRecordRepository{ - override suspend fun getMindRecords(): MindRecordListResponse { - return apiService.getMindRecords() + 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 { 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..8361390e 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 @@ -7,8 +7,13 @@ import javax.inject.Inject class GetMindRecordsUseCase @Inject constructor( private val repository: MindRecordRepositoryImpl ) { - suspend operator fun invoke(): List { - val response = repository.getMindRecords() + suspend operator fun invoke( + type: String, + view: String = "LIST", + year: Int? = null, + month: Int? = null + ): List { + val response = repository.getMindRecords(type, view, year, month) return response.data?.records ?: emptyList() } } diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/EmotionKeyword.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/EmotionKeyword.kt new file mode 100644 index 00000000..b6399b1c --- /dev/null +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/EmotionKeyword.kt @@ -0,0 +1,8 @@ +package com.kuit.afternote.feature.dailyrecord.presentation.component + +data class EmotionKeyword( + val keyword: String, + val weight: Int, // 크기 (버블 크기) + val color: androidx.compose.ui.graphics.Color // 색상 +) + diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordweekendMindKeyword.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordweekendMindKeyword.kt index 46fbe4f1..8fcd13fd 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordweekendMindKeyword.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordweekendMindKeyword.kt @@ -1,24 +1,25 @@ package com.kuit.afternote.feature.dailyrecord.presentation.component import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.kuit.afternote.ui.expand.dropShadow - -/** - * 주간리포트 나의 감정 키워드 컴포넌트 - */ @Composable fun RecordweekendMindKeyword(modifier: Modifier = Modifier) { Column( @@ -26,9 +27,7 @@ fun RecordweekendMindKeyword(modifier: Modifier = Modifier) { .padding(top = 30.dp) .padding(horizontal = 20.dp) ) { - RecordTextComponent( - title = "나의 감정 키워드" - ) + RecordTextComponent(title = "나의 감정 키워드") Spacer(Modifier.height(16.dp)) Box( modifier = Modifier @@ -41,13 +40,55 @@ fun RecordweekendMindKeyword(modifier: Modifier = Modifier) { offsetY = 2.dp, offsetX = 0.dp, spread = 0.dp - ).clip(RoundedCornerShape(16.dp)) + ) + .clip(RoundedCornerShape(16.dp)) .background(Color.White) + .padding(16.dp) ) { - Text( - text = "이번 주 박서연 님의 기록에서는\n" + - "‘가족’을 위한 ‘감사’의 마음이 엿보입니다." - ) + Column { + EmotionBubbleGroup() + Spacer(Modifier.height(16.dp)) + Text( + text = "이번 주 박서연 님의 기록에서는\n‘가족’을 위한 ‘감사’의 마음이 엿보입니다." + ) + } + } + } +} + +@Composable +fun EmotionBubble(emotion: EmotionKeyword) { + Box( + modifier = Modifier + .size(emotion.weight.dp) + .clip(RoundedCornerShape(50)) + .background(emotion.color), + contentAlignment = Alignment.Center + ) { + Text( + text = emotion.keyword, + color = Color.White + ) + } +} + +@Composable +fun EmotionBubbleGroup() { + val emotions = listOf( + EmotionKeyword("가족", 100, Color(0xFFFFD700)), + EmotionKeyword("감사", 80, Color(0xFFFF8C00)), + EmotionKeyword("사랑", 70, Color(0xFFFF69B4)), + EmotionKeyword("그리움", 60, Color(0xFF87CEEB)) + ) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + emotions.forEach { emotion -> + EmotionBubble(emotion) } } + } + 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..fc29f5c9 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 @@ -4,6 +4,7 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.navigation.toRoute import androidx.compose.runtime.getValue import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -32,8 +33,8 @@ fun NavGraphBuilder.recordNavGraph( composable("record_main") { RecordMainScreen( - onDiaryClick = { navController.navigate(RecordRoute.DiaryRoute) }, - onQuestionClick = { navController.navigate(RecordRoute.QuestionRoute) }, + onDiaryClick = { navController.navigate(RecordRoute.ListRoute) }, + onQuestionClick = { navController.navigate(RecordRoute.QuestionRouteList) }, onDeepMindClick = { navController.navigate(RecordRoute.DeepMindRoute) }, onWeekendReportClick = { navController.navigate(RecordRoute.WeekendReportRoute) }, onBottomNavTabSelected = onBottomNavTabSelected @@ -50,10 +51,6 @@ fun NavGraphBuilder.recordNavGraph( onBottomNavTabSelected = onBottomNavTabSelected, viewModel = viewModel ) - LaunchedEffect(Unit) { - viewModel.loadRecords() - } - } composable { val viewModel: MindRecordViewModel = hiltViewModel() @@ -66,10 +63,6 @@ fun NavGraphBuilder.recordNavGraph( onBottomNavTabSelected = onBottomNavTabSelected, viewModel = viewModel ) - LaunchedEffect(Unit) { - viewModel.loadRecords() - } - } composable { val viewModel: MindRecordWriterViewModel = hiltViewModel() @@ -101,6 +94,7 @@ fun NavGraphBuilder.recordNavGraph( RecordQuestionScreen( onLeftClick = { navController.popBackStack() }, + onRegisterSuccess = { navController.popBackStack() }, record = record, viewModel = viewModel ) @@ -126,13 +120,19 @@ 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() }, + onRegisterSuccess = { navController.popBackStack() }, viewModel = viewModel ) 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..0296279a 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 @@ -13,7 +13,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -38,10 +38,10 @@ fun RecordDailyQuestionListScreen( viewModel: MindRecordViewModel // ViewModel 주입 ) { val today = LocalDate.now() - val records by viewModel.records.collectAsState() // UIModel 구독 + val records by viewModel.records.collectAsStateWithLifecycle() LaunchedEffect(Unit) { - viewModel.loadRecords() + viewModel.loadRecords("DAILY_QUESTION") } Scaffold( @@ -82,13 +82,12 @@ fun RecordDailyQuestionListScreen( RecordListItem( record = record, onDeleteClick = { - viewModel.deleteRecord(record.id){ - viewModel.loadRecords() - } + viewModel.deleteRecord( + recordId = record.id, + recordType = record.type ?: "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/RecordFirstDiaryListScreen.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordFirstDiaryListScreen.kt index f4802a85..5b7d363c 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 @@ -19,7 +19,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -52,10 +52,10 @@ fun RecordFirstDiaryListScreen( viewModel: MindRecordViewModel // ViewModel 주입 ) { val today = LocalDate.now() - val records by viewModel.records.collectAsState() // UIModel 구독 + val records by viewModel.records.collectAsStateWithLifecycle() LaunchedEffect(key1 = Unit) { - viewModel.loadRecords() + viewModel.loadRecords("DIARY") } Scaffold( modifier = modifier.fillMaxWidth(), @@ -110,8 +110,11 @@ fun RecordFirstDiaryListScreen( items(records) { record -> RecordListItem( record = record, - onDeleteClick ={ - viewModel.deleteRecord(record.id) + onDeleteClick = { + viewModel.deleteRecord( + recordId = record.id, + recordType = record.type ?: "DIARY" + ) }, 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..6a98359a 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,40 +2,23 @@ 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 -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.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.MindRecordViewModel -import com.kuit.afternote.ui.theme.Gray1 import java.time.LocalDate @RequiresApi(Build.VERSION_CODES.O) @@ -44,21 +27,53 @@ fun RecordQuestionScreen( record: MindRecordUiModel?, modifier: Modifier = Modifier, onLeftClick: () -> Unit, - viewModel: MindRecordViewModel // ViewModel 주입 + onRegisterSuccess: () -> Unit = {}, + viewModel: MindRecordViewModel ) { var title by remember { mutableStateOf("") } var content by remember { mutableStateOf("") } + + LaunchedEffect(record) { + record?.let { + title = it.title + content = it.content ?: "" + } + } + Scaffold { paddingValues -> LazyColumn( modifier = Modifier .fillMaxWidth() - .windowInsetsPadding(WindowInsets.statusBars) // 상태바 만큼 패딩을 줘서 겹치지 않도록 + .windowInsetsPadding(WindowInsets.statusBars) ) { item { RecordSubTopbar( text = "데일리 질문 답변", onLeftClock = onLeftClick, - onRightClick = {} + onRightClick = { + if (record == null) { + viewModel.onCreateRecord( + type = "DAILY_QUESTION", + title = title.ifBlank { "" }, + content = content, + date = LocalDate.now().toString(), + isDraft = false, + questionId = null, + onSuccess = onRegisterSuccess + ) + } else { + viewModel.editRecord( + recordId = record.id, + title = title, + content = content, + date = record.originalDate, + type = record.type ?: "DAILY_QUESTION", + category = record.category, + isDraft = false, + onSuccess = onRegisterSuccess + ) + } + } ) } diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordWeekendReportScreen.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordWeekendReportScreen.kt index 6acdeae8..231864b1 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordWeekendReportScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/screen/RecordWeekendReportScreen.kt @@ -4,16 +4,19 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -24,10 +27,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp 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.EmotionKeyword import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordAllSeeReport import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordCurrentWeek import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordWeekTotal @@ -138,3 +143,4 @@ fun RecordWeekendReportScreen( } } } + 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..27313cdc 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,7 +5,6 @@ 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 @@ -21,7 +20,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.time.DayOfWeek import java.time.LocalDate @@ -81,10 +79,15 @@ 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 response = getMindRecordsUseCase(type, view, year, month) val domainRecords = response ?: emptyList() // null 방어 _records.value = domainRecords.map { summary -> @@ -94,7 +97,8 @@ class MindRecordViewModel @Inject constructor( formattedDate = runCatching { formatDate(summary.date) } .getOrElse { summary.date }, // 포맷 실패 시 원본 날짜 사용 draftLabel = if (summary.isDraft) "임시저장" else "완료", - originalDate = summary.date + originalDate = summary.date, + type = summary.type ) } } catch (e: Exception) { @@ -120,30 +124,32 @@ class MindRecordViewModel @Inject constructor( onSuccess: () -> Unit = {} ) { 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 result = createMindRecordUseCase( + type = type, + title = title.ifBlank { null }, + content = content, + date = date, + isDraft = isDraft, + questionId = questionId, + category = category + ) + result.fold( + onSuccess = { + loadRecords(type) + onSuccess() + }, + onFailure = { e -> + Log.e("MindRecordViewModel", "onCreateRecord 실패", e) + } + ) } } - fun deleteRecord(recordId: Long, onSuccess: () -> Unit = {}) { + fun deleteRecord(recordId: Long, recordType: String, onSuccess: () -> Unit = {}) { viewModelScope.launch { try { deleteMindRecordUseCase(recordId) - loadRecords() + loadRecords(recordType) onSuccess() } catch (e: Exception) { Log.e("MindRecordViewModel", "deleteRecord 실패", e) @@ -197,7 +203,7 @@ class MindRecordViewModel @Inject constructor( try { val request = PostMindRecordRequest(title, content, date, type, isDraft, questionId = null,category) val result = editMindRecordUseCase(recordId, request).getOrThrow() - loadRecords() + loadRecords(type) onSuccess() } catch (e: Exception) { Log.e("MindRecordViewModel", "editRecord 실패", e) 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..d5bcb5b3 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.loadRecords() content = content.copy( From ae8f6f0786040e52c3a58b0f494b5b7cbca0a76c Mon Sep 17 00:00:00 2001 From: 1hyok Date: Tue, 17 Feb 2026 23:31:58 +0900 Subject: [PATCH 2/8] =?UTF-8?q?refactor(dailyrecord):=20=EB=A7=88=EC=9D=8C?= =?UTF-8?q?=EC=9D=98=20=EA=B8=B0=EB=A1=9D=20API=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **API 예외 처리 강화**: `MindRecordRepositoryImpl`의 `delete`, `get`, `edit`, `setMindRecordReceiverEnabled` 메서드에서 API 응답이 성공적이지 않을 경우 `HttpException`을 던지도록 수정하여 예외 처리를 강화했습니다. - **수신자 전달 설정 기능 추가**: - 특정 마음의 기록을 특정 수신자에게 전달하거나 중단하는 `setMindRecordReceiverEnabled` 기능을 `MindRecordRepository`와 `DailyRecordApiService`에 추가했습니다 (`PATCH /mind-records/{recordId}/receivers/{receiverId}`). - 해당 기능을 비즈니스 로직으로 감싸는 `SetMindRecordReceiverEnabledUseCase`를 새로 추가했습니다. - **ViewModel 파라미터 리팩토링**: `MindRecordViewModel`의 `onCreateRecord`와 `editRecord` 메서드 파라미터를 각각 `CreateRecordParams` 및 `EditRecordParams` 데이터 클래스로 묶어 코드 가독성과 유지보수성을 개선했습니다. - **저장 실패 에러 상태 추가**: `MindRecordWriterViewModel`이 저장에 실패했을 때 에러 메시지를 UI에 표시할 수 있도록 `MindRecordUiState`에 `saveErrorMessage` 필드를 추가하고 관련 로직을 업데이트했습니다. - **로그 및 주석 개선**: ViewModel 전반에 걸쳐 일관된 `TAG`를 사용하고, 예외 발생 시 더 상세한 정보를 포함하도록 로그를 개선했습니다. --- .../navgraph/AfternoteNavgraph.kt | 2 - .../data/api/DailyRecordApiService.kt | 5 + .../data/repository/MindRecordRepository.kt | 15 ++- .../repository/MindRecordRepositoryImpl.kt | 28 +++++- .../SetMindRecordReceiverEnabledUseCase.kt | 34 +++++++ .../screen/RecordQuestionScreen.kt | 34 ++++--- .../presentation/uimodel/MindRecordUiState.kt | 4 +- .../viewmodel/MindRecordViewModel.kt | 92 +++++++++++-------- .../viewmodel/MindRecordWriterViewModel.kt | 18 ++-- 9 files changed, 164 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/SetMindRecordReceiverEnabledUseCase.kt diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/navgraph/AfternoteNavgraph.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/navgraph/AfternoteNavgraph.kt index 598b40bb..4db6eb4d 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/navgraph/AfternoteNavgraph.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/navgraph/AfternoteNavgraph.kt @@ -71,8 +71,6 @@ private const val TAG_AFTERNOTE_EDIT = "AfternoteEdit" private const val TAG_FINGERPRINT = "FingerprintLogin" private const val TAG_AFTERNOTE_DETAIL = "AfternoteDetail" -private const val CATEGORY_MEMORIAL_GUIDELINE = "추모 가이드라인" - /** * Holder and clear callback for hoisted edit state (keeps param count ≤7). */ 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 cdd0e136..b4c897b5 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 @@ -64,6 +64,11 @@ interface DailyRecordApiService { @Path("recordId") recordId: Long ): Response + /** + * 마음의기록 수신인 설정. 특정 마음의 기록을 특정 수신인에게 전달(on) 또는 전달 중단(off) 설정. + * PATCH /mind-records/{recordId}/receivers/{receiverId} + * 200 성공, 403 권한 없음. + */ @PATCH("mind-records/{recordId}/receivers/{receiverId}") suspend fun setMindRecordReceiverEnabled( @Path("recordId") recordId: Long, 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 7319526d..b326d32b 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,7 +5,6 @@ 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 - interface MindRecordRepository { suspend fun getMindRecords( type: String, @@ -21,5 +20,17 @@ interface MindRecordRepository { suspend fun editMindRecord(recordId: Long, request: PostMindRecordRequest): MindRecordDetailResponse - + /** + * Sets whether a specific mind record is delivered to a specific receiver. + * PATCH /mind-records/{recordId}/receivers/{receiverId} + * + * @param recordId 마음의 기록 ID + * @param receiverId 수신인 ID + * @param enabled true → 해당 수신인에게 전달, false → 전달 중단 + */ + suspend fun setMindRecordReceiverEnabled( + recordId: Long, + receiverId: Long, + enabled: Boolean + ) } 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 8d7813cd..8ad902c2 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,6 +6,8 @@ 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.data.dto.ReceiverEnabledRequest +import retrofit2.HttpException import javax.inject.Inject class MindRecordRepositoryImpl @Inject constructor( @@ -25,18 +27,40 @@ 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) + if (!response.isSuccessful) { + throw HttpException(response) + } return response.body() ?: throw IllegalStateException("Response body is null") } override suspend fun editMindRecord(recordId: Long, request: PostMindRecordRequest): MindRecordDetailResponse { val response = apiService.editMindRecord(recordId, request) + if (!response.isSuccessful) { + throw HttpException(response) + } return response.body() ?: throw IllegalStateException("Response body is null") } - + override suspend fun setMindRecordReceiverEnabled( + recordId: Long, + receiverId: Long, + enabled: Boolean + ) { + val response = apiService.setMindRecordReceiverEnabled( + recordId, + receiverId, + ReceiverEnabledRequest(enabled = enabled) + ) + if (!response.isSuccessful) { + throw HttpException(response) + } + } } diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/SetMindRecordReceiverEnabledUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/SetMindRecordReceiverEnabledUseCase.kt new file mode 100644 index 00000000..8359be51 --- /dev/null +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/SetMindRecordReceiverEnabledUseCase.kt @@ -0,0 +1,34 @@ +package com.kuit.afternote.feature.dailyrecord.domain.usecase + +import com.kuit.afternote.feature.dailyrecord.data.repository.MindRecordRepository +import javax.inject.Inject + +/** + * Sets whether a specific mind record is delivered to a specific receiver. + * PATCH /mind-records/{recordId}/receivers/{receiverId} + * + * @param recordId 마음의 기록 ID + * @param receiverId 수신인 ID + * @param enabled true → 해당 수신인에게 전달, false → 전달 중단 + * @return [Result] success on 200, failure on 403 or other error + */ +class SetMindRecordReceiverEnabledUseCase @Inject constructor( + private val repository: MindRecordRepository +) { + suspend operator fun invoke( + recordId: Long, + receiverId: Long, + enabled: Boolean + ): Result { + return try { + repository.setMindRecordReceiverEnabled( + recordId = recordId, + receiverId = receiverId, + enabled = enabled + ) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } +} 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 6a98359a..ad794715 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 @@ -18,6 +18,8 @@ import androidx.compose.ui.Modifier import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordDiaryQuestionContentItem 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 java.time.LocalDate @@ -40,7 +42,7 @@ fun RecordQuestionScreen( } } - Scaffold { paddingValues -> + Scaffold { _ -> LazyColumn( modifier = Modifier .fillMaxWidth() @@ -53,23 +55,27 @@ fun RecordQuestionScreen( onRightClick = { if (record == null) { viewModel.onCreateRecord( - type = "DAILY_QUESTION", - title = title.ifBlank { "" }, - content = content, - date = LocalDate.now().toString(), - isDraft = false, - questionId = null, + params = CreateRecordParams( + type = "DAILY_QUESTION", + title = title.ifBlank { "" }, + content = content, + date = LocalDate.now().toString(), + isDraft = false, + questionId = null + ), onSuccess = onRegisterSuccess ) } else { viewModel.editRecord( - recordId = record.id, - title = title, - content = content, - date = record.originalDate, - type = record.type ?: "DAILY_QUESTION", - category = record.category, - isDraft = false, + params = EditRecordParams( + recordId = record.id, + title = title, + content = content, + date = record.originalDate, + type = record.type ?: "DAILY_QUESTION", + category = record.category, + isDraft = false + ), onSuccess = onRegisterSuccess ) } 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..f1fe325c 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 @@ -14,5 +14,7 @@ data class MindRecordUiState( val showRegisteredPopUp: Boolean = false, val showDraftSavePopUp: Boolean = false, val showWaitingAgainPopUp: Boolean = false, - val draftId: Long? = null + val draftId: Long? = null, + /** Error message when save fails (e.g. network). Cleared on next save attempt or success. */ + val saveErrorMessage: String? = null ) 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 27313cdc..ba434deb 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 @@ -28,6 +28,30 @@ 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, @@ -102,8 +126,7 @@ class MindRecordViewModel @Inject constructor( ) } } catch (e: Exception) { - // 예외 로그 찍기 - Log.e("MindRecordViewModel", "loadRecords 실패", e) + Log.e(TAG, "loadRecords failed type=$type view=$view", e) _records.value = emptyList() } } @@ -113,33 +136,23 @@ 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 = {} - ) { + fun onCreateRecord(params: CreateRecordParams, onSuccess: () -> Unit = {}) { viewModelScope.launch { - val result = createMindRecordUseCase( - type = type, - title = title.ifBlank { null }, - content = content, - date = date, - isDraft = isDraft, - questionId = questionId, - category = category - ) - result.fold( + createMindRecordUseCase( + type = params.type, + title = params.title.ifBlank { null }, + content = params.content, + date = params.date, + isDraft = params.isDraft, + questionId = params.questionId, + category = params.category + ).fold( onSuccess = { - loadRecords(type) + loadRecords(params.type) onSuccess() }, onFailure = { e -> - Log.e("MindRecordViewModel", "onCreateRecord 실패", e) + Log.e(TAG, "onCreateRecord failed type=${params.type}", e) } ) } @@ -152,7 +165,7 @@ class MindRecordViewModel @Inject constructor( loadRecords(recordType) onSuccess() } catch (e: Exception) { - Log.e("MindRecordViewModel", "deleteRecord 실패", e) + Log.e(TAG, "deleteRecord failed recordId=$recordId", e) } } } @@ -183,30 +196,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(type) + 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..ebfee160 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 @@ -1,6 +1,5 @@ package com.kuit.afternote.feature.dailyrecord.presentation.viewmodel -import android.content.ContentValues.TAG import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -19,6 +18,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject + +private const val TAG = "MindRecordWriterViewModel" + @HiltViewModel class MindRecordWriterViewModel @Inject @@ -105,7 +107,7 @@ constructor( onSuccess: () -> Unit ) { viewModelScope.launch { - _uiState.update { it.copy(isLoading = true) } + _uiState.update { it.copy(isLoading = true, saveErrorMessage = null) } val state = _uiState.value val sendAt = buildSendAt(state.sendDate) val result = createMindRecordUseCase( @@ -118,8 +120,8 @@ constructor( _uiState.update { it.copy(isLoading = false) } result.onSuccess { _ -> - Log.d(TAG, "saveTimeLetter onSuccess") - //hideWaitingAgainPopUp() + Log.d(TAG, "saveMindRecord onSuccess") + _uiState.update { it.copy(saveErrorMessage = null) } if (showPopUpAfterSuccess) { _uiState.update { it.copy(showRegisteredPopUp = true) } delay(2000L) @@ -129,9 +131,11 @@ constructor( onSuccess() } } - result.onFailure { - Log.e(TAG, "mindRecord failed", it) - // TODO: 에러 메시지 UiState에 반영 + result.onFailure { e -> + Log.e(TAG, "saveMindRecord failed", e) + _uiState.update { + it.copy(saveErrorMessage = e.message ?: "저장에 실패했습니다.") + } } } } From c355e7a438bc28df58f94c0313caa3fa56557ded Mon Sep 17 00:00:00 2001 From: hyhyh Date: Tue, 17 Feb 2026 23:41:11 +0900 Subject: [PATCH 3/8] =?UTF-8?q?"api=20=EC=97=B0=EA=B2=B0=20=EC=A4=91"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/api/DailyRecordApiService.kt | 8 +++++ .../dailyrecord/data/dto/DailyRecordDto.kt | 26 +++++++++++--- .../data/repository/MindRecordRepository.kt | 2 ++ .../repository/MindRecordRepositoryImpl.kt | 8 ++++- .../domain/usecase/GetDailyQuestionUseCase.kt | 18 ++++++++++ .../screen/RecordQuestionScreen.kt | 13 +++++++ .../presentation/uimodel/MindRecordUiState.kt | 1 + .../viewmodel/MindRecordViewModel.kt | 36 +++++++++++++++++-- 8 files changed, 104 insertions(+), 8 deletions(-) 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 cdd0e136..fa794487 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 @@ -1,5 +1,6 @@ package com.kuit.afternote.feature.dailyrecord.data.api +import com.kuit.afternote.feature.dailyrecord.data.dto.DailyQuestionResponse import com.kuit.afternote.feature.dailyrecord.data.dto.CreateMindRecordRequest import com.kuit.afternote.feature.dailyrecord.data.dto.MindRecordDetailResponse import com.kuit.afternote.feature.dailyrecord.data.dto.MindRecordListResponse @@ -30,6 +31,13 @@ interface DailyRecordApiService { * @param year 연도 (view=CALENDAR일 때) * @param month 월 1~12 (view=CALENDAR일 때) */ + /** + * 오늘의 데일리 질문 조회 (GET /daily-question) + * DAILY_QUESTION 타입 기록 작성 시 questionId가 필수이므로, 등록 전에 호출합니다. + */ + @GET("daily-question") + suspend fun getDailyQuestion(): DailyQuestionResponse + @GET("mind-records") suspend fun getMindRecords( @Query("type") type: String, 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..753adacb 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 @@ -79,6 +79,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 +129,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 7319526d..d9d6da8b 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 @@ -7,6 +7,8 @@ import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordRequest import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordResponse interface MindRecordRepository { + suspend fun getDailyQuestion(): Long? + suspend fun getMindRecords( type: String, view: String = "LIST", 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 8d7813cd..6c35ddab 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 @@ -10,7 +10,13 @@ import javax.inject.Inject class MindRecordRepositoryImpl @Inject constructor( private val apiService: DailyRecordApiService -) : MindRecordRepository{ +) : MindRecordRepository { + override suspend fun getDailyQuestion(): Long? = try { + apiService.getDailyQuestion().questionId + } catch (e: Exception) { + null + } + override suspend fun getMindRecords( type: String, view: String, 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..fbd82b3c --- /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.MindRecordRepositoryImpl +import javax.inject.Inject + +/** + * 오늘의 데일리 질문 ID를 조회합니다. + * DAILY_QUESTION 타입 기록 작성 시 questionId가 필수이므로 등록 전에 호출합니다. + * + * @return 오늘의 질문 ID, 실패 시 null + */ +class GetDailyQuestionUseCase +@Inject +constructor( + private val repository: MindRecordRepositoryImpl +) { + suspend operator fun invoke(): Long? = repository.getDailyQuestion() +} 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 6a98359a..fa190927 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 @@ -1,6 +1,7 @@ package com.kuit.afternote.feature.dailyrecord.presentation.screen import android.os.Build +import android.widget.Toast import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth @@ -8,12 +9,14 @@ 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.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.Modifier import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordDiaryQuestionContentItem import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordSubTopbar @@ -33,6 +36,9 @@ fun RecordQuestionScreen( var title by remember { mutableStateOf("") } var content by remember { mutableStateOf("") } + val context = LocalContext.current + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + LaunchedEffect(record) { record?.let { title = it.title @@ -40,6 +46,13 @@ fun RecordQuestionScreen( } } + LaunchedEffect(uiState.createErrorMessage) { + uiState.createErrorMessage?.let { message -> + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + viewModel.clearCreateError() + } + } + Scaffold { paddingValues -> LazyColumn( modifier = Modifier 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..e9b5617d 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,7 @@ package com.kuit.afternote.feature.dailyrecord.presentation.uimodel data class MindRecordUiState( + val createErrorMessage: String? = null, val title: String = "", val content: String = "", val sendDate: String = "", 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 27313cdc..1bfde5c5 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 @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordRequest import com.kuit.afternote.feature.dailyrecord.domain.usecase.CreateMindRecordUseCase +import com.kuit.afternote.feature.dailyrecord.domain.usecase.GetDailyQuestionUseCase 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 @@ -20,7 +21,9 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow 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 @@ -32,11 +35,11 @@ import javax.inject.Inject 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 @@ -124,27 +127,54 @@ class MindRecordViewModel @Inject constructor( onSuccess: () -> Unit = {} ) { viewModelScope.launch { + val resolvedQuestionId = when { + type == "DAILY_QUESTION" && questionId == null -> { + val id = getDailyQuestionUseCase() + if (id == null) { + _uiState.update { + it.copy(createErrorMessage = "오늘의 질문을 불러올 수 없습니다.") + } + return@launch + } + id + } + else -> questionId + } + val result = createMindRecordUseCase( type = type, title = title.ifBlank { null }, content = content, date = date, isDraft = isDraft, - questionId = questionId, + questionId = resolvedQuestionId, category = category ) result.fold( onSuccess = { + _uiState.update { it.copy(createErrorMessage = null) } loadRecords(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) } + } + fun deleteRecord(recordId: Long, recordType: String, onSuccess: () -> Unit = {}) { viewModelScope.launch { try { From 3b34cbbda85825034400838e049433f85ea3e3d8 Mon Sep 17 00:00:00 2001 From: hyhyh Date: Wed, 18 Feb 2026 00:44:30 +0900 Subject: [PATCH 4/8] =?UTF-8?q?"api=20=EC=97=B0=EA=B2=B0=20=EC=A4=91"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dailyrecord/data/dto/DailyRecordDto.kt | 4 +- .../data/repository/MindRecordRepository.kt | 4 +- .../repository/MindRecordRepositoryImpl.kt | 6 +- .../domain/usecase/GetDailyQuestionUseCase.kt | 8 +- .../RecordDiaryQuestionContentItem.kt | 3 +- .../presentation/component/RecordListItem.kt | 6 +- .../navigiation/RecordNavgraph.kt | 76 +++++++++-- .../presentation/navigiation/RecordRoute.kt | 7 + .../screen/RecordDailyQuestionListScreen.kt | 6 +- .../screen/RecordDeepMindScreen.kt | 44 +++---- .../presentation/screen/RecordDiaryScreen.kt | 20 +-- .../screen/RecordFirstDiaryListScreen.kt | 11 +- .../screen/RecordQuestionScreen.kt | 7 + .../presentation/uimodel/MindRecordUiState.kt | 2 + .../viewmodel/MindRecordViewModel.kt | 108 +++++++++++++-- .../viewmodel/MindRecordWriterViewModel.kt | 124 +++++++++++++++--- 16 files changed, 339 insertions(+), 97 deletions(-) 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 753adacb..3098af33 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 @@ -47,7 +47,9 @@ data class MindRecordSummary( /** 기록 날짜 (yyyy-MM-dd) */ val date: String, /** 임시 저장 여부 */ - val isDraft: Boolean + val isDraft: Boolean, + /** 기록 내용 (목록 API에서 제공 시). 없으면 null */ + val content: String? = null ) /** 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 9348c2a0..f0c453d2 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,8 +5,10 @@ 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 getDailyQuestion(): Long? + suspend fun getDailyQuestion(): DailyQuestionData? suspend fun getMindRecords( type: String, 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 dddbb9cc..ad6c7fa4 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 @@ -7,14 +7,16 @@ 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.data.dto.ReceiverEnabledRequest +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 getDailyQuestion(): Long? = try { - apiService.getDailyQuestion().questionId + override suspend fun getDailyQuestion(): DailyQuestionData? = try { + val response = apiService.getDailyQuestion() + DailyQuestionData(questionId = response.questionId, content = response.content) } catch (e: Exception) { null } 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 index fbd82b3c..fb5c4952 100644 --- 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 @@ -1,18 +1,20 @@ package com.kuit.afternote.feature.dailyrecord.domain.usecase import com.kuit.afternote.feature.dailyrecord.data.repository.MindRecordRepositoryImpl +import com.kuit.afternote.feature.dailyrecord.domain.model.DailyQuestionData import javax.inject.Inject /** - * 오늘의 데일리 질문 ID를 조회합니다. + * 오늘의 데일리 질문을 조회합니다. * DAILY_QUESTION 타입 기록 작성 시 questionId가 필수이므로 등록 전에 호출합니다. + * content는 UI에 질문 텍스트를 표시할 때 사용합니다. * - * @return 오늘의 질문 ID, 실패 시 null + * @return 오늘의 질문 (questionId, content), 실패 시 null */ class GetDailyQuestionUseCase @Inject constructor( private val repository: MindRecordRepositoryImpl ) { - suspend operator fun invoke(): Long? = repository.getDailyQuestion() + suspend operator fun invoke(): DailyQuestionData? = repository.getDailyQuestion() } 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..94111f7d 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, color = Black, fontSize = 18.sp, fontWeight = FontWeight.Normal, 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..ec2edcf9 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 @@ -37,13 +37,13 @@ import java.time.LocalDate fun RecordListItem( record: MindRecordUiModel, onDeleteClick: (Long) -> Unit, - onEditClick: (Long) -> Unit + onEditClick: (Long, String?) -> Unit ) { var expanded by remember { mutableStateOf(false) } RecordQnAListItem( question = record.title, - answer = record.draftLabel, + answer = (record.content ?: record.title).ifBlank { "-" }, dateText = record.formattedDate, trailing = { Row( @@ -72,7 +72,7 @@ fun RecordListItem( text = { Text("수정하기", fontFamily = Sansneo) }, onClick = { expanded = false - onEditClick(record.id) // 수정 클릭 시 recordId 전달 + onEditClick(record.id, record.type) } ) DropdownMenuItem( 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 fc29f5c9..18f561b3 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 @@ -45,8 +45,12 @@ fun NavGraphBuilder.recordNavGraph( RecordFirstDiaryListScreen( onBackClick = { navController.popBackStack() }, onPlusRecordClick = { navController.navigate(RecordRoute.DiaryRoute) }, - onEditClick = { recordId -> - navController.navigate(RecordRoute.EditRoute(recordId)) + onEditClick = { recordId, recordType -> + when (recordType) { + "DIARY" -> navController.navigate(RecordRoute.EditDiaryRoute(recordId)) + "DEEP_THOUGHT" -> navController.navigate(RecordRoute.EditDeepMindRoute(recordId)) + else -> navController.navigate(RecordRoute.EditRoute(recordId)) + } }, onBottomNavTabSelected = onBottomNavTabSelected, viewModel = viewModel @@ -57,35 +61,60 @@ fun NavGraphBuilder.recordNavGraph( RecordDailyQuestionListScreen( onBackClick = { navController.popBackStack() }, onPlusRecordClick = { navController.navigate(RecordRoute.QuestionRoute) }, - onEditClick = { recordId -> + onEditClick = { recordId, _ -> navController.navigate(RecordRoute.EditRoute(recordId)) }, onBottomNavTabSelected = onBottomNavTabSelected, viewModel = viewModel ) } - composable { + composable { backStackEntry -> + val route = backStackEntry.toRoute() val viewModel: MindRecordWriterViewModel = hiltViewModel() val uiState by viewModel.uiState.collectAsStateWithLifecycle() + LaunchedEffect(route.recordId) { + viewModel.loadRecordForEdit(route.recordId) + } + RecordDiaryScreen( onLeftClick = { navController.popBackStack() }, + viewModel = viewModel, title = uiState.title, content = uiState.content, onTitleChange = viewModel::updateTitle, onContentChange = viewModel::updateContent, - + onDateClick = viewModel::showDatePicker, sendDate = uiState.sendDate, showDatePicker = uiState.showDatePicker, - onDateClick = viewModel::showDatePicker, onDatePickerDismiss = viewModel::hideDatePicker, - onRegisterClick = { - viewModel.registerWithPopUpThenSave { + viewModel.registerWithPopUpThenSave(type = "DIARY") { navController.popBackStack() } - }, + } + ) + } + composable { + val viewModel: MindRecordWriterViewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + RecordDiaryScreen( + onLeftClick = { navController.popBackStack() }, + viewModel = viewModel, + title = uiState.title, + content = uiState.content, + onTitleChange = viewModel::updateTitle, + onContentChange = viewModel::updateContent, + onDateClick = viewModel::showDatePicker, + sendDate = uiState.sendDate, + showDatePicker = uiState.showDatePicker, + onDatePickerDismiss = viewModel::hideDatePicker, + onRegisterClick = { + viewModel.registerWithPopUpThenSave(type = "DIARY") { + navController.popBackStack() + } + } ) } composable { @@ -99,18 +128,43 @@ fun NavGraphBuilder.recordNavGraph( viewModel = viewModel ) } - composable { + composable { backStackEntry -> + val route = backStackEntry.toRoute() val viewModel: MindRecordWriterViewModel = hiltViewModel() val uiState by viewModel.uiState.collectAsStateWithLifecycle() + LaunchedEffect(route.recordId) { + viewModel.loadRecordForEdit(route.recordId) + } + RecordDeepMindScreen( onLeftClick = { navController.popBackStack() }, + onRegisterSuccess = { navController.popBackStack() }, + viewModel = viewModel, recordId = null, + onDateClick = viewModel::showDatePicker, sendDate = uiState.sendDate, showDatePicker = uiState.showDatePicker, - onDateClick = viewModel::showDatePicker, onDatePickerDismiss = viewModel::hideDatePicker + ) + } + composable { + val viewModel: MindRecordWriterViewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + RecordDeepMindScreen( + onLeftClick = { navController.popBackStack() }, + onRegisterSuccess = { + navController.navigate(RecordRoute.ListRoute) { + popUpTo(RecordRoute.DeepMindRoute) { inclusive = true } + } + }, + viewModel = viewModel, + recordId = null, + onDateClick = viewModel::showDatePicker, + sendDate = uiState.sendDate, + showDatePicker = uiState.showDatePicker, + onDatePickerDismiss = viewModel::hideDatePicker ) } 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..8fdd8abf 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 @@ -27,4 +27,11 @@ sealed interface RecordRoute { @Serializable data class EditRoute(val recordId: Long) : RecordRoute + /** 일기(DIARY) 수정 화면. RecordDiaryScreen을 기존 내용으로 재구성합니다. */ + @Serializable + data class EditDiaryRoute(val recordId: Long) : RecordRoute + + /** 깊은 생각(DEEP_THOUGHT) 수정 화면. RecordDeepMindScreen을 기존 내용으로 재구성합니다. */ + @Serializable + data class EditDeepMindRoute(val recordId: Long) : 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 0296279a..a9768e81 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 @@ -33,7 +33,7 @@ fun RecordDailyQuestionListScreen( modifier: Modifier = Modifier, onBackClick: () -> Unit, onPlusRecordClick: () -> Unit, - onEditClick: (Long) -> Unit, + onEditClick: (Long, String?) -> Unit, onBottomNavTabSelected: (BottomNavItem) -> Unit = {}, viewModel: MindRecordViewModel // ViewModel 주입 ) { @@ -87,7 +87,9 @@ fun RecordDailyQuestionListScreen( recordType = record.type ?: "DAILY_QUESTION" ) }, - onEditClick = { recordId -> onEditClick(recordId) } + onEditClick = { recordId, recordType -> + onEditClick(recordId, recordType) + } ) // 이제 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..c8c5114a 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 @@ -18,28 +18,28 @@ 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 androidx.lifecycle.compose.collectAsStateWithLifecycle import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordDiaryContentItem import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordSubTopbar +import com.kuit.afternote.feature.dailyrecord.presentation.viewmodel.MindRecordWriterViewModel import java.time.LocalDate +import java.time.format.DateTimeFormatter @RequiresApi(Build.VERSION_CODES.O) @Composable fun RecordDeepMindScreen( modifier: Modifier = Modifier, onLeftClick: () -> Unit, - //viewModel: MindRecordViewModel, + onRegisterSuccess: () -> Unit = {}, + viewModel: MindRecordWriterViewModel, recordId: Long?, onDateClick: () -> Unit, sendDate: String, showDatePicker: Boolean, onDatePickerDismiss: () -> Unit - ) { - var title by remember { mutableStateOf("") } - var content by remember { mutableStateOf("") } - - // val uiState by viewModel.uiState.collectAsState() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") Scaffold { paddingValues -> LazyColumn( @@ -52,30 +52,26 @@ fun RecordDeepMindScreen( text = "깊은 생각 기록하기", onLeftClock = onLeftClick, onRightClick = { -// viewModel.onCreateRecord( -// type = "DIARY", -// title = title, -// content = content, -// date = LocalDate.now().toString(), -// isDraft = false -// ) { -// viewModel.loadRecords() // 등록 성공 후 조회 실행 -// onLeftClick() // 성공 시 화면 닫기 -// } + viewModel.registerWithPopUpThenSave(type = "DEEP_THOUGHT") { + onRegisterSuccess() + } } - ) } item { RecordDiaryContentItem( standard = "깊은 생각 기록하기", - onDateSelected = { _, _, _ -> }, - title = title, - onTitleChange = { title = it }, - content = content, - onContentChange = { content = it }, - onDateClick = {}, + onDateSelected = { year, month, day -> + val selectedDate = LocalDate.of(year, month, day) + viewModel.updateSendDate(selectedDate.format(formatter)) + viewModel.hideDatePicker() + }, + title = uiState.title, + onTitleChange = viewModel::updateTitle, + content = uiState.content, + onContentChange = viewModel::updateContent, + onDateClick = onDateClick, sendDate = sendDate, showDatePicker = showDatePicker, onDatePickerDismiss = onDatePickerDismiss, 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..83a405d1 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 @@ -17,6 +17,7 @@ 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 com.kuit.afternote.feature.dailyrecord.presentation.viewmodel.MindRecordWriterViewModel import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -33,25 +34,17 @@ import java.time.format.DateTimeFormatter fun RecordDiaryScreen( modifier: Modifier = Modifier, onLeftClick: () -> Unit, - + viewModel: MindRecordWriterViewModel, title: String, content: String, onTitleChange: (String) -> Unit, onContentChange: (String) -> Unit, - - //발송시간 onDateClick: () -> Unit, sendDate: String, showDatePicker: Boolean, onDatePickerDismiss: () -> Unit, - - //등록 버튼 onRegisterClick: () -> 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( @@ -72,14 +65,15 @@ fun RecordDiaryScreen( standard = "일기 기록하기", onDateSelected = { year, month, day -> val selectedDate = LocalDate.of(year, month, day) - sendDate = selectedDate.format(formatter) // 원하는 형식으로 변환 - showDatePicker = false - } , + viewModel.updateSendDate(selectedDate.format(formatter)) + viewModel.hideDatePicker() + }, 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 5b7d363c..342cbbe4 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 @@ -47,7 +47,7 @@ fun RecordFirstDiaryListScreen( modifier: Modifier = Modifier, onBackClick: () -> Unit, onPlusRecordClick: () -> Unit, - onEditClick: (Long) -> Unit, + onEditClick: (Long, String?) -> Unit, onBottomNavTabSelected: (BottomNavItem) -> Unit = {}, viewModel: MindRecordViewModel // ViewModel 주입 ) { @@ -55,7 +55,7 @@ fun RecordFirstDiaryListScreen( val records by viewModel.records.collectAsStateWithLifecycle() LaunchedEffect(key1 = Unit) { - viewModel.loadRecords("DIARY") + viewModel.loadRecordsForDiaryList() } Scaffold( modifier = modifier.fillMaxWidth(), @@ -113,11 +113,12 @@ fun RecordFirstDiaryListScreen( onDeleteClick = { viewModel.deleteRecord( recordId = record.id, - recordType = record.type ?: "DIARY" + recordType = record.type ?: "DIARY", + onReload = { viewModel.loadRecordsForDiaryList() } ) }, - onEditClick = { recordId -> - onEditClick(recordId) + onEditClick = { recordId, recordType -> + onEditClick(recordId, recordType) } ) // 이제 UIModel을 그대로 넘김 } 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 fef3ad67..8e6370b9 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 @@ -18,6 +18,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.kuit.afternote.R import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordDiaryQuestionContentItem import com.kuit.afternote.feature.dailyrecord.presentation.component.RecordSubTopbar import com.kuit.afternote.feature.dailyrecord.presentation.uimodel.MindRecordUiModel @@ -42,6 +44,9 @@ fun RecordQuestionScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(record) { + if (record == null) { + viewModel.loadDailyQuestion() + } record?.let { title = it.title content = it.content ?: "" @@ -98,6 +103,8 @@ fun RecordQuestionScreen( item { RecordDiaryQuestionContentItem( + questionText = uiState.dailyQuestionText + ?: stringResource(R.string.daily_answer_sample_question), 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 d1e18eef..abb6248a 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 @@ -2,6 +2,8 @@ package com.kuit.afternote.feature.dailyrecord.presentation.uimodel data class MindRecordUiState( val createErrorMessage: String? = null, + /** 오늘의 데일리 질문 텍스트 (GET /daily-question content). null이면 로딩 중 또는 실패. */ + val dailyQuestionText: String? = null, val title: String = "", val content: String = "", val sendDate: String = "", 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 0ee09c60..ae896e97 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 @@ -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 @@ -121,41 +122,106 @@ class MindRecordViewModel @Inject constructor( id = summary.recordId, title = summary.title, formattedDate = runCatching { formatDate(summary.date) } - .getOrElse { 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", "loadRecords 실패", e) _records.value = emptyList() } } } + /** + * 일기(DIARY)와 깊은 생각(DEEP_THOUGHT)을 모두 조회하여 합칩니다. + * RecordFirstDiaryListScreen에서 사용합니다. + */ + fun loadRecordsForDiaryList() { + viewModelScope.launch { + try { + val diaryList = getMindRecordsUseCase("DIARY", "LIST", null, null) + val deepThoughtList = getMindRecordsUseCase("DEEP_THOUGHT", "LIST", null, null) + val combined = (diaryList ?: emptyList()) + (deepThoughtList ?: emptyList()) + val sorted = combined.sortedByDescending { it.date } + + _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() + } + } + } + // UI 상태를 관리할 StateFlow private val _uiState = MutableStateFlow(MindRecordUiState()) val uiState: StateFlow = _uiState + /** + * 오늘의 데일리 질문을 조회하여 UI에 표시합니다. + * RecordQuestionScreen 진입 시(record == null) 호출합니다. + */ + fun loadDailyQuestion() { + viewModelScope.launch { + val data = getDailyQuestionUseCase() + _uiState.update { it.copy(dailyQuestionText = data?.content) } + } + } + fun onCreateRecord(params: CreateRecordParams, onSuccess: () -> Unit = {}) { viewModelScope.launch { + 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 = params.questionId, + 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) } } ) } @@ -165,15 +231,35 @@ class MindRecordViewModel @Inject constructor( _uiState.update { it.copy(createErrorMessage = null) } } - fun deleteRecord(recordId: Long, recordType: String, onSuccess: () -> Unit = {}) { + /** + * 기록을 삭제하고 목록을 갱신합니다. + * + * @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 { - try { - deleteMindRecordUseCase(recordId) - loadRecords(recordType) - onSuccess() - } catch (e: Exception) { - Log.e(TAG, "deleteRecord failed recordId=$recordId", e) - } + deleteMindRecordUseCase(recordId).fold( + onSuccess = { + if (onReload != null) { + onReload() + } else { + loadRecords(recordType) + } + onSuccess() + }, + onFailure = { e -> + Log.e(TAG, "deleteRecord failed recordId=$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 ebfee160..1915bfaf 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 @@ -7,7 +7,9 @@ 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.data.dto.PostMindRecordRequest import com.kuit.afternote.feature.dailyrecord.presentation.uimodel.MindRecordUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -30,12 +32,68 @@ constructor( private val deleteMindRecordUseCase: DeleteMindRecordUseCase, private val editMindRecordUseCase: EditMindRecordUseCase, private val getMindRecordsUseCase: GetMindRecordsUseCase, - private val getMindRecordUseCase: EditMindRecordUseCase, -) : ViewModel(){ + private val getMindRecordUseCase: GetMindRecordUseCase, +) : ViewModel() { private val _uiState = MutableStateFlow(MindRecordUiState()) val uiState: StateFlow = _uiState.asStateFlow() private var waitingAgainPopUpJob: Job? = null + /** 수정 모드일 때의 기록 ID. null이면 신규 작성. */ + private var recordIdForEdit: Long? = null + + /** 수정 모드일 때의 기록 유형 (DIARY, DEEP_THOUGHT). 저장 시 사용. */ + private var recordTypeForEdit: String? = null + + init { + val today = java.time.LocalDate.now() + val formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") + _uiState.update { it.copy(sendDate = today.format(formatter)) } + } + + /** + * 수정 모드: 기존 기록을 불러와 UI 상태를 채웁니다. + * EditDiaryRoute/EditDeepMindRoute 진입 시 호출됩니다. + */ + fun loadRecordForEdit(recordId: Long) { + viewModelScope.launch { + getMindRecordUseCase(recordId).fold( + onSuccess = { response -> + val detail = response.data + if (detail != null) { + recordIdForEdit = recordId + recordTypeForEdit = detail.type + val sendDateFormatted = formatApiDateToDisplay(detail.date) + _uiState.update { + it.copy( + title = detail.title, + content = detail.content, + sendDate = sendDateFormatted.ifBlank { it.sendDate } + ) + } + validateSaveEnabled() + } + }, + onFailure = { e -> + Log.e(TAG, "loadRecordForEdit failed recordId=$recordId", e) + } + ) + } + } + + /** "yyyy-MM-dd" → "yyyy년 MM월 dd일" 변환 */ + private fun formatApiDateToDisplay(apiDate: String): String { + if (apiDate.isBlank()) return "" + return try { + val formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd") + val parsed = java.time.LocalDate.parse(apiDate, formatter) + val displayFormatter = java.time.format.DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") + parsed.format(displayFormatter) + } catch (e: Exception) { + Log.e(TAG, "formatApiDateToDisplay failed: $apiDate", e) + "" + } + } + /** * 제목 업데이트 * @@ -94,33 +152,54 @@ constructor( } /** - * 등록 버튼 클릭 시: 등록 완료 팝업을 잠깐 보여준 뒤 실제 타임레터 정식등록을 수행합니다. + * 등록 버튼 클릭 시: 등록 완료 팝업을 잠깐 보여준 뒤 실제 등록을 수행합니다. * + * @param type 기록 유형 (DIARY, DEEP_THOUGHT) * @param onSuccess 저장 성공 시 콜백 */ - fun registerWithPopUpThenSave(onSuccess: () -> Unit) { - saveMindRecord(onSuccess = onSuccess) + fun registerWithPopUpThenSave(type: String, onSuccess: () -> Unit) { + saveMindRecord(type = type, onSuccess = onSuccess) } fun saveMindRecord( + type: String, showPopUpAfterSuccess: Boolean = true, onSuccess: () -> Unit ) { viewModelScope.launch { _uiState.update { it.copy(isLoading = true, saveErrorMessage = null) } val state = _uiState.value - val sendAt = buildSendAt(state.sendDate) - val result = createMindRecordUseCase( - type = "DIARY", // 혹은 "MIND_RECORD" 등 서버에서 요구하는 타입 - title = state.title.ifBlank { null }, - content = state.content.ifBlank { null }, - date = sendAt, - isDraft = false - ) + val dateStr = parseSendDateToApiFormat(state.sendDate) ?: return@launch.also { + _uiState.update { it.copy(isLoading = false, saveErrorMessage = "날짜 형식이 올바르지 않습니다.") } + } + + val effectiveType = recordTypeForEdit ?: type + val result = if (recordIdForEdit != null) { + val request = PostMindRecordRequest( + type = effectiveType, + title = state.title.ifBlank { "" }, + content = state.content.ifBlank { "" }, + date = dateStr, + isDraft = false, + questionId = null, + category = null + ) + editMindRecordUseCase(recordIdForEdit!!, request) + } else { + createMindRecordUseCase( + type = type, + title = state.title.ifBlank { null }, + content = state.content.ifBlank { null }, + date = dateStr, + isDraft = false, + questionId = null, + category = null + ) + } _uiState.update { it.copy(isLoading = false) } result.onSuccess { _ -> - Log.d(TAG, "saveMindRecord onSuccess") + Log.d(TAG, "saveMindRecord onSuccess type=$effectiveType edit=${recordIdForEdit != null}") _uiState.update { it.copy(saveErrorMessage = null) } if (showPopUpAfterSuccess) { _uiState.update { it.copy(showRegisteredPopUp = true) } @@ -139,14 +218,19 @@ constructor( } } } + /** - * sendAt 문자열 생성 (yyyy. MM. dd + HH:mm -> yyyy-MM-ddTHH:mm:00) + * "yyyy년 MM월 dd일" 형식을 API 요구 형식 "yyyy-MM-dd"로 변환합니다. */ - private fun buildSendAt( - sendDate: String, - ): String? { + private fun parseSendDateToApiFormat(sendDate: String): String? { if (sendDate.isBlank()) return null - val normalizedDate = sendDate.replace(". ", "-").trim() - return "$normalizedDate" + return try { + val formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") + val parsed = java.time.LocalDate.parse(sendDate, formatter) + parsed.toString() + } catch (e: Exception) { + Log.e(TAG, "parseSendDateToApiFormat failed: $sendDate", e) + null + } } } From e3f44ce16695affcf1265fa2c1897b7035995c08 Mon Sep 17 00:00:00 2001 From: hyhyh Date: Wed, 18 Feb 2026 14:11:33 +0900 Subject: [PATCH 5/8] =?UTF-8?q?"api=20=EC=97=B0=EA=B2=B0=20=EC=A4=91"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/usecase/EditMindRecordUseCase.kt | 7 +- .../component/RecordCalendarSort.kt | 39 +++-- .../component/RecordDiaryContentItem.kt | 155 ++++++++++++------ .../presentation/component/RecordListItem.kt | 2 +- .../presentation/component/RecordListSort.kt | 6 +- .../presentation/component/RecordSubTopbar.kt | 8 +- .../navigiation/RecordNavgraph.kt | 28 +++- .../presentation/navigiation/RecordRoute.kt | 4 + .../screen/RecordDailyQuestionListScreen.kt | 3 +- .../screen/RecordDeepMindScreen.kt | 9 +- .../presentation/screen/RecordDiaryScreen.kt | 4 +- .../screen/RecordFirstDiaryListScreen.kt | 36 ++-- .../presentation/uimodel/MindRecordUiState.kt | 2 + .../viewmodel/MindRecordWriterViewModel.kt | 18 +- 14 files changed, 229 insertions(+), 92 deletions(-) diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/EditMindRecordUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/EditMindRecordUseCase.kt index 9da499cf..ae3f5cc0 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/EditMindRecordUseCase.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/EditMindRecordUseCase.kt @@ -1,6 +1,5 @@ package com.kuit.afternote.feature.dailyrecord.domain.usecase -import com.kuit.afternote.feature.dailyrecord.data.dto.MindRecordDetail import com.kuit.afternote.feature.dailyrecord.data.dto.PostMindRecordRequest import com.kuit.afternote.feature.dailyrecord.data.repository.MindRecordRepositoryImpl import javax.inject.Inject @@ -8,10 +7,10 @@ import javax.inject.Inject class EditMindRecordUseCase @Inject constructor( private val repository: MindRecordRepositoryImpl ) { - suspend operator fun invoke(recordId: Long, request: PostMindRecordRequest): Result { + suspend operator fun invoke(recordId: Long, request: PostMindRecordRequest): Result { return try { - val response = repository.editMindRecord(recordId, request) - response.data?.let { Result.success(it) } ?: Result.failure(Exception("No data")) + repository.editMindRecord(recordId, request) + Result.success(Unit) } catch (e: Exception) { Result.failure(e) } 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..d3c39493 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 @@ -39,17 +43,19 @@ import java.util.Locale * 기록들 정렬할 때 쓰이는 컴포넌트 */ +private val BlueCircleColor = Color(0xFF328BFF) + @RequiresApi(Build.VERSION_CODES.O) @Composable fun RecordCalendarSort( modifier: Modifier = Modifier, - today: LocalDate + today: LocalDate, + recordedDates: 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 + var selectedDate by remember { mutableStateOf(today.dayOfMonth) } Box( modifier = Modifier @@ -113,6 +119,14 @@ fun RecordCalendarSort( for (col in 0..6) { val day = row * 7 + col - startOffset + 1 if (day in 1..daysInMonth) { + val cellDate = LocalDate.of(year, month, day) + val dateString = cellDate.toString() + val isToday = + year == today.year && + month == today.monthValue && + day == today.dayOfMonth + val isRecorded = dateString in recordedDates + Box( modifier = Modifier .weight(1f) @@ -122,18 +136,23 @@ fun RecordCalendarSort( .clickable(onClick = { selectedDate = day }) .background( when { - day == selectedDate -> Color(0xFF328BFF) + day == selectedDate -> BlueCircleColor else -> Color.Transparent } - ).then( - if (day in markedDates) { - Modifier.border( + ) + .then( + when { + isToday -> Modifier.border( + width = 2.dp, + color = BlueCircleColor, + shape = CircleShape + ) + isRecorded -> Modifier.border( width = 1.dp, - color = Color(0xFF328BFF), + color = BlueCircleColor, shape = CircleShape ) - } else { - Modifier + else -> Modifier } ), contentAlignment = Alignment.Center 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..1c0ab2f1 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 @@ -12,16 +12,17 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding 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 -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -36,6 +37,7 @@ import androidx.compose.ui.res.painterResource 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.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex @@ -45,8 +47,15 @@ import com.kuit.afternote.core.ui.component.DateWheelPickerDefaults import com.kuit.afternote.ui.theme.Black import com.kuit.afternote.ui.theme.Gray5 import com.kuit.afternote.ui.theme.Sansneo +import com.kuit.afternote.ui.theme.White import java.time.LocalDate import java.time.format.DateTimeFormatter +/** 깊은 생각 기록 시 선택 가능한 카테고리 목록 */ +private val DEEP_THOUGHT_CATEGORIES = listOf( + "나의 가치관", + "자아성찰", + "새로운 관점" +) @RequiresApi(Build.VERSION_CODES.O) @Composable @@ -64,67 +73,116 @@ fun RecordDiaryContentItem( sendDate: String, showDatePicker: Boolean = false, onDatePickerDismiss: () -> Unit, - - - ) { + selectedCategory: String? = null, + showCategoryDropdown: Boolean = false, + onCategoryClick: () -> Unit = {}, + onCategorySelected: (String) -> Unit = {}, + onCategoryDropdownDismiss: () -> Unit = {} +) { // 오늘 날짜 기준으로 받아오기 val today = LocalDate.now() val formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") val formattedDate = today.format(formatter) - var s = " " - if (standard == "일기 기록하기") { - s = "작성 날짜" - } else { - s = "기록 주제" - } + val isDeepThought = standard == "깊은 생각 기록하기" Column( modifier = Modifier .padding(top = 24.dp, start = 20.dp, end = 20.dp) ) { - Text( - text = s, - color = Black, - fontSize = 12.sp, - fontWeight = FontWeight.SemiBold, - fontFamily = Sansneo - ) - - Box( - modifier = Modifier - .padding(all = 8.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + if (isDeepThought) { + Text( + text = "기록 주제", + color = Black, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = Sansneo + ) + Box(modifier = Modifier.padding(all = 8.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Box { + Text( + text = selectedCategory ?: "나의 가치관", + fontSize = 12.sp, + modifier = Modifier + .padding(vertical = 8.dp) + .clickable { onCategoryClick() } + ) + DropdownMenu( + expanded = showCategoryDropdown, + onDismissRequest = onCategoryDropdownDismiss, + offset = DpOffset(x = 0.dp, y = 8.dp), + modifier = Modifier + .width(350.dp) + .height(162.dp) + .background(White) + ) { + DEEP_THOUGHT_CATEGORIES.forEach { category -> + DropdownMenuItem( + text = { + Text( + text = category, + fontFamily = Sansneo, + fontSize = 14.sp + ) + }, + onClick = { + onCategorySelected(category) + onCategoryDropdownDismiss() + } + ) + } + } + } + Image( + painter = painterResource(id = R.drawable.ic_under), + contentDescription = "카테고리 선택", + modifier = Modifier + .size(24.dp) + .clickable { onCategoryClick() } + ) + } + Divider( + modifier = Modifier.align(Alignment.BottomCenter), + color = Color.LightGray, + thickness = 0.8.dp + ) + } + } else { + Text( + text = "작성 날짜", + color = Black, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = Sansneo + ) + Box( + modifier = Modifier + .padding(all = 8.dp) ) { - if (s == "작성 날짜") { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { Text( text = if (sendDate.isNotEmpty()) sendDate else formattedDate, fontSize = 12.sp, modifier = Modifier.padding(vertical = 8.dp) ) - } else if (s == "기록 주제") { - Text( - text = "나의 가치관", - fontSize = 12.sp, - modifier = Modifier.padding(vertical = 8.dp) + Image( + painter = painterResource(id = R.drawable.ic_under), + contentDescription = "날짜 선택", + modifier = Modifier + .size(24.dp) + .clickable { onDateClick() } ) } - - Image( - painter = painterResource(id = R.drawable.ic_under), - contentDescription = "밑 화살표", - modifier = Modifier - .size(24.dp) - .clickable { onDateClick() } - ) - if (sendDate.isNotEmpty()) { - val sendDate = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") - } + Divider(color = Color.LightGray, thickness = 0.8.dp) } - Divider(color = Color.LightGray, thickness = 0.8.dp) } BasicTextField( value = title, @@ -148,7 +206,7 @@ fun RecordDiaryContentItem( innerTextField() } ) - if (showDatePicker) { + if (!isDeepThought && showDatePicker) { var selectedDate by remember { mutableStateOf(LocalDate.now()) } Box( modifier = Modifier @@ -176,10 +234,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/RecordListItem.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordListItem.kt index ec2edcf9..e4c0ac57 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,7 +42,7 @@ fun RecordListItem( var expanded by remember { mutableStateOf(false) } RecordQnAListItem( - question = record.title, + question = record.title.ifBlank { "-" }, answer = (record.content ?: record.title).ifBlank { "-" }, dateText = record.formattedDate, trailing = { 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..df0ca9f5 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, + recordedDates: 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, + recordedDates = recordedDates ) } } diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordSubTopbar.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordSubTopbar.kt index 8abf8a78..6925fe36 100644 --- a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordSubTopbar.kt +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/presentation/component/RecordSubTopbar.kt @@ -36,6 +36,7 @@ fun RecordSubTopbar( text: String, onLeftClock: () -> Unit, onRightClick: () -> Unit, + isRegisterEnabled: Boolean = true ) { Box( modifier = modifier @@ -68,14 +69,13 @@ fun RecordSubTopbar( Box( modifier = Modifier .align(Alignment.CenterEnd) - .clickable(onClick = onRightClick) + .clickable(enabled = isRegisterEnabled) { onRightClick() } .padding(end = 13.dp), - contentAlignment = Alignment.Center, - + contentAlignment = Alignment.Center ) { Text( text = "등록", - color = Gray5, + color = if (isRegisterEnabled) Gray5 else Gray5.copy(alpha = 0.5f), fontSize = 14.sp, fontWeight = FontWeight.Normal, fontFamily = Sansneo 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 18f561b3..5cf026f5 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 @@ -35,7 +35,7 @@ fun NavGraphBuilder.recordNavGraph( RecordMainScreen( onDiaryClick = { navController.navigate(RecordRoute.ListRoute) }, onQuestionClick = { navController.navigate(RecordRoute.QuestionRouteList) }, - onDeepMindClick = { navController.navigate(RecordRoute.DeepMindRoute) }, + onDeepMindClick = { navController.navigate(RecordRoute.DeepMindListRoute) }, onWeekendReportClick = { navController.navigate(RecordRoute.WeekendReportRoute) }, onBottomNavTabSelected = onBottomNavTabSelected ) @@ -43,6 +43,7 @@ fun NavGraphBuilder.recordNavGraph( composable { val viewModel: MindRecordViewModel = hiltViewModel() RecordFirstDiaryListScreen( + listMode = "DIARY", onBackClick = { navController.popBackStack() }, onPlusRecordClick = { navController.navigate(RecordRoute.DiaryRoute) }, onEditClick = { recordId, recordType -> @@ -56,6 +57,23 @@ fun NavGraphBuilder.recordNavGraph( viewModel = viewModel ) } + composable { + val viewModel: MindRecordViewModel = hiltViewModel() + RecordFirstDiaryListScreen( + listMode = "DEEP_THOUGHT", + onBackClick = { navController.popBackStack() }, + onPlusRecordClick = { navController.navigate(RecordRoute.DeepMindRoute) }, + onEditClick = { recordId, recordType -> + when (recordType) { + "DIARY" -> navController.navigate(RecordRoute.EditDiaryRoute(recordId)) + "DEEP_THOUGHT" -> navController.navigate(RecordRoute.EditDeepMindRoute(recordId)) + else -> navController.navigate(RecordRoute.EditRoute(recordId)) + } + }, + onBottomNavTabSelected = onBottomNavTabSelected, + viewModel = viewModel + ) + } composable { val viewModel: MindRecordViewModel = hiltViewModel() RecordDailyQuestionListScreen( @@ -92,7 +110,8 @@ fun NavGraphBuilder.recordNavGraph( viewModel.registerWithPopUpThenSave(type = "DIARY") { navController.popBackStack() } - } + }, + isRegisterEnabled = !uiState.isLoading ) } composable { @@ -114,7 +133,8 @@ fun NavGraphBuilder.recordNavGraph( viewModel.registerWithPopUpThenSave(type = "DIARY") { navController.popBackStack() } - } + }, + isRegisterEnabled = !uiState.isLoading ) } composable { @@ -155,7 +175,7 @@ fun NavGraphBuilder.recordNavGraph( RecordDeepMindScreen( onLeftClick = { navController.popBackStack() }, onRegisterSuccess = { - navController.navigate(RecordRoute.ListRoute) { + navController.navigate(RecordRoute.DeepMindListRoute) { popUpTo(RecordRoute.DeepMindRoute) { inclusive = true } } }, 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 8fdd8abf..a45edcc0 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 @@ -21,6 +21,10 @@ sealed interface RecordRoute { @Serializable data object ListRoute : RecordRoute + /** 깊은 생각 전용 리스트. DEEP_THOUGHT 기록만 표시, FAB → RecordDeepMindScreen */ + @Serializable + data object DeepMindListRoute : 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 a9768e81..bfe764e8 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 @@ -75,7 +75,8 @@ fun RecordDailyQuestionListScreen( LazyColumn { item { RecordListSort( - today = today + today = today, + recordedDates = records.map { it.originalDate }.toSet() ) } items(records) { record -> 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 c8c5114a..fae5856f 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 @@ -55,11 +55,13 @@ fun RecordDeepMindScreen( viewModel.registerWithPopUpThenSave(type = "DEEP_THOUGHT") { onRegisterSuccess() } - } + }, + isRegisterEnabled = !uiState.isLoading ) } item { + var showCategoryDropdown by remember { mutableStateOf(false) } RecordDiaryContentItem( standard = "깊은 생각 기록하기", onDateSelected = { year, month, day -> @@ -75,6 +77,11 @@ fun RecordDeepMindScreen( sendDate = sendDate, showDatePicker = showDatePicker, onDatePickerDismiss = onDatePickerDismiss, + selectedCategory = uiState.category, + showCategoryDropdown = showCategoryDropdown, + onCategoryClick = { showCategoryDropdown = true }, + onCategorySelected = viewModel::updateCategory, + onCategoryDropdownDismiss = { showCategoryDropdown = false } ) } } 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 83a405d1..dffc69b2 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 @@ -44,6 +44,7 @@ fun RecordDiaryScreen( showDatePicker: Boolean, onDatePickerDismiss: () -> Unit, onRegisterClick: () -> Unit, + isRegisterEnabled: Boolean = true ) { val formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") Scaffold { paddingValues -> @@ -56,7 +57,8 @@ fun RecordDiaryScreen( RecordSubTopbar( text = "일기 기록하기", onLeftClock = onLeftClick, - onRightClick = onRegisterClick + onRightClick = onRegisterClick, + isRegisterEnabled = isRegisterEnabled ) } 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 342cbbe4..d657d344 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 @@ -36,26 +36,31 @@ import com.kuit.afternote.feature.dailyrecord.presentation.viewmodel.MindRecordV import java.time.LocalDate /** - * 일기, 깊은 생각하기로 넘어가면 먼저뜨는 리스트형 창 - * - 상단 : 제목 - * - 중간 : 리스트 - * - 하단 : FAB 바 + * 일기 또는 깊은 생각 리스트형 창 + * + * @param listMode "DIARY" = 일기+깊은생각 통합, "DEEP_THOUGHT" = 깊은생각만 */ @RequiresApi(Build.VERSION_CODES.O) @Composable fun RecordFirstDiaryListScreen( modifier: Modifier = Modifier, + listMode: String = "DIARY", onBackClick: () -> Unit, onPlusRecordClick: () -> Unit, onEditClick: (Long, String?) -> Unit, onBottomNavTabSelected: (BottomNavItem) -> Unit = {}, - viewModel: MindRecordViewModel // ViewModel 주입 + viewModel: MindRecordViewModel ) { val today = LocalDate.now() val records by viewModel.records.collectAsStateWithLifecycle() + val title = if (listMode == "DEEP_THOUGHT") "깊은 생각" else "일기" - LaunchedEffect(key1 = Unit) { - viewModel.loadRecordsForDiaryList() + LaunchedEffect(listMode) { + if (listMode == "DEEP_THOUGHT") { + viewModel.loadRecords("DEEP_THOUGHT") + } else { + viewModel.loadRecordsForDiaryList() + } } Scaffold( modifier = modifier.fillMaxWidth(), @@ -100,12 +105,15 @@ fun RecordFirstDiaryListScreen( modifier = Modifier.padding(paddingValues) ) { TopBar( - title = "일기", + title = title, onBackClick = onBackClick, ) LazyColumn { item { - RecordListSort(today = today) + RecordListSort( + today = today, + recordedDates = records.map { it.originalDate }.toSet() + ) } items(records) { record -> RecordListItem( @@ -113,8 +121,14 @@ fun RecordFirstDiaryListScreen( onDeleteClick = { viewModel.deleteRecord( recordId = record.id, - recordType = record.type ?: "DIARY", - onReload = { viewModel.loadRecordsForDiaryList() } + recordType = record.type ?: listMode, + onReload = { + if (listMode == "DEEP_THOUGHT") { + viewModel.loadRecords("DEEP_THOUGHT") + } else { + viewModel.loadRecordsForDiaryList() + } + } ) }, onEditClick = { recordId, recordType -> 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 abb6248a..dc19f28e 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 @@ -7,6 +7,8 @@ data class MindRecordUiState( val title: String = "", val content: String = "", val sendDate: String = "", + /** 깊은 생각 카테고리 (예: 나의 가치관, 자아성찰). DEEP_THOUGHT일 때만 사용. */ + val category: String? = null, val isLoading: Boolean = false, val isSaveEnabled: Boolean = false, val showDatePicker: Boolean = false, 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 1915bfaf..04d0d535 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 @@ -67,7 +67,8 @@ constructor( it.copy( title = detail.title, content = detail.content, - sendDate = sendDateFormatted.ifBlank { it.sendDate } + sendDate = sendDateFormatted.ifBlank { it.sendDate }, + category = detail.category ) } validateSaveEnabled() @@ -123,6 +124,14 @@ constructor( _uiState.update { it.copy(sendDate = date) } validateSaveEnabled() } + + /** + * 깊은 생각 카테고리 업데이트 (나의 가치관 등) + */ + fun updateCategory(category: String?) { + _uiState.update { it.copy(category = category) } + validateSaveEnabled() + } /** * 저장 버튼 활성화 여부 검증 * @@ -174,6 +183,9 @@ constructor( } val effectiveType = recordTypeForEdit ?: type + val categoryForRequest = + if (effectiveType == "DEEP_THOUGHT") (state.category ?: "나의 가치관") else null + val result = if (recordIdForEdit != null) { val request = PostMindRecordRequest( type = effectiveType, @@ -182,7 +194,7 @@ constructor( date = dateStr, isDraft = false, questionId = null, - category = null + category = categoryForRequest ) editMindRecordUseCase(recordIdForEdit!!, request) } else { @@ -193,7 +205,7 @@ constructor( date = dateStr, isDraft = false, questionId = null, - category = null + category = categoryForRequest ) } From 8ce94f03e3a3decc3e67e59781947befa8cbe8c9 Mon Sep 17 00:00:00 2001 From: hyhyh Date: Wed, 18 Feb 2026 13:47:08 +0900 Subject: [PATCH 6/8] Revert "Merge remote-tracking branch 'origin/main' into dailyrecord" This reverts commit b68c20f5a8ccba2e1b3e3618e6437b598343d823, reversing changes made to e3f44ce16695affcf1265fa2c1897b7035995c08. --- .../workflows/firebase_app_distribution.yml | 5 - README.md | 12 +- .../app/navigation/navgraph/NavGraph.kt | 60 ++---- .../MemorialVideoUploadRepositoryImpl.kt | 93 --------- .../feature/admin/data/api/AdminApiService.kt | 77 -------- .../feature/admin/data/di/AdminModule.kt | 28 --- .../admin/data/dto/AdminVerificationDto.kt | 46 ----- .../data/repository/iface/AdminRepository.kt | 52 ----- .../repository/impl/AdminRepositoryImpl.kt | 45 ----- .../data/di/AfternoteRepositoryModule.kt | 9 - .../MemorialVideoUploadRepository.kt | 15 -- .../usecase/UploadMemorialVideoUseCase.kt | 17 -- .../content/MemorialGuidelineEditContent.kt | 1 - .../MemorialGuidelineEditContentParams.kt | 1 - .../edit/upload/FuneralVideoUpload.kt | 58 +----- .../screen/AfternoteEditScreen.kt | 1 - .../screen/AfternoteEditViewModel.kt | 88 +++------ .../home/presentation/component/HomeHeader.kt | 9 +- .../navgraph/OnboardingNavgraph.kt | 2 +- .../receiver/data/api/ReceivedApiService.kt | 3 + .../domain/entity/ReceivedAfternote.kt | 5 +- .../domain/entity/ReceivedAfternoteDetail.kt | 14 -- .../entity/ReceivedAfternotePlaylist.kt | 11 -- .../domain/entity/ReceivedPlaylistSong.kt | 12 -- .../GetAfternoteDetailByAuthCodeRepository.kt | 22 --- .../iface/GetSenderMessageRepository.kt | 17 -- ...GetTimeLetterDetailByAuthCodeRepository.kt | 22 --- .../GetAfternoteDetailByAuthCodeUseCase.kt | 25 --- .../domain/usecase/GetSenderMessageUseCase.kt | 18 -- .../GetTimeLetterDetailByAuthCodeUseCase.kt | 25 --- .../navgraph/ReceiverMainRoute.kt | 5 +- .../navgraph/ReceiverMemorialPlaylistRoute.kt | 27 --- .../uimodel/ReceivedListScreenUiState.kt | 3 - .../ReceiverMemorialPlaylistUiState.kt | 14 -- .../ReceiverAfternoteTriggerViewModel.kt | 26 +-- .../ReceiverAfternotesListViewModel.kt | 1 - .../ReceiverMemorialPlaylistViewModel.kt | 99 ---------- .../ReceiverTimeLetterDetailViewModel.kt | 67 ++----- .../receiverauth/component/PdfInputContent.kt | 18 +- .../component/ReceiveEndContent.kt | 21 +- .../data/api/ReceiverAuthApiService.kt | 101 ---------- .../data/di/ReceiverAuthModule.kt | 48 ----- .../receiverauth/data/dto/ReceiverAuthDto.kt | 187 +----------------- .../data/mapper/ReceiverAuthContentMapper.kt | 57 +----- .../iface/ReceiverAuthRepository.kt | 82 -------- ...eliveryVerificationStatusRepositoryImpl.kt | 29 --- ...ceiverAuthAfternoteDetailRepositoryImpl.kt | 28 --- .../impl/ReceiverAuthMessageRepositoryImpl.kt | 20 -- .../impl/ReceiverAuthRepositoryImpl.kt | 64 ------ ...eiverAuthTimeLetterDetailRepositoryImpl.kt | 28 --- .../ReceiverDocumentUploadRepositoryImpl.kt | 92 --------- ...ubmitDeliveryVerificationRepositoryImpl.kt | 31 --- .../entity/DeliveryVerificationStatus.kt | 18 -- ...GetDeliveryVerificationStatusRepository.kt | 19 -- .../SubmitDeliveryVerificationRepository.kt | 23 --- .../iface/UploadReceiverDocumentRepository.kt | 18 -- .../GetDeliveryVerificationStatusUseCase.kt | 24 --- .../SubmitDeliveryVerificationUseCase.kt | 33 ---- .../usecase/UploadReceiverDocumentUseCase.kt | 26 --- .../viewmodel/VerifySelfUiState.kt | 9 +- .../viewmodel/VerifySelfViewModel.kt | 88 +-------- .../viewmodel/VerifySelfViewModelContract.kt | 1 - .../receiverauth/screen/VerifySelfScreen.kt | 65 +----- .../presentation/navgraph/SettingNavGraph.kt | 2 +- .../timeletter/data/dto/TimeLetterDto.kt | 1 - .../data/mapper/TimeLetterMapper.kt | 2 +- .../screen/TimeLetterWriterScreen.kt | 6 +- .../feature/user/data/api/UserApiService.kt | 23 --- .../feature/user/data/dto/UserDto.kt | 35 ---- .../feature/user/data/mapper/UserMapper.kt | 43 +--- .../data/repository/UserRepositoryImpl.kt | 27 --- .../feature/user/domain/model/UserModel.kt | 19 -- .../user/domain/repository/UserRepository.kt | 20 -- app/src/main/res/values/strings.xml | 12 -- .../screen/AfternoteEditViewModelTest.kt | 10 +- ...etDeliveryVerificationStatusUseCaseTest.kt | 54 ----- .../SubmitDeliveryVerificationUseCaseTest.kt | 54 ----- .../UploadReceiverDocumentUseCaseTest.kt | 56 ------ .../viewmodel/VerifySelfViewModelTest.kt | 87 +------- scripts/get-kakao-swagger-token.py | 62 ++---- 80 files changed, 136 insertions(+), 2542 deletions(-) delete mode 100644 app/src/main/java/com/kuit/afternote/data/upload/MemorialVideoUploadRepositoryImpl.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/admin/data/api/AdminApiService.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/admin/data/di/AdminModule.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/admin/data/dto/AdminVerificationDto.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/admin/data/repository/iface/AdminRepository.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/admin/data/repository/impl/AdminRepositoryImpl.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/afternote/domain/repository/MemorialVideoUploadRepository.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/afternote/domain/usecase/UploadMemorialVideoUseCase.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedAfternoteDetail.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedAfternotePlaylist.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedPlaylistSong.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiver/domain/repository/iface/GetAfternoteDetailByAuthCodeRepository.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiver/domain/repository/iface/GetSenderMessageRepository.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiver/domain/repository/iface/GetTimeLetterDetailByAuthCodeRepository.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiver/domain/usecase/GetAfternoteDetailByAuthCodeUseCase.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiver/domain/usecase/GetSenderMessageUseCase.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiver/domain/usecase/GetTimeLetterDetailByAuthCodeUseCase.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiver/presentation/navgraph/ReceiverMemorialPlaylistRoute.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiver/presentation/uimodel/ReceiverMemorialPlaylistUiState.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverMemorialPlaylistViewModel.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/GetDeliveryVerificationStatusRepositoryImpl.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthAfternoteDetailRepositoryImpl.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthMessageRepositoryImpl.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthTimeLetterDetailRepositoryImpl.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverDocumentUploadRepositoryImpl.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/SubmitDeliveryVerificationRepositoryImpl.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/entity/DeliveryVerificationStatus.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/repository/iface/GetDeliveryVerificationStatusRepository.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/repository/iface/SubmitDeliveryVerificationRepository.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/repository/iface/UploadReceiverDocumentRepository.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/usecase/GetDeliveryVerificationStatusUseCase.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/usecase/SubmitDeliveryVerificationUseCase.kt delete mode 100644 app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/usecase/UploadReceiverDocumentUseCase.kt delete mode 100644 app/src/test/java/com/kuit/afternote/feature/receiverauth/domain/usecase/GetDeliveryVerificationStatusUseCaseTest.kt delete mode 100644 app/src/test/java/com/kuit/afternote/feature/receiverauth/domain/usecase/SubmitDeliveryVerificationUseCaseTest.kt delete mode 100644 app/src/test/java/com/kuit/afternote/feature/receiverauth/domain/usecase/UploadReceiverDocumentUseCaseTest.kt diff --git a/.github/workflows/firebase_app_distribution.yml b/.github/workflows/firebase_app_distribution.yml index bc7e2c64..fd302af8 100644 --- a/.github/workflows/firebase_app_distribution.yml +++ b/.github/workflows/firebase_app_distribution.yml @@ -26,14 +26,9 @@ jobs: run: chmod +x gradlew - name: Create local.properties - env: - KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} run: | echo "sdk.dir=$ANDROID_SDK_ROOT" > local.properties echo "sdk.dir=$ANDROID_SDK_ROOT" > app/local.properties - if [ -n "$KAKAO_NATIVE_APP_KEY" ]; then - echo "KAKAO_NATIVE_APP_KEY=$KAKAO_NATIVE_APP_KEY" >> local.properties - fi - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 diff --git a/README.md b/README.md index be0a9555..ddb3e5b6 100644 --- a/README.md +++ b/README.md @@ -55,17 +55,7 @@ TEST_EMAIL=your_test_email@example.com TEST_PASSWORD=your_test_password ``` -**카카오 로그인 사용 시 (필수):** -카카오 로그인을 쓰려면 [Kakao Developers](https://developers.kakao.com)에서 앱의 **네이티브 앱 키**를 발급받은 뒤, 같은 `local.properties`에 다음을 추가하세요. 없으면 앱에서 "카카오 SDK가 초기화되지 않았습니다" 메시지가 표시됩니다. - -```properties -KAKAO_NATIVE_APP_KEY=your_native_app_key_here -``` - -**배포 빌드(Firebase App Distribution 등):** -CI에서 빌드하는 APK에서도 카카오 로그인이 동작하게 하려면, GitHub 저장소 **Settings → Secrets and variables → Actions**에 `KAKAO_NATIVE_APP_KEY` 시크릿을 추가하세요. 워크플로에서 이 값을 `local.properties`에 넣어 빌드합니다. - -> **참고**: `local.properties`는 `.gitignore`에 포함되어 있어 Git에 커밋되지 않습니다. 따라서 각 개발자/빌드 환경에서 위 값들을 직접 설정해야 합니다. +> **참고**: `local.properties`는 `.gitignore`에 포함되어 있어 Git에 커밋되지 않습니다. ### ⚠️ 중요: OS별 경로 하드코딩 금지 diff --git a/app/src/main/java/com/kuit/afternote/app/navigation/navgraph/NavGraph.kt b/app/src/main/java/com/kuit/afternote/app/navigation/navgraph/NavGraph.kt index 3c026706..e747e5b8 100644 --- a/app/src/main/java/com/kuit/afternote/app/navigation/navgraph/NavGraph.kt +++ b/app/src/main/java/com/kuit/afternote/app/navigation/navgraph/NavGraph.kt @@ -19,8 +19,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.core.content.ContextCompat @@ -33,7 +31,6 @@ import com.kuit.afternote.app.compositionlocal.DataProviderLocals import com.kuit.afternote.app.di.ReceiverAuthSessionEntryPoint import com.kuit.afternote.app.di.TokenManagerEntryPoint import com.kuit.afternote.core.dummy.receiver.AfternoteListItemSeed -import com.kuit.afternote.core.ui.component.list.AfternoteTab import com.kuit.afternote.core.ui.component.navigation.BottomNavItem import com.kuit.afternote.core.ui.screen.afternotedetail.GalleryDetailCallbacks import com.kuit.afternote.core.ui.screen.afternotedetail.GalleryDetailScreen @@ -61,15 +58,12 @@ import com.kuit.afternote.feature.onboarding.presentation.navgraph.OnboardingRou import com.kuit.afternote.feature.onboarding.presentation.navgraph.onboardingNavGraph import com.kuit.afternote.feature.receiver.presentation.navgraph.ReceiverAfternoteListRoute import com.kuit.afternote.feature.receiver.presentation.navgraph.ReceiverMainRoute -import com.kuit.afternote.feature.receiver.presentation.navgraph.ReceiverMemorialPlaylistRoute import com.kuit.afternote.feature.receiver.presentation.navgraph.ReceiverTimeLetterDetailRoute import com.kuit.afternote.feature.receiver.presentation.navgraph.ReceiverTimeLetterRoute import com.kuit.afternote.feature.receiver.presentation.screen.afternote.ReceiverAfternoteListEvent -import com.kuit.afternote.feature.receiver.presentation.viewmodel.ReceiverAfternotesListViewModel import com.kuit.afternote.feature.receiverauth.screen.ReceiverOnboardingScreen import com.kuit.afternote.feature.receiverauth.screen.VerifySelfScreen import com.kuit.afternote.feature.receiver.presentation.uimodel.ReceiverAfternoteListUiState -import com.kuit.afternote.core.ui.util.getAfternoteDisplayRes import com.kuit.afternote.feature.setting.presentation.navgraph.SettingRoute import com.kuit.afternote.feature.setting.presentation.navgraph.settingNavGraph import com.kuit.afternote.feature.timeletter.presentation.navgraph.TimeLetterRoute @@ -144,42 +138,32 @@ private fun FingerprintLoginRouteContent( @Composable private fun ReceiverAfternoteListRouteContent(navHostController: NavHostController) { + val receiverProvider = DataProviderLocals.LocalReceiverDataProvider.current BackHandler { navHostController.popBackStack() } - val viewModel: ReceiverAfternotesListViewModel = hiltViewModel() - val afterNotesState by viewModel.uiState.collectAsStateWithLifecycle() - val afternoteItems = afterNotesState.items.map { item -> - val (stringResId, iconResId) = getAfternoteDisplayRes(item.sourceType) - val serviceName = stringResource(stringResId) - AfternoteListDisplayItem( - id = item.id.toString(), - serviceName = serviceName, - date = item.lastUpdatedAt, - iconResId = iconResId - ) + val afternoteItems = remember(receiverProvider) { + receiverProvider.getAfternoteListSeedsForReceiverList().map { seed -> + AfternoteListDisplayItem( + id = seed.id, + serviceName = seed.serviceNameLiteral ?: "", + date = seed.date, + iconResId = seed.iconResId + ) + } } - var selectedTab by remember { mutableStateOf(AfternoteTab.ALL) } - var selectedBottomNavItem by remember { - mutableStateOf(BottomNavItem.AFTERNOTE) + var listState by remember { + mutableStateOf(ReceiverAfternoteListUiState(items = afternoteItems)) } - val listState = ReceiverAfternoteListUiState( - selectedTab = selectedTab, - selectedBottomNavItem = selectedBottomNavItem, - items = afternoteItems - ) ReceiverAfternoteListRoute( uiState = listState, onEvent = { event -> - when (event) { - is ReceiverAfternoteListEvent.SelectTab -> selectedTab = event.tab + listState = when (event) { + is ReceiverAfternoteListEvent.SelectTab -> + listState.copy(selectedTab = event.tab) is ReceiverAfternoteListEvent.SelectBottomNav -> - selectedBottomNavItem = event.navItem + listState.copy(selectedBottomNavItem = event.navItem) is ReceiverAfternoteListEvent.ClickItem -> { - val item = afterNotesState.items.find { it.id.toString() == event.itemId } - if (item?.sourceType == "PLAYLIST") { - navHostController.navigate("receiver_afternote_playlist/${event.itemId}") - } else { - navHostController.navigate("receiver_afternote_detail/${event.itemId}") - } + navHostController.navigate("receiver_afternote_detail/${event.itemId}") + listState } } } @@ -377,7 +361,7 @@ fun NavGraph(navHostController: NavHostController) { } }, onTImeLetterClick = { - navHostController.navigate("record_main") { + navHostController.navigate(TimeLetterRoute.TimeLetterMainRoute) { launchSingleTop = true } } @@ -443,12 +427,6 @@ fun NavGraph(navHostController: NavHostController) { ) } - composable("receiver_afternote_playlist/{afternoteId}") { - ReceiverMemorialPlaylistRoute( - onBackClick = { navHostController.popBackStack() } - ) - } - composable("receiver_time_letter_list/{receiverId}") { ReceiverTimeLetterRoute( navController = navHostController, diff --git a/app/src/main/java/com/kuit/afternote/data/upload/MemorialVideoUploadRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/data/upload/MemorialVideoUploadRepositoryImpl.kt deleted file mode 100644 index 50545d39..00000000 --- a/app/src/main/java/com/kuit/afternote/data/upload/MemorialVideoUploadRepositoryImpl.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.kuit.afternote.data.upload - -import android.content.Context -import android.net.Uri -import androidx.core.net.toUri -import com.kuit.afternote.data.remote.requireData -import com.kuit.afternote.feature.afternote.domain.repository.MemorialVideoUploadRepository -import com.kuit.afternote.feature.user.data.api.ImageApiService -import com.kuit.afternote.feature.user.data.dto.PresignedUrlRequestDto -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.withContext -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.asRequestBody -import java.io.File -import javax.inject.Inject -import javax.inject.Named - -private const val DIRECTORY_AFTERNOTES = "afternotes" -private const val DEFAULT_VIDEO_EXTENSION = "mp4" - -/** - * Uploads memorial video via POST /files/presigned-url (directory "afternotes") then S3 PUT. - * Reads from content URI (e.g. Photo Picker) and returns the HTTPS fileUrl so the backend - * stores a usable URL, not a local content:// URI. - */ -class MemorialVideoUploadRepositoryImpl - @Inject - constructor( - @ApplicationContext private val context: Context, - private val imageApi: ImageApiService, - @Named("S3Upload") private val okHttpClient: OkHttpClient, - @Named("IoDispatcher") private val ioDispatcher: CoroutineDispatcher - ) : MemorialVideoUploadRepository { - - override suspend fun uploadVideo(contentUriString: String): Result = - runCatching { - val uri = contentUriString.toUri() - val extension = videoExtensionFromUri(uri) - val presigned = imageApi.getPresignedUrl( - PresignedUrlRequestDto( - directory = DIRECTORY_AFTERNOTES, - extension = extension - ) - ).requireData() - - val tempFile = withContext(ioDispatcher) { - context.contentResolver.openInputStream(uri)?.use { input -> - val file = File.createTempFile("memorial_video_", ".$extension") - file.outputStream().use { output -> - input.copyTo(output) - } - file - } ?: throw IllegalStateException("Could not read video from URI") - } - - try { - val contentType = presigned.contentType.ifBlank { "video/$extension" } - val requestBody = tempFile.asRequestBody(contentType.toMediaType()) - val putRequest = - Request.Builder() - .url(presigned.presignedUrl) - .put(requestBody) - .header("Content-Type", contentType) - .build() - - withContext(ioDispatcher) { - okHttpClient.newCall(putRequest).execute().use { response -> - check(response.isSuccessful) { - "S3 video upload failed: ${response.code} ${response.message}" - } - } - } - presigned.fileUrl - } finally { - if (!tempFile.delete()) { - // Temp file cleanup failed; file may remain until system cleanup - } - } - } - - private fun videoExtensionFromUri(uri: Uri): String { - val mime = context.contentResolver.getType(uri) ?: return DEFAULT_VIDEO_EXTENSION - return when { - mime == "video/mp4" -> "mp4" - mime == "video/quicktime" -> "mov" - mime.startsWith("video/") -> mime.removePrefix("video/").takeIf { it.isNotBlank() } ?: DEFAULT_VIDEO_EXTENSION - else -> DEFAULT_VIDEO_EXTENSION - } - } -} diff --git a/app/src/main/java/com/kuit/afternote/feature/admin/data/api/AdminApiService.kt b/app/src/main/java/com/kuit/afternote/feature/admin/data/api/AdminApiService.kt deleted file mode 100644 index 8105d024..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/admin/data/api/AdminApiService.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.kuit.afternote.feature.admin.data.api - -import com.kuit.afternote.data.remote.ApiResponse -import com.kuit.afternote.feature.admin.data.dto.AdminVerificationResponseDto -import com.kuit.afternote.feature.admin.data.dto.ApproveVerificationRequestDto -import com.kuit.afternote.feature.admin.data.dto.RejectVerificationRequestDto -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Path - -/** - * 관리자 API 서비스. (스웨거 기준) - * - * - GET /api/admin/verifications: 대기 중인 인증 요청 목록 조회 - * - GET /api/admin/verifications/{id}: 인증 요청 상세 조회 - * - POST /api/admin/verifications/{id}/approve: 인증 요청 승인 - * - POST /api/admin/verifications/{id}/reject: 인증 요청 거절 - */ -interface AdminApiService { - - /** - * 대기 중인(PENDING) 사망확인 인증 요청 목록을 조회합니다. - * - * 관리자 권한(ADMIN) 필요. 비인증 시 401, 권한 없으면 403. - * - * @return data: 인증 요청 목록 (발신자/수신자 정보, Presigned URL, 제출 일시, 관리자 메모) - */ - @GET("api/admin/verifications") - suspend fun getPendingVerifications(): ApiResponse?> - - /** - * 관리자가 특정 인증 요청의 상세 정보를 조회합니다. - * - * Presigned URL이 포함되어 있으므로, URL 만료 전에 서류를 확인하세요. - * 관리자 권한(ADMIN) 필요. 비인증 시 401, 권한 없으면 403, 미존재 시 404. - * - * @param id 인증 요청 ID (path) - * @return data: 인증 요청 상세 (발신자/수신자 정보, Presigned URL, 상태, 관리자 메모, 생성일시) - */ - @GET("api/admin/verifications/{id}") - suspend fun getVerificationDetail(@Path("id") id: Long): ApiResponse - - /** - * 관리자가 사망확인 인증 요청을 승인합니다. - * - * 승인 시 인증 상태가 APPROVED로 변경되고, 발신자의 conditionFulfilled가 true로 변경되어 - * 수신자가 콘텐츠를 열람할 수 있게 됩니다. adminNote(선택)로 메모를 남길 수 있습니다. - * 이미 처리된(APPROVED/REJECTED) 요청은 재처리할 수 없습니다. - * - * @param id 인증 요청 ID (path) - * @param body adminNote (선택) - * @return 변경된 인증 요청 정보 - */ - @POST("api/admin/verifications/{id}/approve") - suspend fun approveVerification( - @Path("id") id: Long, - @Body body: ApproveVerificationRequestDto - ): ApiResponse - - /** - * 관리자가 사망확인 인증 요청을 거절합니다. - * - * 거절 시 인증 상태가 REJECTED로 변경되고, 수신자는 서류를 다시 제출할 수 있습니다. - * adminNote(선택)로 거절 사유를 남길 수 있습니다. - * 이미 처리된(APPROVED/REJECTED) 요청은 재처리할 수 없습니다. - * - * @param id 인증 요청 ID (path) - * @param body adminNote (선택) - * @return 변경된 인증 요청 정보 - */ - @POST("api/admin/verifications/{id}/reject") - suspend fun rejectVerification( - @Path("id") id: Long, - @Body body: RejectVerificationRequestDto - ): ApiResponse -} diff --git a/app/src/main/java/com/kuit/afternote/feature/admin/data/di/AdminModule.kt b/app/src/main/java/com/kuit/afternote/feature/admin/data/di/AdminModule.kt deleted file mode 100644 index a6a6690c..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/admin/data/di/AdminModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.kuit.afternote.feature.admin.data.di - -import com.kuit.afternote.feature.admin.data.api.AdminApiService -import com.kuit.afternote.feature.admin.data.repository.iface.AdminRepository -import com.kuit.afternote.feature.admin.data.repository.impl.AdminRepositoryImpl -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import retrofit2.Retrofit -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -abstract class AdminModule { - - @Binds - @Singleton - abstract fun bindAdminRepository(impl: AdminRepositoryImpl): AdminRepository - - companion object { - @Provides - @Singleton - fun provideAdminApiService(retrofit: Retrofit): AdminApiService = - retrofit.create(AdminApiService::class.java) - } -} diff --git a/app/src/main/java/com/kuit/afternote/feature/admin/data/dto/AdminVerificationDto.kt b/app/src/main/java/com/kuit/afternote/feature/admin/data/dto/AdminVerificationDto.kt deleted file mode 100644 index e66efa6a..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/admin/data/dto/AdminVerificationDto.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.kuit.afternote.feature.admin.data.dto - -import kotlinx.serialization.Serializable - -/** - * 관리자용 인증 요청 거절 API 요청 body. - * - * POST /api/admin/verifications/{id}/reject 시 선택적으로 adminNote를 전달합니다. - */ -@Serializable -data class RejectVerificationRequestDto( - val adminNote: String? = null -) - -/** - * 관리자용 인증 요청 승인 API 요청 body (AdminVerificationActionRequest). - * - * POST /api/admin/verifications/{id}/approve 시 선택적으로 adminNote를 전달합니다. - */ -@Serializable -data class ApproveVerificationRequestDto( - val adminNote: String? = null -) - -/** - * 관리자용 인증 요청 응답 DTO. - * - * 사망확인 서류 인증 요청 조회/승인/거절 API의 data 필드 스키마. - * - * @param status 사망확인 서류 인증 상태 (PENDING, APPROVED, REJECTED) - * @param adminNote 관리자 메모 (nullable) - */ -@Serializable -data class AdminVerificationResponseDto( - val id: Long, - val userId: Long, - val senderName: String, - val senderEmail: String, - val receiverId: Long, - val receiverName: String, - val status: String, - val deathCertificateUrl: String, - val familyRelationCertificateUrl: String, - val adminNote: String? = null, - val createdAt: String -) diff --git a/app/src/main/java/com/kuit/afternote/feature/admin/data/repository/iface/AdminRepository.kt b/app/src/main/java/com/kuit/afternote/feature/admin/data/repository/iface/AdminRepository.kt deleted file mode 100644 index 748c6ac1..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/admin/data/repository/iface/AdminRepository.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.kuit.afternote.feature.admin.data.repository.iface - -import com.kuit.afternote.feature.admin.data.dto.AdminVerificationResponseDto -import com.kuit.afternote.feature.admin.data.dto.ApproveVerificationRequestDto -import com.kuit.afternote.feature.admin.data.dto.RejectVerificationRequestDto - -/** - * 관리자 API용 Data Repository 인터페이스. - * - * 대기 중인 인증 요청 목록 조회, 인증 요청 승인/거절 등 관리자 전용 API를 호출합니다. - */ -interface AdminRepository { - - /** - * 대기 중인(PENDING) 사망확인 인증 요청 목록을 조회합니다. - * - * @return 성공 시 인증 요청 목록, 실패 시 Result.failure - */ - suspend fun getPendingVerifications(): Result> - - /** - * 관리자가 특정 인증 요청의 상세 정보를 조회합니다. - * - * @param id 인증 요청 ID - * @return 성공 시 인증 요청 상세, 실패 시 Result.failure - */ - suspend fun getVerificationDetail(id: Long): Result - - /** - * 관리자가 사망확인 인증 요청을 승인합니다. - * - * @param id 인증 요청 ID - * @param request adminNote (선택) - * @return 성공 시 변경된 인증 요청 정보, 실패 시 Result.failure - */ - suspend fun approveVerification( - id: Long, - request: ApproveVerificationRequestDto - ): Result - - /** - * 관리자가 사망확인 인증 요청을 거절합니다. - * - * @param id 인증 요청 ID - * @param request adminNote (선택) - * @return 성공 시 변경된 인증 요청 정보, 실패 시 Result.failure - */ - suspend fun rejectVerification( - id: Long, - request: RejectVerificationRequestDto - ): Result -} diff --git a/app/src/main/java/com/kuit/afternote/feature/admin/data/repository/impl/AdminRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/feature/admin/data/repository/impl/AdminRepositoryImpl.kt deleted file mode 100644 index 6fa30305..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/admin/data/repository/impl/AdminRepositoryImpl.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.kuit.afternote.feature.admin.data.repository.impl - -import com.kuit.afternote.data.remote.requireData -import com.kuit.afternote.feature.admin.data.api.AdminApiService -import com.kuit.afternote.feature.admin.data.dto.AdminVerificationResponseDto -import com.kuit.afternote.feature.admin.data.dto.ApproveVerificationRequestDto -import com.kuit.afternote.feature.admin.data.dto.RejectVerificationRequestDto -import com.kuit.afternote.feature.admin.data.repository.iface.AdminRepository -import javax.inject.Inject - -/** - * 관리자 API용 Data Repository 구현체. - */ -class AdminRepositoryImpl - @Inject - constructor( - private val api: AdminApiService - ) : AdminRepository { - - override suspend fun getPendingVerifications(): Result> = - runCatching { - api.getPendingVerifications().requireData() ?: emptyList() - } - - override suspend fun getVerificationDetail(id: Long): Result = - runCatching { - api.getVerificationDetail(id).requireData() - } - - override suspend fun approveVerification( - id: Long, - request: ApproveVerificationRequestDto - ): Result = - runCatching { - api.approveVerification(id = id, body = request).requireData() - } - - override suspend fun rejectVerification( - id: Long, - request: RejectVerificationRequestDto - ): Result = - runCatching { - api.rejectVerification(id = id, body = request).requireData() - } -} diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/data/di/AfternoteRepositoryModule.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/data/di/AfternoteRepositoryModule.kt index 3c46c50e..e9d55390 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/data/di/AfternoteRepositoryModule.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/data/di/AfternoteRepositoryModule.kt @@ -1,13 +1,11 @@ package com.kuit.afternote.feature.afternote.data.di import com.kuit.afternote.data.upload.MemorialThumbnailUploadRepositoryImpl -import com.kuit.afternote.data.upload.MemorialVideoUploadRepositoryImpl import com.kuit.afternote.feature.afternote.data.api.AfternoteApiService import com.kuit.afternote.feature.afternote.data.api.MusicApiService import com.kuit.afternote.feature.afternote.data.repository.AfternoteRepositoryImpl import com.kuit.afternote.feature.afternote.data.repository.MusicSearchRepositoryImpl import com.kuit.afternote.feature.afternote.domain.repository.MemorialThumbnailUploadRepository -import com.kuit.afternote.feature.afternote.domain.repository.MemorialVideoUploadRepository import com.kuit.afternote.feature.afternote.domain.repository.iface.AfternoteRepository import com.kuit.afternote.feature.afternote.domain.repository.iface.MusicSearchRepository import dagger.Binds @@ -51,13 +49,6 @@ abstract class AfternoteRepositoryModule { impl: MemorialThumbnailUploadRepositoryImpl ): MemorialThumbnailUploadRepository - @Suppress("unused") - @Binds - @Singleton - abstract fun bindMemorialVideoUploadRepository( - impl: MemorialVideoUploadRepositoryImpl - ): MemorialVideoUploadRepository - companion object { @Provides @Singleton diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/domain/repository/MemorialVideoUploadRepository.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/domain/repository/MemorialVideoUploadRepository.kt deleted file mode 100644 index 011098ef..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/domain/repository/MemorialVideoUploadRepository.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.kuit.afternote.feature.afternote.domain.repository - -/** - * Memorial (playlist) video upload via POST /files/presigned-url and S3 PUT. - * Used so the server receives an HTTPS video URL, not a local content:// URI. - */ -fun interface MemorialVideoUploadRepository { - /** - * Uploads the video at the given content URI to S3 and returns the file URL. - * - * @param contentUriString Local content URI (e.g. from Photo Picker). - * @return Success with video fileUrl (https) or failure. - */ - suspend fun uploadVideo(contentUriString: String): Result -} diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/domain/usecase/UploadMemorialVideoUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/domain/usecase/UploadMemorialVideoUseCase.kt deleted file mode 100644 index d4150303..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/domain/usecase/UploadMemorialVideoUseCase.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.kuit.afternote.feature.afternote.domain.usecase - -import com.kuit.afternote.feature.afternote.domain.repository.MemorialVideoUploadRepository -import javax.inject.Inject - -/** - * Uploads memorial video from a local content URI to S3 via presigned URL. - * Returns the HTTPS fileUrl so the backend can store a usable video URL. - */ -class UploadMemorialVideoUseCase - @Inject - constructor( - private val repository: MemorialVideoUploadRepository - ) { - suspend operator fun invoke(contentUriString: String): Result = - repository.uploadVideo(contentUriString) - } diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContent.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContent.kt index ab2941c7..f14afd7e 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContent.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContent.kt @@ -67,7 +67,6 @@ fun MemorialGuidelineEditContent( videoContent = { FuneralVideoUpload( videoUrl = params.funeralVideoUrl, - thumbnailUrl = params.funeralThumbnailUrl, onAddVideoClick = params.onVideoAddClick, onThumbnailBytesReady = params.onThumbnailBytesReady ) diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContentParams.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContentParams.kt index 81505025..fe4e6822 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContentParams.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/content/MemorialGuidelineEditContentParams.kt @@ -15,7 +15,6 @@ data class MemorialGuidelineEditContentParams( val selectedLastWish: String?, val lastWishOptions: List, val funeralVideoUrl: String?, - val funeralThumbnailUrl: String? = null, val customLastWishText: String, val onSongAddClick: () -> Unit, val onLastWishSelected: (String) -> Unit, diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/upload/FuneralVideoUpload.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/upload/FuneralVideoUpload.kt index d5448c3d..59e0d9dc 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/upload/FuneralVideoUpload.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/component/edit/upload/FuneralVideoUpload.kt @@ -2,7 +2,6 @@ package com.kuit.afternote.feature.afternote.presentation.component.edit.upload import android.graphics.Bitmap import android.media.MediaMetadataRetriever -import android.util.Log import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -40,10 +39,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.net.toUri -import coil3.compose.AsyncImage -import coil3.network.NetworkHeaders -import coil3.network.httpHeaders -import coil3.request.ImageRequest import com.kuit.afternote.R import com.kuit.afternote.ui.theme.AfternoteTheme import com.kuit.afternote.ui.theme.Gray9 @@ -54,8 +49,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -private const val TAG = "FuneralVideoUpload" - /** * 장례식에 남길 영상 추가 컴포넌트 * @@ -64,15 +57,12 @@ private const val TAG = "FuneralVideoUpload" * - 라벨과 버튼 간 간격: 6dp * - 큰 원형 버튼: B3 (#BDE0FF), 120dp * - 플러스 아이콘: 중앙에 위치, 24dp - * - * @param thumbnailUrl When set (e.g. from API when loading for edit), shown as thumbnail instead of extracting from video. */ @Composable fun FuneralVideoUpload( modifier: Modifier = Modifier, label: String = "장례식에 남길 영상", videoUrl: String? = null, - thumbnailUrl: String? = null, onAddVideoClick: () -> Unit, onThumbnailBytesReady: (ByteArray?) -> Unit = {}, ioDispatcher: CoroutineDispatcher = Dispatchers.IO @@ -88,11 +78,6 @@ fun FuneralVideoUpload( onThumbnailBytesReady(null) return@LaunchedEffect } - if (!videoUrl.startsWith("content://")) { - thumbnailBitmap = null - onThumbnailBytesReady(null) - return@LaunchedEffect - } val (bitmap, jpegBytes) = withContext(ioDispatcher) { val bmp = runCatching { val retriever = MediaMetadataRetriever() @@ -155,7 +140,7 @@ fun FuneralVideoUpload( } } } else { - // 업로드 후 상태: 썸네일(URL 또는 로컬 프레임) 또는 그라데이션 배경 + 재생 아이콘 + // 업로드 후 상태: 썸네일 또는 그라데이션 배경 + 재생 아이콘 Box( modifier = Modifier .fillMaxWidth() @@ -173,40 +158,15 @@ fun FuneralVideoUpload( ) .clickable(onClick = onAddVideoClick) ) { - when { - !thumbnailUrl.isNullOrBlank() -> { - AsyncImage( - model = - ImageRequest.Builder(context) - .data(thumbnailUrl) - .httpHeaders( - NetworkHeaders.Builder().apply { - this["User-Agent"] = "Afternote Android App" - }.build() - ) - .build(), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - error = painterResource(R.drawable.ic_add_circle), - onError = { state -> - Log.e( - TAG, - "Thumbnail load failed: url=$thumbnailUrl", - state.result.throwable - ) - } - ) - } - thumbnailBitmap != null -> { - Image( - bitmap = thumbnailBitmap!!, - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } + if (thumbnailBitmap != null) { + Image( + bitmap = thumbnailBitmap!!, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) } + // 중앙 재생 아이콘 (썸네일 위 또는 그라데이션 위) Image( painter = painterResource(R.drawable.ic_playback), contentDescription = stringResource(R.string.content_description_video_play), diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditScreen.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditScreen.kt index c4859305..0820ad5a 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditScreen.kt @@ -377,7 +377,6 @@ private fun CategoryContent( selectedLastWish = state.selectedLastWish, lastWishOptions = state.lastWishOptions, funeralVideoUrl = state.funeralVideoUrl, - funeralThumbnailUrl = state.funeralThumbnailUrl, customLastWishText = state.customLastWishText, onSongAddClick = onNavigateToAddSong, onLastWishSelected = state::onLastWishSelected, diff --git a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditViewModel.kt index 3150a94c..5011d2ea 100644 --- a/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditViewModel.kt +++ b/app/src/main/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditViewModel.kt @@ -16,7 +16,6 @@ import com.kuit.afternote.feature.afternote.domain.usecase.CreateSocialAfternote import com.kuit.afternote.feature.afternote.domain.usecase.GetAfternoteDetailUseCase import com.kuit.afternote.feature.afternote.domain.usecase.UpdateAfternoteUseCase import com.kuit.afternote.feature.afternote.domain.usecase.UploadMemorialThumbnailUseCase -import com.kuit.afternote.feature.afternote.domain.usecase.UploadMemorialVideoUseCase import com.kuit.afternote.feature.afternote.presentation.component.edit.model.AfternoteEditReceiver import com.kuit.afternote.feature.afternote.presentation.component.edit.model.ProcessingMethodItem import com.kuit.afternote.feature.afternote.presentation.component.edit.model.Song @@ -35,9 +34,6 @@ private const val CATEGORY_SOCIAL = "소셜네트워크" private const val CATEGORY_GALLERY = "갤러리 및 파일" private const val CATEGORY_MEMORIAL = "추모 가이드라인" -/** S3 presigned URLs contain this; we must not send them back on PATCH or the server overwrites the stored key. */ -private const val PRESIGNED_URL_MARKER = "X-Amz-" - /** * 애프터노트 생성/수정 ViewModel. * @@ -58,8 +54,7 @@ class AfternoteEditViewModel private val getDetailUseCase: GetAfternoteDetailUseCase, private val getReceiversUseCase: GetReceiversUseCase, private val getUserIdUseCase: GetUserIdUseCase, - private val uploadMemorialThumbnailUseCase: UploadMemorialThumbnailUseCase, - private val uploadMemorialVideoUseCase: UploadMemorialVideoUseCase + private val uploadMemorialThumbnailUseCase: UploadMemorialThumbnailUseCase ) : ViewModel() { private val _saveState = MutableStateFlow(AfternoteSaveState()) @@ -154,22 +149,15 @@ class AfternoteEditViewModel _saveState.update { it.copy(isSaving = true, error = null, validationError = null) } - val resolvedVideoUrl = resolveVideoUrlForSave(funeralVideoUrl) - if (resolvedVideoUrl == null && funeralVideoUrl != null && funeralVideoUrl.startsWith("content://")) { - return@launch - } - val videoUrlForUpdate = videoUrlForUpdateRequest(editingId != null, resolvedVideoUrl) - val thumbnailForUpdate = - if (videoUrlForUpdate == null) null else funeralThumbnailUrl - (if (editingId != null) { + val result = if (editingId != null) { performUpdate( afternoteId = editingId, category = categoryForApi, payload = payload, receivers = receivers, playlistStateHolder = playlistStateHolder, - funeralVideoUrl = videoUrlForUpdate, - funeralThumbnailUrl = thumbnailForUpdate + funeralVideoUrl = funeralVideoUrl, + funeralThumbnailUrl = funeralThumbnailUrl ) } else { performCreate( @@ -177,10 +165,11 @@ class AfternoteEditViewModel payload = payload, receivers = receivers, playlistStateHolder = playlistStateHolder, - funeralVideoUrl = resolvedVideoUrl, + funeralVideoUrl = funeralVideoUrl, funeralThumbnailUrl = funeralThumbnailUrl ) - }) + } + result .onSuccess { id -> Log.d(TAG, "saveAfternote: SUCCESS, savedId=$id") _saveState.update { @@ -188,55 +177,22 @@ class AfternoteEditViewModel } } .onFailure { e -> - handleSaveFailure(e, categoryForApi) - } - } - } - - /** - * Resolves [funeralVideoUrl] for save: returns as-is if non-blank and not content://; - * uploads and returns file URL if content://; on upload failure updates [saveState] and returns null. - */ - private suspend fun resolveVideoUrlForSave(funeralVideoUrl: String?): String? { - when { - funeralVideoUrl.isNullOrBlank() -> return null - !funeralVideoUrl.startsWith("content://") -> return funeralVideoUrl - } - return uploadMemorialVideoUseCase(funeralVideoUrl).fold( - onSuccess = { it }, - onFailure = { e -> - Log.e(TAG, "saveAfternote: video upload failed", e) - _saveState.update { - it.copy( - isSaving = false, - error = e.message ?: "영상 업로드에 실패했습니다." - ) + Log.e(TAG, "saveAfternote: FAILURE, category=$categoryForApi", e) + when (e) { + is AfternoteValidationException -> _saveState.update { + it.copy( + isSaving = false, + validationError = e.validationError + ) + } + else -> _saveState.update { + it.copy( + isSaving = false, + error = e.message ?: "저장에 실패했습니다." + ) + } + } } - null - } - ) - } - - /** - * For update (PATCH), do not send presigned URLs so the server does not overwrite the stored key. - */ - private fun videoUrlForUpdateRequest(isUpdate: Boolean, resolvedVideoUrl: String?): String? { - if (!isUpdate || resolvedVideoUrl == null) return resolvedVideoUrl - if (resolvedVideoUrl.contains(PRESIGNED_URL_MARKER)) { - Log.d(TAG, "saveAfternote: skipping videoUrl in PATCH (presigned URL)") - return null - } - return resolvedVideoUrl - } - - private fun handleSaveFailure(e: Throwable, categoryForApi: String) { - Log.e(TAG, "saveAfternote: FAILURE, category=$categoryForApi", e) - _saveState.update { - it.copy( - isSaving = false, - validationError = (e as? AfternoteValidationException)?.validationError, - error = if (e is AfternoteValidationException) it.error else (e.message ?: "저장에 실패했습니다.") - ) } } diff --git a/app/src/main/java/com/kuit/afternote/feature/home/presentation/component/HomeHeader.kt b/app/src/main/java/com/kuit/afternote/feature/home/presentation/component/HomeHeader.kt index 0cb26d4e..51b75125 100644 --- a/app/src/main/java/com/kuit/afternote/feature/home/presentation/component/HomeHeader.kt +++ b/app/src/main/java/com/kuit/afternote/feature/home/presentation/component/HomeHeader.kt @@ -42,7 +42,14 @@ fun HomeHeader( modifier = Modifier.height(16.dp) ) Spacer(modifier = Modifier.weight(1f)) - + IconButton(onClick = onProfileClick) { + Icon( + painter = painterResource(R.drawable.ic_user), + contentDescription = "프로필", + modifier = Modifier.size(24.dp), + tint = Gray9 + ) + } IconButton(onClick = onSettingsClick) { Icon( painter = painterResource(R.drawable.setting), diff --git a/app/src/main/java/com/kuit/afternote/feature/onboarding/presentation/navgraph/OnboardingNavgraph.kt b/app/src/main/java/com/kuit/afternote/feature/onboarding/presentation/navgraph/OnboardingNavgraph.kt index 842514e3..1f1bdc32 100644 --- a/app/src/main/java/com/kuit/afternote/feature/onboarding/presentation/navgraph/OnboardingNavgraph.kt +++ b/app/src/main/java/com/kuit/afternote/feature/onboarding/presentation/navgraph/OnboardingNavgraph.kt @@ -56,7 +56,7 @@ fun NavGraphBuilder.onboardingNavGraph( password = route.password, onFinishClick = { android.util.Log.d("OnboardingNavGraph", "onFinishClick 호출됨, AfternoteListRoute로 이동 시도") - navController.navigate("home") + navController.navigate(AfternoteRoute.AfternoteListRoute) android.util.Log.d("OnboardingNavGraph", "navigate 호출 완료") }, onBackClick = { navController.popBackStack() } diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/data/api/ReceivedApiService.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/data/api/ReceivedApiService.kt index 4e512e88..6491f4dd 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/data/api/ReceivedApiService.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiver/data/api/ReceivedApiService.kt @@ -4,10 +4,13 @@ import com.kuit.afternote.data.remote.ApiResponse import com.kuit.afternote.feature.receiver.data.dto.CreateMindRecordReceiverRequestDto import com.kuit.afternote.feature.receiver.data.dto.CreateTimeLetterReceiverRequestDto import retrofit2.http.Body +import retrofit2.http.GET import retrofit2.http.POST +import retrofit2.http.Path /** * 수신자(Received) API 서비스. (스웨거 기준) + * * - POST /api/received/time-letters: 타임레터 수신자 등록 * - POST /api/received/mind-records: 마인드레코드 수신자 등록 */ diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedAfternote.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedAfternote.kt index 5ffe77b6..cd643a9b 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedAfternote.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedAfternote.kt @@ -3,12 +3,9 @@ package com.kuit.afternote.feature.receiver.domain.entity /** * 수신자에게 전달된 애프터노트 한 건을 나타내는 도메인 엔티티. * - * GET /api/receiver-auth/after-notes 응답의 한 항목에 대응합니다. - * - * @param id 애프터노트 ID (상세 조회·플레이리스트 라우트 등에서 사용) + * GET /api/received/{receiverId}/after-notes 응답의 한 항목에 대응합니다. */ data class ReceivedAfternote( - val id: Long, val sourceType: String?, val lastUpdatedAt: String?, val leaveMessage: String? = null diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedAfternoteDetail.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedAfternoteDetail.kt deleted file mode 100644 index ac4c125d..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedAfternoteDetail.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.kuit.afternote.feature.receiver.domain.entity - -/** - * 수신한 애프터노트 상세 (도메인 모델). - * - * GET /api/receiver-auth/after-notes/{afternoteId} 응답에 대응합니다. - * 플레이리스트 화면에서는 [playlist]의 [ReceivedAfternotePlaylist.songs]를 사용합니다. - */ -data class ReceivedAfternoteDetail( - val id: Long, - val category: String? = null, - val title: String? = null, - val playlist: ReceivedAfternotePlaylist? = null -) diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedAfternotePlaylist.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedAfternotePlaylist.kt deleted file mode 100644 index 73b4a303..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedAfternotePlaylist.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.kuit.afternote.feature.receiver.domain.entity - -/** - * 수신한 애프터노트의 플레이리스트 정보 (도메인 모델). - * - * GET /api/receiver-auth/after-notes/{afternoteId} 응답의 playlist에 대응합니다. - */ -data class ReceivedAfternotePlaylist( - val atmosphere: String? = null, - val songs: List = emptyList() -) diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedPlaylistSong.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedPlaylistSong.kt deleted file mode 100644 index c3a5c2b3..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/entity/ReceivedPlaylistSong.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.kuit.afternote.feature.receiver.domain.entity - -/** - * 수신한 애프터노트 플레이리스트의 노래 한 곡 (도메인 모델). - * - * GET /api/receiver-auth/after-notes/{afternoteId} 응답의 playlist.songs 항목에 대응합니다. - */ -data class ReceivedPlaylistSong( - val title: String, - val artist: String, - val coverUrl: String? = null -) diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/repository/iface/GetAfternoteDetailByAuthCodeRepository.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/domain/repository/iface/GetAfternoteDetailByAuthCodeRepository.kt deleted file mode 100644 index 03e2ea59..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/repository/iface/GetAfternoteDetailByAuthCodeRepository.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.kuit.afternote.feature.receiver.domain.repository.iface - -import com.kuit.afternote.feature.receiver.domain.entity.ReceivedAfternoteDetail - -/** - * 수신자 인증번호(authCode)와 애프터노트 ID로 애프터노트 상세를 조회하는 Domain Repository. - * - * GET /api/receiver-auth/after-notes/{afternoteId} (X-Auth-Code) API를 사용합니다. - */ -interface GetAfternoteDetailByAuthCodeRepository { - - /** - * 인증번호와 애프터노트 ID로 상세(플레이리스트 등)를 조회합니다. - * - * @param authCode 수신자 인증번호 (X-Auth-Code 헤더) - * @param afternoteId 애프터노트 ID - */ - suspend fun getAfternoteDetail( - authCode: String, - afternoteId: Long - ): Result -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/repository/iface/GetSenderMessageRepository.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/domain/repository/iface/GetSenderMessageRepository.kt deleted file mode 100644 index c5a1be7c..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/repository/iface/GetSenderMessageRepository.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.kuit.afternote.feature.receiver.domain.repository.iface - -/** - * 수신자 인증번호(authCode)로 발신자가 남긴 메시지를 조회하는 Domain Repository. - * - * GET /api/receiver-auth/message (X-Auth-Code) API를 사용합니다. - */ -interface GetSenderMessageRepository { - - /** - * 인증번호로 발신자 메시지를 조회합니다. - * - * @param authCode 수신자 인증번호 (마스터키, X-Auth-Code 헤더) - * @return 발신자 메시지 (nullable) - */ - suspend fun getMessage(authCode: String): Result -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/repository/iface/GetTimeLetterDetailByAuthCodeRepository.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/domain/repository/iface/GetTimeLetterDetailByAuthCodeRepository.kt deleted file mode 100644 index 22b6e13e..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/repository/iface/GetTimeLetterDetailByAuthCodeRepository.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.kuit.afternote.feature.receiver.domain.repository.iface - -import com.kuit.afternote.feature.receiver.domain.entity.ReceivedTimeLetter - -/** - * 수신자 인증번호(authCode)와 수신 타임레터 ID로 타임레터 상세를 조회하는 Domain Repository. - * - * GET /api/receiver-auth/time-letters/{timeLetterReceiverId} (X-Auth-Code) API를 사용합니다. - */ -interface GetTimeLetterDetailByAuthCodeRepository { - - /** - * 인증번호로 특정 타임레터 상세를 조회합니다. 읽음 처리도 함께 수행됩니다. - * - * @param authCode 수신자 인증번호 (X-Auth-Code 헤더) - * @param timeLetterReceiverId 수신 타임레터 ID - */ - suspend fun getTimeLetterDetail( - authCode: String, - timeLetterReceiverId: Long - ): Result -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/usecase/GetAfternoteDetailByAuthCodeUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/domain/usecase/GetAfternoteDetailByAuthCodeUseCase.kt deleted file mode 100644 index 0005eea9..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/usecase/GetAfternoteDetailByAuthCodeUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.kuit.afternote.feature.receiver.domain.usecase - -import com.kuit.afternote.feature.receiver.domain.entity.ReceivedAfternoteDetail -import com.kuit.afternote.feature.receiver.domain.repository.iface.GetAfternoteDetailByAuthCodeRepository -import javax.inject.Inject - -/** - * 수신자 인증번호(authCode)와 애프터노트 ID로 애프터노트 상세를 조회하는 UseCase. - * - * GET /api/receiver-auth/after-notes/{afternoteId} (X-Auth-Code) API를 사용합니다. - */ -class GetAfternoteDetailByAuthCodeUseCase - @Inject - constructor( - private val getAfternoteDetailByAuthCodeRepository: GetAfternoteDetailByAuthCodeRepository - ) { - suspend operator fun invoke( - authCode: String, - afternoteId: Long - ): Result = - getAfternoteDetailByAuthCodeRepository.getAfternoteDetail( - authCode = authCode, - afternoteId = afternoteId - ) - } diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/usecase/GetSenderMessageUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/domain/usecase/GetSenderMessageUseCase.kt deleted file mode 100644 index c6546ab8..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/usecase/GetSenderMessageUseCase.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.kuit.afternote.feature.receiver.domain.usecase - -import com.kuit.afternote.feature.receiver.domain.repository.iface.GetSenderMessageRepository -import javax.inject.Inject - -/** - * 수신자 인증번호(authCode)로 발신자 메시지 조회 UseCase. - * - * GET /api/receiver-auth/message (X-Auth-Code) API를 사용합니다. - */ -class GetSenderMessageUseCase - @Inject - constructor( - private val getSenderMessageRepository: GetSenderMessageRepository - ) { - suspend operator fun invoke(authCode: String): Result = - getSenderMessageRepository.getMessage(authCode) -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/usecase/GetTimeLetterDetailByAuthCodeUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/domain/usecase/GetTimeLetterDetailByAuthCodeUseCase.kt deleted file mode 100644 index e386fa5a..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/domain/usecase/GetTimeLetterDetailByAuthCodeUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.kuit.afternote.feature.receiver.domain.usecase - -import com.kuit.afternote.feature.receiver.domain.entity.ReceivedTimeLetter -import com.kuit.afternote.feature.receiver.domain.repository.iface.GetTimeLetterDetailByAuthCodeRepository -import javax.inject.Inject - -/** - * 수신자 인증번호(authCode)와 수신 타임레터 ID로 타임레터 상세를 조회하는 UseCase. - * - * GET /api/receiver-auth/time-letters/{timeLetterReceiverId} (X-Auth-Code) API를 사용합니다. - */ -class GetTimeLetterDetailByAuthCodeUseCase - @Inject - constructor( - private val getTimeLetterDetailByAuthCodeRepository: GetTimeLetterDetailByAuthCodeRepository - ) { - suspend operator fun invoke( - authCode: String, - timeLetterReceiverId: Long - ): Result = - getTimeLetterDetailByAuthCodeRepository.getTimeLetterDetail( - authCode = authCode, - timeLetterReceiverId = timeLetterReceiverId - ) - } diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/navgraph/ReceiverMainRoute.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/navgraph/ReceiverMainRoute.kt index 095b3818..f62a7ed8 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/navgraph/ReceiverMainRoute.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/navgraph/ReceiverMainRoute.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -72,6 +73,9 @@ fun ReceiverMainRoute( albumCovers: List, receiverAuthSessionHolder: ReceiverAuthSessionHolder ) { + DisposableEffect(receiverAuthSessionHolder) { + onDispose { receiverAuthSessionHolder.clearAuthCode() } + } var selectedBottomNavItem by remember { mutableStateOf(BottomNavItem.HOME) } var showMindRecordDetail by remember { mutableStateOf(false) } var mindRecordSelectedDate by remember { mutableStateOf(LocalDate.now()) } @@ -96,7 +100,6 @@ fun ReceiverMainRoute( showTimeLetterList = false } selectedBottomNavItem == BottomNavItem.HOME -> { - receiverAuthSessionHolder.clearAuthCode() navController.popBackStack() } else -> { diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/navgraph/ReceiverMemorialPlaylistRoute.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/navgraph/ReceiverMemorialPlaylistRoute.kt deleted file mode 100644 index 0a9c03ee..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/navgraph/ReceiverMemorialPlaylistRoute.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.kuit.afternote.feature.receiver.presentation.navgraph - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.kuit.afternote.feature.receiver.presentation.screen.afternote.MemorialPlaylistScreen -import com.kuit.afternote.feature.receiver.presentation.viewmodel.ReceiverMemorialPlaylistViewModel - -/** - * 수신자 추모 플레이리스트 라우트. - * GET /api/receiver-auth/after-notes/{afternoteId}의 playlist.songs를 표시합니다. - */ -@Composable -fun ReceiverMemorialPlaylistRoute( - modifier: Modifier = Modifier, - onBackClick: () -> Unit, - viewModel: ReceiverMemorialPlaylistViewModel = hiltViewModel() -) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - MemorialPlaylistScreen( - modifier = modifier, - songs = uiState.songs, - onBackClick = onBackClick - ) -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/uimodel/ReceivedListScreenUiState.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/uimodel/ReceivedListScreenUiState.kt index 08018b6d..a5ebf00b 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/uimodel/ReceivedListScreenUiState.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/uimodel/ReceivedListScreenUiState.kt @@ -26,11 +26,8 @@ data class ReceiverTimeLettersListUiState( /** * 수신 애프터노트 목록 화면(설정 플로우)용 리스트 아이템 UI 모델. - * - * @param id 애프터노트 ID (상세·플레이리스트 라우트 이동 시 사용) */ data class ReceivedAfternoteListItemUi( - val id: Long, val sourceType: String, val lastUpdatedAt: String ) diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/uimodel/ReceiverMemorialPlaylistUiState.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/uimodel/ReceiverMemorialPlaylistUiState.kt deleted file mode 100644 index 31058c64..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/uimodel/ReceiverMemorialPlaylistUiState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.kuit.afternote.feature.receiver.presentation.uimodel - -import com.kuit.afternote.core.uimodel.PlaylistSongDisplay - -/** - * 수신자 추모 플레이리스트 화면 UI 상태. - * - * GET /api/receiver-auth/after-notes/{afternoteId}의 playlist.songs를 표시합니다. - */ -data class ReceiverMemorialPlaylistUiState( - val songs: List = emptyList(), - val isLoading: Boolean = false, - val errorMessage: String? = null -) diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverAfternoteTriggerViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverAfternoteTriggerViewModel.kt index cc911577..f3106b5b 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverAfternoteTriggerViewModel.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverAfternoteTriggerViewModel.kt @@ -3,7 +3,6 @@ package com.kuit.afternote.feature.receiver.presentation.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kuit.afternote.feature.receiver.domain.usecase.GetAfterNotesByAuthCodeUseCase -import com.kuit.afternote.feature.receiver.domain.usecase.GetSenderMessageUseCase import com.kuit.afternote.feature.receiver.domain.usecase.GetMindRecordsByAuthCodeUseCase import com.kuit.afternote.feature.receiver.domain.usecase.GetTimeLettersByAuthCodeUseCase import dagger.hilt.android.lifecycle.HiltViewModel @@ -18,9 +17,8 @@ import javax.inject.Inject /** * 수신자 HOME에서 콘텐츠 개수(마음의 기록, 타임레터, 애프터노트) 및 leaveMessage를 로드하는 ViewModel. * - * [loadHomeSummary]로 네 API를 병렬 호출하여 totalCount와 leaveMessage를 수집합니다. - * leaveMessage는 GET /api/receiver-auth/message의 message 필드에서 가져옵니다. - * [loadAfterNotes]는 "애프터노트 확인하러 가기" 버튼 클릭 시 afternoteTotalCount 갱신용으로 호출됩니다. + * [loadHomeSummary]로 세 API를 병렬 호출하여 totalCount를 수집합니다. + * [loadAfterNotes]는 "애프터노트 확인하러 가기" 버튼 클릭 시 leaveMessage 갱신용으로 호출됩니다. */ @HiltViewModel class ReceiverAfternoteTriggerViewModel @@ -28,8 +26,7 @@ class ReceiverAfternoteTriggerViewModel constructor( private val getAfterNotesByAuthCodeUseCase: GetAfterNotesByAuthCodeUseCase, private val getMindRecordsByAuthCodeUseCase: GetMindRecordsByAuthCodeUseCase, - private val getTimeLettersByAuthCodeUseCase: GetTimeLettersByAuthCodeUseCase, - private val getSenderMessageUseCase: GetSenderMessageUseCase + private val getTimeLettersByAuthCodeUseCase: GetTimeLettersByAuthCodeUseCase ) : ViewModel() { private val _leaveMessage = MutableStateFlow(null) @@ -54,18 +51,12 @@ class ReceiverAfternoteTriggerViewModel val afternotesDeferred = async { getAfterNotesByAuthCodeUseCase(authCode) } val mindRecordsDeferred = async { getMindRecordsByAuthCodeUseCase(authCode) } val timeLettersDeferred = async { getTimeLettersByAuthCodeUseCase(authCode) } - val messageDeferred = async { getSenderMessageUseCase(authCode) } - awaitAll( - afternotesDeferred, - mindRecordsDeferred, - timeLettersDeferred, - messageDeferred - ) + awaitAll(afternotesDeferred, mindRecordsDeferred, timeLettersDeferred) - messageDeferred.await().onSuccess { message -> - _leaveMessage.value = message?.takeIf { it.isNotBlank() } - } afternotesDeferred.await().onSuccess { result -> + val first = + result.items.firstOrNull { it.leaveMessage?.isNotBlank() == true } + _leaveMessage.value = first?.leaveMessage _afternoteTotalCount.value = result.totalCount } mindRecordsDeferred.await().onSuccess { result -> @@ -87,6 +78,9 @@ class ReceiverAfternoteTriggerViewModel viewModelScope.launch { getAfterNotesByAuthCodeUseCase(authCode) .onSuccess { result -> + val first = + result.items.firstOrNull { it.leaveMessage?.isNotBlank() == true } + _leaveMessage.value = first?.leaveMessage _afternoteTotalCount.value = result.totalCount } } diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverAfternotesListViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverAfternotesListViewModel.kt index 198630c3..0897d3fd 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverAfternotesListViewModel.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverAfternotesListViewModel.kt @@ -43,7 +43,6 @@ class ReceiverAfternotesListViewModel it.copy( items = data.items.map { item -> ReceivedAfternoteListItemUi( - id = item.id, sourceType = item.sourceType.orEmpty(), lastUpdatedAt = item.lastUpdatedAt.orEmpty() ) diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverMemorialPlaylistViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverMemorialPlaylistViewModel.kt deleted file mode 100644 index b1b399e3..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverMemorialPlaylistViewModel.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.kuit.afternote.feature.receiver.presentation.viewmodel - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.kuit.afternote.core.uimodel.PlaylistSongDisplay -import com.kuit.afternote.feature.receiver.domain.usecase.GetAfternoteDetailByAuthCodeUseCase -import com.kuit.afternote.feature.receiver.presentation.uimodel.ReceiverMemorialPlaylistUiState -import com.kuit.afternote.feature.receiverauth.session.ReceiverAuthSessionHolder -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - -/** - * 수신자 추모 플레이리스트 화면 ViewModel. - * - * GET /api/receiver-auth/after-notes/{afternoteId} (X-Auth-Code)로 상세를 조회한 뒤 - * playlist.songs를 [PlaylistSongDisplay]로 변환하여 표시합니다. - */ -@HiltViewModel -class ReceiverMemorialPlaylistViewModel - @Inject - constructor( - savedStateHandle: SavedStateHandle, - private val receiverAuthSessionHolder: ReceiverAuthSessionHolder, - private val getAfternoteDetailByAuthCodeUseCase: GetAfternoteDetailByAuthCodeUseCase - ) : ViewModel() { - - private val _uiState = MutableStateFlow(ReceiverMemorialPlaylistUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - init { - val afternoteId = savedStateHandle.get("afternoteId")?.toLongOrNull() - val authCode = receiverAuthSessionHolder.getAuthCode() - - when { - authCode == null || authCode.isBlank() -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "인증 정보가 없습니다." - ) - } - } - afternoteId == null -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "애프터노트 정보를 찾을 수 없습니다." - ) - } - } - else -> loadPlaylist(authCode = authCode, afternoteId = afternoteId) - } - } - - private fun loadPlaylist(authCode: String, afternoteId: Long) { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - getAfternoteDetailByAuthCodeUseCase( - authCode = authCode, - afternoteId = afternoteId - ) - .onSuccess { detail -> - val songs = detail.playlist?.songs?.mapIndexed { index, song -> - PlaylistSongDisplay( - id = index.toString(), - title = song.title, - artist = song.artist, - albumImageUrl = song.coverUrl - ) - }.orEmpty() - _uiState.update { - it.copy( - songs = songs, - isLoading = false, - errorMessage = null - ) - } - } - .onFailure { e -> - _uiState.update { - it.copy( - isLoading = false, - errorMessage = e.message ?: "플레이리스트를 불러오는데 실패했습니다." - ) - } - } - } - } - - fun clearError() { - _uiState.update { it.copy(errorMessage = null) } - } -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverTimeLetterDetailViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverTimeLetterDetailViewModel.kt index 48d4f8b7..6442c7e4 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverTimeLetterDetailViewModel.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/viewmodel/ReceiverTimeLetterDetailViewModel.kt @@ -1,10 +1,7 @@ package com.kuit.afternote.feature.receiver.presentation.viewmodel -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.kuit.afternote.core.ui.component.navigation.BottomNavItem -import com.kuit.afternote.feature.receiver.domain.usecase.GetTimeLetterDetailByAuthCodeUseCase import com.kuit.afternote.feature.receiver.presentation.uimodel.ReceiverTimeLetterDetailUiState import com.kuit.afternote.feature.receiverauth.session.ReceiverAuthSessionHolder import dagger.hilt.android.lifecycle.HiltViewModel @@ -12,76 +9,34 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import javax.inject.Inject /** * 수신자 앱 타임레터 상세 화면 ViewModel. * - * GET /api/receiver-auth/time-letters/{timeLetterReceiverId} (X-Auth-Code) API로 상세를 조회합니다. + * receiver-auth 플로우에서는 목록에서 선택한 항목을 [ReceiverAuthSessionHolder]에서 읽어 표시합니다. + * received API는 사용하지 않습니다. */ @HiltViewModel class ReceiverTimeLetterDetailViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - private val receiverAuthSessionHolder: ReceiverAuthSessionHolder, - private val getTimeLetterDetailByAuthCodeUseCase: GetTimeLetterDetailByAuthCodeUseCase + private val receiverAuthSessionHolder: ReceiverAuthSessionHolder ) : ViewModel() { private val _uiState = MutableStateFlow(ReceiverTimeLetterDetailUiState()) val uiState: StateFlow = _uiState.asStateFlow() init { - val timeLetterReceiverId = - savedStateHandle.get("timeLetterReceiverId")?.toLongOrNull() - val authCode = receiverAuthSessionHolder.getAuthCode() - - when { - authCode == null || authCode.isBlank() -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "인증 정보가 없습니다." - ) - } - } - timeLetterReceiverId == null -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "타임레터 정보를 찾을 수 없습니다." - ) - } + receiverAuthSessionHolder.getSelectedTimeLetter()?.let { letter -> + _uiState.update { + it.copy( + letter = letter, + isLoading = false, + errorMessage = null + ) } - else -> loadDetail(authCode = authCode, timeLetterReceiverId = timeLetterReceiverId) - } - } - - private fun loadDetail(authCode: String, timeLetterReceiverId: Long) { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - getTimeLetterDetailByAuthCodeUseCase( - authCode = authCode, - timeLetterReceiverId = timeLetterReceiverId - ) - .onSuccess { letter -> - _uiState.update { - it.copy( - letter = letter, - isLoading = false, - errorMessage = null - ) - } - } - .onFailure { e -> - _uiState.update { - it.copy( - isLoading = false, - errorMessage = e.message ?: "상세 조회에 실패했습니다." - ) - } - } + receiverAuthSessionHolder.setSelectedTimeLetter(null) } } diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/component/PdfInputContent.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/component/PdfInputContent.kt index a79d4f12..356927e8 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/component/PdfInputContent.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiverauth/component/PdfInputContent.kt @@ -26,13 +26,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.kuit.afternote.R import com.kuit.afternote.core.ui.component.OutlineTextField import com.kuit.afternote.ui.theme.B2 import com.kuit.afternote.ui.theme.Gray6 @@ -50,7 +48,7 @@ fun PdfInputContent( ) { Column(modifier = Modifier.fillMaxWidth()) { Text( - text = stringResource(R.string.receiver_verify_document_upload_title), + text = "증빙 서류 업로드", fontSize = 24.sp, fontWeight = FontWeight.Bold, fontFamily = Sansneo @@ -59,7 +57,7 @@ fun PdfInputContent( Spacer(modifier = Modifier.height(8.dp)) Text( - text = stringResource(R.string.receiver_verify_document_upload_description), + text = "사망진단서와 가족관계증명서를 업로드 해주세요.\n담당자의 확인 후 승인됩니다.", fontSize = 16.sp, fontFamily = Sansneo, color = Gray6, @@ -69,9 +67,9 @@ fun PdfInputContent( Spacer(modifier = Modifier.height(24.dp)) SinglePdfInputRow( - title = stringResource(R.string.receiver_verify_death_cert_title), + title = "사망진단서 업로드", textFieldState = deadPdf, - label = stringResource(R.string.receiver_verify_document_input_label), + label = "서류 촬영 또는 파일 첨부", onImageAdd = onDeadImageAdd, onFileAdd = onDeadFileAdd ) @@ -79,9 +77,9 @@ fun PdfInputContent( Spacer(modifier = Modifier.height(16.dp)) SinglePdfInputRow( - title = stringResource(R.string.receiver_verify_family_cert_title), + title = "가족관계증명서 업로드", textFieldState = familyPdf, - label = stringResource(R.string.receiver_verify_document_input_label), + label = "서류 촬영 또는 파일 첨부", onImageAdd = onFamilyImageAdd, onFileAdd = onFamilyFileAdd ) @@ -128,7 +126,7 @@ private fun SinglePdfInputRow( modifier = Modifier.background(Color.White) ) { DropdownMenuItem( - text = { Text(stringResource(R.string.receiver_verify_add_image)) }, + text = { Text("이미지 추가") }, leadingIcon = { Icon( Icons.Outlined.PhotoCamera, @@ -142,7 +140,7 @@ private fun SinglePdfInputRow( } ) DropdownMenuItem( - text = { Text(stringResource(R.string.receiver_verify_add_file)) }, + text = { Text("파일 추가") }, leadingIcon = { Icon( Icons.Outlined.Photo, diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/component/ReceiveEndContent.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/component/ReceiveEndContent.kt index f01374ca..b8ef993b 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/component/ReceiveEndContent.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiverauth/component/ReceiveEndContent.kt @@ -7,21 +7,16 @@ import androidx.compose.foundation.layout.height import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.kuit.afternote.R -import com.kuit.afternote.feature.receiverauth.domain.entity.DeliveryVerificationStatus import com.kuit.afternote.ui.theme.Sansneo @Composable -fun ReceiveEndContent( - deliveryVerificationStatus: DeliveryVerificationStatus? = null -) { +fun ReceiveEndContent() { Column(modifier = Modifier.fillMaxSize()) { Text( - text = stringResource(R.string.receiver_verify_complete_title), + text = "열람 요청 완료", fontSize = 24.sp, fontWeight = FontWeight.Bold, fontFamily = Sansneo @@ -29,18 +24,8 @@ fun ReceiveEndContent( Spacer(modifier = Modifier.height(14.dp)) - val statusMessage = when (deliveryVerificationStatus?.status) { - "APPROVED" -> stringResource(R.string.receiver_verify_status_approved) - "REJECTED" -> { - val base = stringResource(R.string.receiver_verify_status_rejected) - if (!deliveryVerificationStatus.adminNote.isNullOrBlank()) { - "$base\n${deliveryVerificationStatus.adminNote}" - } else base - } - else -> stringResource(R.string.receiver_verify_status_pending) - } Text( - text = statusMessage, + text = "현재 서류 확인중입니다.\n빠르게 처리하여 열람하실 수 있도록 하겠습니다.", fontSize = 16.sp, fontWeight = FontWeight.Medium, fontFamily = Sansneo diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/api/ReceiverAuthApiService.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/api/ReceiverAuthApiService.kt index 59b5097c..9ce00cbf 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/api/ReceiverAuthApiService.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/api/ReceiverAuthApiService.kt @@ -1,39 +1,23 @@ package com.kuit.afternote.feature.receiverauth.data.api import com.kuit.afternote.data.remote.ApiResponse -import com.kuit.afternote.feature.receiverauth.data.dto.DeliveryVerificationRequestDto -import com.kuit.afternote.feature.receiverauth.data.dto.DeliveryVerificationResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceiverAuthPresignedUrlRequestDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceiverAuthPresignedUrlResponseDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceiverAuthVerifyRequestDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceiverAuthVerifyResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceiverMessageResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedAfternoteDetailAuthResponseDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedAfternoteListAuthResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedMindRecordDetailAuthResponseDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedMindRecordListAuthResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedTimeLetterDetailAuthResponseDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedTimeLetterListAuthResponseDto import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header -import retrofit2.http.Path import retrofit2.http.POST /** * 수신자 인증번호 기반 콘텐츠 조회 API 서비스. (스웨거 기준) * * - POST /api/receiver-auth/verify: 인증번호 검증 - * - POST /api/receiver-auth/presigned-url: 수신자 파일 업로드용 Presigned URL 생성 - * - POST /api/receiver-auth/delivery-verification: 사망확인 서류 제출 - * - GET /api/receiver-auth/delivery-verification/status: 사망확인 인증 상태 조회 - * - GET /api/receiver-auth/message: 발신자 메시지 조회 * - GET /api/receiver-auth/time-letters: 타임레터 목록 조회 - * - GET /api/receiver-auth/time-letters/{timeLetterReceiverId}: 타임레터 상세 조회 * - GET /api/receiver-auth/mind-records: 마인드레코드 목록 조회 - * - GET /api/receiver-auth/mind-records/{mindRecordId}: 마인드레코드 상세 조회 * - GET /api/receiver-auth/after-notes: 애프터노트 목록 조회 - * - GET /api/receiver-auth/after-notes/{afternoteId}: 애프터노트 상세 조회 */ interface ReceiverAuthApiService { @@ -48,55 +32,6 @@ interface ReceiverAuthApiService { @Body body: ReceiverAuthVerifyRequestDto ): ApiResponse - /** - * 수신자 파일 업로드용 Presigned URL을 생성합니다. - * - * 수신자가 사망확인 서류(PDF, 이미지)를 S3에 업로드하기 위한 Presigned URL을 생성합니다. - * - * @param authCode 수신자 인증번호 (UUID, X-Auth-Code 헤더) - * @param body extension (예: pdf, jpg, png) - * @return presignedUrl, fileUrl, contentType - */ - @POST("api/receiver-auth/presigned-url") - suspend fun getPresignedUrl( - @Header("X-Auth-Code") authCode: String, - @Body body: ReceiverAuthPresignedUrlRequestDto - ): ApiResponse - - /** - * 사망확인 서류(사망진단서, 가족관계증명서)를 제출합니다. - * - * 전달 조건이 DEATH_CERTIFICATE인 경우 수신자가 인증 서류를 제출합니다. - * - * @param authCode 수신자 인증번호 (UUID, X-Auth-Code 헤더) - * @param body deathCertificateUrl, familyRelationCertificateUrl - */ - @POST("api/receiver-auth/delivery-verification") - suspend fun submitDeliveryVerification( - @Header("X-Auth-Code") authCode: String, - @Body body: DeliveryVerificationRequestDto - ): ApiResponse - - /** - * 수신자가 마지막으로 제출한 인증 요청 상태를 조회합니다. - * - * @param authCode 수신자 인증번호 (UUID, X-Auth-Code 헤더) - */ - @GET("api/receiver-auth/delivery-verification/status") - suspend fun getDeliveryVerificationStatus( - @Header("X-Auth-Code") authCode: String - ): ApiResponse - - /** - * 인증번호를 통해 발신자가 남긴 메시지를 조회합니다. - * - * @param authCode 수신자 인증번호 (UUID, X-Auth-Code 헤더) - */ - @GET("api/receiver-auth/message") - suspend fun getMessage( - @Header("X-Auth-Code") authCode: String - ): ApiResponse - /** * 인증번호를 통해 수신자에게 배달된 타임레터 목록을 조회합니다. * @@ -107,18 +42,6 @@ interface ReceiverAuthApiService { @Header("X-Auth-Code") authCode: String ): ApiResponse - /** - * 인증번호를 통해 수신한 특정 타임레터를 상세 조회합니다. 읽음 처리도 함께 수행됩니다. - * - * @param authCode 수신자 인증번호 (UUID, X-Auth-Code 헤더) - * @param timeLetterReceiverId 수신 타임레터 ID - */ - @GET("api/receiver-auth/time-letters/{timeLetterReceiverId}") - suspend fun getTimeLetterDetail( - @Header("X-Auth-Code") authCode: String, - @Path("timeLetterReceiverId") timeLetterReceiverId: Long - ): ApiResponse - /** * 인증번호를 통해 수신자에게 공유된 마인드레코드 목록을 조회합니다. * @@ -129,18 +52,6 @@ interface ReceiverAuthApiService { @Header("X-Auth-Code") authCode: String ): ApiResponse - /** - * 인증번호를 통해 수신한 특정 마인드레코드의 상세 내용을 조회합니다. - * - * @param authCode 수신자 인증번호 (UUID, X-Auth-Code 헤더) - * @param mindRecordId 마인드레코드 ID - */ - @GET("api/receiver-auth/mind-records/{mindRecordId}") - suspend fun getMindRecordDetail( - @Header("X-Auth-Code") authCode: String, - @Path("mindRecordId") mindRecordId: Long - ): ApiResponse - /** * 인증번호를 통해 수신자에게 전달된 애프터노트 목록을 조회합니다. * @@ -150,16 +61,4 @@ interface ReceiverAuthApiService { suspend fun getAfterNotes( @Header("X-Auth-Code") authCode: String ): ApiResponse - - /** - * 인증번호를 통해 수신한 특정 애프터노트의 상세 내용을 조회합니다. - * - * @param authCode 수신자 인증번호 (UUID, X-Auth-Code 헤더) - * @param afternoteId 애프터노트 ID - */ - @GET("api/receiver-auth/after-notes/{afternoteId}") - suspend fun getAfternoteDetail( - @Header("X-Auth-Code") authCode: String, - @Path("afternoteId") afternoteId: Long - ): ApiResponse } diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/di/ReceiverAuthModule.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/di/ReceiverAuthModule.kt index bb94f7e4..c144dc30 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/di/ReceiverAuthModule.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/di/ReceiverAuthModule.kt @@ -1,27 +1,15 @@ package com.kuit.afternote.feature.receiverauth.data.di -import com.kuit.afternote.feature.receiver.domain.repository.iface.GetAfternoteDetailByAuthCodeRepository import com.kuit.afternote.feature.receiver.domain.repository.iface.GetAfterNotesByAuthCodeRepository -import com.kuit.afternote.feature.receiver.domain.repository.iface.GetSenderMessageRepository import com.kuit.afternote.feature.receiver.domain.repository.iface.GetMindRecordsByAuthCodeRepository -import com.kuit.afternote.feature.receiver.domain.repository.iface.GetTimeLetterDetailByAuthCodeRepository import com.kuit.afternote.feature.receiver.domain.repository.iface.GetTimeLettersByAuthCodeRepository import com.kuit.afternote.feature.receiverauth.data.api.ReceiverAuthApiService -import com.kuit.afternote.feature.receiverauth.data.repository.impl.GetDeliveryVerificationStatusRepositoryImpl -import com.kuit.afternote.feature.receiverauth.data.repository.impl.ReceiverAuthAfternoteDetailRepositoryImpl import com.kuit.afternote.feature.receiverauth.data.repository.impl.ReceiverAuthAfternoteRepositoryImpl -import com.kuit.afternote.feature.receiverauth.data.repository.impl.ReceiverAuthMessageRepositoryImpl import com.kuit.afternote.feature.receiverauth.data.repository.impl.ReceiverAuthMindRecordRepositoryImpl import com.kuit.afternote.feature.receiverauth.data.repository.impl.ReceiverAuthRepositoryImpl -import com.kuit.afternote.feature.receiverauth.data.repository.impl.ReceiverAuthTimeLetterDetailRepositoryImpl import com.kuit.afternote.feature.receiverauth.data.repository.impl.ReceiverAuthTimeLetterRepositoryImpl -import com.kuit.afternote.feature.receiverauth.data.repository.impl.ReceiverDocumentUploadRepositoryImpl -import com.kuit.afternote.feature.receiverauth.data.repository.impl.SubmitDeliveryVerificationRepositoryImpl import com.kuit.afternote.feature.receiverauth.data.repository.iface.ReceiverAuthRepository -import com.kuit.afternote.feature.receiverauth.domain.repository.iface.GetDeliveryVerificationStatusRepository import com.kuit.afternote.feature.receiverauth.domain.repository.iface.ReceiverAuthRepository as DomainReceiverAuthRepository -import com.kuit.afternote.feature.receiverauth.domain.repository.iface.SubmitDeliveryVerificationRepository -import com.kuit.afternote.feature.receiverauth.domain.repository.iface.UploadReceiverDocumentRepository import com.kuit.afternote.feature.receiverauth.session.ReceiverAuthSessionHolder import dagger.Binds import dagger.Module @@ -47,12 +35,6 @@ abstract class ReceiverAuthModule { impl: ReceiverAuthRepositoryImpl ): ReceiverAuthRepository - @Binds - @Singleton - abstract fun bindGetTimeLetterDetailByAuthCodeRepository( - impl: ReceiverAuthTimeLetterDetailRepositoryImpl - ): GetTimeLetterDetailByAuthCodeRepository - @Binds @Singleton abstract fun bindGetTimeLettersByAuthCodeRepository( @@ -71,36 +53,6 @@ abstract class ReceiverAuthModule { impl: ReceiverAuthAfternoteRepositoryImpl ): GetAfterNotesByAuthCodeRepository - @Binds - @Singleton - abstract fun bindGetAfternoteDetailByAuthCodeRepository( - impl: ReceiverAuthAfternoteDetailRepositoryImpl - ): GetAfternoteDetailByAuthCodeRepository - - @Binds - @Singleton - abstract fun bindGetSenderMessageRepository( - impl: ReceiverAuthMessageRepositoryImpl - ): GetSenderMessageRepository - - @Binds - @Singleton - abstract fun bindUploadReceiverDocumentRepository( - impl: ReceiverDocumentUploadRepositoryImpl - ): UploadReceiverDocumentRepository - - @Binds - @Singleton - abstract fun bindSubmitDeliveryVerificationRepository( - impl: SubmitDeliveryVerificationRepositoryImpl - ): SubmitDeliveryVerificationRepository - - @Binds - @Singleton - abstract fun bindGetDeliveryVerificationStatusRepository( - impl: GetDeliveryVerificationStatusRepositoryImpl - ): GetDeliveryVerificationStatusRepository - companion object { @Provides @Singleton diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/dto/ReceiverAuthDto.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/dto/ReceiverAuthDto.kt index df75ccb0..42b10056 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/dto/ReceiverAuthDto.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/dto/ReceiverAuthDto.kt @@ -25,34 +25,18 @@ data class ReceiverAuthVerifyResponseDto( @SerialName("relation") val relation: String? = null ) -// --- GET /api/receiver-auth/message (발신자 메시지 조회) --- - -/** - * 수신자 메시지 조회 응답. - * - * 인증번호를 통해 발신자가 남긴 메시지를 조회합니다. - */ -@Serializable -data class ReceiverMessageResponseDto( - val senderName: String, - val message: String? = null -) - // --- GET /api/receiver-auth/time-letters (타임레터 목록) --- @Serializable data class ReceivedTimeLetterAuthResponseDto( @SerialName("id") val id: Long, - @SerialName("timeLetterReceiverId") val timeLetterReceiverId: Long, @SerialName("title") val title: String? = null, @SerialName("content") val content: String? = null, @SerialName("sendAt") val sendAt: String? = null, @SerialName("status") val status: String? = null, @SerialName("senderName") val senderName: String? = null, @SerialName("deliveredAt") val deliveredAt: String? = null, - @SerialName("createdAt") val createdAt: String? = null, - val mediaList: List = emptyList(), - val isRead: Boolean = false + @SerialName("createdAt") val createdAt: String? = null ) @Serializable @@ -61,38 +45,6 @@ data class ReceivedTimeLetterListAuthResponseDto( @SerialName("totalCount") val totalCount: Int = 0 ) -// --- GET /api/receiver-auth/time-letters/{timeLetterReceiverId} (타임레터 상세 조회) --- - -/** 타임레터 미디어 응답. */ -@Serializable -data class TimeLetterMediaAuthResponseDto( - val id: Long, - val mediaType: String? = null, - val mediaUrl: String -) - -/** - * 수신한 타임레터 상세 응답. - * - * 인증번호로 특정 타임레터 상세 조회 시 반환. 읽음 처리도 함께 수행됩니다. - * - * @param status 상태 (DRAFT, SCHEDULED, SENT) - */ -@Serializable -data class ReceivedTimeLetterDetailAuthResponseDto( - val id: Long, - val timeLetterReceiverId: Long, - val title: String? = null, - val content: String? = null, - val sendAt: String? = null, - val status: String? = null, - val senderName: String? = null, - val deliveredAt: String? = null, - val createdAt: String? = null, - val mediaList: List = emptyList(), - val isRead: Boolean = false -) - // --- GET /api/receiver-auth/mind-records (마인드레코드 목록) --- @Serializable @@ -112,38 +64,6 @@ data class ReceivedMindRecordListAuthResponseDto( @SerialName("totalCount") val totalCount: Int = 0 ) -// --- GET /api/receiver-auth/mind-records/{mindRecordId} (마인드레코드 상세 조회) --- - -/** 마인드레코드 이미지 응답. */ -@Serializable -data class MindRecordImageAuthResponseDto( - val id: Long, - val mediaType: String? = null, - val imageUrl: String -) - -/** - * 수신한 마인드레코드 상세 응답. - * - * 인증번호로 특정 마인드레코드 상세 조회 시 반환. - * - * @param type 마인드레코드 타입 (예: DIARY) - */ -@Serializable -data class ReceivedMindRecordDetailAuthResponseDto( - val id: Long, - val type: String? = null, - val title: String? = null, - val recordDate: String? = null, - val content: String? = null, - val questionId: Long? = null, - val questionContent: String? = null, - val category: String? = null, - val senderName: String? = null, - val createdAt: String? = null, - val imageList: List = emptyList() -) - // --- GET /api/receiver-auth/after-notes (애프터노트 목록) --- @Serializable @@ -162,108 +82,3 @@ data class ReceivedAfternoteListAuthResponseDto( @SerialName("afternotes") val afternotes: List = emptyList(), @SerialName("totalCount") val totalCount: Int = 0 ) - -// --- GET /api/receiver-auth/after-notes/{afternoteId} (애프터노트 상세 조회) --- - -/** 노래 정보 (PLAYLIST 전용). */ -@Serializable -data class ReceivedAfternoteSongInfoDto( - val title: String? = null, - val artist: String? = null, - val coverUrl: String? = null -) - -/** 플레이리스트 정보 (PLAYLIST 전용). */ -@Serializable -data class ReceivedAfternotePlaylistInfoDto( - val atmosphere: String? = null, - val songs: List = emptyList(), - val memorialVideo: ReceivedAfternoteMemorialVideoInfoDto? = null -) - -/** 추모 영상. */ -@Serializable -data class ReceivedAfternoteMemorialVideoInfoDto( - val videoUrl: String? = null, - val thumbnailUrl: String? = null -) - -/** - * 수신한 애프터노트 상세 응답. - * - * 인증번호로 특정 애프터노트 상세 조회 시 반환. - * - * @param category 카테고리 (SOCIAL, GALLERY, PLAYLIST) - * @param processMethod 처리 방식 (MEMORIAL, DELETE, TRANSFER, ADDITIONAL - SOCIAL/GALLERY 전용) - * @param actions 체크리스트 (SOCIAL/GALLERY 전용) - * @param playlist 플레이리스트 정보 (PLAYLIST 전용) - */ -@Serializable -data class ReceivedAfternoteDetailAuthResponseDto( - val id: Long, - val category: String? = null, - val title: String? = null, - val processMethod: String? = null, - val actions: List = emptyList(), - val leaveMessage: String? = null, - val senderName: String? = null, - val createdAt: String? = null, - val playlist: ReceivedAfternotePlaylistInfoDto? = null -) - -// --- POST /api/receiver-auth/presigned-url (수신자 파일 업로드용 Presigned URL 생성) --- - -/** - * 수신자 파일 업로드용 Presigned URL 요청. - * - * 수신자가 사망확인 서류(PDF, 이미지)를 S3에 업로드하기 위한 Presigned URL을 생성합니다. - * - * @param extension 파일 확장자 (예: pdf, jpg, png) - */ -@Serializable -data class ReceiverAuthPresignedUrlRequestDto( - val extension: String -) - -/** - * 수신자 파일 업로드용 Presigned URL 응답. - * - * @param presignedUrl S3 Presigned PUT URL (파일 업로드용) - * @param fileUrl 업로드 완료 후 사용할 파일 URL - * @param contentType PUT 요청 시 사용할 Content-Type 헤더 값 - */ -@Serializable -data class ReceiverAuthPresignedUrlResponseDto( - val presignedUrl: String, - val fileUrl: String, - val contentType: String -) - -// --- POST /api/receiver-auth/delivery-verification (사망확인 서류 제출) --- - -/** - * 사망확인 서류 제출 요청. - * - * 전달 조건이 DEATH_CERTIFICATE인 경우 수신자가 인증 서류를 제출합니다. - */ -@Serializable -data class DeliveryVerificationRequestDto( - val deathCertificateUrl: String, - val familyRelationCertificateUrl: String -) - -/** - * 사망확인 서류 제출 응답. - * - * @param status 사망확인 서류 인증 상태 (PENDING, APPROVED, REJECTED) - * @param adminNote 관리자 메모 (nullable) - */ -@Serializable -data class DeliveryVerificationResponseDto( - val id: Long, - val status: String, - val deathCertificateUrl: String, - val familyRelationCertificateUrl: String, - val adminNote: String? = null, - val createdAt: String -) diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/mapper/ReceiverAuthContentMapper.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/mapper/ReceiverAuthContentMapper.kt index 5bbfe1b6..2685993a 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/mapper/ReceiverAuthContentMapper.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/mapper/ReceiverAuthContentMapper.kt @@ -1,19 +1,11 @@ package com.kuit.afternote.feature.receiverauth.data.mapper import com.kuit.afternote.feature.receiver.domain.entity.ReceivedAfternote -import com.kuit.afternote.feature.receiver.domain.entity.ReceivedAfternoteDetail -import com.kuit.afternote.feature.receiver.domain.entity.ReceivedAfternotePlaylist import com.kuit.afternote.feature.receiver.domain.entity.ReceivedMindRecord -import com.kuit.afternote.feature.receiver.domain.entity.ReceivedPlaylistSong import com.kuit.afternote.feature.receiver.domain.entity.ReceivedTimeLetter -import com.kuit.afternote.feature.receiver.domain.entity.ReceivedTimeLetterMedia import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedAfternoteAuthResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedAfternoteDetailAuthResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedAfternoteSongInfoDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedMindRecordAuthResponseDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedTimeLetterAuthResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedTimeLetterDetailAuthResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.TimeLetterMediaAuthResponseDto /** * receiver-auth API DTO를 receiver 도메인 엔티티로 매핑합니다. @@ -22,7 +14,7 @@ import com.kuit.afternote.feature.receiverauth.data.dto.TimeLetterMediaAuthRespo fun ReceivedTimeLetterAuthResponseDto.toReceivedTimeLetter(): ReceivedTimeLetter = ReceivedTimeLetter( timeLetterId = id, - timeLetterReceiverId = timeLetterReceiverId, + timeLetterReceiverId = id, title = title, content = content, sendAt = sendAt, @@ -30,30 +22,8 @@ fun ReceivedTimeLetterAuthResponseDto.toReceivedTimeLetter(): ReceivedTimeLetter senderName = senderName, deliveredAt = deliveredAt, createdAt = createdAt, - mediaList = mediaList.map { it.toReceivedTimeLetterMedia() }, - isRead = isRead - ) - -fun TimeLetterMediaAuthResponseDto.toReceivedTimeLetterMedia(): ReceivedTimeLetterMedia = - ReceivedTimeLetterMedia( - id = id, - mediaType = mediaType, - mediaUrl = mediaUrl - ) - -fun ReceivedTimeLetterDetailAuthResponseDto.toReceivedTimeLetter(): ReceivedTimeLetter = - ReceivedTimeLetter( - timeLetterId = id, - timeLetterReceiverId = timeLetterReceiverId, - title = title, - content = content, - sendAt = sendAt, - status = status, - senderName = senderName, - deliveredAt = deliveredAt, - createdAt = createdAt, - mediaList = mediaList.map { it.toReceivedTimeLetterMedia() }, - isRead = isRead + mediaList = emptyList(), + isRead = false ) fun ReceivedMindRecordAuthResponseDto.toReceivedMindRecord(): ReceivedMindRecord = @@ -66,28 +36,7 @@ fun ReceivedMindRecordAuthResponseDto.toReceivedMindRecord(): ReceivedMindRecord fun ReceivedAfternoteAuthResponseDto.toReceivedAfternote(): ReceivedAfternote = ReceivedAfternote( - id = id, sourceType = category.orEmpty(), lastUpdatedAt = createdAt.orEmpty(), leaveMessage = leaveMessage ) - -fun ReceivedAfternoteSongInfoDto.toReceivedPlaylistSong(): ReceivedPlaylistSong = - ReceivedPlaylistSong( - title = title.orEmpty(), - artist = artist.orEmpty(), - coverUrl = coverUrl - ) - -fun ReceivedAfternoteDetailAuthResponseDto.toReceivedAfternoteDetail(): ReceivedAfternoteDetail = - ReceivedAfternoteDetail( - id = id, - category = category, - title = title, - playlist = playlist?.let { p -> - ReceivedAfternotePlaylist( - atmosphere = p.atmosphere, - songs = p.songs.map { it.toReceivedPlaylistSong() } - ) - } - ) diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/iface/ReceiverAuthRepository.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/iface/ReceiverAuthRepository.kt index 47f30287..85b9c06d 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/iface/ReceiverAuthRepository.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/iface/ReceiverAuthRepository.kt @@ -1,14 +1,7 @@ package com.kuit.afternote.feature.receiverauth.data.repository.iface -import com.kuit.afternote.feature.receiverauth.data.dto.DeliveryVerificationRequestDto -import com.kuit.afternote.feature.receiverauth.data.dto.DeliveryVerificationResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceiverAuthPresignedUrlResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceiverMessageResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedAfternoteDetailAuthResponseDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedAfternoteListAuthResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedMindRecordDetailAuthResponseDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedMindRecordListAuthResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedTimeLetterDetailAuthResponseDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedTimeLetterListAuthResponseDto /** @@ -18,48 +11,6 @@ import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedTimeLetterListAu */ interface ReceiverAuthRepository { - /** - * 수신자 파일 업로드용 Presigned URL을 생성합니다. - * - * 수신자가 사망확인 서류(PDF, 이미지)를 S3에 업로드하기 위한 Presigned URL을 생성합니다. - * - * @param authCode 수신자 인증번호 (UUID) - * @param extension 파일 확장자 (예: pdf, jpg, png) - * @return presignedUrl, fileUrl, contentType - */ - suspend fun getPresignedUrl( - authCode: String, - extension: String - ): Result - - /** - * 사망확인 서류(사망진단서, 가족관계증명서)를 제출합니다. - * - * 전달 조건이 DEATH_CERTIFICATE인 경우 사용합니다. - * - * @param authCode 수신자 인증번호 (UUID) - * @param request deathCertificateUrl, familyRelationCertificateUrl - * @return 제출 결과 (인증 요청 ID, 상태 등) - */ - suspend fun submitDeliveryVerification( - authCode: String, - request: DeliveryVerificationRequestDto - ): Result - - /** - * 수신자가 마지막으로 제출한 인증 요청 상태를 조회합니다. - * - * @param authCode 수신자 인증번호 (UUID) - */ - suspend fun getDeliveryVerificationStatus(authCode: String): Result - - /** - * 인증번호를 통해 발신자가 남긴 메시지를 조회합니다. - * - * @param authCode 수신자 인증번호 (UUID) - */ - suspend fun getMessage(authCode: String): Result - /** * 인증번호를 통해 수신자에게 배달된 타임레터 목록을 조회합니다. * @@ -67,17 +18,6 @@ interface ReceiverAuthRepository { */ suspend fun getTimeLetters(authCode: String): Result - /** - * 인증번호를 통해 수신한 특정 타임레터를 상세 조회합니다. 읽음 처리도 함께 수행됩니다. - * - * @param authCode 수신자 인증번호 (UUID) - * @param timeLetterReceiverId 수신 타임레터 ID - */ - suspend fun getTimeLetterDetail( - authCode: String, - timeLetterReceiverId: Long - ): Result - /** * 인증번호를 통해 수신자에게 공유된 마인드레코드 목록을 조회합니다. * @@ -85,32 +25,10 @@ interface ReceiverAuthRepository { */ suspend fun getMindRecords(authCode: String): Result - /** - * 인증번호를 통해 수신한 특정 마인드레코드의 상세 내용을 조회합니다. - * - * @param authCode 수신자 인증번호 (UUID) - * @param mindRecordId 마인드레코드 ID - */ - suspend fun getMindRecordDetail( - authCode: String, - mindRecordId: Long - ): Result - /** * 인증번호를 통해 수신자에게 전달된 애프터노트 목록을 조회합니다. * * @param authCode 수신자 인증번호 (UUID) */ suspend fun getAfterNotes(authCode: String): Result - - /** - * 인증번호를 통해 수신한 특정 애프터노트의 상세 내용을 조회합니다. - * - * @param authCode 수신자 인증번호 (UUID) - * @param afternoteId 애프터노트 ID - */ - suspend fun getAfternoteDetail( - authCode: String, - afternoteId: Long - ): Result } diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/GetDeliveryVerificationStatusRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/GetDeliveryVerificationStatusRepositoryImpl.kt deleted file mode 100644 index 5241a542..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/GetDeliveryVerificationStatusRepositoryImpl.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.data.repository.impl - -import com.kuit.afternote.feature.receiverauth.data.repository.iface.ReceiverAuthRepository -import com.kuit.afternote.feature.receiverauth.domain.entity.DeliveryVerificationStatus -import com.kuit.afternote.feature.receiverauth.domain.repository.iface.GetDeliveryVerificationStatusRepository -import javax.inject.Inject - -/** - * 수신자 사망확인 인증 상태 조회 Data Repository 구현체. - * - * Data [ReceiverAuthRepository]의 getDeliveryVerificationStatus에 위임하고, - * DTO를 도메인 [DeliveryVerificationStatus]로 매핑합니다. - */ -class GetDeliveryVerificationStatusRepositoryImpl - @Inject - constructor( - private val receiverAuthRepository: ReceiverAuthRepository - ) : GetDeliveryVerificationStatusRepository { - - override suspend fun getStatus(authCode: String): Result = - receiverAuthRepository.getDeliveryVerificationStatus(authCode).map { dto -> - DeliveryVerificationStatus( - id = dto.id, - status = dto.status, - adminNote = dto.adminNote, - createdAt = dto.createdAt - ) - } -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthAfternoteDetailRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthAfternoteDetailRepositoryImpl.kt deleted file mode 100644 index 485e1155..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthAfternoteDetailRepositoryImpl.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.data.repository.impl - -import com.kuit.afternote.feature.receiver.domain.entity.ReceivedAfternoteDetail -import com.kuit.afternote.feature.receiver.domain.repository.iface.GetAfternoteDetailByAuthCodeRepository -import com.kuit.afternote.feature.receiverauth.data.mapper.toReceivedAfternoteDetail -import com.kuit.afternote.feature.receiverauth.data.repository.iface.ReceiverAuthRepository -import javax.inject.Inject - -/** - * 인증번호 기반 애프터노트 상세 조회 Repository 구현. - * - * receiver-auth API 응답을 receiver 도메인 엔티티로 매핑하여 반환합니다. - */ -class ReceiverAuthAfternoteDetailRepositoryImpl - @Inject - constructor( - private val receiverAuthRepository: ReceiverAuthRepository - ) : GetAfternoteDetailByAuthCodeRepository { - - override suspend fun getAfternoteDetail( - authCode: String, - afternoteId: Long - ): Result = - receiverAuthRepository.getAfternoteDetail( - authCode = authCode, - afternoteId = afternoteId - ).map { it.toReceivedAfternoteDetail() } -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthMessageRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthMessageRepositoryImpl.kt deleted file mode 100644 index dd17a6f3..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthMessageRepositoryImpl.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.data.repository.impl - -import com.kuit.afternote.feature.receiver.domain.repository.iface.GetSenderMessageRepository -import com.kuit.afternote.feature.receiverauth.data.repository.iface.ReceiverAuthRepository -import javax.inject.Inject - -/** - * 인증번호 기반 발신자 메시지 조회 Repository 구현. - * - * GET /api/receiver-auth/message API 응답의 message 필드를 반환합니다. - */ -class ReceiverAuthMessageRepositoryImpl - @Inject - constructor( - private val receiverAuthRepository: ReceiverAuthRepository - ) : GetSenderMessageRepository { - - override suspend fun getMessage(authCode: String): Result = - receiverAuthRepository.getMessage(authCode).map { it.message } -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthRepositoryImpl.kt index e85d90c3..454f0b9a 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthRepositoryImpl.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthRepositoryImpl.kt @@ -2,18 +2,10 @@ package com.kuit.afternote.feature.receiverauth.data.repository.impl import com.kuit.afternote.data.remote.requireData import com.kuit.afternote.feature.receiverauth.data.api.ReceiverAuthApiService -import com.kuit.afternote.feature.receiverauth.data.dto.DeliveryVerificationRequestDto -import com.kuit.afternote.feature.receiverauth.data.dto.DeliveryVerificationResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceiverAuthPresignedUrlRequestDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceiverAuthPresignedUrlResponseDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceiverAuthVerifyRequestDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceiverAuthVerifyResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedAfternoteDetailAuthResponseDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedAfternoteListAuthResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceiverMessageResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedMindRecordDetailAuthResponseDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedMindRecordListAuthResponseDto -import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedTimeLetterDetailAuthResponseDto import com.kuit.afternote.feature.receiverauth.data.dto.ReceivedTimeLetterListAuthResponseDto import com.kuit.afternote.feature.receiverauth.data.repository.iface.ReceiverAuthRepository import com.kuit.afternote.feature.receiverauth.domain.entity.ReceiverAuthVerifyResult @@ -40,76 +32,20 @@ class ReceiverAuthRepositoryImpl dto.toDomain() } - override suspend fun getPresignedUrl( - authCode: String, - extension: String - ): Result = - runCatching { - api.getPresignedUrl( - authCode = authCode, - body = ReceiverAuthPresignedUrlRequestDto(extension = extension) - ).requireData() - } - - override suspend fun submitDeliveryVerification( - authCode: String, - request: DeliveryVerificationRequestDto - ): Result = - runCatching { - api.submitDeliveryVerification(authCode = authCode, body = request).requireData() - } - - override suspend fun getDeliveryVerificationStatus( - authCode: String - ): Result = - runCatching { - api.getDeliveryVerificationStatus(authCode = authCode).requireData() - } - - override suspend fun getMessage(authCode: String): Result = - runCatching { - api.getMessage(authCode = authCode).requireData() - } - override suspend fun getTimeLetters(authCode: String): Result = runCatching { api.getTimeLetters(authCode = authCode).requireData() } - override suspend fun getTimeLetterDetail( - authCode: String, - timeLetterReceiverId: Long - ): Result = - runCatching { - api.getTimeLetterDetail(authCode = authCode, timeLetterReceiverId = timeLetterReceiverId) - .requireData() - } - override suspend fun getMindRecords(authCode: String): Result = runCatching { api.getMindRecords(authCode = authCode).requireData() } - override suspend fun getMindRecordDetail( - authCode: String, - mindRecordId: Long - ): Result = - runCatching { - api.getMindRecordDetail(authCode = authCode, mindRecordId = mindRecordId).requireData() - } - override suspend fun getAfterNotes(authCode: String): Result = runCatching { api.getAfterNotes(authCode = authCode).requireData() } - - override suspend fun getAfternoteDetail( - authCode: String, - afternoteId: Long - ): Result = - runCatching { - api.getAfternoteDetail(authCode = authCode, afternoteId = afternoteId).requireData() - } } private fun ReceiverAuthVerifyResponseDto.toDomain(): ReceiverAuthVerifyResult = diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthTimeLetterDetailRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthTimeLetterDetailRepositoryImpl.kt deleted file mode 100644 index 868f8f84..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverAuthTimeLetterDetailRepositoryImpl.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.data.repository.impl - -import com.kuit.afternote.feature.receiver.domain.entity.ReceivedTimeLetter -import com.kuit.afternote.feature.receiver.domain.repository.iface.GetTimeLetterDetailByAuthCodeRepository -import com.kuit.afternote.feature.receiverauth.data.mapper.toReceivedTimeLetter -import com.kuit.afternote.feature.receiverauth.data.repository.iface.ReceiverAuthRepository -import javax.inject.Inject - -/** - * 인증번호 기반 타임레터 상세 조회 Repository 구현. - * - * GET /api/receiver-auth/time-letters/{timeLetterReceiverId} (X-Auth-Code) API를 사용합니다. - */ -class ReceiverAuthTimeLetterDetailRepositoryImpl - @Inject - constructor( - private val receiverAuthRepository: ReceiverAuthRepository - ) : GetTimeLetterDetailByAuthCodeRepository { - - override suspend fun getTimeLetterDetail( - authCode: String, - timeLetterReceiverId: Long - ): Result = - receiverAuthRepository.getTimeLetterDetail( - authCode = authCode, - timeLetterReceiverId = timeLetterReceiverId - ).map { it.toReceivedTimeLetter() } -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverDocumentUploadRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverDocumentUploadRepositoryImpl.kt deleted file mode 100644 index c0bb5914..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/ReceiverDocumentUploadRepositoryImpl.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.data.repository.impl - -import android.content.Context -import android.net.Uri -import com.kuit.afternote.feature.receiverauth.data.repository.iface.ReceiverAuthRepository -import com.kuit.afternote.feature.receiverauth.domain.repository.iface.UploadReceiverDocumentRepository -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.withContext -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import javax.inject.Inject -import javax.inject.Named - -/** - * 수신자 증빙 서류 S3 업로드 Data Repository 구현체. - * - * receiver-auth Presigned URL 발급 후 S3 PUT으로 업로드하고 fileUrl을 반환합니다. - * 확장자는 API 허용 값(jpg, jpeg, png, gif, webp, heic, pdf)으로 Uri/ContentType에서 추론합니다. - */ -class ReceiverDocumentUploadRepositoryImpl - @Inject - constructor( - @ApplicationContext private val context: Context, - private val receiverAuthRepository: ReceiverAuthRepository, - @Named("S3Upload") private val okHttpClient: OkHttpClient, - @Named("IoDispatcher") private val ioDispatcher: CoroutineDispatcher - ) : UploadReceiverDocumentRepository { - - override suspend fun uploadDocument(authCode: String, uriString: String): Result = - runCatching { - val uri = Uri.parse(uriString) - val extension = extensionFromUri(uri) - val presignedResult = receiverAuthRepository.getPresignedUrl(authCode, extension) - val presigned = presignedResult.getOrThrow() - - val bytes = withContext(ioDispatcher) { - context.contentResolver.openInputStream(uri)?.use { it.readBytes() } - ?: throw IllegalStateException("Could not read file from URI") - } - - val contentType = presigned.contentType.ifBlank { defaultContentType(extension) } - val requestBody = bytes.toRequestBody(contentType.toMediaType()) - val putRequest = - Request.Builder() - .url(presigned.presignedUrl) - .put(requestBody) - .header("Content-Type", contentType) - .build() - - withContext(ioDispatcher) { - okHttpClient.newCall(putRequest).execute().use { response -> - check(response.isSuccessful) { - "S3 upload failed: ${response.code} ${response.message}" - } - } - } - presigned.fileUrl - } - - /** - * Uri/ContentType에서 API 허용 확장자(jpg, jpeg, png, gif, webp, heic, pdf)로 매핑. 점 없음. - */ - private fun extensionFromUri(uri: Uri): String { - val mime = context.contentResolver.getType(uri) ?: return DEFAULT_EXTENSION - return when { - mime == "image/jpeg" || mime == "image/jpg" -> "jpg" - mime == "image/png" -> "png" - mime == "image/gif" -> "gif" - mime == "image/webp" -> "webp" - mime == "image/heic" -> "heic" - mime == "application/pdf" -> "pdf" - else -> DEFAULT_EXTENSION - } - } - - private fun defaultContentType(extension: String): String = - when (extension) { - "pdf" -> "application/pdf" - "png" -> "image/png" - "gif" -> "image/gif" - "webp" -> "image/webp" - "heic" -> "image/heic" - else -> "image/jpeg" - } - - companion object { - private const val DEFAULT_EXTENSION = "jpg" - } -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/SubmitDeliveryVerificationRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/SubmitDeliveryVerificationRepositoryImpl.kt deleted file mode 100644 index 4348d7ed..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/data/repository/impl/SubmitDeliveryVerificationRepositoryImpl.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.data.repository.impl - -import com.kuit.afternote.feature.receiverauth.data.dto.DeliveryVerificationRequestDto -import com.kuit.afternote.feature.receiverauth.data.repository.iface.ReceiverAuthRepository -import com.kuit.afternote.feature.receiverauth.domain.repository.iface.SubmitDeliveryVerificationRepository -import javax.inject.Inject - -/** - * 수신자 사망확인 서류 제출 Data Repository 구현체. - * - * Data [ReceiverAuthRepository]의 submitDeliveryVerification에 위임합니다. - */ -class SubmitDeliveryVerificationRepositoryImpl - @Inject - constructor( - private val receiverAuthRepository: ReceiverAuthRepository - ) : SubmitDeliveryVerificationRepository { - - override suspend fun submit( - authCode: String, - deathCertificateUrl: String, - familyRelationCertificateUrl: String - ): Result = - receiverAuthRepository.submitDeliveryVerification( - authCode = authCode, - request = DeliveryVerificationRequestDto( - deathCertificateUrl = deathCertificateUrl, - familyRelationCertificateUrl = familyRelationCertificateUrl - ) - ).map { } -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/entity/DeliveryVerificationStatus.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/entity/DeliveryVerificationStatus.kt deleted file mode 100644 index 2d71849a..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/entity/DeliveryVerificationStatus.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.domain.entity - -/** - * 수신자 사망확인 서류 제출·인증 상태 도메인 모델. - * - * GET /api/receiver-auth/delivery-verification/status 응답에 대응합니다. - * - * @param id 인증 요청 ID - * @param status 인증 상태 (PENDING, APPROVED, REJECTED) - * @param adminNote 관리자 메모 (nullable) - * @param createdAt 생성 일시 - */ -data class DeliveryVerificationStatus( - val id: Long, - val status: String, - val adminNote: String? = null, - val createdAt: String -) diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/repository/iface/GetDeliveryVerificationStatusRepository.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/repository/iface/GetDeliveryVerificationStatusRepository.kt deleted file mode 100644 index dda542a8..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/repository/iface/GetDeliveryVerificationStatusRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.domain.repository.iface - -import com.kuit.afternote.feature.receiverauth.domain.entity.DeliveryVerificationStatus - -/** - * 수신자 사망확인 인증 상태 조회용 Domain Repository. - * - * GET /api/receiver-auth/delivery-verification/status 호출을 래핑합니다. - */ -interface GetDeliveryVerificationStatusRepository { - - /** - * 수신자가 마지막으로 제출한 인증 요청 상태를 조회합니다. - * - * @param authCode 수신자 인증번호 (X-Auth-Code) - * @return 성공 시 [DeliveryVerificationStatus], 실패 시 [Result.failure] - */ - suspend fun getStatus(authCode: String): Result -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/repository/iface/SubmitDeliveryVerificationRepository.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/repository/iface/SubmitDeliveryVerificationRepository.kt deleted file mode 100644 index 4c5dcf02..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/repository/iface/SubmitDeliveryVerificationRepository.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.domain.repository.iface - -/** - * 수신자 사망확인 서류 제출용 Domain Repository. - * - * POST /api/receiver-auth/delivery-verification 호출을 래핑합니다. - */ -interface SubmitDeliveryVerificationRepository { - - /** - * 사망진단서·가족관계증명서 URL을 서버에 제출합니다. - * - * @param authCode 수신자 인증번호 (X-Auth-Code) - * @param deathCertificateUrl 사망진단서 업로드 완료 fileUrl - * @param familyRelationCertificateUrl 가족관계증명서 업로드 완료 fileUrl - * @return 성공 시 [Result.success], 실패 시 [Result.failure] - */ - suspend fun submit( - authCode: String, - deathCertificateUrl: String, - familyRelationCertificateUrl: String - ): Result -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/repository/iface/UploadReceiverDocumentRepository.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/repository/iface/UploadReceiverDocumentRepository.kt deleted file mode 100644 index d6402675..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/repository/iface/UploadReceiverDocumentRepository.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.domain.repository.iface - -/** - * 수신자 증빙 서류(사망진단서, 가족관계증명서) S3 업로드용 Domain Repository. - * - * Presigned URL 발급 후 S3에 PUT하여 업로드하고, 업로드 완료된 fileUrl을 반환합니다. - */ -interface UploadReceiverDocumentRepository { - - /** - * 수신자 인증번호와 파일 URI로 증빙 서류를 S3에 업로드합니다. - * - * @param authCode 수신자 인증번호 (X-Auth-Code) - * @param uriString 파일 content URI 문자열 - * @return 성공 시 업로드된 파일의 fileUrl, 실패 시 [Result.failure] - */ - suspend fun uploadDocument(authCode: String, uriString: String): Result -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/usecase/GetDeliveryVerificationStatusUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/usecase/GetDeliveryVerificationStatusUseCase.kt deleted file mode 100644 index 41512d23..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/usecase/GetDeliveryVerificationStatusUseCase.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.domain.usecase - -import com.kuit.afternote.feature.receiverauth.domain.entity.DeliveryVerificationStatus -import com.kuit.afternote.feature.receiverauth.domain.repository.iface.GetDeliveryVerificationStatusRepository -import javax.inject.Inject - -/** - * 수신자가 마지막으로 제출한 사망확인 인증 요청 상태를 조회하는 UseCase. - */ -class GetDeliveryVerificationStatusUseCase - @Inject - constructor( - private val getDeliveryVerificationStatusRepository: GetDeliveryVerificationStatusRepository - ) { - - /** - * 인증 요청 상태를 조회합니다. - * - * @param authCode 수신자 인증번호 (X-Auth-Code) - * @return 성공 시 [DeliveryVerificationStatus], 실패 시 [Result.failure] - */ - suspend operator fun invoke(authCode: String): Result = - getDeliveryVerificationStatusRepository.getStatus(authCode) -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/usecase/SubmitDeliveryVerificationUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/usecase/SubmitDeliveryVerificationUseCase.kt deleted file mode 100644 index c19e3ac9..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/usecase/SubmitDeliveryVerificationUseCase.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.domain.usecase - -import com.kuit.afternote.feature.receiverauth.domain.repository.iface.SubmitDeliveryVerificationRepository -import javax.inject.Inject - -/** - * 수신자 사망확인 서류(사망진단서, 가족관계증명서) URL을 서버에 제출하는 UseCase. - */ -class SubmitDeliveryVerificationUseCase - @Inject - constructor( - private val submitDeliveryVerificationRepository: SubmitDeliveryVerificationRepository - ) { - - /** - * 사망진단서·가족관계증명서 fileUrl을 서버에 제출합니다. - * - * @param authCode 수신자 인증번호 (X-Auth-Code) - * @param deathCertificateUrl 사망진단서 업로드 완료 fileUrl - * @param familyRelationCertificateUrl 가족관계증명서 업로드 완료 fileUrl - * @return 성공 시 [Result.success], 실패 시 [Result.failure] - */ - suspend operator fun invoke( - authCode: String, - deathCertificateUrl: String, - familyRelationCertificateUrl: String - ): Result = - submitDeliveryVerificationRepository.submit( - authCode = authCode, - deathCertificateUrl = deathCertificateUrl, - familyRelationCertificateUrl = familyRelationCertificateUrl - ) -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/usecase/UploadReceiverDocumentUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/usecase/UploadReceiverDocumentUseCase.kt deleted file mode 100644 index a1e7c477..00000000 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/domain/usecase/UploadReceiverDocumentUseCase.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.domain.usecase - -import com.kuit.afternote.feature.receiverauth.domain.repository.iface.UploadReceiverDocumentRepository -import javax.inject.Inject - -/** - * 수신자 증빙 서류를 S3에 업로드하는 UseCase. - * - * Presigned URL 발급 후 S3 PUT으로 업로드하고, 업로드된 fileUrl을 반환합니다. - */ -class UploadReceiverDocumentUseCase - @Inject - constructor( - private val uploadReceiverDocumentRepository: UploadReceiverDocumentRepository - ) { - - /** - * 증빙 서류 파일을 업로드합니다. - * - * @param authCode 수신자 인증번호 (X-Auth-Code) - * @param uriString 파일 content URI 문자열 - * @return 성공 시 fileUrl, 실패 시 [Result.failure] - */ - suspend operator fun invoke(authCode: String, uriString: String): Result = - uploadReceiverDocumentRepository.uploadDocument(authCode, uriString) -} diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfUiState.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfUiState.kt index 85abb750..ebcb0f54 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfUiState.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfUiState.kt @@ -1,6 +1,5 @@ package com.kuit.afternote.feature.receiverauth.presentation.viewmodel -import com.kuit.afternote.feature.receiverauth.domain.entity.DeliveryVerificationStatus import com.kuit.afternote.feature.receiverauth.uimodel.VerifyStep /** @@ -12,9 +11,6 @@ import com.kuit.afternote.feature.receiverauth.uimodel.VerifyStep * @param verifyError 검증 실패 시 에러 타입 (화면에서 stringResource로 메시지 표시) * @param verifiedReceiverId 마스터키 검증 성공 시 수신받은 receiverId (열람요청 완료에서 수신자 홈 이동 시 사용) * @param verifiedSenderName 마스터키 검증 성공 시 수신받은 발신자 이름 (수신자 홈 상단 "故 OO님이 남기신 기록" 표시용) - * @param isSubmitting 증빙 서류 업로드·제출 중 여부 - * @param submitError 증빙 서류 제출 실패 시 에러 타입 - * @param deliveryVerificationStatus END 단계에서 표시할 인증 요청 상태 (PENDING/APPROVED/REJECTED) */ data class VerifySelfUiState( val currentStep: VerifyStep = VerifyStep.MASTER_KEY_AUTH, @@ -22,10 +18,7 @@ data class VerifySelfUiState( val isLoading: Boolean = false, val verifyError: VerifyErrorType? = null, val verifiedReceiverId: Long? = null, - val verifiedSenderName: String? = null, - val isSubmitting: Boolean = false, - val submitError: VerifyErrorType? = null, - val deliveryVerificationStatus: DeliveryVerificationStatus? = null + val verifiedSenderName: String? = null ) /** diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfViewModel.kt index 2bfab0a2..7496a5ff 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfViewModel.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfViewModel.kt @@ -4,9 +4,6 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kuit.afternote.data.remote.ApiException -import com.kuit.afternote.feature.receiverauth.domain.usecase.GetDeliveryVerificationStatusUseCase -import com.kuit.afternote.feature.receiverauth.domain.usecase.SubmitDeliveryVerificationUseCase -import com.kuit.afternote.feature.receiverauth.domain.usecase.UploadReceiverDocumentUseCase import com.kuit.afternote.feature.receiverauth.domain.usecase.VerifyReceiverAuthUseCase import com.kuit.afternote.feature.receiverauth.uimodel.VerifyStep import dagger.hilt.android.lifecycle.HiltViewModel @@ -27,10 +24,7 @@ import javax.inject.Inject class VerifySelfViewModel @Inject constructor( - private val verifyReceiverAuthUseCase: VerifyReceiverAuthUseCase, - private val uploadReceiverDocumentUseCase: UploadReceiverDocumentUseCase, - private val submitDeliveryVerificationUseCase: SubmitDeliveryVerificationUseCase, - private val getDeliveryVerificationStatusUseCase: GetDeliveryVerificationStatusUseCase + private val verifyReceiverAuthUseCase: VerifyReceiverAuthUseCase ) : ViewModel(), VerifySelfViewModelContract { private val _uiState = MutableStateFlow(VerifySelfUiState()) @@ -115,86 +109,6 @@ class VerifySelfViewModel _uiState.update { it.copy(verifyError = null) } } - /** - * 증빙 서류(사망진단서, 가족관계증명서) 업로드 후 delivery-verification 제출. - * - * 두 URI가 모두 있을 때만 업로드·제출하고, 성공 시 END 단계로 이동한 뒤 인증 상태를 조회합니다. - */ - override fun submitDocuments(deathCertUri: String?, familyCertUri: String?) { - if (deathCertUri.isNullOrBlank() || familyCertUri.isNullOrBlank()) { - _uiState.update { - it.copy(submitError = VerifyErrorType.Required) - } - return - } - val authCode = _uiState.value.masterKeyInput.trim() - if (authCode.isBlank()) { - _uiState.update { it.copy(submitError = VerifyErrorType.Unknown) } - return - } - viewModelScope.launch { - _uiState.update { - it.copy(isSubmitting = true, submitError = null) - } - val deathUrlResult = uploadReceiverDocumentUseCase(authCode, deathCertUri) - val deathUrl = deathUrlResult.getOrElse { e -> - Log.e(TAG, "Upload death certificate failed", e) - _uiState.update { - it.copy( - isSubmitting = false, - submitError = mapVerifyErrorToType(e) - ) - } - return@launch - } - val familyUrlResult = uploadReceiverDocumentUseCase(authCode, familyCertUri) - val familyUrl = familyUrlResult.getOrElse { e -> - Log.e(TAG, "Upload family relation certificate failed", e) - _uiState.update { - it.copy( - isSubmitting = false, - submitError = mapVerifyErrorToType(e) - ) - } - return@launch - } - submitDeliveryVerificationUseCase(authCode, deathUrl, familyUrl) - .onSuccess { - _uiState.update { - it.copy( - currentStep = VerifyStep.END, - isSubmitting = false, - submitError = null - ) - } - loadDeliveryVerificationStatus(authCode) - } - .onFailure { e -> - Log.e(TAG, "Submit delivery verification failed", e) - _uiState.update { - it.copy( - isSubmitting = false, - submitError = mapVerifyErrorToType(e) - ) - } - } - } - } - - private fun loadDeliveryVerificationStatus(authCode: String) { - viewModelScope.launch { - getDeliveryVerificationStatusUseCase(authCode) - .onSuccess { status -> - _uiState.update { - it.copy(deliveryVerificationStatus = status) - } - } - .onFailure { e -> - Log.e(TAG, "Load delivery verification status failed", e) - } - } - } - private fun mapVerifyErrorToType(e: Throwable): VerifyErrorType { return when (e) { is ApiException -> e.message?.takeIf { it.isNotBlank() }?.let { VerifyErrorType.Server(it) } diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfViewModelContract.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfViewModelContract.kt index 15064706..8b08f189 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfViewModelContract.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfViewModelContract.kt @@ -17,5 +17,4 @@ interface VerifySelfViewModelContract { fun goToNextStep() fun goToPreviousStep(): VerifyStep? fun clearVerifyError() - fun submitDocuments(deathCertUri: String?, familyCertUri: String?) } diff --git a/app/src/main/java/com/kuit/afternote/feature/receiverauth/screen/VerifySelfScreen.kt b/app/src/main/java/com/kuit/afternote/feature/receiverauth/screen/VerifySelfScreen.kt index b3daf147..576c744f 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiverauth/screen/VerifySelfScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiverauth/screen/VerifySelfScreen.kt @@ -7,15 +7,12 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.ui.Alignment import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -36,7 +33,6 @@ import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.kuit.afternote.R -import com.kuit.afternote.core.ui.component.button.ClickButton import com.kuit.afternote.core.ui.component.button.SignUpContentButton import com.kuit.afternote.core.ui.component.navigation.TopBar import com.kuit.afternote.feature.receiverauth.component.MasterKeyInputContent @@ -140,12 +136,9 @@ fun VerifySelfScreen( VerifyStep.UPLOAD_PDF_AUTH -> VerifySelfPdfStep( deathCertificate = deathCertificate, familyRelationCertificate = familyRelationCertificate, - deathCertificateUriState = deathCertificateUriState, - familyCertificateUriState = familyCertificateUriState, pendingDocumentTargetState = pendingDocumentTargetState, imagePickerLauncher = imagePickerLauncher, filePickerLauncher = filePickerLauncher, - uiState = uiState, viewModel = viewModel ) VerifyStep.END -> VerifySelfEndStep( @@ -200,19 +193,15 @@ private fun VerifySelfMasterKeyStep( private fun VerifySelfPdfStep( deathCertificate: TextFieldState, familyRelationCertificate: TextFieldState, - deathCertificateUriState: MutableState, - familyCertificateUriState: MutableState, pendingDocumentTargetState: MutableState, imagePickerLauncher: ActivityResultLauncher, filePickerLauncher: ActivityResultLauncher, - uiState: VerifySelfUiState, viewModel: VerifySelfViewModelContract ) { - val deathUri = deathCertificateUriState.value - val familyUri = familyCertificateUriState.value - val bothSelected = deathUri != null && familyUri != null - - Column(modifier = Modifier.fillMaxSize()) { + SignUpContentButton( + onNextClick = { viewModel.goToNextStep() }, + contentSpacing = 64.dp + ) { PdfInputContent( deadPdf = deathCertificate, familyPdf = familyRelationCertificate, @@ -241,45 +230,6 @@ private fun VerifySelfPdfStep( filePickerLauncher.launch("*/*") } ) - if (uiState.submitError != null) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = when (val err = uiState.submitError) { - is VerifyErrorType.Required -> - stringResource(R.string.receiver_verify_documents_required) - is VerifyErrorType.Network -> - stringResource(R.string.receiver_verify_error_network) - is VerifyErrorType.Server -> err.message - VerifyErrorType.Unknown -> - stringResource(R.string.receiver_verify_error_unknown) - null -> "" - }, - color = Red - ) - } - if (uiState.isSubmitting) { - Spacer(modifier = Modifier.height(16.dp)) - CircularProgressIndicator( - modifier = Modifier.padding(16.dp), - color = B3 - ) - } - Spacer(modifier = Modifier.height(64.dp)) - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - ClickButton( - title = stringResource(R.string.receiver_verify_next_button), - onButtonClick = { - viewModel.submitDocuments( - deathUri?.toString(), - familyUri?.toString() - ) - }, - isTrue = bothSelected - ) - } } } @@ -297,9 +247,7 @@ private fun VerifySelfEndStep( }, buttonTitle = stringResource(R.string.receiver_verify_complete_confirm) ) { - ReceiveEndContent( - deliveryVerificationStatus = uiState.deliveryVerificationStatus - ) + ReceiveEndContent() } } @@ -333,9 +281,6 @@ private class FakeVerifySelfViewModel : VerifySelfViewModelContract { override fun clearVerifyError() { // Fake: no-op for Preview. } - override fun submitDocuments(deathCertUri: String?, familyCertUri: String?) { - // Fake: no-op for Preview. - } } @Preview(showBackground = true) diff --git a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt index 0784b127..e2929db9 100644 --- a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt +++ b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/navgraph/SettingNavGraph.kt @@ -392,7 +392,7 @@ private fun ReceiverAfternoteListRouteContent( val (stringResId, iconResId) = getAfternoteDisplayRes(item.sourceType) val serviceName = stringResource(stringResId) AfternoteListDisplayItem( - id = item.id.toString(), + id = item.sourceType, serviceName = serviceName, date = item.lastUpdatedAt, iconResId = iconResId diff --git a/app/src/main/java/com/kuit/afternote/feature/timeletter/data/dto/TimeLetterDto.kt b/app/src/main/java/com/kuit/afternote/feature/timeletter/data/dto/TimeLetterDto.kt index 3dad22eb..3cc82765 100644 --- a/app/src/main/java/com/kuit/afternote/feature/timeletter/data/dto/TimeLetterDto.kt +++ b/app/src/main/java/com/kuit/afternote/feature/timeletter/data/dto/TimeLetterDto.kt @@ -53,7 +53,6 @@ data class TimeLetterResponse( @SerialName("sendAt") val sendAt: String? = null, @SerialName("status") val status: TimeLetterStatus, @SerialName("mediaList") val mediaList: List? = null, - val receiverIds: List = emptyList(), @SerialName("createdAt") val createdAt: String? = null, @SerialName("updatedAt") val updatedAt: String? = null ) diff --git a/app/src/main/java/com/kuit/afternote/feature/timeletter/data/mapper/TimeLetterMapper.kt b/app/src/main/java/com/kuit/afternote/feature/timeletter/data/mapper/TimeLetterMapper.kt index 4dcff412..836237c6 100644 --- a/app/src/main/java/com/kuit/afternote/feature/timeletter/data/mapper/TimeLetterMapper.kt +++ b/app/src/main/java/com/kuit/afternote/feature/timeletter/data/mapper/TimeLetterMapper.kt @@ -46,7 +46,7 @@ object TimeLetterMapper { mediaList = dto.mediaList?.map { toDomainMedia(it) } ?: emptyList(), createdAt = dto.createdAt, updatedAt = dto.updatedAt, - receiverIds = dto.receiverIds + receiverIds = emptyList() ) fun toTimeLetterList(dto: TimeLetterListResponse): TimeLetterList = diff --git a/app/src/main/java/com/kuit/afternote/feature/timeletter/presentation/screen/TimeLetterWriterScreen.kt b/app/src/main/java/com/kuit/afternote/feature/timeletter/presentation/screen/TimeLetterWriterScreen.kt index 9064d910..c16a0d80 100644 --- a/app/src/main/java/com/kuit/afternote/feature/timeletter/presentation/screen/TimeLetterWriterScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/timeletter/presentation/screen/TimeLetterWriterScreen.kt @@ -221,9 +221,9 @@ fun TimeLetterWriterScreen( navigationIcon = { IconButton(onClick = { keyboardController?.hide() }) { Image( - painter = painterResource(R.drawable.ic_arrow_back), - contentDescription = "뒤로가기", - modifier = Modifier.size(width = 6.dp, height = 12.dp) + painter = painterResource(R.drawable.img_writing), + contentDescription = "키보드 숨기기", + modifier = Modifier.size(width = 20.dp, height = 22.dp) ) } }, diff --git a/app/src/main/java/com/kuit/afternote/feature/user/data/api/UserApiService.kt b/app/src/main/java/com/kuit/afternote/feature/user/data/api/UserApiService.kt index 3445e53e..277c3ea4 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/data/api/UserApiService.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/data/api/UserApiService.kt @@ -1,8 +1,6 @@ package com.kuit.afternote.feature.user.data.api import com.kuit.afternote.data.remote.ApiResponse -import com.kuit.afternote.feature.user.data.dto.DeliveryConditionRequestDto -import com.kuit.afternote.feature.user.data.dto.DeliveryConditionResponseDto import com.kuit.afternote.feature.user.data.dto.ReceiverDailyQuestionsResponseDto import com.kuit.afternote.feature.user.data.dto.ReceiverDetailResponseDto import com.kuit.afternote.feature.user.data.dto.ReceiverItemDto @@ -103,25 +101,4 @@ interface UserApiService { @Query("page") page: Int, @Query("size") size: Int ): ApiResponse - - /** - * 전달 조건 조회. - * GET /users/delivery-condition — 로그인한 사용자의 전달 조건 설정을 조회합니다. - * - * @return data: conditionType, inactivityPeriodDays, specificDate, conditionFulfilled, conditionMet - */ - @GET("users/delivery-condition") - suspend fun getDeliveryCondition(): ApiResponse - - /** - * 전달 조건 수정. - * PATCH /users/delivery-condition — 로그인한 사용자의 전달 조건을 설정하거나 변경합니다. - * - * @param body conditionType, inactivityPeriodDays, specificDate - * @return data: 동일한 DeliveryConditionResponseDto - */ - @PATCH("users/delivery-condition") - suspend fun updateDeliveryCondition( - @Body body: DeliveryConditionRequestDto - ): ApiResponse } diff --git a/app/src/main/java/com/kuit/afternote/feature/user/data/dto/UserDto.kt b/app/src/main/java/com/kuit/afternote/feature/user/data/dto/UserDto.kt index aea500e8..70a54b00 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/data/dto/UserDto.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/data/dto/UserDto.kt @@ -93,41 +93,6 @@ data class ReceiverDailyQuestionsResponseDto( val hasNext: Boolean ) -// --- GET /users/delivery-condition, PATCH /users/delivery-condition (전달 조건) --- - -/** - * 전달 조건 타입 - 콘텐츠가 수신자에게 전달되는 조건. - */ -@Serializable -enum class DeliveryConditionTypeDto { - NONE, - DEATH_CERTIFICATE, - INACTIVITY, - SPECIFIC_DATE -} - -/** - * GET /users/delivery-condition 응답 data. 전달 조건 설정 응답. - */ -@Serializable -data class DeliveryConditionResponseDto( - val conditionType: DeliveryConditionTypeDto, - val inactivityPeriodDays: Int? = null, - val specificDate: String? = null, - val conditionFulfilled: Boolean, - val conditionMet: Boolean -) - -/** - * PATCH /users/delivery-condition 요청 body. - */ -@Serializable -data class DeliveryConditionRequestDto( - val conditionType: DeliveryConditionTypeDto, - val inactivityPeriodDays: Int? = null, - val specificDate: String? = null -) - // --- File API (POST /files/presigned-url) --- /** Request for S3 presigned URL. directory: e.g. profiles, afternotes. extension: e.g. jpg, png. */ diff --git a/app/src/main/java/com/kuit/afternote/feature/user/data/mapper/UserMapper.kt b/app/src/main/java/com/kuit/afternote/feature/user/data/mapper/UserMapper.kt index f0e6e809..86689727 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/data/mapper/UserMapper.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/data/mapper/UserMapper.kt @@ -1,18 +1,13 @@ package com.kuit.afternote.feature.user.data.mapper import com.kuit.afternote.feature.user.data.dto.DailyQuestionAnswerItemDto -import com.kuit.afternote.feature.user.data.dto.DeliveryConditionRequestDto -import com.kuit.afternote.feature.user.data.dto.DeliveryConditionResponseDto -import com.kuit.afternote.feature.user.data.dto.DeliveryConditionTypeDto import com.kuit.afternote.feature.user.data.dto.ReceiverDetailResponseDto import com.kuit.afternote.feature.user.data.dto.ReceiverItemDto import com.kuit.afternote.feature.user.data.dto.UserPushSettingResponse import com.kuit.afternote.feature.user.data.dto.UserResponse import com.kuit.afternote.feature.user.domain.model.DailyQuestionAnswerItem -import com.kuit.afternote.feature.user.domain.model.DeliveryCondition -import com.kuit.afternote.feature.user.domain.model.DeliveryConditionType -import com.kuit.afternote.feature.user.domain.model.PushSettings import com.kuit.afternote.feature.user.domain.model.ReceiverDailyQuestionsResult +import com.kuit.afternote.feature.user.domain.model.PushSettings import com.kuit.afternote.feature.user.domain.model.ReceiverDetail import com.kuit.afternote.feature.user.domain.model.ReceiverListItem import com.kuit.afternote.feature.user.domain.model.UserProfile @@ -68,40 +63,4 @@ object UserMapper { hasNext: Boolean ): ReceiverDailyQuestionsResult = ReceiverDailyQuestionsResult(items = items, hasNext = hasNext) - - fun toDeliveryCondition(dto: DeliveryConditionResponseDto): DeliveryCondition = - DeliveryCondition( - conditionType = dto.conditionType.toDomain(), - inactivityPeriodDays = dto.inactivityPeriodDays, - specificDate = dto.specificDate, - conditionFulfilled = dto.conditionFulfilled, - conditionMet = dto.conditionMet - ) - - fun toDeliveryConditionRequestDto( - conditionType: DeliveryConditionType, - inactivityPeriodDays: Int?, - specificDate: String? - ): DeliveryConditionRequestDto = - DeliveryConditionRequestDto( - conditionType = conditionType.toDto(), - inactivityPeriodDays = inactivityPeriodDays, - specificDate = specificDate - ) - - private fun DeliveryConditionTypeDto.toDomain(): DeliveryConditionType = - when (this) { - DeliveryConditionTypeDto.NONE -> DeliveryConditionType.NONE - DeliveryConditionTypeDto.DEATH_CERTIFICATE -> DeliveryConditionType.DEATH_CERTIFICATE - DeliveryConditionTypeDto.INACTIVITY -> DeliveryConditionType.INACTIVITY - DeliveryConditionTypeDto.SPECIFIC_DATE -> DeliveryConditionType.SPECIFIC_DATE - } - - private fun DeliveryConditionType.toDto(): DeliveryConditionTypeDto = - when (this) { - DeliveryConditionType.NONE -> DeliveryConditionTypeDto.NONE - DeliveryConditionType.DEATH_CERTIFICATE -> DeliveryConditionTypeDto.DEATH_CERTIFICATE - DeliveryConditionType.INACTIVITY -> DeliveryConditionTypeDto.INACTIVITY - DeliveryConditionType.SPECIFIC_DATE -> DeliveryConditionTypeDto.SPECIFIC_DATE - } } diff --git a/app/src/main/java/com/kuit/afternote/feature/user/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/kuit/afternote/feature/user/data/repository/UserRepositoryImpl.kt index c021f727..4f86a6de 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/data/repository/UserRepositoryImpl.kt @@ -8,8 +8,6 @@ import com.kuit.afternote.feature.user.data.dto.RegisterReceiverRequestDto import com.kuit.afternote.feature.user.data.dto.UserUpdateProfileRequest import com.kuit.afternote.feature.user.data.dto.UserUpdatePushSettingRequest import com.kuit.afternote.feature.user.data.mapper.UserMapper -import com.kuit.afternote.feature.user.domain.model.DeliveryCondition -import com.kuit.afternote.feature.user.domain.model.DeliveryConditionType import com.kuit.afternote.feature.user.domain.model.PushSettings import com.kuit.afternote.feature.user.domain.model.ReceiverDailyQuestionsResult import com.kuit.afternote.feature.user.domain.model.ReceiverDetail @@ -152,31 +150,6 @@ class UserRepositoryImpl UserMapper.toReceiverDailyQuestionsResult(items = items, hasNext = body.hasNext) } - override suspend fun getDeliveryCondition(): Result = - runCatching { - Log.d(TAG, "getDeliveryCondition: request") - val response = api.getDeliveryCondition() - Log.d(TAG, "getDeliveryCondition: response=$response") - UserMapper.toDeliveryCondition(response.requireData()) - } - - override suspend fun updateDeliveryCondition( - conditionType: DeliveryConditionType, - inactivityPeriodDays: Int?, - specificDate: String? - ): Result = - runCatching { - Log.d(TAG, "updateDeliveryCondition: conditionType=$conditionType") - val body = UserMapper.toDeliveryConditionRequestDto( - conditionType = conditionType, - inactivityPeriodDays = inactivityPeriodDays, - specificDate = specificDate - ) - val response = api.updateDeliveryCondition(body) - Log.d(TAG, "updateDeliveryCondition: response=$response") - UserMapper.toDeliveryCondition(response.requireData()) - } - companion object { private const val TAG = "UserRepositoryImpl" } diff --git a/app/src/main/java/com/kuit/afternote/feature/user/domain/model/UserModel.kt b/app/src/main/java/com/kuit/afternote/feature/user/domain/model/UserModel.kt index 0ae293f3..a80a9ce2 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/domain/model/UserModel.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/domain/model/UserModel.kt @@ -48,22 +48,3 @@ data class ReceiverDailyQuestionsResult( val items: List, val hasNext: Boolean ) - -// --- Delivery condition (GET/PATCH /users/delivery-condition) --- - -/** 전달 조건 타입 - 콘텐츠가 수신자에게 전달되는 조건. */ -enum class DeliveryConditionType { - NONE, - DEATH_CERTIFICATE, - INACTIVITY, - SPECIFIC_DATE -} - -/** 전달 조건 설정 (조회/수정 응답). */ -data class DeliveryCondition( - val conditionType: DeliveryConditionType, - val inactivityPeriodDays: Int?, - val specificDate: String?, - val conditionFulfilled: Boolean, - val conditionMet: Boolean -) diff --git a/app/src/main/java/com/kuit/afternote/feature/user/domain/repository/UserRepository.kt b/app/src/main/java/com/kuit/afternote/feature/user/domain/repository/UserRepository.kt index d0b98db0..22788005 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/domain/repository/UserRepository.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/domain/repository/UserRepository.kt @@ -1,7 +1,5 @@ package com.kuit.afternote.feature.user.domain.repository -import com.kuit.afternote.feature.user.domain.model.DeliveryCondition -import com.kuit.afternote.feature.user.domain.model.DeliveryConditionType import com.kuit.afternote.feature.user.domain.model.PushSettings import com.kuit.afternote.feature.user.domain.model.ReceiverDetail import com.kuit.afternote.feature.user.domain.model.ReceiverDailyQuestionsResult @@ -48,22 +46,4 @@ interface UserRepository { page: Int, size: Int ): Result - - /** - * GET /users/delivery-condition — 로그인한 사용자의 전달 조건 설정 조회. - */ - suspend fun getDeliveryCondition(): Result - - /** - * PATCH /users/delivery-condition — 로그인한 사용자의 전달 조건 설정/변경. - * - * @param conditionType 전달 조건 타입 - * @param inactivityPeriodDays 비활동 기간(일), INACTIVITY일 때 사용 - * @param specificDate 특정 날짜(yyyy-MM-dd), SPECIFIC_DATE일 때 사용 - */ - suspend fun updateDeliveryCondition( - conditionType: DeliveryConditionType, - inactivityPeriodDays: Int?, - specificDate: String? - ): Result } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index faf93151..e44c6f92 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,18 +74,6 @@ 네트워크 연결을 확인해주세요. 마스터 키 검증에 실패했습니다. 확인 - 사망진단서와 가족관계증명서를 모두 업로드해주세요. - 열람 요청 완료 - 현재 서류 확인중입니다.\n빠르게 처리하여 열람하실 수 있도록 하겠습니다. - 승인되었습니다. 열람이 가능합니다. - 제출하신 서류 검토 결과 거절되었습니다. 다시 제출해 주세요. - 증빙 서류 업로드 - 사망진단서와 가족관계증명서를 업로드 해주세요.\n담당자의 확인 후 승인됩니다. - 사망진단서 업로드 - 가족관계증명서 업로드 - 서류 촬영 또는 파일 첨부 - 이미지 추가 - 파일 추가 카카오로 로그인 카카오 토큰을 발급받았습니다. 서버 연동(/auth/kakao) 대기 중입니다. diff --git a/app/src/test/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditViewModelTest.kt b/app/src/test/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditViewModelTest.kt index 39c52b87..49f83efe 100644 --- a/app/src/test/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditViewModelTest.kt +++ b/app/src/test/java/com/kuit/afternote/feature/afternote/presentation/screen/AfternoteEditViewModelTest.kt @@ -6,8 +6,6 @@ import com.kuit.afternote.feature.afternote.domain.usecase.CreatePlaylistAfterno import com.kuit.afternote.feature.afternote.domain.usecase.CreateSocialAfternoteUseCase import com.kuit.afternote.feature.afternote.domain.usecase.GetAfternoteDetailUseCase import com.kuit.afternote.feature.afternote.domain.usecase.UpdateAfternoteUseCase -import com.kuit.afternote.feature.afternote.domain.usecase.UploadMemorialThumbnailUseCase -import com.kuit.afternote.feature.afternote.domain.usecase.UploadMemorialVideoUseCase import com.kuit.afternote.feature.user.domain.usecase.GetReceiversUseCase import com.kuit.afternote.feature.user.domain.usecase.GetUserIdUseCase import com.kuit.afternote.util.MainCoroutineRule @@ -40,8 +38,6 @@ class AfternoteEditViewModelTest { private lateinit var getDetailUseCase: GetAfternoteDetailUseCase private lateinit var getReceiversUseCase: GetReceiversUseCase private lateinit var getUserIdUseCase: GetUserIdUseCase - private lateinit var uploadMemorialThumbnailUseCase: UploadMemorialThumbnailUseCase - private lateinit var uploadMemorialVideoUseCase: UploadMemorialVideoUseCase private lateinit var viewModel: AfternoteEditViewModel private val socialPayload = @@ -66,8 +62,6 @@ class AfternoteEditViewModelTest { getDetailUseCase = mockk() getReceiversUseCase = mockk() getUserIdUseCase = mockk() - uploadMemorialThumbnailUseCase = mockk() - uploadMemorialVideoUseCase = mockk() coEvery { getUserIdUseCase() } returns null viewModel = AfternoteEditViewModel( @@ -77,9 +71,7 @@ class AfternoteEditViewModelTest { updateUseCase = updateUseCase, getDetailUseCase = getDetailUseCase, getReceiversUseCase = getReceiversUseCase, - getUserIdUseCase = getUserIdUseCase, - uploadMemorialThumbnailUseCase = uploadMemorialThumbnailUseCase, - uploadMemorialVideoUseCase = uploadMemorialVideoUseCase + getUserIdUseCase = getUserIdUseCase ) } diff --git a/app/src/test/java/com/kuit/afternote/feature/receiverauth/domain/usecase/GetDeliveryVerificationStatusUseCaseTest.kt b/app/src/test/java/com/kuit/afternote/feature/receiverauth/domain/usecase/GetDeliveryVerificationStatusUseCaseTest.kt deleted file mode 100644 index a654a235..00000000 --- a/app/src/test/java/com/kuit/afternote/feature/receiverauth/domain/usecase/GetDeliveryVerificationStatusUseCaseTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.domain.usecase - -import com.kuit.afternote.feature.receiverauth.domain.entity.DeliveryVerificationStatus -import com.kuit.afternote.feature.receiverauth.domain.repository.iface.GetDeliveryVerificationStatusRepository -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test - -/** - * [GetDeliveryVerificationStatusUseCase] 단위 테스트. - */ -class GetDeliveryVerificationStatusUseCaseTest { - - private lateinit var getDeliveryVerificationStatusRepository: GetDeliveryVerificationStatusRepository - private lateinit var useCase: GetDeliveryVerificationStatusUseCase - - @Before - fun setUp() { - getDeliveryVerificationStatusRepository = mockk() - useCase = GetDeliveryVerificationStatusUseCase(getDeliveryVerificationStatusRepository) - } - - @Test - fun invoke_whenSuccess_returnsDeliveryVerificationStatus() = runTest { - val expected = DeliveryVerificationStatus( - id = 1L, - status = "PENDING", - adminNote = null, - createdAt = "2025-01-01T00:00:00" - ) - coEvery { getDeliveryVerificationStatusRepository.getStatus(any()) } returns Result.success(expected) - - val result = useCase("auth-code") - - assertTrue(result.isSuccess) - assertEquals(expected, result.getOrNull()) - coVerify(exactly = 1) { getDeliveryVerificationStatusRepository.getStatus("auth-code") } - } - - @Test - fun invoke_whenFailure_returnsFailure() = runTest { - coEvery { getDeliveryVerificationStatusRepository.getStatus(any()) } returns - Result.failure(RuntimeException("Network error")) - - val result = useCase("auth-code") - - assertTrue(result.isFailure) - } -} diff --git a/app/src/test/java/com/kuit/afternote/feature/receiverauth/domain/usecase/SubmitDeliveryVerificationUseCaseTest.kt b/app/src/test/java/com/kuit/afternote/feature/receiverauth/domain/usecase/SubmitDeliveryVerificationUseCaseTest.kt deleted file mode 100644 index f43114a6..00000000 --- a/app/src/test/java/com/kuit/afternote/feature/receiverauth/domain/usecase/SubmitDeliveryVerificationUseCaseTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.domain.usecase - -import com.kuit.afternote.feature.receiverauth.domain.repository.iface.SubmitDeliveryVerificationRepository -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test - -/** - * [SubmitDeliveryVerificationUseCase] 단위 테스트. - */ -class SubmitDeliveryVerificationUseCaseTest { - - private lateinit var submitDeliveryVerificationRepository: SubmitDeliveryVerificationRepository - private lateinit var useCase: SubmitDeliveryVerificationUseCase - - @Before - fun setUp() { - submitDeliveryVerificationRepository = mockk() - useCase = SubmitDeliveryVerificationUseCase(submitDeliveryVerificationRepository) - } - - @Test - fun invoke_whenSuccess_returnsSuccess() = runTest { - coEvery { - submitDeliveryVerificationRepository.submit(any(), any(), any()) - } returns Result.success(Unit) - - val result = useCase("auth-code", "https://death.pdf", "https://family.pdf") - - assertTrue(result.isSuccess) - coVerify(exactly = 1) { - submitDeliveryVerificationRepository.submit( - "auth-code", - "https://death.pdf", - "https://family.pdf" - ) - } - } - - @Test - fun invoke_whenFailure_returnsFailure() = runTest { - coEvery { - submitDeliveryVerificationRepository.submit(any(), any(), any()) - } returns Result.failure(RuntimeException("Server error")) - - val result = useCase("auth-code", "url1", "url2") - - assertTrue(result.isFailure) - } -} diff --git a/app/src/test/java/com/kuit/afternote/feature/receiverauth/domain/usecase/UploadReceiverDocumentUseCaseTest.kt b/app/src/test/java/com/kuit/afternote/feature/receiverauth/domain/usecase/UploadReceiverDocumentUseCaseTest.kt deleted file mode 100644 index 4f6e73d4..00000000 --- a/app/src/test/java/com/kuit/afternote/feature/receiverauth/domain/usecase/UploadReceiverDocumentUseCaseTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.kuit.afternote.feature.receiverauth.domain.usecase - -import com.kuit.afternote.feature.receiverauth.domain.repository.iface.UploadReceiverDocumentRepository -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test - -/** - * [UploadReceiverDocumentUseCase] 단위 테스트. - */ -class UploadReceiverDocumentUseCaseTest { - - private lateinit var uploadReceiverDocumentRepository: UploadReceiverDocumentRepository - private lateinit var useCase: UploadReceiverDocumentUseCase - - @Before - fun setUp() { - uploadReceiverDocumentRepository = mockk() - useCase = UploadReceiverDocumentUseCase(uploadReceiverDocumentRepository) - } - - @Test - fun invoke_whenSuccess_returnsFileUrl() = runTest { - val authCode = "auth-123" - val uriString = "content://uri" - val fileUrl = "https://bucket.s3.amazonaws.com/receiver-docs/abc.pdf" - coEvery { - uploadReceiverDocumentRepository.uploadDocument(authCode, uriString) - } returns Result.success(fileUrl) - - val result = useCase(authCode, uriString) - - assertTrue(result.isSuccess) - assertEquals(fileUrl, result.getOrNull()) - coVerify(exactly = 1) { - uploadReceiverDocumentRepository.uploadDocument(authCode, uriString) - } - } - - @Test - fun invoke_whenFailure_returnsFailure() = runTest { - coEvery { - uploadReceiverDocumentRepository.uploadDocument(any(), any()) - } returns Result.failure(RuntimeException("Upload failed")) - - val result = useCase("code", "uri") - - assertTrue(result.isFailure) - assertEquals("Upload failed", result.exceptionOrNull()?.message) - } -} diff --git a/app/src/test/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfViewModelTest.kt b/app/src/test/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfViewModelTest.kt index 6c92911c..4868cf1b 100644 --- a/app/src/test/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfViewModelTest.kt +++ b/app/src/test/java/com/kuit/afternote/feature/receiverauth/presentation/viewmodel/VerifySelfViewModelTest.kt @@ -1,10 +1,6 @@ package com.kuit.afternote.feature.receiverauth.presentation.viewmodel -import com.kuit.afternote.feature.receiverauth.domain.entity.DeliveryVerificationStatus import com.kuit.afternote.feature.receiverauth.domain.entity.ReceiverAuthVerifyResult -import com.kuit.afternote.feature.receiverauth.domain.usecase.GetDeliveryVerificationStatusUseCase -import com.kuit.afternote.feature.receiverauth.domain.usecase.SubmitDeliveryVerificationUseCase -import com.kuit.afternote.feature.receiverauth.domain.usecase.UploadReceiverDocumentUseCase import com.kuit.afternote.feature.receiverauth.domain.usecase.VerifyReceiverAuthUseCase import com.kuit.afternote.feature.receiverauth.uimodel.VerifyStep import com.kuit.afternote.util.MainCoroutineRule @@ -32,23 +28,12 @@ class VerifySelfViewModelTest { val mainRule = MainCoroutineRule() private lateinit var verifyReceiverAuthUseCase: VerifyReceiverAuthUseCase - private lateinit var uploadReceiverDocumentUseCase: UploadReceiverDocumentUseCase - private lateinit var submitDeliveryVerificationUseCase: SubmitDeliveryVerificationUseCase - private lateinit var getDeliveryVerificationStatusUseCase: GetDeliveryVerificationStatusUseCase private lateinit var viewModel: VerifySelfViewModel @Before fun setUp() { verifyReceiverAuthUseCase = mockk() - uploadReceiverDocumentUseCase = mockk() - submitDeliveryVerificationUseCase = mockk() - getDeliveryVerificationStatusUseCase = mockk() - viewModel = VerifySelfViewModel( - verifyReceiverAuthUseCase = verifyReceiverAuthUseCase, - uploadReceiverDocumentUseCase = uploadReceiverDocumentUseCase, - submitDeliveryVerificationUseCase = submitDeliveryVerificationUseCase, - getDeliveryVerificationStatusUseCase = getDeliveryVerificationStatusUseCase - ) + viewModel = VerifySelfViewModel(verifyReceiverAuthUseCase) } @Test @@ -127,74 +112,4 @@ class VerifySelfViewModelTest { assertEquals(VerifyStep.MASTER_KEY_AUTH, previous) assertEquals(VerifyStep.MASTER_KEY_AUTH, viewModel.uiState.value.currentStep) } - - @Test - fun submitDocuments_whenDeathUriNull_setsSubmitErrorRequired() { - viewModel.updateMasterKey("auth-code") - viewModel.submitDocuments(null, "content://family") - - assertTrue(viewModel.uiState.value.submitError is VerifyErrorType.Required) - assertFalse(viewModel.uiState.value.isSubmitting) - } - - @Test - fun submitDocuments_whenFamilyUriNull_setsSubmitErrorRequired() { - viewModel.updateMasterKey("auth-code") - viewModel.submitDocuments("content://death", null) - - assertTrue(viewModel.uiState.value.submitError is VerifyErrorType.Required) - assertFalse(viewModel.uiState.value.isSubmitting) - } - - @Test - fun submitDocuments_whenSuccess_advancesToEndAndLoadsStatus() = runTest { - viewModel.updateMasterKey("auth-code") - coEvery { uploadReceiverDocumentUseCase(any(), any()) } returns - Result.success("https://file1.pdf") - coEvery { submitDeliveryVerificationUseCase(any(), any(), any()) } returns - Result.success(Unit) - val status = DeliveryVerificationStatus( - id = 1L, - status = "PENDING", - adminNote = null, - createdAt = "2025-01-01T00:00:00" - ) - coEvery { getDeliveryVerificationStatusUseCase(any()) } returns Result.success(status) - - viewModel.submitDocuments("content://death", "content://family") - advanceUntilIdle() - - assertEquals(VerifyStep.END, viewModel.uiState.value.currentStep) - assertNull(viewModel.uiState.value.submitError) - assertFalse(viewModel.uiState.value.isSubmitting) - assertEquals(status, viewModel.uiState.value.deliveryVerificationStatus) - } - - @Test - fun submitDocuments_whenUploadFails_setsSubmitError() = runTest { - viewModel.updateMasterKey("auth-code") - coEvery { uploadReceiverDocumentUseCase(any(), any()) } returns - Result.failure(IOException("Network error")) - - viewModel.submitDocuments("content://death", "content://family") - advanceUntilIdle() - - assertTrue(viewModel.uiState.value.submitError is VerifyErrorType.Network) - assertFalse(viewModel.uiState.value.isSubmitting) - } - - @Test - fun submitDocuments_whenSubmitFails_setsSubmitError() = runTest { - viewModel.updateMasterKey("auth-code") - coEvery { uploadReceiverDocumentUseCase(any(), any()) } returns - Result.success("https://file1.pdf") - coEvery { submitDeliveryVerificationUseCase(any(), any(), any()) } returns - Result.failure(com.kuit.afternote.data.remote.ApiException(400, 400, "Invalid URL")) - - viewModel.submitDocuments("content://death", "content://family") - advanceUntilIdle() - - assertTrue(viewModel.uiState.value.submitError is VerifyErrorType.Server) - assertFalse(viewModel.uiState.value.isSubmitting) - } } diff --git a/scripts/get-kakao-swagger-token.py b/scripts/get-kakao-swagger-token.py index ecc6e3cb..859edf0c 100755 --- a/scripts/get-kakao-swagger-token.py +++ b/scripts/get-kakao-swagger-token.py @@ -1,20 +1,14 @@ #!/usr/bin/env python3 """ -Get a backend JWT for Swagger by applying the Kakao token to POST /auth/social/login. +Automation to get a token for Swagger "Available authorizations". Flow: 1. Starts a local server on http://localhost:3000 to catch the OAuth redirect. 2. Opens the Kakao authorize URL in the default browser. 3. You log in with Kakao if prompted; redirect brings the code to this script. -4. Script exchanges the code for a Kakao access_token. -5. Applies that token to POST /auth/social/login with body: - { "provider": "KAKAO", "accessToken": "" } -6. Reads the response accessToken (and optionally refreshToken, newUser). -7. Prints the service JWT accessToken for pasting into Swagger Authorize. - -API: POST /auth/social/login - Request: { "provider": "KAKAO", "accessToken": "..." } - Response: { "status", "code", "message", "data": { "accessToken", "refreshToken", "newUser" } } +4. Script exchanges the code for a Kakao access_token, then calls the backend + POST /auth/social/login to get the token used for Swagger. +5. Prints the backend token (and Kakao access_token) for pasting into Swagger. Usage: python3 scripts/get-kakao-swagger-token.py @@ -25,10 +19,8 @@ import json import urllib.parse import urllib.request -import urllib.error from http.server import HTTPServer, BaseHTTPRequestHandler from threading import Thread -from typing import Tuple import webbrowser # OAuth / backend config (from your flow) @@ -96,11 +88,7 @@ def exchange_code_for_token(code: str) -> str: return data.get("access_token", "") -def backend_social_login(kakao_access_token: str) -> Tuple[dict, int]: - """ - POST /auth/social/login with provider KAKAO and the Kakao accessToken. - Returns (parsed_response_json, http_status_code). - """ +def backend_social_login(kakao_access_token: str) -> dict: body = json.dumps({ "provider": "KAKAO", "accessToken": kakao_access_token, @@ -111,15 +99,8 @@ def backend_social_login(kakao_access_token: str) -> Tuple[dict, int]: method="POST", headers={"Content-Type": "application/json"}, ) - try: - with urllib.request.urlopen(req) as resp: - return json.loads(resp.read().decode()), resp.status - except urllib.error.HTTPError as e: - body = e.read().decode() if e.fp else "" - try: - return json.loads(body), e.code - except json.JSONDecodeError: - return {"message": body, "status": e.code}, e.code + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode()) def main(): @@ -141,39 +122,30 @@ def main(): if not kakao_token: print("Failed to get Kakao access token.") return 1 - print("Calling backend POST /auth/social/login with Kakao accessToken...") + print("Calling backend POST /auth/social/login ...") try: - backend, status = backend_social_login(kakao_token) + backend = backend_social_login(kakao_token) except Exception as e: - print(f"Backend login request failed: {e}") + print(f"Backend login failed: {e}") print("Kakao access_token (for manual use):") print(kakao_token) return 1 - if status != 200: - print(f"Backend returned HTTP {status}:") - print(json.dumps(backend, indent=2)) - return 1 - # Response: { "status", "code", "message", "data": { "accessToken", "refreshToken", "newUser" } } - data = backend.get("data") or {} - access_token = ( + # Common response shapes: { "data": { "accessToken": "..." } } or { "accessToken": "..." } + data = backend.get("data", backend) + swagger_token = ( data.get("accessToken") or data.get("access_token") or backend.get("accessToken") or backend.get("access_token") ) - if access_token: - print("\n--- Service JWT accessToken (for Swagger Authorize) ---") - print(access_token) + if swagger_token: + print("\n--- Token for Swagger (Available authorizations) ---") + print(swagger_token) print("---") - refresh_token = data.get("refreshToken") or data.get("refresh_token") - new_user = data.get("newUser") - if refresh_token or new_user is not None: - print("(refreshToken and newUser are in the response; use accessToken above in Swagger.)") print("In Swagger UI: Authorize → paste the token (or Bearer if required).") else: - print("Backend response (no accessToken in data):") + print("Backend response (no accessToken found):") print(json.dumps(backend, indent=2)) - return 1 return 0 From dfa61835a4dca53fc9b010da365230c3ae373d97 Mon Sep 17 00:00:00 2001 From: hyhyh Date: Wed, 18 Feb 2026 14:16:04 +0900 Subject: [PATCH 7/8] =?UTF-8?q?"api=20=EC=97=B0=EA=B2=B0=20=EC=A4=91"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dailyrecord/domain/model/DailyQuestionData.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/model/DailyQuestionData.kt 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 +) From f49cd3fb34e8dc6abc0acabf965f7ec9d2123cea Mon Sep 17 00:00:00 2001 From: hyhyh Date: Wed, 18 Feb 2026 17:39:40 +0900 Subject: [PATCH 8/8] =?UTF-8?q?"api=20=EC=97=B0=EA=B2=B0=20=EC=A4=91"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dailyrecord/data/dto/DailyRecordDto.kt | 15 +++++++++------ .../presentation/viewmodel/MindRecordViewModel.kt | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) 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 3098af33..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,21 +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, - /** 기록 내용 (목록 API에서 제공 시). 없으면 null */ - val content: String? = null + /** 기록 내용 (API: content 또는 answer). DAILY_QUESTION은 answer로 반환될 수 있음. */ + @JsonNames("answer", "content") val content: String? = null ) /** 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 ae896e97..72123acf 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 @@ -120,7 +120,7 @@ class MindRecordViewModel @Inject constructor( _records.value = domainRecords.map { summary -> MindRecordUiModel( id = summary.recordId, - title = summary.title, + title = summary.title ?: "", formattedDate = runCatching { formatDate(summary.date) } .getOrElse { summary.date }, draftLabel = if (summary.isDraft) "임시저장" else "완료", @@ -151,7 +151,7 @@ class MindRecordViewModel @Inject constructor( _records.value = sorted.map { summary -> MindRecordUiModel( id = summary.recordId, - title = summary.title, + title = summary.title ?: "", formattedDate = runCatching { formatDate(summary.date) } .getOrElse { summary.date }, draftLabel = if (summary.isDraft) "임시저장" else "완료",