From 76460555cac4eb5047f6d276b7714fc345228e15 Mon Sep 17 00:00:00 2001 From: sunghyun Date: Mon, 15 Jun 2026 14:36:46 +0900 Subject: [PATCH 1/4] =?UTF-8?q?:sparkles:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timecapsule/TimeCapsuleContentResponse.kt | 18 +++ .../network/service/TimeCapsuleService.kt | 26 ++++ .../timecapsule/TimeCapsuleDataSource.kt | 20 +++ .../timecapsule/TimeCapsuleDataSourceImpl.kt | 37 +++++ .../timecapsule/TimeCapsuleRepository.kt | 20 +++ .../timecapsule/TimeCapsuleRepositoryImpl.kt | 43 ++++++ .../CreateTimeCapsuleContentUseCase.kt | 23 +++ .../DeleteTimeCapsuleContentUseCase.kt | 13 ++ .../GetMyTimeCapsuleContentUseCase.kt | 14 ++ .../GetTimeCapsuleContentUseCase.kt | 14 ++ .../ModifyTimeCapsuleContentUseCase.kt | 20 +++ .../com/idiotfrogs/message/MessageScreen.kt | 125 +++++++++------ .../idiotfrogs/message/MessageViewModel.kt | 142 +++++++++++++++++- 13 files changed, 462 insertions(+), 53 deletions(-) create mode 100644 core/model/src/main/java/com/idiotfrogs/model/timecapsule/TimeCapsuleContentResponse.kt create mode 100644 domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/CreateTimeCapsuleContentUseCase.kt create mode 100644 domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/DeleteTimeCapsuleContentUseCase.kt create mode 100644 domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/GetMyTimeCapsuleContentUseCase.kt create mode 100644 domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/GetTimeCapsuleContentUseCase.kt create mode 100644 domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/ModifyTimeCapsuleContentUseCase.kt diff --git a/core/model/src/main/java/com/idiotfrogs/model/timecapsule/TimeCapsuleContentResponse.kt b/core/model/src/main/java/com/idiotfrogs/model/timecapsule/TimeCapsuleContentResponse.kt new file mode 100644 index 0000000..23473e6 --- /dev/null +++ b/core/model/src/main/java/com/idiotfrogs/model/timecapsule/TimeCapsuleContentResponse.kt @@ -0,0 +1,18 @@ +package com.idiotfrogs.model.timecapsule + +import kotlinx.serialization.Serializable + +@Serializable +data class TimeCapsuleContentResponse( + val userId: Long, + val nickname: String, + val profileImageUrl: String, + val capsuleContents: List, +) + +@Serializable +data class CapsuleContentsData( + val contentId: Long, + val content: String?, + val attachedFileUrls: List?, +) diff --git a/core/network/src/main/java/com/idiotfrogs/network/service/TimeCapsuleService.kt b/core/network/src/main/java/com/idiotfrogs/network/service/TimeCapsuleService.kt index b290c15..a9de3c4 100644 --- a/core/network/src/main/java/com/idiotfrogs/network/service/TimeCapsuleService.kt +++ b/core/network/src/main/java/com/idiotfrogs/network/service/TimeCapsuleService.kt @@ -1,15 +1,18 @@ package com.idiotfrogs.network.service import com.idiotfrogs.model.timecapsule.BuryTimeCapsuleRequest +import com.idiotfrogs.model.timecapsule.CapsuleContentsData import com.idiotfrogs.model.timecapsule.MyTimeCapsuleResponse import com.idiotfrogs.model.timecapsule.ProcessCollaboratorRequest import com.idiotfrogs.model.timecapsule.PendingCollaboratorsRequest import com.idiotfrogs.model.timecapsule.TimeCapsuleCollaboratorsResponse +import com.idiotfrogs.model.timecapsule.TimeCapsuleContentResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleCreateRequest import com.idiotfrogs.model.timecapsule.TimeCapsuleCreateResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleInviteCodeResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleResponse import okhttp3.MultipartBody +import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET @@ -87,4 +90,27 @@ interface TimeCapsuleService { suspend fun leaveTimeCapsule( @Path("capsuleId") capsuleId: Long ) + + @GET("api/time-capsule-content/{timeCapsuleId}/contents") + suspend fun getTimeCapsuleContent(@Path("timeCapsuleId") timeCapsuleId: Long): List + + @GET("api/time-capsule-content/{timeCapsuleId}/my-contents") + suspend fun getMyTimeCapsuleContent(@Path("timeCapsuleId") timeCapsuleId: Long): List + + @POST("api/time-capsule-content/{timeCapsuleId}") + @Multipart + suspend fun createTimeCapsuleContent( + @Path("timeCapsuleId") timeCapsuleId: Long, + @Part("content") content: RequestBody, + @Part files: List, + ): CapsuleContentsData + + @PUT("api/time-capsule-content/{contentId}") + suspend fun modifyTimeCapsuleContent( + @Path("contentId") contentId: Long, + @Query("content") content: String, + ): CapsuleContentsData + + @DELETE("api/time-capsule-content") + suspend fun deleteTimeCapsuleContent(@Query("contentIds") contentIds: List) } diff --git a/data/src/main/java/com/idiotfrogs/data/datasource/timecapsule/TimeCapsuleDataSource.kt b/data/src/main/java/com/idiotfrogs/data/datasource/timecapsule/TimeCapsuleDataSource.kt index 5ddd081..bdb2316 100644 --- a/data/src/main/java/com/idiotfrogs/data/datasource/timecapsule/TimeCapsuleDataSource.kt +++ b/data/src/main/java/com/idiotfrogs/data/datasource/timecapsule/TimeCapsuleDataSource.kt @@ -1,15 +1,18 @@ package com.idiotfrogs.data.datasource.timecapsule import com.idiotfrogs.model.timecapsule.BuryTimeCapsuleRequest +import com.idiotfrogs.model.timecapsule.CapsuleContentsData import com.idiotfrogs.model.timecapsule.MyTimeCapsuleResponse import com.idiotfrogs.model.timecapsule.PendingCollaboratorsRequest import com.idiotfrogs.model.timecapsule.ProcessCollaboratorRequest import com.idiotfrogs.model.timecapsule.TimeCapsuleCollaboratorsResponse +import com.idiotfrogs.model.timecapsule.TimeCapsuleContentResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleCreateRequest import com.idiotfrogs.model.timecapsule.TimeCapsuleCreateResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleInviteCodeResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleResponse import okhttp3.MultipartBody +import okhttp3.RequestBody interface TimeCapsuleDataSource { suspend fun createTimeCapsule( @@ -58,4 +61,21 @@ interface TimeCapsuleDataSource { ): TimeCapsuleCollaboratorsResponse suspend fun leaveTimeCapsule(capsuleId: Long) + + suspend fun getTimeCapsuleContent(timeCapsuleId: Long): List + + suspend fun getMyTimeCapsuleContent(timeCapsuleId: Long): List + + suspend fun createTimeCapsuleContent( + timeCapsuleId: Long, + content: RequestBody, + files: List, + ): CapsuleContentsData + + suspend fun modifyTimeCapsuleContent( + contentId: Long, + content: String, + ): CapsuleContentsData + + suspend fun deleteTimeCapsuleContent(contentIds: List) } diff --git a/data/src/main/java/com/idiotfrogs/data/datasource/timecapsule/TimeCapsuleDataSourceImpl.kt b/data/src/main/java/com/idiotfrogs/data/datasource/timecapsule/TimeCapsuleDataSourceImpl.kt index bfcd47b..95e8040 100644 --- a/data/src/main/java/com/idiotfrogs/data/datasource/timecapsule/TimeCapsuleDataSourceImpl.kt +++ b/data/src/main/java/com/idiotfrogs/data/datasource/timecapsule/TimeCapsuleDataSourceImpl.kt @@ -1,16 +1,19 @@ package com.idiotfrogs.data.datasource.timecapsule import com.idiotfrogs.model.timecapsule.BuryTimeCapsuleRequest +import com.idiotfrogs.model.timecapsule.CapsuleContentsData import com.idiotfrogs.model.timecapsule.MyTimeCapsuleResponse import com.idiotfrogs.model.timecapsule.PendingCollaboratorsRequest import com.idiotfrogs.model.timecapsule.ProcessCollaboratorRequest import com.idiotfrogs.model.timecapsule.TimeCapsuleCollaboratorsResponse +import com.idiotfrogs.model.timecapsule.TimeCapsuleContentResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleCreateRequest import com.idiotfrogs.model.timecapsule.TimeCapsuleCreateResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleInviteCodeResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleResponse import com.idiotfrogs.network.service.TimeCapsuleService import okhttp3.MultipartBody +import okhttp3.RequestBody import javax.inject.Inject class TimeCapsuleDataSourceImpl @Inject constructor( @@ -96,4 +99,38 @@ class TimeCapsuleDataSourceImpl @Inject constructor( override suspend fun leaveTimeCapsule(capsuleId: Long) { return timeCapsuleService.leaveTimeCapsule(capsuleId) } + + override suspend fun getTimeCapsuleContent(timeCapsuleId: Long): List { + return timeCapsuleService.getTimeCapsuleContent(timeCapsuleId) + } + + override suspend fun getMyTimeCapsuleContent(timeCapsuleId: Long): List { + return timeCapsuleService.getMyTimeCapsuleContent(timeCapsuleId) + } + + override suspend fun createTimeCapsuleContent( + timeCapsuleId: Long, + content: RequestBody, + files: List + ): CapsuleContentsData { + return timeCapsuleService.createTimeCapsuleContent( + timeCapsuleId = timeCapsuleId, + content = content, + files = files + ) + } + + override suspend fun modifyTimeCapsuleContent( + contentId: Long, + content: String + ): CapsuleContentsData { + return timeCapsuleService.modifyTimeCapsuleContent( + contentId = contentId, + content = content + ) + } + + override suspend fun deleteTimeCapsuleContent(contentIds: List) { + return timeCapsuleService.deleteTimeCapsuleContent(contentIds) + } } diff --git a/data/src/main/java/com/idiotfrogs/data/repository/timecapsule/TimeCapsuleRepository.kt b/data/src/main/java/com/idiotfrogs/data/repository/timecapsule/TimeCapsuleRepository.kt index 81783e8..ea13022 100644 --- a/data/src/main/java/com/idiotfrogs/data/repository/timecapsule/TimeCapsuleRepository.kt +++ b/data/src/main/java/com/idiotfrogs/data/repository/timecapsule/TimeCapsuleRepository.kt @@ -1,14 +1,17 @@ package com.idiotfrogs.data.repository.timecapsule import com.idiotfrogs.model.timecapsule.BuryTimeCapsuleRequest +import com.idiotfrogs.model.timecapsule.CapsuleContentsData import com.idiotfrogs.model.timecapsule.MyTimeCapsuleResponse import com.idiotfrogs.model.timecapsule.PendingCollaboratorsRequest import com.idiotfrogs.model.timecapsule.ProcessCollaboratorRequest import com.idiotfrogs.model.timecapsule.TimeCapsuleCollaboratorsResponse +import com.idiotfrogs.model.timecapsule.TimeCapsuleContentResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleCreateRequest import com.idiotfrogs.model.timecapsule.TimeCapsuleCreateResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleInviteCodeResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleResponse +import okhttp3.MultipartBody import java.io.File interface TimeCapsuleRepository { @@ -58,4 +61,21 @@ interface TimeCapsuleRepository { ): TimeCapsuleCollaboratorsResponse suspend fun leaveTimeCapsule(capsuleId: Long) + + suspend fun getTimeCapsuleContent(timeCapsuleId: Long): List + + suspend fun getMyTimeCapsuleContent(timeCapsuleId: Long): List + + suspend fun createTimeCapsuleContent( + timeCapsuleId: Long, + content: String, + files: List, + ): CapsuleContentsData + + suspend fun modifyTimeCapsuleContent( + contentId: Long, + content: String, + ): CapsuleContentsData + + suspend fun deleteTimeCapsuleContent(contentIds: List) } diff --git a/data/src/main/java/com/idiotfrogs/data/repository/timecapsule/TimeCapsuleRepositoryImpl.kt b/data/src/main/java/com/idiotfrogs/data/repository/timecapsule/TimeCapsuleRepositoryImpl.kt index 6176fa4..7bb072b 100644 --- a/data/src/main/java/com/idiotfrogs/data/repository/timecapsule/TimeCapsuleRepositoryImpl.kt +++ b/data/src/main/java/com/idiotfrogs/data/repository/timecapsule/TimeCapsuleRepositoryImpl.kt @@ -2,10 +2,12 @@ package com.idiotfrogs.data.repository.timecapsule import com.idiotfrogs.data.datasource.timecapsule.TimeCapsuleDataSource import com.idiotfrogs.model.timecapsule.BuryTimeCapsuleRequest +import com.idiotfrogs.model.timecapsule.CapsuleContentsData import com.idiotfrogs.model.timecapsule.MyTimeCapsuleResponse import com.idiotfrogs.model.timecapsule.PendingCollaboratorsRequest import com.idiotfrogs.model.timecapsule.ProcessCollaboratorRequest import com.idiotfrogs.model.timecapsule.TimeCapsuleCollaboratorsResponse +import com.idiotfrogs.model.timecapsule.TimeCapsuleContentResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleCreateRequest import com.idiotfrogs.model.timecapsule.TimeCapsuleCreateResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleInviteCodeResponse @@ -13,6 +15,7 @@ import com.idiotfrogs.model.timecapsule.TimeCapsuleResponse import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody import java.io.File import javax.inject.Inject @@ -101,4 +104,44 @@ class TimeCapsuleRepositoryImpl @Inject constructor( override suspend fun leaveTimeCapsule(capsuleId: Long) { return timeCapsuleDataSource.leaveTimeCapsule(capsuleId) } + + override suspend fun getTimeCapsuleContent(timeCapsuleId: Long): List { + return timeCapsuleDataSource.getTimeCapsuleContent(timeCapsuleId) + } + + override suspend fun getMyTimeCapsuleContent(timeCapsuleId: Long): List { + return timeCapsuleDataSource.getMyTimeCapsuleContent(timeCapsuleId) + } + + override suspend fun createTimeCapsuleContent( + timeCapsuleId: Long, + content: String, + files: List + ): CapsuleContentsData { + val contentBody = content.toRequestBody("text/plain".toMediaType()) + val fileParts = files.map { + val requestBody = it.asRequestBody("image/jpeg".toMediaType()) + MultipartBody.Part.createFormData("files", it.name, requestBody) + } + + return timeCapsuleDataSource.createTimeCapsuleContent( + timeCapsuleId = timeCapsuleId, + content = contentBody, + files = fileParts + ) + } + + override suspend fun modifyTimeCapsuleContent( + contentId: Long, + content: String + ): CapsuleContentsData { + return timeCapsuleDataSource.modifyTimeCapsuleContent( + contentId = contentId, + content = content + ) + } + + override suspend fun deleteTimeCapsuleContent(contentIds: List) { + return timeCapsuleDataSource.deleteTimeCapsuleContent(contentIds) + } } diff --git a/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/CreateTimeCapsuleContentUseCase.kt b/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/CreateTimeCapsuleContentUseCase.kt new file mode 100644 index 0000000..9d45f50 --- /dev/null +++ b/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/CreateTimeCapsuleContentUseCase.kt @@ -0,0 +1,23 @@ +package com.idiotfrogs.domain.usecase.timecapsule + +import com.idiotfrogs.data.repository.timecapsule.TimeCapsuleRepository +import com.idiotfrogs.model.timecapsule.CapsuleContentsData +import com.idiotfrogs.util.safeCatching +import java.io.File +import javax.inject.Inject + +class CreateTimeCapsuleContentUseCase @Inject constructor( + private val timeCapsuleRepository: TimeCapsuleRepository +) { + suspend operator fun invoke( + timeCapsuleId: Long, + content: String, + files: List, + ): Result = safeCatching { + timeCapsuleRepository.createTimeCapsuleContent( + timeCapsuleId = timeCapsuleId, + content = content, + files = files + ) + } +} diff --git a/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/DeleteTimeCapsuleContentUseCase.kt b/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/DeleteTimeCapsuleContentUseCase.kt new file mode 100644 index 0000000..eb3177f --- /dev/null +++ b/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/DeleteTimeCapsuleContentUseCase.kt @@ -0,0 +1,13 @@ +package com.idiotfrogs.domain.usecase.timecapsule + +import com.idiotfrogs.data.repository.timecapsule.TimeCapsuleRepository +import com.idiotfrogs.util.safeCatching +import javax.inject.Inject + +class DeleteTimeCapsuleContentUseCase @Inject constructor( + private val timeCapsuleRepository: TimeCapsuleRepository +) { + suspend operator fun invoke(contentIds: List): Result = safeCatching { + timeCapsuleRepository.deleteTimeCapsuleContent(contentIds) + } +} diff --git a/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/GetMyTimeCapsuleContentUseCase.kt b/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/GetMyTimeCapsuleContentUseCase.kt new file mode 100644 index 0000000..9de4483 --- /dev/null +++ b/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/GetMyTimeCapsuleContentUseCase.kt @@ -0,0 +1,14 @@ +package com.idiotfrogs.domain.usecase.timecapsule + +import com.idiotfrogs.data.repository.timecapsule.TimeCapsuleRepository +import com.idiotfrogs.model.timecapsule.CapsuleContentsData +import com.idiotfrogs.util.safeCatching +import javax.inject.Inject + +class GetMyTimeCapsuleContentUseCase @Inject constructor( + private val timeCapsuleRepository: TimeCapsuleRepository +) { + suspend operator fun invoke(timeCapsuleId: Long): Result> = safeCatching { + timeCapsuleRepository.getMyTimeCapsuleContent(timeCapsuleId) + } +} diff --git a/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/GetTimeCapsuleContentUseCase.kt b/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/GetTimeCapsuleContentUseCase.kt new file mode 100644 index 0000000..65ecf15 --- /dev/null +++ b/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/GetTimeCapsuleContentUseCase.kt @@ -0,0 +1,14 @@ +package com.idiotfrogs.domain.usecase.timecapsule + +import com.idiotfrogs.data.repository.timecapsule.TimeCapsuleRepository +import com.idiotfrogs.model.timecapsule.TimeCapsuleContentResponse +import com.idiotfrogs.util.safeCatching +import javax.inject.Inject + +class GetTimeCapsuleContentUseCase @Inject constructor( + private val timeCapsuleRepository: TimeCapsuleRepository +) { + suspend operator fun invoke(timeCapsuleId: Long): Result> = safeCatching { + timeCapsuleRepository.getTimeCapsuleContent(timeCapsuleId) + } +} diff --git a/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/ModifyTimeCapsuleContentUseCase.kt b/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/ModifyTimeCapsuleContentUseCase.kt new file mode 100644 index 0000000..7ec26cd --- /dev/null +++ b/domain/src/main/java/com/idiotfrogs/domain/usecase/timecapsule/ModifyTimeCapsuleContentUseCase.kt @@ -0,0 +1,20 @@ +package com.idiotfrogs.domain.usecase.timecapsule + +import com.idiotfrogs.data.repository.timecapsule.TimeCapsuleRepository +import com.idiotfrogs.model.timecapsule.CapsuleContentsData +import com.idiotfrogs.util.safeCatching +import javax.inject.Inject + +class ModifyTimeCapsuleContentUseCase @Inject constructor( + private val timeCapsuleRepository: TimeCapsuleRepository +) { + suspend operator fun invoke( + contentId: Long, + content: String, + ): Result = safeCatching { + timeCapsuleRepository.modifyTimeCapsuleContent( + contentId = contentId, + content = content + ) + } +} diff --git a/feature/message/src/main/java/com/idiotfrogs/message/MessageScreen.kt b/feature/message/src/main/java/com/idiotfrogs/message/MessageScreen.kt index 5cae141..b44f958 100644 --- a/feature/message/src/main/java/com/idiotfrogs/message/MessageScreen.kt +++ b/feature/message/src/main/java/com/idiotfrogs/message/MessageScreen.kt @@ -1,6 +1,5 @@ package com.idiotfrogs.message -import android.net.Uri import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -47,6 +46,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource @@ -64,6 +64,7 @@ import com.idiotfrogs.designsystem.theme.MSTheme import com.idiotfrogs.designsystem.util.noRippleClickable import com.idiotfrogs.designsystem.util.rememberMultiPickerState import com.idiotfrogs.designsystem.util.wavyStroke +import com.idiotfrogs.extension.toFile import com.idiotfrogs.message.component.MessageCheckBox import com.idiotfrogs.message.component.MessagePreviewBanner import com.idiotfrogs.message.component.MessageSettingListItem @@ -77,7 +78,7 @@ import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun MessageRoute( capsuleId: Long, - viewModel: MessageViewModel = hiltViewModel(), + viewModel: MessageViewModel = hiltViewModel(key = capsuleId.toString()) { it.create(capsuleId) }, ) { val navigator = LocalComposeMSNavigator.current val uiState by viewModel.collectAsState() @@ -88,11 +89,12 @@ fun MessageRoute( } } - when (uiState) { + when (val state = uiState) { UiState.Init -> Unit is UiState.Success -> { MessageScreen( capsuleId = capsuleId, + data = state.data, onAction = viewModel::onAction, ) } @@ -103,15 +105,17 @@ fun MessageRoute( @Composable fun MessageScreen( capsuleId: Long, + data: MessageData, onAction: (MessageAction) -> Unit, modifier: Modifier = Modifier, ) { + val context = LocalContext.current var currentTab by remember { mutableStateOf(MessageTab.MESSAGE) } var isDeleteMode by rememberSaveable { mutableStateOf(false) } var showMessageInput by rememberSaveable { mutableStateOf(false) } var activeMessageId by rememberSaveable { mutableStateOf(null) } var messageItems by remember { mutableStateOf(emptyList()) } - var selectedIds by rememberSaveable { mutableStateOf(emptySet()) } + var selectedIds by rememberSaveable { mutableStateOf(emptySet()) } var photoItems by remember { mutableStateOf(emptyList()) } val messageTextFieldState = rememberTextFieldState() @@ -121,7 +125,36 @@ fun MessageScreen( val (pickedPhotoUris, launchPhotoPicker) = rememberMultiPickerState() val pagerState = rememberPagerState { MessageTab.entries.size } val canDelete = selectedIds.isNotEmpty() - val activeMessageItem = messageItems.firstOrNull { it.id == activeMessageId } + val activeMessageItem = messageItems.firstOrNull { it.contentId == activeMessageId } + + LaunchedEffect(data.contents) { + messageItems = data.contents + .filter { !it.content.isNullOrBlank() } + .mapIndexed { index, item -> + MessageListItemUiModel( + id = "message-${item.contentId}", + contentId = item.contentId, + title = "${MessageTab.MESSAGE.title} ${index + 1}", + description = item.content.orEmpty(), + ) + } + + photoItems = data.contents + .flatMap { content -> + content.attachedFileUrls.orEmpty().mapIndexed { index, imageUrl -> + PhotoListItemUiModel( + id = "photo-${content.contentId}-$index", + contentId = content.contentId, + imageModel = imageUrl, + ) + } + } + + isDeleteMode = false + showMessageInput = false + activeMessageId = null + selectedIds = emptySet() + } fun closeMessageInput() { showMessageInput = false @@ -136,21 +169,19 @@ fun MessageScreen( if (message.isBlank()) return - if (activeMessageId == null) { - messageItems = messageItems + MessageListItemUiModel( - id = (messageItems.maxOfOrNull { it.id } ?: 0L) + 1L, - title = "메시지 ${messageItems.size + 1}", - description = message, + activeMessageId?.let { contentId -> + onAction( + MessageAction.ModifyContent( + contentId = contentId, + content = message, + ) ) - } else { - messageItems = messageItems.map { item -> - if (item.id == activeMessageId) { - item.copy(description = message) - } else { - item - } - } - } + } ?: onAction( + MessageAction.CreateContent( + content = message, + files = emptyList(), + ) + ) closeMessageInput() } @@ -169,16 +200,16 @@ fun MessageScreen( LaunchedEffect(pickedPhotoUris) { if (pickedPhotoUris.isNotEmpty()) { - val existingUris = photoItems.map { it.uri }.toSet() - val newUris = pickedPhotoUris - .distinct() - .filterNot { it in existingUris } - val nextPhotoId = (photoItems.maxOfOrNull { it.id } ?: 0L) + 1L - - photoItems = photoItems + newUris.mapIndexed { index, uri -> - PhotoListItemUiModel( - id = nextPhotoId + index, - uri = uri, + val files = pickedPhotoUris.mapIndexedNotNull { index, uri -> + uri.toFile(context, "photo-$index") + } + + if (files.isNotEmpty()) { + onAction( + MessageAction.CreateContent( + content = "", + files = files, + ) ) } } @@ -264,7 +295,7 @@ fun MessageScreen( if (isDeleteMode) { selectedIds = selectedIds.toggle(item.id) } else { - activeMessageId = item.id + activeMessageId = item.contentId } }, ) @@ -321,7 +352,7 @@ fun MessageScreen( ) { GlideImage( modifier = Modifier.matchParentSize(), - imageModel = { item.uri }, + imageModel = { item.imageModel }, ) if (isDeleteMode) { @@ -379,20 +410,17 @@ fun MessageScreen( .height(48.dp), enabled = canDelete, onClick = { - when (currentTab) { - MessageTab.MESSAGE -> { - messageItems = messageItems.filterNot { - it.id in selectedIds - } - } - - MessageTab.PHOTO -> { - photoItems = photoItems.filterNot { - it.id in selectedIds - } - } + val contentIds = when (currentTab) { + MessageTab.MESSAGE -> messageItems + .filter { it.id in selectedIds } + .map { it.contentId } + MessageTab.PHOTO -> photoItems + .filter { it.id in selectedIds } + .map { it.contentId } + .distinct() } + onAction(MessageAction.DeleteContent(contentIds)) selectedIds = emptySet() isDeleteMode = false }, @@ -613,17 +641,19 @@ private enum class MessageTab( } private data class MessageListItemUiModel( - val id: Long, + val id: String, + val contentId: Long, val title: String, val description: String, ) private data class PhotoListItemUiModel( - val id: Long, - val uri: Uri, + val id: String, + val contentId: Long, + val imageModel: String, ) -private fun Set.toggle(id: Long): Set = +private fun Set.toggle(id: String): Set = if (id in this) this - id else this + id @Preview @@ -631,6 +661,7 @@ private fun Set.toggle(id: Long): Set = fun MessageScreenPreview() { MessageScreen( capsuleId = 0L, + data = MessageData(), onAction = {}, ) } diff --git a/feature/message/src/main/java/com/idiotfrogs/message/MessageViewModel.kt b/feature/message/src/main/java/com/idiotfrogs/message/MessageViewModel.kt index 3bf2f72..08dc7cf 100644 --- a/feature/message/src/main/java/com/idiotfrogs/message/MessageViewModel.kt +++ b/feature/message/src/main/java/com/idiotfrogs/message/MessageViewModel.kt @@ -1,30 +1,160 @@ package com.idiotfrogs.message +import androidx.compose.runtime.Immutable +import com.idiotfrogs.domain.usecase.timecapsule.CreateTimeCapsuleContentUseCase +import com.idiotfrogs.domain.usecase.timecapsule.DeleteTimeCapsuleContentUseCase +import com.idiotfrogs.domain.usecase.timecapsule.GetMyTimeCapsuleContentUseCase +import com.idiotfrogs.domain.usecase.timecapsule.ModifyTimeCapsuleContentUseCase +import com.idiotfrogs.model.timecapsule.CapsuleContentsData import com.idiotfrogs.util.UiState import com.idiotfrogs.util.base.BaseViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.viewmodel.container -import javax.inject.Inject +import java.io.File -@HiltViewModel -class MessageViewModel @Inject constructor() : - BaseViewModel, MessageSideEffect, MessageAction>() { +@HiltViewModel(assistedFactory = MessageViewModel.Factory::class) +class MessageViewModel @AssistedInject constructor( + @Assisted private val capsuleId: Long, + private val getMyTimeCapsuleContentUseCase: GetMyTimeCapsuleContentUseCase, + private val createTimeCapsuleContentUseCase: CreateTimeCapsuleContentUseCase, + private val modifyTimeCapsuleContentUseCase: ModifyTimeCapsuleContentUseCase, + private val deleteTimeCapsuleContentUseCase: DeleteTimeCapsuleContentUseCase, +) : BaseViewModel, MessageSideEffect, MessageAction>() { - override val container: Container, MessageSideEffect> = - container(UiState.Success(Unit)) + override val container: Container, MessageSideEffect> = container( + initialState = UiState.Init, + onCreate = { fetchTimeCapsuleContent() } + ) + + private fun fetchTimeCapsuleContent() = safeLaunch { + getMyTimeCapsuleContentUseCase(capsuleId).onSuccess { + intent { reduce { UiState.Success(MessageData(contents = it)) } } + }.onFailure { + intent { reduce { UiState.Error(it.message) } } + } + } + + private fun createTimeCapsuleContent( + content: String, + files: List + ) { + safeLaunch { + createTimeCapsuleContentUseCase( + timeCapsuleId = capsuleId, + content = content, + files = files + ).onSuccess { response -> + intent { + reduce { + val currentData = (state as? UiState.Success)?.data ?: MessageData() + UiState.Success(currentData.copy(contents = currentData.contents + response)) + } + } + }.onFailure { + intent { reduce { UiState.Error(it.message) } } + } + } + } + + private fun modifyTimeCapsuleContent( + contentId: Long, + content: String + ) { + safeLaunch { + modifyTimeCapsuleContentUseCase( + contentId = contentId, + content = content + ).onSuccess { response -> + intent { + reduce { + val currentData = (state as? UiState.Success)?.data ?: MessageData() + + UiState.Success( + currentData.copy( + contents = currentData.contents.map { content -> + if (content.contentId == response.contentId) response else content + } + ) + ) + } + } + }.onFailure { + intent { reduce { UiState.Error(it.message) } } + } + } + } + + private fun deleteTimeCapsuleContent(contentIds: List) { + safeLaunch { + deleteTimeCapsuleContentUseCase(contentIds).onSuccess { + intent { + reduce { + val currentData = (state as? UiState.Success)?.data ?: MessageData() + + UiState.Success( + currentData.copy( + contents = currentData.contents.filterNot { it.contentId in contentIds } + ) + ) + } + } + }.onFailure { + intent { reduce { UiState.Error(it.message) } } + } + } + } override fun onAction(action: MessageAction) { when (action) { MessageAction.NavigateToBack -> intent { postSideEffect(MessageSideEffect.NavigateToBack) } + + is MessageAction.CreateContent -> createTimeCapsuleContent( + content = action.content, + files = action.files + ) + + is MessageAction.ModifyContent -> modifyTimeCapsuleContent( + contentId = action.contentId, + content = action.content + ) + + is MessageAction.DeleteContent -> deleteTimeCapsuleContent(action.contentIds) } } + + @AssistedFactory + interface Factory { + fun create(capsuleId: Long): MessageViewModel + } } +@Immutable +data class MessageData( + val contents: List = emptyList(), +) + sealed interface MessageAction { data object NavigateToBack : MessageAction + + data class CreateContent( + val content: String, + val files: List, + ) : MessageAction + + data class ModifyContent( + val contentId: Long, + val content: String, + ) : MessageAction + + data class DeleteContent( + val contentIds: List, + ) : MessageAction } sealed interface MessageSideEffect { From 2cbe41dc335360d95f8fafdfb7ce2602de116194 Mon Sep 17 00:00:00 2001 From: sunghyun Date: Mon, 15 Jun 2026 14:37:15 +0900 Subject: [PATCH 2/4] =?UTF-8?q?:bug:=20=EB=B9=BC=EB=A8=B9=EC=9D=80=20?= =?UTF-8?q?=EB=94=94=ED=85=8C=EC=9D=BC=20=ED=99=94=EB=A9=B4=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EA=B0=AF=EC=88=98=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/idiotfrogs/detail/DetailScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/detail/src/main/java/com/idiotfrogs/detail/DetailScreen.kt b/feature/detail/src/main/java/com/idiotfrogs/detail/DetailScreen.kt index 14e27c0..d0c4abc 100644 --- a/feature/detail/src/main/java/com/idiotfrogs/detail/DetailScreen.kt +++ b/feature/detail/src/main/java/com/idiotfrogs/detail/DetailScreen.kt @@ -473,7 +473,7 @@ fun DetailScreen( ) Spacer(Modifier.height(4.dp)) MSText( - text = "2개 등록", + text = "${capsule?.myContentCount}개 등록", fontSize = 12.dp, fontWeight = FontWeight.Medium, color = MSTheme.color.primaryDark, @@ -497,7 +497,7 @@ fun DetailScreen( ) Spacer(Modifier.height(4.dp)) MSText( - text = "12개 등록", + text = "${capsule?.myImageCount}개 등록", fontSize = 12.dp, fontWeight = FontWeight.Medium, color = MSTheme.color.primaryDark, From 233fc8084de06436e4fc17b4e39618c79b78caac Mon Sep 17 00:00:00 2001 From: sunghyun Date: Mon, 15 Jun 2026 14:46:51 +0900 Subject: [PATCH 3/4] =?UTF-8?q?:fire:=20=EB=B6=88=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0(capsuleId)=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/idiotfrogs/message/MessageScreen.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/feature/message/src/main/java/com/idiotfrogs/message/MessageScreen.kt b/feature/message/src/main/java/com/idiotfrogs/message/MessageScreen.kt index b44f958..dabcb93 100644 --- a/feature/message/src/main/java/com/idiotfrogs/message/MessageScreen.kt +++ b/feature/message/src/main/java/com/idiotfrogs/message/MessageScreen.kt @@ -93,7 +93,6 @@ fun MessageRoute( UiState.Init -> Unit is UiState.Success -> { MessageScreen( - capsuleId = capsuleId, data = state.data, onAction = viewModel::onAction, ) @@ -104,7 +103,6 @@ fun MessageRoute( @Composable fun MessageScreen( - capsuleId: Long, data: MessageData, onAction: (MessageAction) -> Unit, modifier: Modifier = Modifier, @@ -660,7 +658,6 @@ private fun Set.toggle(id: String): Set = @Composable fun MessageScreenPreview() { MessageScreen( - capsuleId = 0L, data = MessageData(), onAction = {}, ) From de81f694f4b16c2f80eeaefa1616dff624f4011c Mon Sep 17 00:00:00 2001 From: sunghyun Date: Mon, 15 Jun 2026 14:47:48 +0900 Subject: [PATCH 4/4] =?UTF-8?q?:memo:=20stability=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/message/stability/message-debug.stability | 6 +++--- feature/message/stability/message-release.stability | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/feature/message/stability/message-debug.stability b/feature/message/stability/message-debug.stability index ca429b5..8d1f2d9 100644 --- a/feature/message/stability/message-debug.stability +++ b/feature/message/stability/message-debug.stability @@ -10,14 +10,14 @@ public fun com.idiotfrogs.message.MessageRoute(capsuleId: kotlin.Long, viewModel restartable: true params: - capsuleId: STABLE (primitive type) - - viewModel: RUNTIME (requires runtime check) + - viewModel: UNSTABLE (has mutable properties or unstable members) @Composable -public fun com.idiotfrogs.message.MessageScreen(capsuleId: kotlin.Long, onAction: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit +public fun com.idiotfrogs.message.MessageScreen(data: com.idiotfrogs.message.MessageData, onAction: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true restartable: true params: - - capsuleId: STABLE (primitive type) + - data: STABLE (marked @Stable or @Immutable) - onAction: STABLE (function type) - modifier: STABLE (marked @Stable or @Immutable) diff --git a/feature/message/stability/message-release.stability b/feature/message/stability/message-release.stability index ca429b5..8d1f2d9 100644 --- a/feature/message/stability/message-release.stability +++ b/feature/message/stability/message-release.stability @@ -10,14 +10,14 @@ public fun com.idiotfrogs.message.MessageRoute(capsuleId: kotlin.Long, viewModel restartable: true params: - capsuleId: STABLE (primitive type) - - viewModel: RUNTIME (requires runtime check) + - viewModel: UNSTABLE (has mutable properties or unstable members) @Composable -public fun com.idiotfrogs.message.MessageScreen(capsuleId: kotlin.Long, onAction: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit +public fun com.idiotfrogs.message.MessageScreen(data: com.idiotfrogs.message.MessageData, onAction: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true restartable: true params: - - capsuleId: STABLE (primitive type) + - data: STABLE (marked @Stable or @Immutable) - onAction: STABLE (function type) - modifier: STABLE (marked @Stable or @Immutable)