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 134c2d99..e701ac7a 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 @@ -34,17 +34,7 @@ import com.kuit.afternote.R 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.navigation.BottomNavItem -import com.kuit.afternote.core.ui.screen.afternotedetail.GalleryDetailCallbacks -import com.kuit.afternote.core.ui.screen.afternotedetail.GalleryDetailScreen -import com.kuit.afternote.core.ui.screen.afternotedetail.GalleryDetailState -import com.kuit.afternote.core.ui.screen.afternotedetail.MemorialGuidelineDetailCallbacks -import com.kuit.afternote.core.ui.screen.afternotedetail.MemorialGuidelineDetailScreen -import com.kuit.afternote.core.ui.screen.afternotedetail.MemorialGuidelineDetailState -import com.kuit.afternote.core.ui.screen.afternotedetail.SocialNetworkDetailContent -import com.kuit.afternote.core.ui.screen.afternotedetail.SocialNetworkDetailScreen -import com.kuit.afternote.core.ui.screen.afternotedetail.rememberAfternoteDetailState import com.kuit.afternote.core.uimodel.AfternoteListDisplayItem import com.kuit.afternote.feature.afternote.domain.model.AfternoteItem import com.kuit.afternote.feature.afternote.presentation.navgraph.AfternoteEditStateHandling @@ -61,6 +51,7 @@ import com.kuit.afternote.feature.home.presentation.screen.HomeScreen import com.kuit.afternote.feature.home.presentation.screen.HomeScreenEvent import com.kuit.afternote.feature.onboarding.presentation.navgraph.OnboardingRoute import com.kuit.afternote.feature.onboarding.presentation.navgraph.onboardingNavGraph +import com.kuit.afternote.feature.receiver.presentation.navgraph.ReceiverAfternoteDetailRoute 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.ReceiverTimeLetterDetailRoute @@ -218,72 +209,6 @@ private fun ReceiverAfternoteListRouteContent(navHostController: NavHostControll ) } -private enum class ReceiverDetailCategory { GALLERY, MEMORIAL_GUIDELINE, SOCIAL } - -@Composable -private fun ReceiverAfternoteDetailContent( - navHostController: NavHostController, - itemId: String? -) { - val receiverProvider = DataProviderLocals.LocalReceiverDataProvider.current - val seed = - remember(receiverProvider, itemId) { - receiverProvider - .getAfternoteListSeedsForReceiverList() - .firstOrNull { it.id == itemId } - ?: receiverProvider.getAfternoteListSeedsForReceiverList().firstOrNull() - } - val category = receiverDetailCategoryFromSeed(seed) - val serviceName = seed?.serviceNameLiteral ?: "" - val userName = receiverProvider.getDefaultReceiverTitle() - val defaultState = rememberAfternoteDetailState( - defaultBottomNavItem = BottomNavItem.AFTERNOTE - ) - when (category) { - ReceiverDetailCategory.GALLERY -> GalleryDetailScreen( - detailState = GalleryDetailState( - serviceName = serviceName.ifEmpty { "갤러리" }, - userName = userName, - finalWriteDate = seed?.date ?: "" - ), - callbacks = GalleryDetailCallbacks( - onBackClick = { navHostController.popBackStack() }, - onEditClick = {} - ), - isEditable = false, - uiState = defaultState - ) - ReceiverDetailCategory.MEMORIAL_GUIDELINE -> MemorialGuidelineDetailScreen( - detailState = MemorialGuidelineDetailState( - userName = userName, - finalWriteDate = seed?.date ?: "" - ), - callbacks = MemorialGuidelineDetailCallbacks( - onBackClick = { navHostController.popBackStack() } - ), - isEditable = false, - uiState = defaultState - ) - ReceiverDetailCategory.SOCIAL -> SocialNetworkDetailScreen( - content = SocialNetworkDetailContent( - serviceName = serviceName, - userName = userName - ), - isEditable = false, - onBackClick = { navHostController.popBackStack() }, - state = defaultState - ) - } -} - -private fun receiverDetailCategoryFromSeed(seed: AfternoteListItemSeed?): ReceiverDetailCategory { - return when (seed?.serviceNameLiteral) { - "갤러리" -> ReceiverDetailCategory.GALLERY - "추모 가이드라인" -> ReceiverDetailCategory.MEMORIAL_GUIDELINE - else -> ReceiverDetailCategory.SOCIAL - } -} - @Composable private fun HomeScreenContent( onBottomNavTabSelected: (BottomNavItem) -> Unit, @@ -454,7 +379,7 @@ fun NavGraph(navHostController: NavHostController) { } composable("receiver_afternote_detail/{itemId}") { backStackEntry -> - ReceiverAfternoteDetailContent( + ReceiverAfternoteDetailRoute( navHostController = navHostController, itemId = backStackEntry.arguments?.getString("itemId") ) diff --git a/app/src/main/java/com/kuit/afternote/core/ui/component/list/MemorialPlaylist.kt b/app/src/main/java/com/kuit/afternote/core/ui/component/list/MemorialPlaylist.kt index f287fa3a..09f889d7 100644 --- a/app/src/main/java/com/kuit/afternote/core/ui/component/list/MemorialPlaylist.kt +++ b/app/src/main/java/com/kuit/afternote/core/ui/component/list/MemorialPlaylist.kt @@ -128,6 +128,7 @@ private fun MemorialPlaylistAddButton( * 앨범 행: albumCovers를 사용. albumItemContent를 넘기면 해당 슬롯으로 그리며, null이면 각 앨범을 회색 박스로 표시. * * @param onAddSongClick null이면 view 모드(버튼·편집 UI 없음), non-null이면 edit 모드 + * @param onPlaylistClick view 모드에서 카드(오른쪽 화살표 영역 포함) 클릭 시 호출. null이면 클릭 비활성화 * @param albumItemContent 앨범 셀 커스텀; null이면 기본 회색 박스 (view/placeholder용) */ @Composable @@ -137,9 +138,16 @@ fun MemorialPlaylist( songCount: Int = 0, albumCovers: List = emptyList(), onAddSongClick: (() -> Unit)? = null, + onPlaylistClick: (() -> Unit)? = null, albumItemContent: (@Composable (album: AlbumCover, index: Int) -> Unit)? = null ) { val isEditMode = onAddSongClick != null + val cardModifier = when { + !isEditMode && onPlaylistClick != null -> modifier + .fillMaxWidth() + .clickable(onClick = onPlaylistClick) + else -> modifier.fillMaxWidth() + } Column( verticalArrangement = Arrangement.spacedBy(space = 16.dp) ) { @@ -154,8 +162,7 @@ fun MemorialPlaylist( ) ) Column( - modifier = modifier - .fillMaxWidth() + modifier = cardModifier .background(color = White, shape = RoundedCornerShape(size = 16.dp)) .padding(all = 16.dp), verticalArrangement = Arrangement.spacedBy(space = 8.dp) 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 209b0c7d..8f51affd 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 @@ -26,4 +26,14 @@ interface MindRecordRepository { suspend fun editMindRecord(recordId: Long, request: PostMindRecordRequest): MindRecordDetailResponse suspend fun getEmotions(): EmotionResponse + + /** + * PATCH mind-records/{recordId}/receivers/{receiverId} + * 해당 마음의 기록을 해당 수신인에게 전달할지 여부를 설정합니다. + */ + 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 ee97db95..9a6da906 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 @@ -9,6 +9,7 @@ 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 com.kuit.afternote.feature.dailyrecord.domain.model.DailyQuestionData import retrofit2.HttpException import javax.inject.Inject @@ -57,5 +58,20 @@ class MindRecordRepositoryImpl @Inject constructor( override suspend fun getEmotions(): EmotionResponse { return apiService.getEmotions().body()!! } + + override suspend fun setMindRecordReceiverEnabled( + recordId: Long, + receiverId: Long, + enabled: Boolean + ) { + val response = apiService.setMindRecordReceiverEnabled( + recordId = recordId, + receiverId = receiverId, + request = ReceiverEnabledRequest(enabled = enabled) + ) + if (!response.isSuccessful) { + throw HttpException(response) + } + } } diff --git a/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/SetMindRecordReceiverEnabledForAllUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/SetMindRecordReceiverEnabledForAllUseCase.kt new file mode 100644 index 00000000..9a1ec5aa --- /dev/null +++ b/app/src/main/java/com/kuit/afternote/feature/dailyrecord/domain/usecase/SetMindRecordReceiverEnabledForAllUseCase.kt @@ -0,0 +1,35 @@ +package com.kuit.afternote.feature.dailyrecord.domain.usecase + +import com.kuit.afternote.feature.dailyrecord.data.repository.MindRecordRepository +import javax.inject.Inject + +/** + * 수신인에게 "나의 모든 마음의 기록" 전달 여부를 일괄 설정합니다. + * PATCH mind-records/{recordId}/receivers/{receiverId} 를 + * 현재 사용자의 모든 마음의 기록에 대해 호출합니다. + */ +class SetMindRecordReceiverEnabledForAllUseCase + @Inject + constructor( + private val repository: MindRecordRepository + ) { + suspend operator fun invoke(receiverId: Long, enabled: Boolean) { + val recordIds = collectAllMindRecordIds() + recordIds.forEach { recordId -> + repository.setMindRecordReceiverEnabled( + recordId = recordId, + receiverId = receiverId, + enabled = enabled + ) + } + } + + private suspend fun collectAllMindRecordIds(): List { + val types = listOf("DAILY_QUESTION", "DIARY", "DEEP_THOUGHT") + val allIds = types.flatMap { type -> + repository.getMindRecords(type, "LIST", null, null) + .data?.records?.map { it.recordId } ?: emptyList() + } + return allIds.distinct() + } + } diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/navgraph/ReceiverAfternoteDetailRoute.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/navgraph/ReceiverAfternoteDetailRoute.kt new file mode 100644 index 00000000..60321a86 --- /dev/null +++ b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/navgraph/ReceiverAfternoteDetailRoute.kt @@ -0,0 +1,87 @@ +package com.kuit.afternote.feature.receiver.presentation.navgraph + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavHostController +import com.kuit.afternote.app.compositionlocal.DataProviderLocals +import com.kuit.afternote.core.dummy.receiver.AfternoteListItemSeed +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 +import com.kuit.afternote.core.ui.screen.afternotedetail.GalleryDetailState +import com.kuit.afternote.core.ui.screen.afternotedetail.MemorialGuidelineDetailCallbacks +import com.kuit.afternote.core.ui.screen.afternotedetail.MemorialGuidelineDetailScreen +import com.kuit.afternote.core.ui.screen.afternotedetail.MemorialGuidelineDetailState +import com.kuit.afternote.core.ui.screen.afternotedetail.SocialNetworkDetailContent +import com.kuit.afternote.core.ui.screen.afternotedetail.SocialNetworkDetailScreen +import com.kuit.afternote.core.ui.screen.afternotedetail.rememberAfternoteDetailState + +/** + * 수신자 애프터노트 상세 라우트. + * ReceiverDataProvider에서 seed를 조회해 카테고리별 상세 스크린(Gallery/MemorialGuideline/Social)을 표시합니다. + */ +@Composable +fun ReceiverAfternoteDetailRoute( + navHostController: NavHostController, + itemId: String? +) { + val receiverProvider = DataProviderLocals.LocalReceiverDataProvider.current + val seed = + remember(receiverProvider, itemId) { + receiverProvider + .getAfternoteListSeedsForReceiverList() + .firstOrNull { it.id == itemId } + ?: receiverProvider.getAfternoteListSeedsForReceiverList().firstOrNull() + } + val category = receiverDetailCategoryFromSeed(seed) + val serviceName = seed?.serviceNameLiteral ?: "" + val userName = receiverProvider.getDefaultReceiverTitle() + val defaultState = rememberAfternoteDetailState( + defaultBottomNavItem = BottomNavItem.AFTERNOTE + ) + when (category) { + ReceiverDetailCategory.GALLERY -> GalleryDetailScreen( + detailState = GalleryDetailState( + serviceName = serviceName.ifEmpty { "갤러리" }, + userName = userName, + finalWriteDate = seed?.date ?: "" + ), + callbacks = GalleryDetailCallbacks( + onBackClick = { navHostController.popBackStack() }, + onEditClick = {} + ), + isEditable = false, + uiState = defaultState + ) + ReceiverDetailCategory.MEMORIAL_GUIDELINE -> MemorialGuidelineDetailScreen( + detailState = MemorialGuidelineDetailState( + userName = userName, + finalWriteDate = seed?.date ?: "" + ), + callbacks = MemorialGuidelineDetailCallbacks( + onBackClick = { navHostController.popBackStack() } + ), + isEditable = false, + uiState = defaultState + ) + ReceiverDetailCategory.SOCIAL -> SocialNetworkDetailScreen( + content = SocialNetworkDetailContent( + serviceName = serviceName, + userName = userName + ), + isEditable = false, + onBackClick = { navHostController.popBackStack() }, + state = defaultState + ) + } +} + +private enum class ReceiverDetailCategory { GALLERY, MEMORIAL_GUIDELINE, SOCIAL } + +private fun receiverDetailCategoryFromSeed(seed: AfternoteListItemSeed?): ReceiverDetailCategory { + return when (seed?.serviceNameLiteral) { + "갤러리" -> ReceiverDetailCategory.GALLERY + "추모 가이드라인" -> ReceiverDetailCategory.MEMORIAL_GUIDELINE + else -> ReceiverDetailCategory.SOCIAL + } +} 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..91646ea0 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 @@ -22,6 +22,7 @@ import com.kuit.afternote.core.ui.component.navigation.BottomNavItem import com.kuit.afternote.core.ui.component.navigation.BottomNavigationBar import com.kuit.afternote.feature.receiver.domain.entity.ReceivedTimeLetter import com.kuit.afternote.feature.receiver.presentation.screen.ReceiverAfterNoteScreen +import com.kuit.afternote.feature.receiver.presentation.navgraph.ReceiverMemorialPlaylistRoute import com.kuit.afternote.feature.receiver.presentation.screen.afternote.ReceiverAfterNoteMainScreen import com.kuit.afternote.feature.receiver.presentation.screen.mindrecord.MindRecordDetailScreen import com.kuit.afternote.feature.receiver.presentation.screen.mindrecord.MindRecordScreen @@ -77,6 +78,7 @@ fun ReceiverMainRoute( var mindRecordSelectedDate by remember { mutableStateOf(LocalDate.now()) } var showTimeLetterList by remember { mutableStateOf(false) } var timeLetterListMode by remember { mutableStateOf(TimeLetterListMode.SortByDate) } + var showMemorialPlaylist by remember { mutableStateOf(false) } val timeLetterViewModel: ReceiverTimeLetterViewModel = hiltViewModel() val timeLetterUiState by timeLetterViewModel.uiState.collectAsStateWithLifecycle() val afternoteTriggerViewModel: ReceiverAfternoteTriggerViewModel = hiltViewModel() @@ -95,6 +97,9 @@ fun ReceiverMainRoute( selectedBottomNavItem == BottomNavItem.TIME_LETTER && showTimeLetterList -> { showTimeLetterList = false } + selectedBottomNavItem == BottomNavItem.AFTERNOTE && showMemorialPlaylist -> { + showMemorialPlaylist = false + } selectedBottomNavItem == BottomNavItem.HOME -> { receiverAuthSessionHolder.clearAuthCode() navController.popBackStack() @@ -230,15 +235,22 @@ fun ReceiverMainRoute( BottomNavItem.AFTERNOTE -> { val afternoteSenderName = receiverAuthSessionHolder.getSenderName().orEmpty() Log.d(TAG_RECEIVER_MAIN, "AFTERNOTE tab getSenderName=${receiverAuthSessionHolder.getSenderName()}, orEmpty='$afternoteSenderName'") - ReceiverAfterNoteMainScreen( - senderName = afternoteSenderName, - albumCovers = albumCovers, - onNavigateToFullList = { - navController.navigate("receiver_afternote_list") - }, - onBackClick = { selectedBottomNavItem = BottomNavItem.HOME }, - showBottomBar = false - ) + if (showMemorialPlaylist) { + ReceiverMemorialPlaylistRoute( + onBackClick = { showMemorialPlaylist = false } + ) + } else { + ReceiverAfterNoteMainScreen( + senderName = afternoteSenderName, + albumCovers = albumCovers, + onNavigateToFullList = { + navController.navigate("receiver_afternote_list") + }, + onNavigateToPlaylist = { showMemorialPlaylist = true }, + onBackClick = { selectedBottomNavItem = BottomNavItem.HOME }, + showBottomBar = false + ) + } } } } diff --git a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/screen/afternote/ReceiverAfterNoteMainScreen.kt b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/screen/afternote/ReceiverAfterNoteMainScreen.kt index 373701d3..4475f6c6 100644 --- a/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/screen/afternote/ReceiverAfterNoteMainScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/receiver/presentation/screen/afternote/ReceiverAfterNoteMainScreen.kt @@ -54,6 +54,7 @@ import com.kuit.afternote.ui.theme.Sansneo fun ReceiverAfterNoteMainScreen( senderName: String, onNavigateToFullList: () -> Unit = {}, + onNavigateToPlaylist: () -> Unit = {}, onBackClick: () -> Unit = {}, profileImageResId: Int? = null, albumCovers: List, @@ -119,7 +120,8 @@ fun ReceiverAfterNoteMainScreen( label = "추모 플레이리스트", songCount = songCount, albumCovers = albumCovers, - onAddSongClick = null + onAddSongClick = null, + onPlaylistClick = onNavigateToPlaylist ) }, lastWishContent = { 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 index 79b949c9..33ed31e7 100644 --- 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 @@ -4,6 +4,7 @@ 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.GetAfterNotesByAuthCodeUseCase 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 @@ -18,8 +19,8 @@ import javax.inject.Inject /** * 수신자 추모 플레이리스트 화면 ViewModel. * - * GET /api/receiver-auth/after-notes/{afternoteId} (X-Auth-Code)로 상세를 조회한 뒤 - * playlist.songs를 [PlaylistSongDisplay]로 변환하여 표시합니다. + * GET /api/receiver-auth/after-notes (X-Auth-Code)로 목록 조회 후 첫 항목으로 상세를 조회하거나, + * afternoteId가 있으면 해당 ID로 상세를 조회하여 playlist.songs를 [PlaylistSongDisplay]로 표시합니다. */ @HiltViewModel class ReceiverMemorialPlaylistViewModel @@ -27,6 +28,7 @@ class ReceiverMemorialPlaylistViewModel constructor( savedStateHandle: SavedStateHandle, private val receiverAuthSessionHolder: ReceiverAuthSessionHolder, + private val getAfterNotesByAuthCodeUseCase: GetAfterNotesByAuthCodeUseCase, private val getAfternoteDetailByAuthCodeUseCase: GetAfternoteDetailByAuthCodeUseCase ) : ViewModel() { @@ -46,15 +48,36 @@ class ReceiverMemorialPlaylistViewModel ) } } - afternoteId == null -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "애프터노트 정보를 찾을 수 없습니다." - ) + afternoteId != null -> loadPlaylist(authCode = authCode, afternoteId = afternoteId) + else -> resolveFirstAfternoteAndLoad(authCode = authCode) + } + } + + private fun resolveFirstAfternoteAndLoad(authCode: String) { + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + viewModelScope.launch { + getAfterNotesByAuthCodeUseCase(authCode) + .onSuccess { result -> + val firstId = result.items.firstOrNull()?.id + if (firstId == null) { + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "애프터노트 정보를 찾을 수 없습니다." + ) + } + } else { + loadPlaylist(authCode = authCode, afternoteId = firstId) + } + } + .onFailure { e -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = e.message ?: "플레이리스트를 불러오는데 실패했습니다." + ) + } } - } - else -> loadPlaylist(authCode = authCode, afternoteId = afternoteId) } } 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 c832df02..f0c9e6d9 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 @@ -315,7 +315,8 @@ private fun ReceiverDetailRouteContent( emailState = emailState, dailyQuestionCount = detailState.dailyQuestionCount, timeLetterCount = detailState.timeLetterCount, - afternoteCount = detailState.afterNoteCount + afternoteCount = detailState.afterNoteCount, + mindRecordDeliveryEnabled = detailState.mindRecordDeliveryEnabled ), callbacks = ReceiverDetailEditCallbacks( onBackClick = { navController.popBackStack() }, @@ -348,6 +349,11 @@ private fun ReceiverDetailRouteContent( receiverName = detailState.name ) ) + }, + onMindRecordDeliveryChange = { enabled -> + route.receiverId.toLongOrNull()?.let { receiverId -> + detailViewModel.setMindRecordDeliveryEnabled(receiverId, enabled) + } } ) ) diff --git a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/postdelivery/PostDeliveryConditionScreen.kt b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/postdelivery/PostDeliveryConditionScreen.kt index 141267fe..1d156c1b 100644 --- a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/postdelivery/PostDeliveryConditionScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/postdelivery/PostDeliveryConditionScreen.kt @@ -22,21 +22,27 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.foundation.text.input.rememberTextFieldState 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.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.kuit.afternote.core.ui.component.button.ClickButton import com.kuit.afternote.core.ui.component.Label +import com.kuit.afternote.core.ui.component.OutlineTextField import com.kuit.afternote.core.ui.component.SelectableRadioCard import com.kuit.afternote.core.ui.component.navigation.TopBar import com.kuit.afternote.feature.afternote.presentation.component.edit.model.ProcessingMethodOption @@ -48,13 +54,16 @@ import com.kuit.afternote.feature.setting.presentation.model.TriggerConditionOpt import com.kuit.afternote.feature.setting.presentation.viewmodel.PostDeliveryConditionState import com.kuit.afternote.feature.setting.presentation.viewmodel.PostDeliveryConditionViewModel import com.kuit.afternote.feature.setting.presentation.viewmodel.PostDeliveryConditionViewModelContract +import com.kuit.afternote.R import com.kuit.afternote.ui.theme.AfternoteTheme import com.kuit.afternote.ui.theme.Gray1 import com.kuit.afternote.ui.theme.Gray3 +import com.kuit.afternote.ui.theme.B3 import com.kuit.afternote.ui.theme.Gray9 import com.kuit.afternote.ui.theme.Sansneo import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import java.time.LocalDate @Composable @@ -67,10 +76,23 @@ fun PostDeliveryConditionScreen( val selectedDeliveryMethod = state.selectedDeliveryMethod val selectedTriggerCondition = state.selectedTriggerCondition val selectedDate = state.selectedDate + val lastGreetingMessage = state.lastGreetingMessage val isLoading = state.isLoading val errorMessage = state.errorMessage var showDatePickerDialog by remember { mutableStateOf(false) } + val lastGreetingTextFieldState = rememberTextFieldState(initialText = lastGreetingMessage) + LaunchedEffect(lastGreetingMessage) { + if (lastGreetingTextFieldState.text.toString() != lastGreetingMessage) { + lastGreetingTextFieldState.edit { replace(0, length, lastGreetingMessage) } + } + } + LaunchedEffect(Unit) { + snapshotFlow { lastGreetingTextFieldState.text.toString() } + .distinctUntilChanged() + .collect { viewModel.onLastGreetingChanged(it) } + } + val deliveryMethods = listOf( DeliveryMethodOption.AutomaticTransfer, DeliveryMethodOption.ReceiverApprovalTransfer @@ -94,133 +116,159 @@ fun PostDeliveryConditionScreen( } ) { paddingValues -> Column( - modifier = modifier - .fillMaxSize() - .padding(paddingValues) - .verticalScroll(rememberScrollState()) - ) { - // 첫 번째 섹션: 정보 처리 방법 (전달 방식) - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) + modifier = modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) ) { - Spacer(modifier = Modifier.height(16.dp)) + // 첫 번째 섹션: 정보 처리 방법 (전달 방식) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) - Label(text = "정보 전달 방법") + Label(text = "정보 전달 방법") - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - Column( - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - deliveryMethods.forEach { method -> - val option = object : ProcessingMethodOption { - override val title: String = method.title - override val description: String = method.description - } - val isSelected = selectedDeliveryMethod == method - SelectableRadioCard( - selected = isSelected, - onClick = { viewModel.onDeliveryMethodSelected(method) }, - modifier = Modifier.fillMaxWidth(), - content = { - OptionRadioCardContent( - option = option, - selected = isSelected - ) + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + deliveryMethods.forEach { method -> + val option = object : ProcessingMethodOption { + override val title: String = method.title + override val description: String = method.description } - ) + val isSelected = selectedDeliveryMethod == method + SelectableRadioCard( + selected = isSelected, + onClick = { viewModel.onDeliveryMethodSelected(method) }, + modifier = Modifier.fillMaxWidth(), + content = { + OptionRadioCardContent( + option = option, + selected = isSelected + ) + } + ) + } } } - } - Spacer(modifier = Modifier.height(32.dp)) - - // 두 번째 섹션: 정보 처리 방법 (트리거 조건) - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - ) { - Label(text = "정보 처리 방법") + Spacer(modifier = Modifier.height(32.dp)) - Spacer(modifier = Modifier.height(16.dp)) + // 두 번째 섹션: 정보 처리 방법 (트리거 조건) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + Label(text = "정보 처리 방법") - // SelectedDateText - 특정 날짜가 선택되었을 때만 표시 - if (selectedTriggerCondition == TriggerConditionOption.SpecificDate && selectedDate != null) { - SelectedDateText(date = selectedDate) Spacer(modifier = Modifier.height(16.dp)) - } - Column( - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - triggerConditions.forEach { condition -> - val option = object : ProcessingMethodOption { - override val title: String = condition.title - override val description: String = condition.description - } - val isSelected = selectedTriggerCondition == condition - SelectableRadioCard( - selected = isSelected, - onClick = { - viewModel.onTriggerConditionSelected(condition) - if (condition == TriggerConditionOption.SpecificDate) { - showDatePickerDialog = true - } - }, - modifier = Modifier.fillMaxWidth(), - content = { - OptionRadioCardContent( - option = option, - selected = isSelected - ) + // SelectedDateText - 특정 날짜가 선택되었을 때만 표시 + if (selectedTriggerCondition == TriggerConditionOption.SpecificDate && selectedDate != null) { + SelectedDateText(date = selectedDate) + Spacer(modifier = Modifier.height(16.dp)) + } + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + triggerConditions.forEach { condition -> + val option = object : ProcessingMethodOption { + override val title: String = condition.title + override val description: String = condition.description } - ) + val isSelected = selectedTriggerCondition == condition + SelectableRadioCard( + selected = isSelected, + onClick = { + viewModel.onTriggerConditionSelected(condition) + if (condition == TriggerConditionOption.SpecificDate) { + showDatePickerDialog = true + } + }, + modifier = Modifier.fillMaxWidth(), + content = { + OptionRadioCardContent( + option = option, + selected = isSelected + ) + } + ) + } } } - } - Spacer(modifier = Modifier.height(40.dp)) + Spacer(modifier = Modifier.height(40.dp)) - // 구분선 - HorizontalDivider( - modifier = Modifier.padding(horizontal = 20.dp), - thickness = 1.dp, - color = Gray3 - ) + // 구분선 + HorizontalDivider( + modifier = Modifier.padding(horizontal = 20.dp), + thickness = 1.dp, + color = Gray3 + ) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(24.dp)) - // 하단 안내 문구 - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - verticalArrangement = Arrangement.spacedBy(0.dp) - ) { - BulletItem(text = "해당 정보는 지정된 조건이 충족 시에만 전달됩니다.") - BulletItem(text = "예기치 않은 상황에서도 설정된 방식에 따라 안전하게 전달됩니다.") - } + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + Label(text = "마지막 인삿말") + } + Spacer(modifier = Modifier.height(24.dp)) - Spacer(modifier = Modifier.height(32.dp)) - - if (errorMessage != null) { - Text( - text = errorMessage, - style = TextStyle( - fontSize = 12.sp, - fontFamily = Sansneo, - fontWeight = FontWeight.Normal, - color = Gray9 - ), + OutlineTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + textFieldState = lastGreetingTextFieldState, + placeholder = stringResource(R.string.post_delivery_last_greeting_placeholder), + containerColor = Gray1 + ) + Spacer(modifier = Modifier.height(16.dp)) + ClickButton( + color = B3, + onButtonClick = { viewModel.onSaveLastGreeting() }, + title = stringResource(R.string.post_delivery_confirm), modifier = Modifier.padding(horizontal = 20.dp) ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(24.dp)) + + // 하단 안내 문구 + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(0.dp) + ) { + BulletItem(text = "해당 정보는 지정된 조건이 충족 시에만 전달됩니다.") + BulletItem(text = "예기치 않은 상황에서도 설정된 방식에 따라 안전하게 전달됩니다.") + } + + Spacer(modifier = Modifier.height(32.dp)) + + if (errorMessage != null) { + Text( + text = errorMessage, + style = TextStyle( + fontSize = 12.sp, + fontFamily = Sansneo, + fontWeight = FontWeight.Normal, + color = Gray9 + ), + modifier = Modifier.padding(horizontal = 20.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + } } } - } if (isLoading) { Box( @@ -284,6 +332,7 @@ private class FakePostDeliveryConditionViewModel : PostDeliveryConditionViewMode selectedTriggerCondition = TriggerConditionOption.AppInactivity, selectedDate = null, inactivityPeriodDays = null, + lastGreetingMessage = "", isLoading = false, errorMessage = null ) @@ -300,6 +349,14 @@ private class FakePostDeliveryConditionViewModel : PostDeliveryConditionViewMode override fun onDateSelected(date: LocalDate?) { // No-op: Fake for Preview only; no persistence. } + + override fun onLastGreetingChanged(text: String) { + // No-op: Fake for Preview only; no persistence. + } + + override fun onSaveLastGreeting() { + // No-op: Fake for Preview only; no persistence. + } } @Preview(showBackground = true) diff --git a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverDetailScreen.kt b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverDetailScreen.kt index 9a9371b1..9494098d 100644 --- a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverDetailScreen.kt +++ b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/screen/receiver/ReceiverDetailScreen.kt @@ -35,9 +35,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.kuit.afternote.R +import com.kuit.afternote.feature.setting.presentation.component.ThumbSwitch import com.kuit.afternote.core.ui.component.navigation.TopBar import com.kuit.afternote.ui.theme.AfternoteTheme import com.kuit.afternote.ui.theme.B1 @@ -59,7 +61,8 @@ data class ReceiverDetailEditCallbacks( val onReceiverDetailImageClick: () -> Unit = {}, val onDailyQuestionClick: () -> Unit = {}, val onTimeLetterClick: () -> Unit = {}, - val onAfternoteClick: () -> Unit = {} + val onAfternoteClick: () -> Unit = {}, + val onMindRecordDeliveryChange: (Boolean) -> Unit = {} ) @Immutable @@ -70,7 +73,8 @@ data class ReceiverDetailScreenParams( val emailState: TextFieldState, val dailyQuestionCount: Int, val timeLetterCount: Int, - val afternoteCount: Int + val afternoteCount: Int, + val mindRecordDeliveryEnabled: Boolean = true ) @Composable @@ -167,8 +171,48 @@ fun ReceiverDetailScreen( onClick = callbacks.onAfternoteClick ) } + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "전달 허용 기록", + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + style = TextStyle( + fontSize = 18.sp, + lineHeight = 24.sp, + fontFamily = Sansneo, + fontWeight = FontWeight.Bold, + color = Black + ) + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .background(color = White, shape = RoundedCornerShape(8.dp)) + .padding(horizontal = 20.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.daily_answer_title), + style = TextStyle( + fontSize = 16.sp, + lineHeight = 22.sp, + fontFamily = Sansneo, + fontWeight = FontWeight.Medium, + color = Gray9 + ) + ) + ThumbSwitch( + checked = params.mindRecordDeliveryEnabled, + onCheckedChange = callbacks.onMindRecordDeliveryChange + ) + } + - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(32.dp)) } } } @@ -365,7 +409,8 @@ private fun ReceiverDetailScreenPreview() { emailState = emailState, dailyQuestionCount = 8, timeLetterCount = 12, - afternoteCount = 4 + afternoteCount = 4, + mindRecordDeliveryEnabled = true ) ) } diff --git a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/viewmodel/PostDeliveryConditionViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/viewmodel/PostDeliveryConditionViewModel.kt index 84f8e0ee..6b4e9fe5 100644 --- a/app/src/main/java/com/kuit/afternote/feature/setting/presentation/viewmodel/PostDeliveryConditionViewModel.kt +++ b/app/src/main/java/com/kuit/afternote/feature/setting/presentation/viewmodel/PostDeliveryConditionViewModel.kt @@ -29,6 +29,7 @@ data class PostDeliveryConditionState( val selectedTriggerCondition: TriggerConditionOption = TriggerConditionOption.AppInactivity, val selectedDate: LocalDate? = null, val inactivityPeriodDays: Int? = null, + val lastGreetingMessage: String = "", val isLoading: Boolean = false, val errorMessage: String? = null ) @@ -41,6 +42,8 @@ interface PostDeliveryConditionViewModelContract { fun onDeliveryMethodSelected(option: DeliveryMethodOption) fun onTriggerConditionSelected(option: TriggerConditionOption) fun onDateSelected(date: LocalDate?) + fun onLastGreetingChanged(text: String) + fun onSaveLastGreeting() } /** @@ -73,6 +76,7 @@ constructor( selectedTriggerCondition = conditionTypeToTriggerOption(condition.conditionType), selectedDate = condition.specificDate?.let { iso -> LocalDate.parse(iso) }, inactivityPeriodDays = condition.inactivityPeriodDays, + lastGreetingMessage = condition.leaveMessage ?: "", isLoading = false, errorMessage = null ) @@ -114,6 +118,14 @@ constructor( viewModelScope.launch { updateDeliveryConditionApi() } } + override fun onLastGreetingChanged(text: String) { + _state.update { it.copy(lastGreetingMessage = text) } + } + + override fun onSaveLastGreeting() { + viewModelScope.launch { updateDeliveryConditionApi() } + } + private suspend fun updateDeliveryConditionApi() { val s = _state.value val conditionType = triggerOptionToConditionType(s.selectedTriggerCondition) @@ -128,7 +140,8 @@ constructor( updateDeliveryConditionUseCase( conditionType = conditionType, inactivityPeriodDays = inactivityPeriodDays, - specificDate = specificDate + specificDate = specificDate, + leaveMessage = s.lastGreetingMessage.ifBlank { null } ) .onSuccess { condition -> _state.update { 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 0ff44cbf..af0883f3 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 @@ -114,6 +114,7 @@ data class DeliveryConditionResponseDto( val conditionType: DeliveryConditionTypeDto, val inactivityPeriodDays: Int? = null, val specificDate: String? = null, + val leaveMessage: String? = null, val conditionFulfilled: Boolean, val conditionMet: Boolean ) @@ -125,7 +126,8 @@ data class DeliveryConditionResponseDto( data class DeliveryConditionRequestDto( val conditionType: DeliveryConditionTypeDto, val inactivityPeriodDays: Int? = null, - val specificDate: String? = null + val specificDate: String? = null, + val leaveMessage: String? = null ) // --- File API (POST /files/presigned-url) --- 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..d1090550 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 @@ -74,6 +74,7 @@ object UserMapper { conditionType = dto.conditionType.toDomain(), inactivityPeriodDays = dto.inactivityPeriodDays, specificDate = dto.specificDate, + leaveMessage = dto.leaveMessage, conditionFulfilled = dto.conditionFulfilled, conditionMet = dto.conditionMet ) @@ -81,12 +82,14 @@ object UserMapper { fun toDeliveryConditionRequestDto( conditionType: DeliveryConditionType, inactivityPeriodDays: Int?, - specificDate: String? + specificDate: String?, + leaveMessage: String? = null ): DeliveryConditionRequestDto = DeliveryConditionRequestDto( conditionType = conditionType.toDto(), inactivityPeriodDays = inactivityPeriodDays, - specificDate = specificDate + specificDate = specificDate, + leaveMessage = leaveMessage ) private fun DeliveryConditionTypeDto.toDomain(): DeliveryConditionType = 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 cd248927..68d9e17d 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 @@ -192,14 +192,16 @@ class UserRepositoryImpl override suspend fun updateDeliveryCondition( conditionType: DeliveryConditionType, inactivityPeriodDays: Int?, - specificDate: String? + specificDate: String?, + leaveMessage: String? ): Result = runCatching { Log.d(TAG, "updateDeliveryCondition: conditionType=$conditionType") val body = UserMapper.toDeliveryConditionRequestDto( conditionType = conditionType, inactivityPeriodDays = inactivityPeriodDays, - specificDate = specificDate + specificDate = specificDate, + leaveMessage = leaveMessage ) val response = api.updateDeliveryCondition(body) Log.d(TAG, "updateDeliveryCondition: response=$response") 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..f7f8f39d 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 @@ -64,6 +64,7 @@ data class DeliveryCondition( val conditionType: DeliveryConditionType, val inactivityPeriodDays: Int?, val specificDate: String?, + val leaveMessage: String? = null, 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 70d31e73..b03e1bc7 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 @@ -68,10 +68,12 @@ interface UserRepository { * @param conditionType 전달 조건 타입 * @param inactivityPeriodDays 비활동 기간(일), INACTIVITY일 때 사용 * @param specificDate 특정 날짜(yyyy-MM-dd), SPECIFIC_DATE일 때 사용 + * @param leaveMessage 마지막 인사말 (수신자에게 전달되는 메시지) */ suspend fun updateDeliveryCondition( conditionType: DeliveryConditionType, inactivityPeriodDays: Int?, - specificDate: String? + specificDate: String?, + leaveMessage: String? = null ): Result } diff --git a/app/src/main/java/com/kuit/afternote/feature/user/domain/usecase/UpdateDeliveryConditionUseCase.kt b/app/src/main/java/com/kuit/afternote/feature/user/domain/usecase/UpdateDeliveryConditionUseCase.kt index 30fe9331..16ad7a8e 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/domain/usecase/UpdateDeliveryConditionUseCase.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/domain/usecase/UpdateDeliveryConditionUseCase.kt @@ -18,16 +18,19 @@ class UpdateDeliveryConditionUseCase * @param conditionType 전달 조건 타입 * @param inactivityPeriodDays 비활동 기간(일), INACTIVITY일 때 사용 * @param specificDate 특정 날짜(yyyy-MM-dd), SPECIFIC_DATE일 때 사용 + * @param leaveMessage 마지막 인사말 (수신자에게 전달되는 메시지) * @return 업데이트된 [DeliveryCondition] */ suspend operator fun invoke( conditionType: DeliveryConditionType, inactivityPeriodDays: Int?, - specificDate: String? + specificDate: String?, + leaveMessage: String? = null ): Result = userRepository.updateDeliveryCondition( conditionType = conditionType, inactivityPeriodDays = inactivityPeriodDays, - specificDate = specificDate + specificDate = specificDate, + leaveMessage = leaveMessage ) - } +} diff --git a/app/src/main/java/com/kuit/afternote/feature/user/presentation/uimodel/UserUiState.kt b/app/src/main/java/com/kuit/afternote/feature/user/presentation/uimodel/UserUiState.kt index a57c10e4..e26663d0 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/presentation/uimodel/UserUiState.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/presentation/uimodel/UserUiState.kt @@ -53,6 +53,7 @@ data class ReceiverDetailUiState( val dailyQuestionCount: Int = 0, val timeLetterCount: Int = 0, val afterNoteCount: Int = 0, + val mindRecordDeliveryEnabled: Boolean = true, val isLoading: Boolean = false, val errorMessage: String? = null ) diff --git a/app/src/main/java/com/kuit/afternote/feature/user/presentation/viewmodel/ReceiverDetailViewModel.kt b/app/src/main/java/com/kuit/afternote/feature/user/presentation/viewmodel/ReceiverDetailViewModel.kt index b2de61b0..3d56e9a3 100644 --- a/app/src/main/java/com/kuit/afternote/feature/user/presentation/viewmodel/ReceiverDetailViewModel.kt +++ b/app/src/main/java/com/kuit/afternote/feature/user/presentation/viewmodel/ReceiverDetailViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.kuit.afternote.feature.dailyrecord.domain.usecase.SetMindRecordReceiverEnabledForAllUseCase import com.kuit.afternote.feature.setting.presentation.navgraph.SettingRoute import com.kuit.afternote.feature.user.domain.usecase.GetReceiverDetailUseCase import com.kuit.afternote.feature.user.presentation.uimodel.ReceiverDetailUiState @@ -24,7 +25,8 @@ class ReceiverDetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - private val getReceiverDetailUseCase: GetReceiverDetailUseCase + private val getReceiverDetailUseCase: GetReceiverDetailUseCase, + private val setMindRecordReceiverEnabledForAllUseCase: SetMindRecordReceiverEnabledForAllUseCase ) : ViewModel() { private val _uiState = MutableStateFlow(ReceiverDetailUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -78,4 +80,28 @@ class ReceiverDetailViewModel fun clearError() { _uiState.update { it.copy(errorMessage = null) } } + + /** + * "나의 모든 기록" 전달 허용 토글. + * PATCH mind-records/{recordId}/receivers/{receiverId} 를 모든 마음의 기록에 대해 호출합니다. + */ + fun setMindRecordDeliveryEnabled(receiverId: Long, enabled: Boolean) { + viewModelScope.launch { + val previous = _uiState.value.mindRecordDeliveryEnabled + _uiState.update { it.copy(mindRecordDeliveryEnabled = enabled) } + runCatching { + setMindRecordReceiverEnabledForAllUseCase( + receiverId = receiverId, + enabled = enabled + ) + }.onFailure { e -> + _uiState.update { + it.copy( + mindRecordDeliveryEnabled = previous, + errorMessage = e.message ?: "전달 설정 변경에 실패했습니다." + ) + } + } + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 55d24575..4312ddff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -133,4 +133,7 @@ %d명에게 아래로 영상 재생 + + 마지막 인사말을 입력해주세요 + 확인