From ec8878c261ab645d5c59a8f6bdb210e7655da8b9 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:13:39 +0900 Subject: [PATCH 01/51] =?UTF-8?q?[feat]:=20=ED=88=AC=ED=91=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20response=20=EC=83=9D=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/rooms/response/RoomsDeleteVoteResponse.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDeleteVoteResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDeleteVoteResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDeleteVoteResponse.kt new file mode 100644 index 00000000..53f21581 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDeleteVoteResponse.kt @@ -0,0 +1,8 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsDeleteVoteResponse( + val roomId: Int +) From 24549b34540c4cf9f774936e69f886e58d3d71cd Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:13:48 +0900 Subject: [PATCH 02/51] =?UTF-8?q?[feat]:=20=ED=88=AC=ED=91=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20service=20=EC=9E=91=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/data/service/RoomsService.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt b/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt index be07b09f..74819b06 100644 --- a/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt @@ -11,14 +11,15 @@ import com.texthip.thip.data.model.rooms.request.RoomsVoteRequest import com.texthip.thip.data.model.rooms.response.CreateRoomResponse import com.texthip.thip.data.model.rooms.response.JoinedRoomListResponse import com.texthip.thip.data.model.rooms.response.MyRoomListResponse +import com.texthip.thip.data.model.rooms.response.RoomCloseResponse import com.texthip.thip.data.model.rooms.response.RoomJoinResponse -import com.texthip.thip.data.model.rooms.response.RoomRecruitingResponse import com.texthip.thip.data.model.rooms.response.RoomMainList +import com.texthip.thip.data.model.rooms.response.RoomRecruitingResponse import com.texthip.thip.data.model.rooms.response.RoomSecretRoomResponse -import com.texthip.thip.data.model.rooms.response.RoomCloseResponse import com.texthip.thip.data.model.rooms.response.RoomsBookPageResponse import com.texthip.thip.data.model.rooms.response.RoomsCreateVoteResponse import com.texthip.thip.data.model.rooms.response.RoomsDeleteRecordResponse +import com.texthip.thip.data.model.rooms.response.RoomsDeleteVoteResponse import com.texthip.thip.data.model.rooms.response.RoomsPlayingResponse import com.texthip.thip.data.model.rooms.response.RoomsPostsLikesResponse import com.texthip.thip.data.model.rooms.response.RoomsPostsResponse @@ -138,6 +139,12 @@ interface RoomsService { @Path("recordId") recordId: Int ): BaseResponse + @DELETE("rooms/{roomId}/vote/{voteId}") + suspend fun deleteRoomsVote( + @Path("roomId") roomId: Int, + @Path("voteId") voteId: Int + ): BaseResponse + @POST("room-posts/{postId}/likes") suspend fun postRoomsPostsLikes( @Path("postId") postId: Int, From cc52a8097ec9ae8262524e024abcdd8c2ff930fd Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:13:55 +0900 Subject: [PATCH 03/51] =?UTF-8?q?[feat]:=20=ED=88=AC=ED=91=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20repository=20=EC=9E=91=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/data/repository/RoomsRepository.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt index e4ba9c01..edd5c191 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt @@ -223,6 +223,16 @@ class RoomsRepository @Inject constructor( ).handleBaseResponse().getOrThrow() } + suspend fun deleteRoomsVote( + roomId: Int, + voteId: Int + ) = runCatching { + roomsService.deleteRoomsVote( + roomId = roomId, + voteId = voteId + ).handleBaseResponse().getOrThrow() + } + suspend fun postRoomsPostsLikes( postId: Int, type: Boolean, From 5a5e2a00a6f02636f7012d9a6d920f5dd9ba6d0e Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:38:51 +0900 Subject: [PATCH 04/51] =?UTF-8?q?[feat]:=20=ED=88=AC=ED=91=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20viewmodel=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20screen?= =?UTF-8?q?=EC=97=90=20=EC=A0=81=EC=9A=A9=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/note/screen/GroupNoteScreen.kt | 45 ++++++++++++++----- .../note/viewmodel/GroupNoteViewModel.kt | 26 ++++++----- app/src/main/res/values/strings.xml | 2 + 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt index 2c2c8944..429b6d0f 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt @@ -161,6 +161,9 @@ fun GroupNoteContent( var showDeleteDialog by remember { mutableStateOf(false) } var isPinDialogVisible by remember { mutableStateOf(false) } var showToast by remember { mutableStateOf(false) } + val isOverlayVisible = + isCommentBottomSheetVisible || selectedPostForMenu != null || isPinDialogVisible || showDeleteDialog + var postToDelete by remember { mutableStateOf(null) } val commentsViewModel: CommentsViewModel = hiltViewModel() val commentsUiState by commentsViewModel.uiState.collectAsStateWithLifecycle() @@ -198,7 +201,7 @@ fun GroupNoteContent( } Box( - if (isCommentBottomSheetVisible || selectedPostForMenu != null || isPinDialogVisible) { + if (isOverlayVisible) { Modifier .fillMaxSize() .blur(5.dp) @@ -498,16 +501,15 @@ fun GroupNoteContent( } if (selectedPostForMenu != null) { - val isWriter = selectedPostForMenu!!.isWriter - - val menuItems = if (isWriter) { + val post = selectedPostForMenu!! + val menuItems = if (post.isWriter) { listOf( MenuBottomSheetItem( text = stringResource(R.string.delete), color = colors.Red, onClick = { - onEvent(GroupNoteEvent.OnDeleteRecord(selectedPostForMenu!!.postId)) - showDeleteDialog = false + postToDelete = post // 삭제할 포스트 정보를 기억 + showDeleteDialog = true selectedPostForMenu = null } ) @@ -527,12 +529,34 @@ fun GroupNoteContent( MenuBottomSheet( items = menuItems, - onDismiss = { - selectedPostForMenu = null - } + onDismiss = { selectedPostForMenu = null } ) } + if (showDeleteDialog) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + DialogPopup( + title = stringResource(R.string.delete_post_title), + description = stringResource(R.string.delete_post_content), + onConfirm = { + postToDelete?.let { + onEvent(GroupNoteEvent.OnDeleteRecord(it.postId, it.postType)) + } + showDeleteDialog = false + postToDelete = null + }, + onCancel = { + showDeleteDialog = false + postToDelete = null + } + ) + } + } + if (isPinDialogVisible) { Box( modifier = Modifier @@ -575,7 +599,8 @@ private fun GroupNoteScreenPreview() { likeCount = 10, commentCount = 2, isLocked = false, - isWriter = true + isWriter = true, + isOverview = false ) ), selectedTabIndex = 0, diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt index e79db56a..3149dedb 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt @@ -47,7 +47,7 @@ sealed interface GroupNoteEvent { data object ApplyPageFilter : GroupNoteEvent data object LoadMorePosts : GroupNoteEvent data class OnVote(val postId: Int, val voteItemId: Int, val type: Boolean) : GroupNoteEvent - data class OnDeleteRecord(val postId: Int) : GroupNoteEvent + data class OnDeleteRecord(val postId: Int, val postType: String) : GroupNoteEvent data class OnLikeRecord(val postId: Int, val postType: String) : GroupNoteEvent } @@ -144,7 +144,7 @@ class GroupNoteViewModel @Inject constructor( type = event.type ) - is GroupNoteEvent.OnDeleteRecord -> deleteRecord(event.postId) + is GroupNoteEvent.OnDeleteRecord -> deletePost(event.postId, event.postType) is GroupNoteEvent.OnLikeRecord -> likeRecord(event.postId, event.postType) } } @@ -180,16 +180,20 @@ class GroupNoteViewModel @Inject constructor( } } - private fun deleteRecord(postId: Int) { + private fun deletePost(postId: Int, postType: String) { viewModelScope.launch { - roomsRepository.deleteRoomsRecord(roomId = roomId, recordId = postId) - .onSuccess { - val updatedPosts = _uiState.value.posts.filter { it.postId != postId } - _uiState.update { it.copy(posts = updatedPosts) } - } - .onFailure { throwable -> - _uiState.update { it.copy(error = throwable.message) } - } + val result = when (postType) { + "RECORD" -> roomsRepository.deleteRoomsRecord(roomId = roomId, recordId = postId) + "VOTE" -> roomsRepository.deleteRoomsVote(roomId = roomId, voteId = postId) + else -> Result.failure(IllegalArgumentException("Unknown post type for deletion: $postType")) + } + + result.onSuccess { + val updatedPosts = _uiState.value.posts.filter { it.postId != postId } + _uiState.update { it.copy(posts = updatedPosts) } + }.onFailure { throwable -> + _uiState.update { it.copy(error = throwable.message) } + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b58bc2c7..e2942465 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -231,6 +231,8 @@ 첫번째 댓글을 남겨보세요 이 기록을 피드에 핀할까요? 핀하면 내 피드에 글을 옮길 수 있어요. + 이 기록을 삭제하시겠어요? + 삭제 후에는 되돌릴 수 없어요. 댓글 많은 순 기록을 게시 중입니다... 기록이 게시되었습니다! From 8381d6e229b50e2d475fed93268f575db8b2eddd Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:41:00 +0900 Subject: [PATCH 05/51] =?UTF-8?q?[refactor]:=20=EA=B8=B0=EB=A1=9D=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=B4=9D=ED=8F=89=EC=9D=BC=20=EB=95=8C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=A7=90=EA=B3=A0=20=EC=B4=9D=ED=8F=89?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=9C=A8=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/model/rooms/response/RoomsPostsResponse.kt | 1 + .../thip/ui/group/note/component/TextCommentCard.kt | 9 ++++++++- .../thip/ui/group/note/component/VoteCommentCard.kt | 9 ++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPostsResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPostsResponse.kt index 7a77e1f8..8df0be26 100644 --- a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPostsResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPostsResponse.kt @@ -24,6 +24,7 @@ data class PostList( val content: String, val likeCount: Int, val commentCount: Int, + val isOverview: Boolean, val isLiked: Boolean, val isWriter: Boolean, val isLocked: Boolean, diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/TextCommentCard.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/TextCommentCard.kt index aa55b37c..3dc8208a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/TextCommentCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/TextCommentCard.kt @@ -31,6 +31,12 @@ fun TextCommentCard( val isLocked = data.isLocked val isWriter = data.isWriter + val pageText = if (data.isOverview) { + stringResource(id = R.string.general_review) + } else { + data.page.toString() + stringResource(R.string.page) + } + Column( modifier = modifier .blur(if (isLocked) 5.dp else 0.dp) @@ -46,7 +52,7 @@ fun TextCommentCard( modifier = Modifier.padding(0.dp), profileImage = "https://example.com/image1.jpg", topText = data.nickName, - bottomText = data.page.toString() + stringResource(R.string.page), + bottomText = pageText, bottomTextColor = colors.Purple, showSubscriberInfo = false, hoursAgo = data.postDate @@ -94,6 +100,7 @@ fun TextCommentCardPreview() { isLiked = true, isWriter = false, isLocked = false, + isOverview = false, voteItems = emptyList() ) ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/VoteCommentCard.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/VoteCommentCard.kt index 9705ef67..8149929c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/VoteCommentCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/VoteCommentCard.kt @@ -36,6 +36,12 @@ fun VoteCommentCard( val isLocked = data.isLocked val isWriter = data.isWriter + val pageText = if (data.isOverview) { + stringResource(id = R.string.general_review) + } else { + data.page.toString() + stringResource(R.string.page) + } + Column( modifier = modifier .blur(if (isLocked) 5.dp else 0.dp) @@ -50,7 +56,7 @@ fun VoteCommentCard( ProfileBar( profileImage = "https://example.com/image1.jpg", topText = data.nickName, - bottomText = data.page.toString() + stringResource(R.string.page), + bottomText = pageText, bottomTextColor = colors.Purple, showSubscriberInfo = false, hoursAgo = data.postDate @@ -119,6 +125,7 @@ private fun VoteCommentCardPreview() { isLiked = true, isWriter = false, isLocked = false, + isOverview = false, voteItems = emptyList() ) ) From 2e051ebacc26969ec4736fcb119d102c9497b958 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 22:18:05 +0900 Subject: [PATCH 06/51] =?UTF-8?q?[feat]:=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20response=20=EC=9E=91=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/comments/response/CommentsDeleteResponse.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsDeleteResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsDeleteResponse.kt b/app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsDeleteResponse.kt new file mode 100644 index 00000000..21d8cfc9 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsDeleteResponse.kt @@ -0,0 +1,8 @@ +package com.texthip.thip.data.model.comments.response + +import kotlinx.serialization.Serializable + +@Serializable +data class CommentsDeleteResponse( + val postId: Long +) From c877cce21520c7883dd055f466d65df2e0f67e74 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 22:18:14 +0900 Subject: [PATCH 07/51] =?UTF-8?q?[feat]:=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20service=20=EC=9E=91=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/texthip/thip/data/service/CommentsService.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/texthip/thip/data/service/CommentsService.kt b/app/src/main/java/com/texthip/thip/data/service/CommentsService.kt index 4d50efdc..b8594590 100644 --- a/app/src/main/java/com/texthip/thip/data/service/CommentsService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/CommentsService.kt @@ -4,9 +4,11 @@ import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.comments.request.CommentsCreateRequest import com.texthip.thip.data.model.comments.request.CommentsLikesRequest import com.texthip.thip.data.model.comments.response.CommentsCreateResponse +import com.texthip.thip.data.model.comments.response.CommentsDeleteResponse import com.texthip.thip.data.model.comments.response.CommentsLikesResponse import com.texthip.thip.data.model.comments.response.CommentsResponse import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Path @@ -31,4 +33,9 @@ interface CommentsService { @Path("postId") postId: Long, @Body request: CommentsCreateRequest ): BaseResponse + + @DELETE("comments/{commentId}") + suspend fun deleteComment( + @Path("commentId") commentId: Long + ): BaseResponse } \ No newline at end of file From a31ce3210bdefbcede1c07c44a3f28fb06ad7c58 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 22:18:24 +0900 Subject: [PATCH 08/51] =?UTF-8?q?[feat]:=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20repository=20=EC=9E=91=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/data/repository/CommentsRepository.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt index 90e618ee..a7b10c00 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/CommentsRepository.kt @@ -50,4 +50,10 @@ class CommentsRepository @Inject constructor( ) ).handleBaseResponse().getOrThrow() } + + suspend fun deleteComment( + commentId: Long + ) = runCatching { + commentsService.deleteComment(commentId).handleBaseResponse().getOrThrow() + } } \ No newline at end of file From 600c53d209aeba739ed0676deca8d565ee95b668 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 22:18:50 +0900 Subject: [PATCH 09/51] =?UTF-8?q?[refactor]:=20=EB=8C=93=EA=B8=80=20respon?= =?UTF-8?q?se=EC=97=90=20isWriter=20=EC=B6=94=EA=B0=80=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/model/comments/response/CommentsResponse.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt b/app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt index 0db6c4d7..6df91629 100644 --- a/app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt @@ -21,6 +21,7 @@ data class CommentList( val content: String, val likeCount: Int, val isDeleted: Boolean, + val isWriter: Boolean, val isLike: Boolean, val replyList: List, ) @@ -38,4 +39,5 @@ data class ReplyList( val content: String, val likeCount: Int, val isLike: Boolean, + val isWriter: Boolean, ) \ No newline at end of file From 063466944abbf387a3e9db52347609c208540512 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 22:21:17 +0900 Subject: [PATCH 10/51] =?UTF-8?q?[feat]:=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20viewmodel=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20screen?= =?UTF-8?q?=EC=97=90=20=EC=A0=81=EC=9A=A9=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../note/component/CommentBottomSheet.kt | 216 +++++++++++++----- .../ui/group/note/component/CommentItem.kt | 112 +++++---- .../ui/group/note/component/CommentSection.kt | 18 +- .../thip/ui/group/note/component/ReplyItem.kt | 10 +- .../ui/group/note/screen/GroupNoteScreen.kt | 1 - .../group/note/viewmodel/CommentsViewModel.kt | 34 +++ app/src/main/res/values/strings.xml | 2 + 7 files changed, 278 insertions(+), 115 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt index 84038c4c..090b8330 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt @@ -1,5 +1,6 @@ package com.texthip.thip.ui.group.note.component +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -22,15 +23,20 @@ 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.blur import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.texthip.thip.R import com.texthip.thip.data.model.comments.response.CommentList +import com.texthip.thip.data.model.comments.response.ReplyList import com.texthip.thip.ui.common.bottomsheet.CustomBottomSheet +import com.texthip.thip.ui.common.bottomsheet.MenuBottomSheet import com.texthip.thip.ui.common.forms.CommentTextField +import com.texthip.thip.ui.common.modal.DialogPopup import com.texthip.thip.ui.group.note.viewmodel.CommentsEvent import com.texthip.thip.ui.group.note.viewmodel.CommentsUiState +import com.texthip.thip.ui.group.room.mock.MenuBottomSheetItem import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -47,75 +53,168 @@ fun CommentBottomSheet( var replyingToCommentId by remember { mutableStateOf(null) } var replyingToNickname by remember { mutableStateOf(null) } - CustomBottomSheet(onDismiss = onDismiss) { - Column( - modifier = Modifier - .fillMaxWidth() - .height(600.dp) - .advancedImePadding() - ) { + var selectedCommentForMenu by remember { mutableStateOf(null) } + var selectedReplyForMenu by remember { mutableStateOf(null) } + var showDeleteDialog by remember { mutableStateOf(false) } + var itemToDelete by remember { mutableStateOf(null) } // 삭제할 댓글/답글 임시 저장 + + val isOverlayVisible = + selectedCommentForMenu != null || selectedReplyForMenu != null || showDeleteDialog + + Box( + if (isOverlayVisible) { + Modifier + .fillMaxSize() + .background(color = colors.Black.copy(alpha = 0.8f),) + .blur(5.dp) + } else { + Modifier.fillMaxSize() + } + ) { + CustomBottomSheet(onDismiss = onDismiss) { Column( modifier = Modifier - .weight(1f) - .fillMaxHeight(0.8f) + .fillMaxWidth() + .height(600.dp) + .advancedImePadding() ) { - Text( - text = stringResource(R.string.comments), - style = typography.title_b700_s20_h24, - color = colors.White, - modifier = Modifier.padding(start = 20.dp, top = 20.dp, end = 20.dp) - ) + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight(0.8f) + ) { + Text( + text = stringResource(R.string.comments), + style = typography.title_b700_s20_h24, + color = colors.White, + modifier = Modifier.padding(start = 20.dp, top = 20.dp, end = 20.dp) + ) - Box(modifier = Modifier.weight(1f)) { - if (uiState.isLoading) { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - CircularProgressIndicator() + Box(modifier = Modifier.weight(1f)) { + if (uiState.isLoading) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + } + } else if (uiState.comments.isEmpty()) { + EmptyCommentView() + } else { + CommentLazyList( + commentList = uiState.comments, + isLoadingMore = uiState.isLoadingMore, + isLastPage = uiState.isLast, + onLoadMore = { onEvent(CommentsEvent.LoadMoreComments) }, + onReplyClick = { commentId, nickname -> + replyingToCommentId = commentId + replyingToNickname = nickname + }, + onEvent = onEvent, + onCommentLongPress = { comment -> + selectedCommentForMenu = comment + }, + onReplyLongPress = { reply -> selectedReplyForMenu = reply } + ) } - } else if (uiState.comments.isEmpty()) { - EmptyCommentView() - } else { - CommentLazyList( - commentList = uiState.comments, - isLoadingMore = uiState.isLoadingMore, - isLastPage = uiState.isLast, - onLoadMore = { onEvent(CommentsEvent.LoadMoreComments) }, - onReplyClick = { commentId, nickname -> - replyingToCommentId = commentId - replyingToNickname = nickname - }, - onEvent = onEvent - ) } } + + CommentTextField( + modifier = Modifier.fillMaxWidth(), + hint = stringResource(R.string.reply_to), + input = inputText, + onInputChange = { inputText = it }, + onSendClick = { + onSendReply( + inputText, + replyingToCommentId, + replyingToNickname + ) + inputText = "" + replyingToCommentId = null + replyingToNickname = null + }, + replyTo = replyingToNickname, + onCancelReply = { + replyingToCommentId = null + replyingToNickname = null + } + ) } + } + } + + val itemForMenu = selectedCommentForMenu ?: selectedReplyForMenu + if (itemForMenu != null) { + val isWriter = when (itemForMenu) { + is CommentList -> itemForMenu.isWriter + is ReplyList -> itemForMenu.isWriter + else -> false + } - CommentTextField( - modifier = Modifier.fillMaxWidth(), - hint = stringResource(R.string.reply_to), - input = inputText, - onInputChange = { inputText = it }, - onSendClick = { - onSendReply( - inputText, - replyingToCommentId, - replyingToNickname + MenuBottomSheet( + items = if (isWriter) { + listOf( + MenuBottomSheetItem( + text = stringResource(R.string.delete), + color = colors.Red, + onClick = { + itemToDelete = itemForMenu + showDeleteDialog = true + selectedCommentForMenu = null + selectedReplyForMenu = null + } ) - inputText = "" - replyingToCommentId = null - replyingToNickname = null + ) + } else { + listOf( + MenuBottomSheetItem( + text = stringResource(R.string.report), + color = colors.Red, + onClick = { + // TODO: 신고 로직 + selectedCommentForMenu = null + selectedReplyForMenu = null + } + ) + ) + }, + onDismiss = { + selectedCommentForMenu = null + selectedReplyForMenu = null + } + ) + } + + if (showDeleteDialog) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + DialogPopup( + title = stringResource(R.string.delete_comment_title), // todo: 댓글 삭제 모달 임의로 설정 + description = stringResource(R.string.delete_post_content), + onConfirm = { + val commentId = when (val item = itemToDelete) { + is CommentList -> item.commentId + is ReplyList -> item.commentId + else -> null + } + commentId?.let { onEvent(CommentsEvent.DeleteComment(it)) } + showDeleteDialog = false + itemToDelete = null }, - replyTo = replyingToNickname, - onCancelReply = { - replyingToCommentId = null - replyingToNickname = null + onCancel = { + showDeleteDialog = false + itemToDelete = null } ) } } + } @Composable @@ -125,7 +224,9 @@ private fun CommentLazyList( isLastPage: Boolean, onLoadMore: () -> Unit, onReplyClick: (commentId: Int, nickname: String) -> Unit, - onEvent: (CommentsEvent) -> Unit + onEvent: (CommentsEvent) -> Unit, + onCommentLongPress: (CommentList) -> Unit, + onReplyLongPress: (ReplyList) -> Unit ) { val lazyListState = rememberLazyListState() @@ -152,7 +253,9 @@ private fun CommentLazyList( CommentSection( commentItem = comment, onReplyClick = onReplyClick, - onEvent = onEvent + onEvent = onEvent, + onCommentLongPress = onCommentLongPress, + onReplyLongPress = onReplyLongPress ) } @@ -216,6 +319,7 @@ private fun CommentBottomSheetPreview() { aliasColor = "#A0F8E8", isDeleted = false, isLike = false, + isWriter = false, replyList = emptyList() ) ), diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt index af7a5ad5..7b848062 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt @@ -1,6 +1,7 @@ package com.texthip.thip.ui.group.note.component import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -11,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -28,56 +30,67 @@ fun CommentItem( modifier: Modifier = Modifier, data: CommentList, onReplyClick: (String) -> Unit = { }, - onLikeClick: () -> Unit = {} + onLikeClick: () -> Unit = {}, + onLongPress: () -> Unit = {} ) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - ProfileBarFeed( - profileImage = data.creatorProfileImageUrl, - nickname = data.creatorNickname, - genreName = data.aliasName, - genreColor = hexToColor(data.aliasColor), - date = data.postDate + if (data.isDeleted) { + Text( + text = stringResource(R.string.comment_deleted), + style = typography.feedcopy_r400_s14_h20, + color = colors.Grey02, ) - - Row( - horizontalArrangement = Arrangement.spacedBy(20.dp) + } else { + Column( + modifier = modifier.pointerInput(Unit) { + detectTapGestures(onLongPress = { onLongPress() }) + }, + verticalArrangement = Arrangement.spacedBy(12.dp) ) { - Column( - modifier = Modifier - .weight(1f), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = data.content, - color = colors.Grey, - style = typography.feedcopy_r400_s14_h20, - ) - Text( - modifier = Modifier.clickable(onClick = { onReplyClick(data.creatorNickname) }), - text = stringResource(R.string.write_reply), - style = typography.menu_sb600_s12, - color = colors.Grey02, - ) - } + ProfileBarFeed( + profileImage = data.creatorProfileImageUrl, + nickname = data.creatorNickname, + genreName = data.aliasName, + genreColor = hexToColor(data.aliasColor), + date = data.postDate + ) - Column( - modifier = Modifier.clickable(onClick = onLikeClick), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(2.dp), + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp) ) { - Icon( - painter = painterResource(if (data.isLike) R.drawable.ic_heart_center_filled else R.drawable.ic_heart_center), - contentDescription = null, - tint = Color.Unspecified - ) - Text( - text = data.likeCount.toString(), - style = typography.navi_m500_s10, - color = colors.White, - ) + Column( + modifier = Modifier + .weight(1f), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = data.content, + color = colors.Grey, + style = typography.feedcopy_r400_s14_h20, + ) + Text( + modifier = Modifier.clickable(onClick = { onReplyClick(data.creatorNickname) }), + text = stringResource(R.string.write_reply), + style = typography.menu_sb600_s12, + color = colors.Grey02, + ) + } + + Column( + modifier = Modifier.clickable(onClick = onLikeClick), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Icon( + painter = painterResource(if (data.isLike) R.drawable.ic_heart_center_filled else R.drawable.ic_heart_center), + contentDescription = null, + tint = Color.Unspecified + ) + Text( + text = data.likeCount.toString(), + style = typography.navi_m500_s10, + color = colors.White, + ) + } } } } @@ -100,13 +113,14 @@ private fun CommentItemPreview() { creatorId = 1, creatorNickname = "User1", creatorProfileImageUrl = "https://example.com/image1.jpg", - aliasName= "칭호칭호", + aliasName = "칭호칭호", aliasColor = "#FF5733", content = "This is a comment.", postDate = "2023-10-01T12:00:00Z", isLike = false, likeCount = 10, isDeleted = false, + isWriter = false, replyList = emptyList() ) ) @@ -117,13 +131,14 @@ private fun CommentItemPreview() { creatorId = 1, creatorNickname = "User1", creatorProfileImageUrl = "https://example.com/image1.jpg", - aliasName= "칭호칭호", + aliasName = "칭호칭호", aliasColor = "#FF5733", content = "This is a comment.", postDate = "2023-10-01T12:00:00Z", isLike = false, likeCount = 10, isDeleted = false, + isWriter = false, replyList = emptyList() ) ) @@ -134,13 +149,14 @@ private fun CommentItemPreview() { creatorId = 1, creatorNickname = "User1", creatorProfileImageUrl = "https://example.com/image1.jpg", - aliasName= "칭호칭호", + aliasName = "칭호칭호", aliasColor = "#FF5733", content = "This is a comment.", postDate = "2023-10-01T12:00:00Z", isLike = false, likeCount = 10, isDeleted = false, + isWriter = false, replyList = emptyList() ) ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt index fcc2ea4d..4afe8e57 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt @@ -11,15 +11,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.texthip.thip.data.model.comments.response.CommentList +import com.texthip.thip.data.model.comments.response.ReplyList import com.texthip.thip.ui.group.note.viewmodel.CommentsEvent import com.texthip.thip.ui.theme.ThipTheme @Composable fun CommentSection( commentItem: CommentList, - onSendReply: (String, Int?, String?) -> Unit = { _, _, _ -> }, onReplyClick: (commentId: Int, nickname: String) -> Unit, - onEvent: (CommentsEvent) -> Unit = { _ -> } + onEvent: (CommentsEvent) -> Unit = { _ -> }, + onCommentLongPress: (CommentList) -> Unit = { _ -> }, + onReplyLongPress: (ReplyList) -> Unit = { _ -> }, ) { Box { Column( @@ -32,7 +34,8 @@ fun CommentSection( CommentItem( data = commentItem, onReplyClick = { onReplyClick(commentItem.commentId, commentItem.creatorNickname) }, - onLikeClick = { onEvent(CommentsEvent.LikeComment(commentItem.commentId)) } + onLikeClick = { onEvent(CommentsEvent.LikeComment(commentItem.commentId)) }, + onLongPress = { onCommentLongPress(commentItem) } ) commentItem.replyList.forEach { reply -> @@ -46,8 +49,8 @@ fun CommentSection( reply.commentId ) ) - } - + }, + onLongPress = { onReplyLongPress(reply) } ) } } @@ -73,10 +76,9 @@ fun CommentSectionPreview() { isLike = false, likeCount = 10, isDeleted = false, - replyList = emptyList() - + replyList = emptyList(), + isWriter = false ), - onSendReply = { _, _, _ -> }, onReplyClick = { commentId, nickname -> // Handle reply click } diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt index d0729746..19b0d6c9 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/ReplyItem.kt @@ -1,6 +1,7 @@ package com.texthip.thip.ui.group.note.component import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -11,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.buildAnnotatedString @@ -30,7 +32,8 @@ fun ReplyItem( modifier: Modifier = Modifier, data: ReplyList, onReplyClick: () -> Unit = { }, - onLikeClick: () -> Unit = {} + onLikeClick: () -> Unit = {}, + onLongPress: () -> Unit = {} ) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -42,7 +45,9 @@ fun ReplyItem( ) Column( - modifier = modifier, + modifier = modifier.pointerInput(Unit) { + detectTapGestures(onLongPress = { onLongPress() }) + }, verticalArrangement = Arrangement.spacedBy(12.dp) ) { ProfileBarFeed( @@ -130,6 +135,7 @@ private fun ReplyItemPreview() { content = "This is a reply.", postDate = "2023-10-01T12:05:00Z", isLike = false, + isWriter = false, likeCount = 5 ) ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt index 429b6d0f..12d6a6fc 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt @@ -176,7 +176,6 @@ fun GroupNoteContent( } val tabs = listOf(stringResource(R.string.group_record), stringResource(R.string.my_record)) - val sortOptions = remember { SortType.entries.map { it.displayNameRes } } val sortDisplayStrings = remember { SortType.entries.map { it.displayNameRes } } .map { stringResource(it) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt index 4247ab32..b4ded9b8 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt @@ -24,6 +24,7 @@ sealed interface CommentsEvent { data class LikeComment(val commentId: Int) : CommentsEvent // 댓글 좋아요 이벤트 data class LikeReply(val parentCommentId: Int, val replyId: Int) : CommentsEvent // 대댓글 좋아요 이벤트 data class CreateComment(val content: String, val parentId: Int?) : CommentsEvent + data class DeleteComment(val commentId: Int) : CommentsEvent } @HiltViewModel @@ -54,6 +55,39 @@ class CommentsViewModel @Inject constructor( content = event.content, parentId = event.parentId ) + + is CommentsEvent.DeleteComment -> deleteComment(event.commentId) + } + } + + private fun deleteComment(commentId: Int) { + val originalComments = _uiState.value.comments + var targetPostId: Long? = null // 삭제 성공 시 postId를 저장할 변수 + + val newComments = originalComments.map { comment -> + // 부모 댓글이 삭제 대상인 경우 + if (comment.commentId == commentId) { + targetPostId = currentPostId // postId 저장 + comment.copy(isDeleted = true) + } else { + // 답글이 삭제 대상인 경우 + comment.copy( + replyList = comment.replyList.map { reply -> + reply + } + ) + } + } + _uiState.update { it.copy(comments = newComments) } + + viewModelScope.launch { + commentsRepository.deleteComment(commentId.toLong()) + .onSuccess { response -> + // 성공 시 별도 처리 필요 없음 + } + .onFailure { + _uiState.update { it.copy(comments = originalComments, error = "삭제 실패") } + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e2942465..4658decf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -236,6 +236,8 @@ 댓글 많은 순 기록을 게시 중입니다... 기록이 게시되었습니다! + 이 댓글을 삭제하시겠어요? + 삭제된 댓글이에요. 피드 From 10adad151b3066de533d2df527ea726a61f134e5 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 23:04:22 +0900 Subject: [PATCH 11/51] =?UTF-8?q?[refactor]:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EB=90=90=EC=9D=84=20=EB=95=8C=20id=20null?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=94=EB=80=8C=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comments/response/CommentsResponse.kt | 16 ++++----- .../note/component/CommentBottomSheet.kt | 6 ++-- .../ui/group/note/component/CommentItem.kt | 23 ++++++------ .../ui/group/note/component/CommentSection.kt | 36 ++++++++++++++----- .../ui/group/note/screen/GroupNoteScreen.kt | 12 ++++--- .../group/note/viewmodel/CommentsViewModel.kt | 12 ++++--- 6 files changed, 66 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt b/app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt index 6df91629..3e35e956 100644 --- a/app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/comments/response/CommentsResponse.kt @@ -11,14 +11,14 @@ data class CommentsResponse( @Serializable data class CommentList( - val commentId: Int, - val creatorId: Int, - val creatorProfileImageUrl: String, - val creatorNickname: String, - val aliasName: String, - val aliasColor: String, - val postDate: String, - val content: String, + val commentId: Int?, + val creatorId: Int?, + val creatorProfileImageUrl: String?, + val creatorNickname: String?, + val aliasName: String?, + val aliasColor: String?, + val postDate: String?, + val content: String?, val likeCount: Int, val isDeleted: Boolean, val isWriter: Boolean, diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt index 090b8330..c3fecb4a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt @@ -223,7 +223,7 @@ private fun CommentLazyList( isLoadingMore: Boolean, isLastPage: Boolean, onLoadMore: () -> Unit, - onReplyClick: (commentId: Int, nickname: String) -> Unit, + onReplyClick: (commentId: Int, nickname: String?) -> Unit, onEvent: (CommentsEvent) -> Unit, onCommentLongPress: (CommentList) -> Unit, onReplyLongPress: (ReplyList) -> Unit @@ -248,7 +248,9 @@ private fun CommentLazyList( LazyColumn(state = lazyListState) { items( items = commentList, - key = { it.commentId } + key = { comment -> + comment.commentId ?: comment.replyList.first().commentId + } ) { comment -> CommentSection( commentItem = comment, diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt index 7b848062..fe6ec29f 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentItem.kt @@ -29,7 +29,7 @@ import com.texthip.thip.utils.color.hexToColor fun CommentItem( modifier: Modifier = Modifier, data: CommentList, - onReplyClick: (String) -> Unit = { }, + onReplyClick: (String?) -> Unit = { }, onLikeClick: () -> Unit = {}, onLongPress: () -> Unit = {} ) { @@ -46,12 +46,13 @@ fun CommentItem( }, verticalArrangement = Arrangement.spacedBy(12.dp) ) { + data ProfileBarFeed( profileImage = data.creatorProfileImageUrl, - nickname = data.creatorNickname, - genreName = data.aliasName, - genreColor = hexToColor(data.aliasColor), - date = data.postDate + nickname = data.creatorNickname ?: "", + genreName = data.aliasName ?: "", + genreColor = hexToColor(data.aliasColor ?: "#FFFFFF"), + date = data.postDate ?: "" ) Row( @@ -62,11 +63,13 @@ fun CommentItem( .weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - Text( - text = data.content, - color = colors.Grey, - style = typography.feedcopy_r400_s14_h20, - ) + data.content?.let { + Text( + text = it, + color = colors.Grey, + style = typography.feedcopy_r400_s14_h20, + ) + } Text( modifier = Modifier.clickable(onClick = { onReplyClick(data.creatorNickname) }), text = stringResource(R.string.write_reply), diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt index 4afe8e57..d05677db 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt @@ -18,7 +18,7 @@ import com.texthip.thip.ui.theme.ThipTheme @Composable fun CommentSection( commentItem: CommentList, - onReplyClick: (commentId: Int, nickname: String) -> Unit, + onReplyClick: (commentId: Int, nickname: String?) -> Unit, onEvent: (CommentsEvent) -> Unit = { _ -> }, onCommentLongPress: (CommentList) -> Unit = { _ -> }, onReplyLongPress: (ReplyList) -> Unit = { _ -> }, @@ -33,22 +33,40 @@ fun CommentSection( ) { CommentItem( data = commentItem, - onReplyClick = { onReplyClick(commentItem.commentId, commentItem.creatorNickname) }, - onLikeClick = { onEvent(CommentsEvent.LikeComment(commentItem.commentId)) }, + onReplyClick = { + // commentId가 null이 아닐 때만 답글 달기 가능 + // todo: 수정 가능 + commentItem.commentId?.let { id -> + onReplyClick(id, commentItem.creatorNickname) + } + }, + onLikeClick = { + // commentId가 null이 아닐 때만 좋아요 가능 + // todo: 수정 가능 + commentItem.commentId?.let { id -> + onEvent(CommentsEvent.LikeComment(id)) + } + }, onLongPress = { onCommentLongPress(commentItem) } ) commentItem.replyList.forEach { reply -> ReplyItem( data = reply, - onReplyClick = { onReplyClick(commentItem.commentId, reply.creatorNickname) }, + onReplyClick = { + commentItem.commentId?.let { parentId -> + onReplyClick(parentId, reply.creatorNickname) + } + }, onLikeClick = { - onEvent( - CommentsEvent.LikeReply( - commentItem.commentId, - reply.commentId + commentItem.commentId?.let { parentId -> + onEvent( + CommentsEvent.LikeReply( + parentId, + reply.commentId + ) ) - ) + } }, onLongPress = { onReplyLongPress(reply) } ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt index 12d6a6fc..5f7d8eae 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt @@ -472,11 +472,13 @@ fun GroupNoteContent( } if (isCommentBottomSheetVisible && selectedPostForComment != null) { - LaunchedEffect(selectedPostForComment) { - commentsViewModel.initialize( - postId = selectedPostForComment!!.postId.toLong(), - postType = selectedPostForComment!!.postType - ) + LaunchedEffect(selectedPostForComment?.postId) { + selectedPostForComment?.let { post -> + commentsViewModel.initialize( + postId = post.postId.toLong(), + postType = post.postType + ) + } } CommentBottomSheet( diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt index b4ded9b8..6b88081c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt @@ -62,18 +62,20 @@ class CommentsViewModel @Inject constructor( private fun deleteComment(commentId: Int) { val originalComments = _uiState.value.comments - var targetPostId: Long? = null // 삭제 성공 시 postId를 저장할 변수 val newComments = originalComments.map { comment -> - // 부모 댓글이 삭제 대상인 경우 if (comment.commentId == commentId) { - targetPostId = currentPostId // postId 저장 comment.copy(isDeleted = true) } else { - // 답글이 삭제 대상인 경우 comment.copy( replyList = comment.replyList.map { reply -> - reply + if (reply.commentId == commentId) { + reply + } else { + reply + } + }.filterNot { reply -> + reply.commentId == commentId } ) } From 5ed4e6835ca8c311983a826db350dca13578984f Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 23:11:13 +0900 Subject: [PATCH 12/51] =?UTF-8?q?[refactor]:=20=EC=82=AD=EC=A0=9C=EB=90=9C?= =?UTF-8?q?=20=EB=8C=93=EA=B8=80=EC=9D=98=20=EB=8B=B5=EA=B8=80=EC=97=90=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=EB=88=84=EB=A5=BC=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/note/component/CommentSection.kt | 9 +---- .../group/note/viewmodel/CommentsViewModel.kt | 39 ++++++++++++------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt index d05677db..bd50ea28 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentSection.kt @@ -59,14 +59,7 @@ fun CommentSection( } }, onLikeClick = { - commentItem.commentId?.let { parentId -> - onEvent( - CommentsEvent.LikeReply( - parentId, - reply.commentId - ) - ) - } + onEvent(CommentsEvent.LikeReply(reply.commentId)) }, onLongPress = { onReplyLongPress(reply) } ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt index 6b88081c..0d276ddf 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt @@ -3,6 +3,7 @@ package com.texthip.thip.ui.group.note.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.model.comments.response.CommentList +import com.texthip.thip.data.model.comments.response.ReplyList import com.texthip.thip.data.repository.CommentsRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -22,7 +23,7 @@ data class CommentsUiState( sealed interface CommentsEvent { data object LoadMoreComments : CommentsEvent data class LikeComment(val commentId: Int) : CommentsEvent // 댓글 좋아요 이벤트 - data class LikeReply(val parentCommentId: Int, val replyId: Int) : CommentsEvent // 대댓글 좋아요 이벤트 + data class LikeReply(val replyId: Int) : CommentsEvent // 답글 좋아요 이벤트 data class CreateComment(val content: String, val parentId: Int?) : CommentsEvent data class DeleteComment(val commentId: Int) : CommentsEvent } @@ -50,7 +51,7 @@ class CommentsViewModel @Inject constructor( when (event) { is CommentsEvent.LoadMoreComments -> fetchComments(isRefresh = false) is CommentsEvent.LikeComment -> toggleCommentLike(event.commentId) - is CommentsEvent.LikeReply -> toggleReplyLike(event.parentCommentId, event.replyId) + is CommentsEvent.LikeReply -> toggleReplyLike(event.replyId) is CommentsEvent.CreateComment -> createComment( content = event.content, parentId = event.parentId @@ -140,33 +141,43 @@ class CommentsViewModel @Inject constructor( } } - private fun toggleReplyLike(parentCommentId: Int, replyId: Int) { - // 부모 댓글 및 대댓글 찾기 + private fun toggleReplyLike(replyId: Int) { val comments = _uiState.value.comments - val parentCommentIndex = comments.indexOfFirst { it.commentId == parentCommentId } - if (parentCommentIndex == -1) return + var parentComment: CommentList? = null + var reply: ReplyList? = null + var parentCommentIndex = -1 + var replyIndex = -1 + + // 전체 댓글 목록을 돌면서 좋아요 누른 답글과 그 부모를 찾기 + for ((pIndex, pComment) in comments.withIndex()) { + val rIndex = pComment.replyList.indexOfFirst { it.commentId == replyId } + if (rIndex != -1) { + parentComment = pComment + reply = pComment.replyList[rIndex] + parentCommentIndex = pIndex + replyIndex = rIndex + break // 찾았으면 루프 종료 + } + } - val parentComment = comments[parentCommentIndex] - val replyIndex = parentComment.replyList.indexOfFirst { it.commentId == replyId } - if (replyIndex == -1) return + // 답글이나 부모를 못 찾으면 함수 종료 + if (parentComment == null || reply == null || parentCommentIndex == -1 || replyIndex == -1) return - val reply = parentComment.replyList[replyIndex] val currentIsLiked = reply.isLike val newLikeCount = if (currentIsLiked) reply.likeCount - 1 else reply.likeCount + 1 // 즉시 UI 업데이트 val updatedReply = reply.copy(isLike = !currentIsLiked, likeCount = newLikeCount) - val newReplyList = - parentComment.replyList.toMutableList().apply { set(replyIndex, updatedReply) } + val newReplyList = parentComment.replyList.toMutableList().apply { set(replyIndex, updatedReply) } val updatedParentComment = parentComment.copy(replyList = newReplyList) - val newComments = - comments.toMutableList().apply { set(parentCommentIndex, updatedParentComment) } + val newComments = comments.toMutableList().apply { set(parentCommentIndex, updatedParentComment) } _uiState.update { it.copy(comments = newComments) } viewModelScope.launch { commentsRepository.likeComment(replyId.toLong(), !currentIsLiked) .onFailure { _uiState.update { + // 실패 시 롤백 로직 (기존과 동일) val originalComments = it.comments.toMutableList() originalComments[parentCommentIndex] = parentComment it.copy(comments = originalComments) From 8e6380478531ab8844d96d1040382bce3b200960 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 23:12:34 +0900 Subject: [PATCH 13/51] =?UTF-8?q?[refactor]:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20=EB=8B=AB=EC=9C=BC?= =?UTF-8?q?=EB=A9=B4=20=EA=B8=B0=EB=A1=9D=20=EC=9E=AC=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt | 1 + .../texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt index 5f7d8eae..817cb231 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt @@ -487,6 +487,7 @@ fun GroupNoteContent( onDismiss = { isCommentBottomSheetVisible = false selectedPostForComment = null + onEvent(GroupNoteEvent.RefreshPosts) }, onSendReply = { text, parentId, _ -> if (text.isNotBlank()) { diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt index 3149dedb..4315555b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt @@ -49,6 +49,7 @@ sealed interface GroupNoteEvent { data class OnVote(val postId: Int, val voteItemId: Int, val type: Boolean) : GroupNoteEvent data class OnDeleteRecord(val postId: Int, val postType: String) : GroupNoteEvent data class OnLikeRecord(val postId: Int, val postType: String) : GroupNoteEvent + data object RefreshPosts : GroupNoteEvent } @@ -146,6 +147,7 @@ class GroupNoteViewModel @Inject constructor( is GroupNoteEvent.OnDeleteRecord -> deletePost(event.postId, event.postType) is GroupNoteEvent.OnLikeRecord -> likeRecord(event.postId, event.postType) + is GroupNoteEvent.RefreshPosts -> loadPosts(isRefresh = true) } } From ffce475af04bd05513bdbd2d1ec7be4f6ae720bd Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Fri, 15 Aug 2025 23:34:01 +0900 Subject: [PATCH 14/51] =?UTF-8?q?[refactor]:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../note/component/CommentBottomSheet.kt | 52 ++++++------------- .../group/note/viewmodel/CommentsViewModel.kt | 34 +++++++----- .../note/viewmodel/GroupNoteViewModel.kt | 4 ++ 3 files changed, 39 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt index c3fecb4a..72437913 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt @@ -33,7 +33,6 @@ import com.texthip.thip.data.model.comments.response.ReplyList import com.texthip.thip.ui.common.bottomsheet.CustomBottomSheet import com.texthip.thip.ui.common.bottomsheet.MenuBottomSheet import com.texthip.thip.ui.common.forms.CommentTextField -import com.texthip.thip.ui.common.modal.DialogPopup import com.texthip.thip.ui.group.note.viewmodel.CommentsEvent import com.texthip.thip.ui.group.note.viewmodel.CommentsUiState import com.texthip.thip.ui.group.room.mock.MenuBottomSheetItem @@ -55,17 +54,14 @@ fun CommentBottomSheet( var selectedCommentForMenu by remember { mutableStateOf(null) } var selectedReplyForMenu by remember { mutableStateOf(null) } - var showDeleteDialog by remember { mutableStateOf(false) } - var itemToDelete by remember { mutableStateOf(null) } // 삭제할 댓글/답글 임시 저장 - val isOverlayVisible = - selectedCommentForMenu != null || selectedReplyForMenu != null || showDeleteDialog + val isOverlayVisible = selectedCommentForMenu != null || selectedReplyForMenu != null Box( if (isOverlayVisible) { Modifier .fillMaxSize() - .background(color = colors.Black.copy(alpha = 0.8f),) + .background(color = colors.Black.copy(alpha = 0.8f)) .blur(5.dp) } else { Modifier.fillMaxSize() @@ -161,8 +157,13 @@ fun CommentBottomSheet( text = stringResource(R.string.delete), color = colors.Red, onClick = { - itemToDelete = itemForMenu - showDeleteDialog = true + val commentId = when (val item = itemForMenu) { + is CommentList -> item.commentId + is ReplyList -> item.commentId + else -> null + } + commentId?.let { onEvent(CommentsEvent.DeleteComment(it)) } + selectedCommentForMenu = null selectedReplyForMenu = null } @@ -187,34 +188,6 @@ fun CommentBottomSheet( } ) } - - if (showDeleteDialog) { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - DialogPopup( - title = stringResource(R.string.delete_comment_title), // todo: 댓글 삭제 모달 임의로 설정 - description = stringResource(R.string.delete_post_content), - onConfirm = { - val commentId = when (val item = itemToDelete) { - is CommentList -> item.commentId - is ReplyList -> item.commentId - else -> null - } - commentId?.let { onEvent(CommentsEvent.DeleteComment(it)) } - showDeleteDialog = false - itemToDelete = null - }, - onCancel = { - showDeleteDialog = false - itemToDelete = null - } - ) - } - } - } @Composable @@ -249,7 +222,12 @@ private fun CommentLazyList( items( items = commentList, key = { comment -> - comment.commentId ?: comment.replyList.first().commentId + // commentId가 있으면 사용 + comment.commentId + // 없다면(삭제된 댓글), replyList의 첫 번째 항목 ID를 사용 + ?: comment.replyList.firstOrNull()?.commentId + // 그것마저 없다면(마지막 답글까지 삭제된 경우), 객체 자체의 hashCode를 사용 + ?: comment.hashCode() } ) { comment -> CommentSection( diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt index 0d276ddf..07390989 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/CommentsViewModel.kt @@ -41,7 +41,6 @@ class CommentsViewModel @Inject constructor( private var currentPostType: String = "RECORD" fun initialize(postId: Long, postType: String) { - if (currentPostId == postId) return this.currentPostId = postId this.currentPostType = postType fetchComments(isRefresh = true) @@ -64,23 +63,30 @@ class CommentsViewModel @Inject constructor( private fun deleteComment(commentId: Int) { val originalComments = _uiState.value.comments - val newComments = originalComments.map { comment -> - if (comment.commentId == commentId) { - comment.copy(isDeleted = true) + // 삭제하려는 대상이 부모 댓글인지 먼저 확인 + val parentCommentToDelete = originalComments.firstOrNull { it.commentId == commentId } + + val newComments = if (parentCommentToDelete != null) { + // 부모 댓글을 삭제하는 경우 + if (parentCommentToDelete.replyList.isEmpty()) { + // 답글이 없으면 목록에서 완전히 제거 + originalComments.filterNot { it.commentId == commentId } } else { - comment.copy( - replyList = comment.replyList.map { reply -> - if (reply.commentId == commentId) { - reply - } else { - reply - } - }.filterNot { reply -> - reply.commentId == commentId - } + // 답글이 있으면 "삭제됨" 상태로 변경 + originalComments.map { + if (it.commentId == commentId) it.copy(isDeleted = true) else it + } + } + } else { + // 답글을 삭제하는 경우 + // 모든 부모 댓글을 순회하며, 해당하는 답글만 목록에서 제거 + originalComments.map { parentComment -> + parentComment.copy( + replyList = parentComment.replyList.filterNot { reply -> reply.commentId == commentId } ) } } + _uiState.update { it.copy(comments = newComments) } viewModelScope.launch { diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt index 4315555b..ff1bcae1 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt @@ -1,5 +1,6 @@ package com.texthip.thip.ui.group.note.viewmodel +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.model.rooms.request.RoomsPostsRequestParams @@ -148,6 +149,9 @@ class GroupNoteViewModel @Inject constructor( is GroupNoteEvent.OnDeleteRecord -> deletePost(event.postId, event.postType) is GroupNoteEvent.OnLikeRecord -> likeRecord(event.postId, event.postType) is GroupNoteEvent.RefreshPosts -> loadPosts(isRefresh = true) + else -> { + Log.w("GroupNoteViewModel", "Unhandled event received: $event") + } } } From e84f0bac3a4b12243d457271694ad20e75649708 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 13:09:40 +0900 Subject: [PATCH 15/51] =?UTF-8?q?[ui]:=20bottom=20sheet=20=EB=9C=B0=20?= =?UTF-8?q?=EB=95=8C=20=EB=B0=B0=EA=B2=BD=EC=83=89=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt | 1 + .../texthip/thip/ui/group/note/component/CommentBottomSheet.kt | 2 -- app/src/main/java/com/texthip/thip/ui/theme/Color.kt | 3 +++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt b/app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt index 6cc0aa71..ec81fc0e 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt @@ -63,6 +63,7 @@ fun CustomBottomSheet( Box( modifier = Modifier .fillMaxSize() + .background(color = colors.Black30) .clickable( indication = null, interactionSource = remember { MutableInteractionSource() } diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt index 72437913..f5f2ac63 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/CommentBottomSheet.kt @@ -1,6 +1,5 @@ package com.texthip.thip.ui.group.note.component -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -61,7 +60,6 @@ fun CommentBottomSheet( if (isOverlayVisible) { Modifier .fillMaxSize() - .background(color = colors.Black.copy(alpha = 0.8f)) .blur(5.dp) } else { Modifier.fillMaxSize() diff --git a/app/src/main/java/com/texthip/thip/ui/theme/Color.kt b/app/src/main/java/com/texthip/thip/ui/theme/Color.kt index 89d77798..41c82ba2 100644 --- a/app/src/main/java/com/texthip/thip/ui/theme/Color.kt +++ b/app/src/main/java/com/texthip/thip/ui/theme/Color.kt @@ -41,6 +41,7 @@ val DarkGrey02 = Color(0xFF282828) val DarkGrey01 = Color(0x4B4B4B4B) val Black = Color(0xFF121212) val Black50 = Color(0x80121212) +val Black30 = Color(0x4D121212) val Black10 = Color(0x1A121212) val Black00 = Color(0x00121212) val Black700 = Color(0xFF090909) @@ -82,6 +83,7 @@ data class ThipColors( val DarkGrey02: Color, val Black: Color, val Black50: Color, + val Black30: Color, val Black10: Color, val Black00: Color, val Black800: Color, @@ -121,6 +123,7 @@ val defaultThipColors = ThipColors( DarkGrey02 = DarkGrey02, Black = Black, Black50 = Black50, + Black30 = Black30, Black10 = Black10, Black00 = Black00, Black700 = Black700, From 2756b8ea7c3e05547952f318dbd5afb75bb77dda Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 13:41:10 +0900 Subject: [PATCH 16/51] =?UTF-8?q?[chore]:=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?svg=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/feed/component/MySubscribelistBar.kt | 2 +- .../main/res/drawable/ic_search_character.xml | 22 ++++++++++++++++++ .../res/drawable/search_character_image.png | Bin 920 -> 0 bytes 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/ic_search_character.xml delete mode 100644 app/src/main/res/drawable/search_character_image.png diff --git a/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt b/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt index eea3a257..c5f0f0ae 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt @@ -143,7 +143,7 @@ private fun EmptyMySubscriptionBar() { ) Icon( - painter = painterResource(id = R.drawable.search_character_image), + painter = painterResource(id = R.drawable.ic_search_character), contentDescription = null, tint = Color.Unspecified, modifier = Modifier diff --git a/app/src/main/res/drawable/ic_search_character.xml b/app/src/main/res/drawable/ic_search_character.xml new file mode 100644 index 00000000..2485534e --- /dev/null +++ b/app/src/main/res/drawable/ic_search_character.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/drawable/search_character_image.png b/app/src/main/res/drawable/search_character_image.png deleted file mode 100644 index 0e8a899c95dca599d65d3f3efda35a5832d8c506..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 920 zcmV;J184k+P)00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP?*`9NNIo$nN+4Ulu$q< zTp}eD=2Lhx@AmdM+XprnJ4O7Y)9vl&=FQBT-32=6po0$ncUX-~T3%MMav9M;0HaDF zWLXHYU)sN={Y#!hX&2II>jKY&Rmbq6{CL9{tot(y1)2Xa`HsbBqSP}nT(Y}qXb^lxNAVC<-=q82vmK4eof~*; z$F$-p0#v%Z7SI|*98En=K(K<=BIY<*OTY=wd$b-4z69>>9?ETC00@U`R}Ky7v1Z`t zp#s6`f#G2wkpNClfovAIy3+mA(|T-o7uegwi{Q7R84W~CM?Md%u4->nsRyq&Hnd?9 zA;G7>HH;XadIELV^dSgj+FOb`MibweSLN` zM1bh&(eJ@Q|8W}e5*Qr?mX?%DzF5@#V`Ge(0{1*2@uw~U3XmRhAyF#n_rwGcjl%Wr z=B9F8Jg$weuUqaKf?6t-luhFL+B#3CE7#c?kfXL>I+)|hNuak^f746mbaoaMp*=pX z^qzAlTCZ_~ogGbTZcg=8kJ$v?ys=L&*Pxd@g-6fWHW6WnO?43AuyWQue!8EWXs*@$ zrVQN(92{tY8Dj>l0(!~N`*}%FH`S@}jtOc+81S1+1Aa#6wO_yS+?ygV`ulZ-D5VO% z1iHIxnLP(g4^u}W(wGa{_<60%u~XA0KVl%2xMOG{Fw>}oQC?&|7# z2R!JFuw&<3M9$G9s1a^NBO{enbaZ4D=I3ppM$6^$VlWu|Zr0clklSuZO0j8BBa8$= z0v^2-5(z6M8psukfi2mvcvb6j35fBAZoSWeSFBQHGsz@(K`Ue;V_-B8K?~uWf}cFZ zewwkG)36;7LYt;-A`nHJz!OpOe`vk~4x_jDM+opf=yz3W6{v?~Qq8yoy!XL&L8|VX uv-cn%-p9~pI8~7&32LAn*Kt2YNa8Q`=rjdk-U|Q#0000 Date: Sat, 16 Aug 2025 14:27:05 +0900 Subject: [PATCH 17/51] =?UTF-8?q?[refactor]:=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=ED=94=BC=EB=93=9C=20=EC=9E=91=EC=84=B1=ED=95=9C=20=ED=8C=94?= =?UTF-8?q?=EB=A1=9C=EC=9E=89=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../users/response/MyFollowingsResponse.kt | 12 --- .../UsersMyFollowingsRecentFeedsResponse.kt | 15 ++++ .../thip/data/repository/UserRepository.kt | 9 +- .../texthip/thip/data/service/UserService.kt | 8 +- .../ui/feed/component/MySubscribelistBar.kt | 15 ++-- .../texthip/thip/ui/feed/screen/FeedScreen.kt | 84 ++++--------------- .../thip/ui/feed/viewmodel/FeedViewModel.kt | 9 +- app/src/main/res/values/strings.xml | 1 + 8 files changed, 52 insertions(+), 101 deletions(-) create mode 100644 app/src/main/java/com/texthip/thip/data/model/users/response/UsersMyFollowingsRecentFeedsResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt b/app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt index 21b395df..aa96cb6e 100644 --- a/app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt @@ -19,16 +19,4 @@ data class FollowingList( @SerializedName("aliasName") val aliasName: String, @SerializedName("aliasColor") val aliasColor: String, @SerializedName("isFollowing") val isFollowing: Boolean -) - -@Serializable -data class MyRecentFollowingsResponse( - @SerializedName("recentWriters") val recentWriters: List -) - -@Serializable -data class RecentWriterList( - @SerializedName("userId") val userId: Long, - @SerializedName("nickname") val nickname: String, - @SerializedName("profileImageUrl") val profileImageUrl: String? ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/users/response/UsersMyFollowingsRecentFeedsResponse.kt b/app/src/main/java/com/texthip/thip/data/model/users/response/UsersMyFollowingsRecentFeedsResponse.kt new file mode 100644 index 00000000..e275b5d7 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/users/response/UsersMyFollowingsRecentFeedsResponse.kt @@ -0,0 +1,15 @@ +package com.texthip.thip.data.model.users.response + +import kotlinx.serialization.Serializable + +@Serializable +data class UsersMyFollowingsRecentFeedsResponse( + val recentWriters: List +) + +@Serializable +data class RecentWriterList( + val userId: Long, + val nickname: String, + val profileImageUrl: String +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt index 5cfd6244..425c76c1 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt @@ -2,12 +2,11 @@ package com.texthip.thip.data.repository import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.users.request.FollowRequest -import com.texthip.thip.data.model.users.response.MyFollowingsResponse -import com.texthip.thip.data.model.users.response.MyPageInfoResponse import com.texthip.thip.data.model.users.request.NicknameRequest import com.texthip.thip.data.model.users.response.AliasChoiceResponse import com.texthip.thip.data.model.users.response.FollowResponse -import com.texthip.thip.data.model.users.response.MyRecentFollowingsResponse +import com.texthip.thip.data.model.users.response.MyFollowingsResponse +import com.texthip.thip.data.model.users.response.MyPageInfoResponse import com.texthip.thip.data.model.users.response.NicknameResponse import com.texthip.thip.data.model.users.response.OthersFollowersResponse import com.texthip.thip.data.service.UserService @@ -28,8 +27,8 @@ class UserRepository @Inject constructor( .getOrThrow() } - suspend fun getRecentWriters(): Result = runCatching { - userService.getRecentWriters() + suspend fun getMyFollowingsRecentFeeds() = runCatching { + userService.getMyFollowingsRecentFeeds() .handleBaseResponse() .getOrThrow() } diff --git a/app/src/main/java/com/texthip/thip/data/service/UserService.kt b/app/src/main/java/com/texthip/thip/data/service/UserService.kt index 6b2549f7..36f261b7 100644 --- a/app/src/main/java/com/texthip/thip/data/service/UserService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/UserService.kt @@ -2,14 +2,14 @@ package com.texthip.thip.data.service import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.users.request.FollowRequest -import com.texthip.thip.data.model.users.response.MyFollowingsResponse -import com.texthip.thip.data.model.users.response.MyPageInfoResponse import com.texthip.thip.data.model.users.request.NicknameRequest import com.texthip.thip.data.model.users.response.AliasChoiceResponse import com.texthip.thip.data.model.users.response.FollowResponse -import com.texthip.thip.data.model.users.response.MyRecentFollowingsResponse +import com.texthip.thip.data.model.users.response.MyFollowingsResponse +import com.texthip.thip.data.model.users.response.MyPageInfoResponse import com.texthip.thip.data.model.users.response.NicknameResponse import com.texthip.thip.data.model.users.response.OthersFollowersResponse +import com.texthip.thip.data.model.users.response.UsersMyFollowingsRecentFeedsResponse import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -24,7 +24,7 @@ interface UserService { ): BaseResponse @GET("users/my-followings/recent-feeds") - suspend fun getRecentWriters(): BaseResponse + suspend fun getMyFollowingsRecentFeeds(): BaseResponse @GET("users/{userId}/followers") suspend fun getUserFollowers( diff --git a/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt b/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt index c5f0f0ae..5bf67212 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.texthip.thip.R -import com.texthip.thip.ui.feed.mock.MySubscriptionData +import com.texthip.thip.data.model.users.response.RecentWriterList import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -39,7 +39,7 @@ import com.texthip.thip.ui.theme.ThipTheme.typography @Composable fun MySubscribeBarlist( modifier: Modifier = Modifier, - subscriptions: List, + subscriptions: List, onClick: () -> Unit ) { // 이미지 + 간격 너비 @@ -123,6 +123,7 @@ fun MySubscribeBarlist( } } } + @Composable private fun EmptyMySubscriptionBar() { Box( @@ -160,12 +161,10 @@ private fun EmptyMySubscriptionBar() { private fun MySubscribeBarlistPrev() { ThipTheme { val previewData = List(10) { - MySubscriptionData( + RecentWriterList( + userId = it.toLong(), profileImageUrl = "https://example.com/profile$it.jpg", - nickname = "닉네임임$it", - role = "문학가", - roleColor = colors.Red, - subscriberCount = 100 + it + nickname = "닉네임임$it" ) } @@ -183,7 +182,7 @@ private fun MySubscribeBarlistPrev() { @Composable private fun MySubscribeBarlistWithoutDataPrev() { ThipTheme { - val previewData = emptyList() + val previewData = emptyList() Column { MySubscribeBarlist( diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt index f6bf43b4..79088d90 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt @@ -46,9 +46,7 @@ import com.texthip.thip.ui.common.topappbar.LogoTopAppBar import com.texthip.thip.ui.feed.component.FeedSubscribeBarlist import com.texthip.thip.ui.feed.component.MyFeedCard import com.texthip.thip.ui.feed.component.MySubscribeBarlist -import com.texthip.thip.ui.feed.mock.MySubscriptionData import com.texthip.thip.ui.feed.viewmodel.FeedViewModel -import com.texthip.thip.ui.feed.viewmodel.MySubscriptionViewModel import com.texthip.thip.ui.mypage.component.SavedFeedCard import com.texthip.thip.ui.mypage.mock.FeedItem import com.texthip.thip.ui.theme.ThipTheme @@ -68,10 +66,9 @@ fun FeedScreen( totalFeedCount: Int = 0, selectedTabIndex: Int = 0, followerProfileImageUrls: List = emptyList(), - feedViewModel: FeedViewModel = hiltViewModel(), resultFeedId: Int? = null, onResultConsumed: () -> Unit = {}, - mySubscriptionViewModel: MySubscriptionViewModel = hiltViewModel() + feedViewModel: FeedViewModel = hiltViewModel(), ) { val feedUiState by feedViewModel.uiState.collectAsState() val selectedIndex = rememberSaveable { mutableIntStateOf(selectedTabIndex) } @@ -81,14 +78,20 @@ fun FeedScreen( } } val scope = rememberCoroutineScope() - + var showProgressBar by remember { mutableStateOf(false) } val progress = remember { Animatable(0f) } - + + val feedTabTitles = listOf(stringResource(R.string.feed), stringResource(R.string.my_feed)) + + LaunchedEffect(Unit) { + feedViewModel.refreshData() + } + LaunchedEffect(resultFeedId) { if (resultFeedId != null) { onResultConsumed() - + showProgressBar = true progress.snapTo(0f) scope.launch { @@ -103,57 +106,7 @@ fun FeedScreen( } } } - val mySubscriptions = listOf( - MySubscriptionData( - profileImageUrl = "https://example.com/image1.jpg", - nickname = "abcabcabcabc", - role = "문학가", - roleColor = colors.SocialScience - ), - MySubscriptionData( - profileImageUrl = "https://example.com/image.jpg", - nickname = "aaaaaaa", - role = "작가", - roleColor = colors.SocialScience - ), - MySubscriptionData( - profileImageUrl = "https://example.com/image1.jpg", - nickname = "abcabcabcabc", - role = "문학가", - roleColor = colors.SocialScience - ), - MySubscriptionData( - profileImageUrl = "https://example.com/image.jpg", - nickname = "aaaaaaa", - role = "작가", - roleColor = colors.SocialScience - ), - MySubscriptionData( - profileImageUrl = "https://example.com/image1.jpg", - nickname = "abcabcabcabc", - role = "문학가", - roleColor = colors.SocialScience - ), - MySubscriptionData( - profileImageUrl = "https://example.com/image.jpg", - nickname = "aaaaaaa", - role = "작가", - roleColor = colors.SocialScience - ), - MySubscriptionData( - profileImageUrl = "https://example.com/image1.jpg", - nickname = "abcabcabcabc", - role = "문학가", - roleColor = colors.SocialScience - ), - MySubscriptionData( - profileImageUrl = "https://example.com/image.jpg", - nickname = "aaaaaaa", - role = "작가", - roleColor = colors.SocialScience - ) - ) - val subscriptionUiState by mySubscriptionViewModel.uiState.collectAsState() + Box(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier.fillMaxSize() @@ -166,7 +119,7 @@ fun FeedScreen( ) Spacer(modifier = Modifier.height(32.dp)) HeaderMenuBarTab( - titles = listOf("피드", "내 피드"), + titles = feedTabTitles, selectedTabIndex = selectedIndex.value, onTabSelected = { selectedIndex.value = it } ) @@ -291,19 +244,10 @@ fun FeedScreen( //피드 item { Spacer(modifier = Modifier.height(20.dp)) - val subscriptionsForBar = feedUiState.recentWriters.map { user -> - MySubscriptionData( - profileImageUrl = user.profileImageUrl, - nickname = user.nickname, - role = "", - roleColor = colors.White, - subscriberCount = 0, - isSubscribed = true - ) - } + MySubscribeBarlist( modifier = Modifier.padding(horizontal = 20.dp), - subscriptions = subscriptionsForBar, + subscriptions = feedUiState.recentWriters, onClick = onNavigateToMySubscription ) } diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt index 04b3bdd5..cefa68b5 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt @@ -30,15 +30,20 @@ class FeedViewModel @Inject constructor( fetchRecentWriters() } + fun refreshData() { + fetchRecentWriters() + } + private fun fetchRecentWriters() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } - userRepository.getRecentWriters() + userRepository.getMyFollowingsRecentFeeds() .onSuccess { data -> + val writers = data?.recentWriters ?: emptyList() _uiState.update { it.copy( isLoading = false, - recentWriters = data?.recentWriters ?: emptyList() + recentWriters = writers ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 28745655..18d66a97 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -314,6 +314,7 @@ 피드에 글을 작성해보세요 피드에 작성된 글이 없어요 관심있는 독서메이트를 찾아보세요! + 내 피드 모집을 마감하시겠습니까? 독서메이트 모집을 마감하면\n지금 바로 모임방 활동을 바로 시작할 수 있어요. From ac6c77aaf883c147eb4e4c461d4ed8daea76f09b Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 14:56:24 +0900 Subject: [PATCH 18/51] =?UTF-8?q?[feat]:=20=ED=8A=B9=EC=A0=95=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EC=9D=98=20=EA=B3=B5=EA=B0=9C=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=83=81=EB=8B=A8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?response=20=EC=83=9D=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/feed/response/FeedUsersInfoResponse.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/feed/response/FeedUsersInfoResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedUsersInfoResponse.kt b/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedUsersInfoResponse.kt new file mode 100644 index 00000000..ac5c5b06 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedUsersInfoResponse.kt @@ -0,0 +1,16 @@ +package com.texthip.thip.data.model.feed.response + +import kotlinx.serialization.Serializable + +@Serializable +data class FeedUsersInfoResponse( + val creatorId: Int, + val profileImageUrl: String, + val nickname: String, + val aliasName: String, + val aliasColor: String, + val followerCount: Int, + val totalFeedCount: Int, + val isFollowing: Boolean, + val latestFollowerProfileImageUrls: List +) From 42742de90c065c032492a546d5369cab96fbf24b Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 14:59:28 +0900 Subject: [PATCH 19/51] =?UTF-8?q?[feat]:=20=ED=8A=B9=EC=A0=95=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EC=9D=98=20=EA=B3=B5=EA=B0=9C=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=83=81=EB=8B=A8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?service,=20repository=20=EC=83=9D=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/repository/FeedRepository.kt | 26 +++++++++++++------ .../texthip/thip/data/service/FeedService.kt | 7 +++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt index 0a8dc1ae..64f63dd4 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt @@ -32,7 +32,7 @@ class FeedRepository @Inject constructor( val response = feedService.getFeedWriteInfo() .handleBaseResponse() .getOrThrow() - + // 카테고리 순서 조정 val orderedCategories = response?.categoryList?.sortedBy { category -> when (category.category) { @@ -44,7 +44,7 @@ class FeedRepository @Inject constructor( else -> 999 } } ?: emptyList() - + response?.copy(categoryList = orderedCategories) } @@ -69,7 +69,7 @@ class FeedRepository @Inject constructor( // 임시 파일 목록 추적 val tempFiles = mutableListOf() - + try { // 이미지 파일들을 MultipartBody.Part로 변환 val imageParts = if (imageUris.isNotEmpty()) { @@ -95,7 +95,11 @@ class FeedRepository @Inject constructor( } } - private fun uriToMultipartBodyPart(uri: Uri, paramName: String, tempFiles: MutableList): MultipartBody.Part? { + private fun uriToMultipartBodyPart( + uri: Uri, + paramName: String, + tempFiles: MutableList + ): MultipartBody.Part? { return try { // MIME 타입 확인 val mimeType = context.contentResolver.getType(uri) ?: "image/jpeg" @@ -105,21 +109,21 @@ class FeedRepository @Inject constructor( "image/jpeg", "image/jpg" -> "jpg" else -> "jpg" // 기본값 } - + // 파일명 생성 val fileName = "feed_image_${System.currentTimeMillis()}.$extension" val tempFile = File(context.cacheDir, fileName) - + // 임시 파일 목록에 추가 tempFiles.add(tempFile) - + // InputStream을 use 블록으로 안전하게 관리 context.contentResolver.openInputStream(uri)?.use { inputStream -> FileOutputStream(tempFile).use { outputStream -> inputStream.copyTo(outputStream) } } ?: return null - + // MultipartBody.Part 생성 val requestFile = tempFile.asRequestBody(mimeType.toMediaType()) MultipartBody.Part.createFormData(paramName, fileName, requestFile) @@ -141,4 +145,10 @@ class FeedRepository @Inject constructor( } } } + + suspend fun getFeedUsersInfo(userId: Long) = runCatching { + feedService.getFeedUsersInfo(userId) + .handleBaseResponse() + .getOrThrow() + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/FeedService.kt b/app/src/main/java/com/texthip/thip/data/service/FeedService.kt index fc216f1e..08341c1e 100644 --- a/app/src/main/java/com/texthip/thip/data/service/FeedService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/FeedService.kt @@ -2,6 +2,7 @@ package com.texthip.thip.data.service import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.feed.response.CreateFeedResponse +import com.texthip.thip.data.model.feed.response.FeedUsersInfoResponse import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse import okhttp3.MultipartBody import okhttp3.RequestBody @@ -9,6 +10,7 @@ import retrofit2.http.GET import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part +import retrofit2.http.Path interface FeedService { @@ -23,4 +25,9 @@ interface FeedService { @Part("request") request: RequestBody, @Part images: List? ): BaseResponse + + @GET("feeds/users/{userId}/info") + suspend fun getFeedUsersInfo( + @Path("userId") userId: Long + ): BaseResponse } \ No newline at end of file From e984dbdb94398ad69e501c2f64c94abf024a8b6f Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 15:00:06 +0900 Subject: [PATCH 20/51] =?UTF-8?q?[feat]:=20=ED=8A=B9=EC=A0=95=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EC=9D=98=20=EA=B3=B5=EA=B0=9C=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=83=81=EB=8B=A8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?viewmodel=20=EC=83=9D=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/feed/viewmodel/FeedOthersViewModel.kt | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedOthersViewModel.kt diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedOthersViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedOthersViewModel.kt new file mode 100644 index 00000000..6d9ae031 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedOthersViewModel.kt @@ -0,0 +1,54 @@ +package com.texthip.thip.ui.feed.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.feed.response.FeedUsersInfoResponse +import com.texthip.thip.data.repository.FeedRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class FeedOthersUiState( + val isLoading: Boolean = true, + val userInfo: FeedUsersInfoResponse? = null, + // TODO: 유저 피드 목록 불러오기 + // val feeds: List = emptyList(), + val errorMessage: String? = null +) + +@HiltViewModel +class FeedOthersViewModel @Inject constructor( + private val feedRepository: FeedRepository, + savedStateHandle: SavedStateHandle +) : ViewModel() { + + private val userId: Long = requireNotNull(savedStateHandle["userId"]) + + private val _uiState = MutableStateFlow(FeedOthersUiState()) + val uiState = _uiState.asStateFlow() + + init { + fetchUserInfo() + } + + private fun fetchUserInfo() { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + feedRepository.getFeedUsersInfo(userId) + .onSuccess { data -> + _uiState.update { + it.copy(isLoading = false, userInfo = data) + } + } + .onFailure { exception -> + _uiState.update { + it.copy(isLoading = false, errorMessage = exception.message) + } + } + } + } +} \ No newline at end of file From 488d76c702caf1459b4e2362e39b61ef994ab699 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 15:03:43 +0900 Subject: [PATCH 21/51] =?UTF-8?q?[feat]:=20=ED=8A=B9=EC=A0=95=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EC=9D=98=20=EA=B3=B5=EA=B0=9C=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=83=81=EB=8B=A8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?screen=EC=97=90=20viewmodel,=20navigation=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/common/header/AuthorHeader.kt | 13 +- .../thip/ui/feed/screen/FeedOthersScreen.kt | 290 +++++++++--------- .../feed/screen/MySubscriptionListScreen.kt | 16 +- .../texthip/thip/ui/navigator/MainNavHost.kt | 6 +- .../extensions/FeedNavigationExtensions.kt | 5 + .../navigator/navigations/FeedNavigation.kt | 24 +- .../thip/ui/navigator/routes/FeedRoutes.kt | 3 +- 7 files changed, 196 insertions(+), 161 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/common/header/AuthorHeader.kt b/app/src/main/java/com/texthip/thip/ui/common/header/AuthorHeader.kt index 94b4c152..7cd96399 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/header/AuthorHeader.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/header/AuthorHeader.kt @@ -18,6 +18,7 @@ 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.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -29,7 +30,6 @@ import com.texthip.thip.ui.common.buttons.OutlinedButton import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography -import androidx.compose.ui.graphics.Color @Composable fun AuthorHeader( @@ -37,7 +37,7 @@ fun AuthorHeader( profileImage: String?, nickname: String, badgeText: String, - badgeTextColor: Color = colors.NeonGreen, + badgeTextColor: Color = colors.NeonGreen, buttonText: String = "", buttonWidth: Dp = 60.dp, showButton: Boolean = true, @@ -89,8 +89,8 @@ fun AuthorHeader( if (showButton) { OutlinedButton( modifier = Modifier - .width(buttonWidth) - .height(33.dp), + .width(buttonWidth) + .height(33.dp), text = buttonText, textStyle = typography.view_m500_s14, onClick = onButtonClick @@ -102,7 +102,10 @@ fun AuthorHeader( verticalAlignment = Alignment.CenterVertically ) { Text( - text = stringResource(R.string.thip_num, thipNum)+ stringResource(R.string.thip_ing), + text = stringResource( + R.string.thip_num, + thipNum + ) + stringResource(R.string.thip_ing), style = typography.view_r400_s11_h20, color = colors.White ) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt index 5c4c15d1..3212e9ac 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt @@ -9,40 +9,34 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R import com.texthip.thip.ui.common.header.AuthorHeader import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar import com.texthip.thip.ui.feed.component.FeedSubscribeBarlist -import com.texthip.thip.ui.feed.component.MyFeedCard -import com.texthip.thip.ui.mypage.mock.FeedItem -import com.texthip.thip.ui.theme.ThipTheme +import com.texthip.thip.ui.feed.viewmodel.FeedOthersViewModel import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography +import com.texthip.thip.utils.color.hexToColor @Composable fun FeedOthersScreen( - nickname: String, - userRole: String, - feeds: List = emptyList(), - totalFeedCount: Int = 0, - followerProfileImageUrls: List = emptyList() + onNavigateBack: () -> Unit, + viewModel: FeedOthersViewModel = hiltViewModel() ) { - val feedStateList = remember { - mutableStateListOf().apply { - addAll(feeds) - } - } + val uiState by viewModel.uiState.collectAsState() + val userInfo = uiState.userInfo + Box(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier.fillMaxSize() @@ -50,81 +44,90 @@ fun FeedOthersScreen( DefaultTopAppBar( isRightIconVisible = false, isTitleVisible = false, - onLeftClick = {}, + onLeftClick = onNavigateBack, ) - // 스크롤 영역 - LazyColumn( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - item { - Spacer(modifier = Modifier.height(32.dp)) - AuthorHeader( - profileImage = null, - nickname = nickname, - badgeText = userRole, - buttonText = stringResource(R.string.thip), - onButtonClick = {}, - modifier = Modifier.padding(bottom = 20.dp) - ) - Spacer(modifier = Modifier.height(16.dp)) - FeedSubscribeBarlist( - modifier = Modifier.padding(horizontal = 20.dp), - followerProfileImageUrls = followerProfileImageUrls, - onClick = { - } - ) - Spacer(modifier = Modifier.height(40.dp)) - Text( - text = stringResource(R.string.whole_num, totalFeedCount), - style = typography.menu_m500_s14_h24, - color = colors.Grey, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp, start = 20.dp) - ) - HorizontalDivider( - color = colors.DarkGrey02, - thickness = 1.dp, - modifier = Modifier.padding(horizontal = 20.dp) - ) + + if (uiState.isLoading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() } - if (feedStateList.isEmpty()) { + } else if (userInfo != null) { + // 스크롤 영역 + LazyColumn( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { item { - Box( + Spacer(modifier = Modifier.height(32.dp)) + AuthorHeader( + profileImage = userInfo.profileImageUrl, + nickname = userInfo.nickname, + badgeText = userInfo.aliasName, + badgeTextColor = hexToColor(userInfo.aliasColor), + buttonText = if (userInfo.isFollowing) stringResource(R.string.thip_cancel) else stringResource(R.string.thip), + // TODO: 띱하기/취소하기 로직 연결 + onButtonClick = {}, + modifier = Modifier.padding(bottom = 20.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + FeedSubscribeBarlist( + modifier = Modifier.padding(horizontal = 20.dp), + followerProfileImageUrls = userInfo.latestFollowerProfileImageUrls, + onClick = {} + ) + Spacer(modifier = Modifier.height(40.dp)) + Text( + text = stringResource(R.string.whole_num, userInfo.totalFeedCount), + style = typography.menu_m500_s14_h24, + color = colors.Grey, modifier = Modifier .fillMaxWidth() - .padding(top = 244.dp), - contentAlignment = Alignment.TopCenter - ) { - Text( - text = stringResource(R.string.empty_feed), - style = typography.smalltitle_sb600_s18_h24, - color = colors.White - ) - } + .padding(bottom = 12.dp, start = 20.dp) + ) + HorizontalDivider( + color = colors.DarkGrey02, + thickness = 1.dp, + modifier = Modifier.padding(horizontal = 20.dp) + ) } - } else { - itemsIndexed(feedStateList, key = { _, item -> item.id }) { index, feed -> - Spacer(modifier = Modifier.height(if (index == 0) 20.dp else 40.dp)) - MyFeedCard( - feedItem = feed, - onLikeClick = { - val updated = feed.copy( - isLiked = !feed.isLiked, - likeCount = if (feed.isLiked) feed.likeCount - 1 else feed.likeCount + 1 + if (userInfo.totalFeedCount == 0) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 244.dp), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = stringResource(R.string.empty_feed), + style = typography.smalltitle_sb600_s18_h24, + color = colors.White ) - feedStateList[index] = updated - }, - onContentClick = {} //TODO FeedCommentScreen으로 - ) - Spacer(modifier = Modifier.height(40.dp)) - if (index != feedStateList.lastIndex) { - HorizontalDivider( - color = colors.DarkGrey02, - thickness = 10.dp - ) + } } + } else { + // TODO: 피드 아이템 리스트 불러오기 +// itemsIndexed(feedStateList, key = { _, item -> item.id }) { index, feed -> +// Spacer(modifier = Modifier.height(if (index == 0) 20.dp else 40.dp)) +// MyFeedCard( +// feedItem = feed, +// onLikeClick = { +// val updated = feed.copy( +// isLiked = !feed.isLiked, +// likeCount = if (feed.isLiked) feed.likeCount - 1 else feed.likeCount + 1 +// ) +// feedStateList[index] = updated +// }, +// onContentClick = {} //TODO FeedCommentScreen으로 +// ) +// Spacer(modifier = Modifier.height(40.dp)) +// if (index != feedStateList.lastIndex) { +// HorizontalDivider( +// color = colors.DarkGrey02, +// thickness = 10.dp +// ) +// } +// } } } } @@ -132,62 +135,63 @@ fun FeedOthersScreen( } } -@Preview -@Composable -private fun FeedOthersScreenPrev() { - ThipTheme { - val mockFeeds = List(5) { - FeedItem( - id = it + 1, - userProfileImage = R.drawable.character_literature, - userName = "user.$it", - userRole = "문학 칭호", - bookTitle = "책 제목 ", - authName = "한강", - timeAgo = "1시간 전", - content = "내용내용내용 입니다. 내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.", - likeCount = it, - commentCount = it, - isLiked = false, - isSaved = false, - isLocked = it % 2 == 0, - imageUrls = listOf(R.drawable.img_book_cover_sample) - ) - } - val mockFollowerImages = listOf( - "https://example.com/image1.jpg", - "https://example.com/image2.jpg", - "https://example.com/image3.jpg", - "https://example.com/image4.jpg", - "https://example.com/image5.jpg" - ) - ThipTheme { - FeedOthersScreen( - nickname = "ThipUser01", - userRole = "문학 칭호", - feeds = mockFeeds, - totalFeedCount = mockFeeds.size, - followerProfileImageUrls = mockFollowerImages - ) - } - } -} - -@Preview -@Composable -private fun FeedOthersScreenWithoutFeedPrev() { - ThipTheme { - val mockFeeds: List = emptyList() - val mockFollowerImages = emptyList() - - ThipTheme { - FeedOthersScreen( - nickname = "ThipUser01", - userRole = "문학 칭호", - feeds = mockFeeds, - totalFeedCount = mockFeeds.size, - followerProfileImageUrls = mockFollowerImages - ) - } - } -} +//@Preview +//@Composable +//private fun FeedOthersScreenPrev() { +// ThipTheme { +// val mockFeeds = List(5) { +// FeedItem( +// id = it + 1, +// userProfileImage = R.drawable.character_literature, +// userName = "user.$it", +// userRole = "문학 칭호", +// bookTitle = "책 제목 ", +// authName = "한강", +// timeAgo = "1시간 전", +// content = "내용내용내용 입니다. 내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.", +// likeCount = it, +// commentCount = it, +// isLiked = false, +// isSaved = false, +// isLocked = it % 2 == 0, +// imageUrls = listOf(R.drawable.img_book_cover_sample) +// ) +// } +// val mockFollowerImages = listOf( +// "https://example.com/image1.jpg", +// "https://example.com/image2.jpg", +// "https://example.com/image3.jpg", +// "https://example.com/image4.jpg", +// "https://example.com/image5.jpg" +// ) +// ThipTheme { +// FeedOthersScreen( +// nickname = "ThipUser01", +// userRole = "문학 칭호", +// feeds = mockFeeds, +// totalFeedCount = mockFeeds.size, +// followerProfileImageUrls = mockFollowerImages, +// onNavigateBack = {} +// ) +// } +// } +//} +// +//@Preview +//@Composable +//private fun FeedOthersScreenWithoutFeedPrev() { +// ThipTheme { +// val mockFeeds: List = emptyList() +// val mockFollowerImages = emptyList() +// +// ThipTheme { +// FeedOthersScreen( +// nickname = "ThipUser01", +// userRole = "문학 칭호", +// feeds = mockFeeds, +// totalFeedCount = mockFeeds.size, +// followerProfileImageUrls = mockFollowerImages +// ) +// } +// } +//} diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt index 96e75127..6bbbdbe7 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt @@ -1,6 +1,7 @@ package com.texthip.thip.ui.feed.screen import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -32,7 +33,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavController import com.texthip.thip.R import com.texthip.thip.data.model.users.response.FollowingList import com.texthip.thip.ui.common.header.AuthorHeader @@ -48,7 +48,8 @@ import kotlinx.coroutines.delay @Composable fun MySubscriptionScreen( - navController: NavController?= null, + onNavigateBack: () -> Unit, + onNavigateToUserProfile: (Long) -> Unit, viewModel: MySubscriptionViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() @@ -73,12 +74,15 @@ fun MySubscriptionScreen( MySubscriptionContent( uiState = uiState, lazyListState = lazyListState, - onNavigateBack = { navController?.popBackStack() }, + onNavigateBack = onNavigateBack, onToggleFollow = { userId, nickname -> val followedMessage = context.getString(R.string.toast_thip, nickname) val unfollowedMessage = context.getString(R.string.toast_thip_cancel, nickname) viewModel.toggleFollow(userId, followedMessage, unfollowedMessage) }, + onUserClick = { userId -> + onNavigateToUserProfile(userId) + }, onHideToast = { viewModel.hideToast() } ) } @@ -88,6 +92,7 @@ fun MySubscriptionContent( lazyListState: LazyListState, onNavigateBack: () -> Unit, onToggleFollow: (userId: Long, nickname: String) -> Unit, + onUserClick: (userId: Long) -> Unit, onHideToast: () -> Unit ) { LaunchedEffect(uiState.showToast) { @@ -147,7 +152,7 @@ fun MySubscriptionContent( items = uiState.followings, key = { _, user -> user.userId } ) { index, user -> - Column(modifier = Modifier.padding(horizontal = 20.dp)) { + Column(modifier = Modifier.padding(horizontal = 20.dp).clickable { onUserClick(user.userId) }) { AuthorHeader( profileImage = user.profileImageUrl, nickname = user.nickname, @@ -156,7 +161,7 @@ fun MySubscriptionContent( buttonText = stringResource(if (user.isFollowing) R.string.thip_cancel else R.string.thip), buttonWidth = 64.dp, profileImageSize = 36.dp, - onButtonClick = { onToggleFollow(user.userId, user.nickname) } + onButtonClick = { onToggleFollow(user.userId, user.nickname) }, ) if (index < uiState.followings.lastIndex) { @@ -215,6 +220,7 @@ private fun MySubscriptionListScreenPrev() { lazyListState = rememberLazyListState(), onNavigateBack = {}, onToggleFollow = { _, _ -> }, + onUserClick = {}, onHideToast = {} ) } diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/MainNavHost.kt b/app/src/main/java/com/texthip/thip/ui/navigator/MainNavHost.kt index 894a09d8..0bda5892 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/MainNavHost.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/MainNavHost.kt @@ -3,18 +3,18 @@ package com.texthip.thip.ui.navigator import androidx.compose.runtime.Composable import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost -import com.texthip.thip.ui.navigator.routes.MainTabRoutes +import com.texthip.thip.ui.navigator.navigations.commonNavigation import com.texthip.thip.ui.navigator.navigations.feedNavigation import com.texthip.thip.ui.navigator.navigations.groupNavigation import com.texthip.thip.ui.navigator.navigations.myPageNavigation import com.texthip.thip.ui.navigator.navigations.searchNavigation -import com.texthip.thip.ui.navigator.navigations.commonNavigation +import com.texthip.thip.ui.navigator.routes.MainTabRoutes // 메인 네비게이션 @Composable fun MainNavHost(navController: NavHostController) { NavHost(navController = navController, startDestination = MainTabRoutes.Feed) { - feedNavigation(navController) + feedNavigation(navController, navigateBack = navController::popBackStack) groupNavigation( navController = navController, navigateBack = navController::popBackStack diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt index 02387a67..5f5f7f46 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt @@ -17,4 +17,9 @@ fun NavHostController.navigateToMySubscription() { // 피드 작성으로 fun NavHostController.navigateToFeedWrite() { navigate(FeedRoutes.Write) +} + +// 유저 프로필(피드)로 +fun NavHostController.navigateToUserProfile(userId: Long) { + navigate(FeedRoutes.Others(userId)) } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt index 40df9757..7212d086 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt @@ -3,6 +3,7 @@ package com.texthip.thip.ui.navigator.navigations import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable +import com.texthip.thip.ui.feed.screen.FeedOthersScreen import com.texthip.thip.ui.feed.screen.FeedScreen import com.texthip.thip.ui.feed.screen.FeedWriteScreen import com.texthip.thip.ui.feed.screen.MySubscriptionScreen @@ -12,10 +13,10 @@ import com.texthip.thip.ui.navigator.routes.FeedRoutes import com.texthip.thip.ui.navigator.routes.MainTabRoutes // Feed -fun NavGraphBuilder.feedNavigation(navController: NavHostController) { +fun NavGraphBuilder.feedNavigation(navController: NavHostController, navigateBack: () -> Unit) { composable { backStackEntry -> val resultFeedId = backStackEntry.savedStateHandle.get("feedId") - + FeedScreen( nickname = "ThipUser01", userRole = "문학가", @@ -36,12 +37,19 @@ fun NavGraphBuilder.feedNavigation(navController: NavHostController) { ) } composable { - MySubscriptionScreen(navController = navController) + MySubscriptionScreen( + onNavigateBack = { + navigateBack() + }, + onNavigateToUserProfile = { userId -> + navController.navigate(FeedRoutes.Others(userId)) + } + ) } composable { FeedWriteScreen( onNavigateBack = { - navController.popBackStack() + navigateBack() }, onFeedCreated = { feedId -> // 피드 생성 성공 시 결과를 저장하고 피드 목록으로 돌아가기 @@ -52,4 +60,12 @@ fun NavGraphBuilder.feedNavigation(navController: NavHostController) { } ) } + composable { backStackEntry -> + // 다른 유저의 피드 화면 + FeedOthersScreen( + onNavigateBack = { + navigateBack() + } + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt b/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt index c8c9eeb6..c91b26b9 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt @@ -4,7 +4,8 @@ import kotlinx.serialization.Serializable @Serializable sealed class FeedRoutes : Routes() { - @Serializable data object MySubscription : FeedRoutes() @Serializable data object Write : FeedRoutes() + + @Serializable data class Others(val userId: Long) : FeedRoutes() } \ No newline at end of file From e5467191eaa694de080335908cb0b0967bfc98a3 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:41:01 +0900 Subject: [PATCH 22/51] =?UTF-8?q?[feat]:=20=ED=8A=B9=EC=A0=95=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EC=9D=98=20=EA=B3=B5=EA=B0=9C=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20response=20=EC=83=9D=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/feed/response/FeedUsersResponse.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/feed/response/FeedUsersResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedUsersResponse.kt b/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedUsersResponse.kt new file mode 100644 index 00000000..5ba8eb19 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedUsersResponse.kt @@ -0,0 +1,27 @@ +package com.texthip.thip.data.model.feed.response + +import kotlinx.serialization.Serializable + +@Serializable +data class FeedUsersResponse ( + val feedList: List, + val nextCursor: String? = null, + val isLast: Boolean = false, +) + +@Serializable +data class FeedList( + val feedId: Long, + val postDate: String, + val isbn: String, + val bookTitle: String, + val bookAuthor: String, + val contentBody: String, + val contentUrls: List, + val likeCount: Int, + val commentCount: Int, + val isPublic: Boolean, + val isSaved: Boolean, + val isLiked: Boolean, + val isWriter: Boolean, +) \ No newline at end of file From b563be3fbcdb8ee9819f264309fbeee490b5dc21 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:41:09 +0900 Subject: [PATCH 23/51] =?UTF-8?q?[feat]:=20=ED=8A=B9=EC=A0=95=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EC=9D=98=20=EA=B3=B5=EA=B0=9C=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20service,=20repository=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/texthip/thip/data/repository/FeedRepository.kt | 6 ++++++ .../main/java/com/texthip/thip/data/service/FeedService.kt | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt index 64f63dd4..9a3c722e 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt @@ -151,4 +151,10 @@ class FeedRepository @Inject constructor( .handleBaseResponse() .getOrThrow() } + + suspend fun getFeedUsers(userId: Long) = runCatching { + feedService.getFeedUsers(userId) + .handleBaseResponse() + .getOrThrow() + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/FeedService.kt b/app/src/main/java/com/texthip/thip/data/service/FeedService.kt index 08341c1e..8a7bc3ea 100644 --- a/app/src/main/java/com/texthip/thip/data/service/FeedService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/FeedService.kt @@ -3,6 +3,7 @@ package com.texthip.thip.data.service import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.feed.response.CreateFeedResponse import com.texthip.thip.data.model.feed.response.FeedUsersInfoResponse +import com.texthip.thip.data.model.feed.response.FeedUsersResponse import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse import okhttp3.MultipartBody import okhttp3.RequestBody @@ -30,4 +31,9 @@ interface FeedService { suspend fun getFeedUsersInfo( @Path("userId") userId: Long ): BaseResponse + + @GET("feeds/users/{userId}") + suspend fun getFeedUsers( + @Path("userId") userId: Long + ): BaseResponse } \ No newline at end of file From feaa81f60dfc7713a26ac7fc981272c53c397af7 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:46:37 +0900 Subject: [PATCH 24/51] =?UTF-8?q?[ui]:=20=ED=8A=B9=EC=A0=95=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EC=9D=98=20=EA=B3=B5=EA=B0=9C=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20card=20=EC=83=9D=EC=84=B1=20(#?= =?UTF-8?q?90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/feed/component/OthersFeedCard.kt | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/ui/feed/component/OthersFeedCard.kt diff --git a/app/src/main/java/com/texthip/thip/ui/feed/component/OthersFeedCard.kt b/app/src/main/java/com/texthip/thip/ui/feed/component/OthersFeedCard.kt new file mode 100644 index 00000000..d8aef755 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/feed/component/OthersFeedCard.kt @@ -0,0 +1,123 @@ +package com.texthip.thip.ui.feed.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.texthip.thip.data.model.feed.response.FeedList +import com.texthip.thip.ui.common.buttons.ActionBarButton +import com.texthip.thip.ui.common.buttons.ActionBookButton +import com.texthip.thip.ui.theme.ThipTheme.colors +import com.texthip.thip.ui.theme.ThipTheme.typography + +@Composable +fun OthersFeedCard( + modifier: Modifier = Modifier, + feedItem: FeedList, + onLikeClick: () -> Unit = {}, + onContentClick: () -> Unit = {} +) { + val images = feedItem.contentUrls + val hasImages = images.isNotEmpty() + val maxLines = if (hasImages) 3 else 8 + + var isLiked by remember { mutableStateOf(false) } + + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + ActionBookButton( + bookTitle = feedItem.bookTitle, + bookAuthor = feedItem.bookAuthor, + onClick = {} + ) + + Text( + text = feedItem.contentBody, + style = typography.feedcopy_r400_s14_h20, + color = colors.White, + maxLines = maxLines, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + .clickable { onContentClick() } + ) + + if (hasImages) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + images.take(3).forEach { image -> + AsyncImage( + model = image, + contentDescription = null, + modifier = Modifier + .padding(end = 10.dp) + .size(100.dp), + contentScale = ContentScale.Crop + ) + } + } + } + + ActionBarButton( + isLiked = feedItem.isLiked, + likeCount = feedItem.likeCount, + commentCount = feedItem.commentCount, + isSaveVisible = true, + onLikeClick = { +// onLikeClick(feedItem.feedId) + }, + onCommentClick = { +// onCommentClick() + }, + onBookmarkClick = { + + } + ) + } +} + +@Preview +@Composable +private fun OthersFeedCardPreview() { + val feed = FeedList( + feedId = 1, + postDate = "3시간 전", + isbn = "12345", bookTitle = "미드나이트 라이브러리", bookAuthor = "매트 헤이그", + contentBody = "피드 내용입니다. 정말 재미있게 읽은 책이에요. 여러분도 꼭 읽어보세요.", + contentUrls = listOf("https://picsum.photos/100"), + likeCount = 10, commentCount = 5, isPublic = true, + isSaved = true, isLiked = false, isWriter = false + ) + + Column { + OthersFeedCard( + feedItem = feed + ) + OthersFeedCard( + feedItem = feed + ) + } + + +} \ No newline at end of file From b4a7639dbdd7e51c1585178308b0d00828a3b6b2 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:46:56 +0900 Subject: [PATCH 25/51] =?UTF-8?q?[feat]:=20=ED=8A=B9=EC=A0=95=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EC=9D=98=20=EA=B3=B5=EA=B0=9C=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20viewmodel=20=EC=83=9D=EC=84=B1=20=EB=B0=8F?= =?UTF-8?q?=20screen=EC=97=90=20=EC=97=B0=EA=B2=B0=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/feed/screen/FeedOthersScreen.kt | 158 +++++++++--------- .../ui/feed/viewmodel/FeedOthersViewModel.kt | 43 +++-- 2 files changed, 103 insertions(+), 98 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt index 3212e9ac..c44ba053 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text @@ -18,13 +19,19 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R +import com.texthip.thip.data.model.feed.response.FeedList +import com.texthip.thip.data.model.feed.response.FeedUsersInfoResponse import com.texthip.thip.ui.common.header.AuthorHeader import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar import com.texthip.thip.ui.feed.component.FeedSubscribeBarlist +import com.texthip.thip.ui.feed.component.OthersFeedCard +import com.texthip.thip.ui.feed.viewmodel.FeedOthersUiState import com.texthip.thip.ui.feed.viewmodel.FeedOthersViewModel +import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography import com.texthip.thip.utils.color.hexToColor @@ -35,6 +42,18 @@ fun FeedOthersScreen( viewModel: FeedOthersViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() + + FeedOthersContent( + uiState = uiState, + onNavigateBack = onNavigateBack + ) +} + +@Composable +fun FeedOthersContent( + uiState: FeedOthersUiState, + onNavigateBack: () -> Unit +) { val userInfo = uiState.userInfo Box(modifier = Modifier.fillMaxSize()) { @@ -64,7 +83,9 @@ fun FeedOthersScreen( nickname = userInfo.nickname, badgeText = userInfo.aliasName, badgeTextColor = hexToColor(userInfo.aliasColor), - buttonText = if (userInfo.isFollowing) stringResource(R.string.thip_cancel) else stringResource(R.string.thip), + buttonText = if (userInfo.isFollowing) stringResource(R.string.thip_cancel) else stringResource( + R.string.thip + ), // TODO: 띱하기/취소하기 로직 연결 onButtonClick = {}, modifier = Modifier.padding(bottom = 20.dp) @@ -106,28 +127,24 @@ fun FeedOthersScreen( } } } else { - // TODO: 피드 아이템 리스트 불러오기 -// itemsIndexed(feedStateList, key = { _, item -> item.id }) { index, feed -> -// Spacer(modifier = Modifier.height(if (index == 0) 20.dp else 40.dp)) -// MyFeedCard( -// feedItem = feed, -// onLikeClick = { -// val updated = feed.copy( -// isLiked = !feed.isLiked, -// likeCount = if (feed.isLiked) feed.likeCount - 1 else feed.likeCount + 1 -// ) -// feedStateList[index] = updated -// }, -// onContentClick = {} //TODO FeedCommentScreen으로 -// ) -// Spacer(modifier = Modifier.height(40.dp)) -// if (index != feedStateList.lastIndex) { -// HorizontalDivider( -// color = colors.DarkGrey02, -// thickness = 10.dp -// ) -// } -// } + itemsIndexed( + items = uiState.feeds, + key = { _, item -> item.feedId } + ) { index, feed -> + Spacer(modifier = Modifier.height(if (index == 0) 20.dp else 40.dp)) + OthersFeedCard( + feedItem = feed, + onLikeClick = { /* TODO: 좋아요 로직 연결 */ }, + onContentClick = { /* TODO: 피드 상세 댓글 화면으로 이동 */ } + ) + Spacer(modifier = Modifier.height(40.dp)) + if (index < uiState.feeds.lastIndex) { + HorizontalDivider( + color = colors.DarkGrey02, + thickness = 10.dp + ) + } + } } } } @@ -135,63 +152,38 @@ fun FeedOthersScreen( } } -//@Preview -//@Composable -//private fun FeedOthersScreenPrev() { -// ThipTheme { -// val mockFeeds = List(5) { -// FeedItem( -// id = it + 1, -// userProfileImage = R.drawable.character_literature, -// userName = "user.$it", -// userRole = "문학 칭호", -// bookTitle = "책 제목 ", -// authName = "한강", -// timeAgo = "1시간 전", -// content = "내용내용내용 입니다. 내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.내용내용내용 입니다.", -// likeCount = it, -// commentCount = it, -// isLiked = false, -// isSaved = false, -// isLocked = it % 2 == 0, -// imageUrls = listOf(R.drawable.img_book_cover_sample) -// ) -// } -// val mockFollowerImages = listOf( -// "https://example.com/image1.jpg", -// "https://example.com/image2.jpg", -// "https://example.com/image3.jpg", -// "https://example.com/image4.jpg", -// "https://example.com/image5.jpg" -// ) -// ThipTheme { -// FeedOthersScreen( -// nickname = "ThipUser01", -// userRole = "문학 칭호", -// feeds = mockFeeds, -// totalFeedCount = mockFeeds.size, -// followerProfileImageUrls = mockFollowerImages, -// onNavigateBack = {} -// ) -// } -// } -//} -// -//@Preview -//@Composable -//private fun FeedOthersScreenWithoutFeedPrev() { -// ThipTheme { -// val mockFeeds: List = emptyList() -// val mockFollowerImages = emptyList() -// -// ThipTheme { -// FeedOthersScreen( -// nickname = "ThipUser01", -// userRole = "문학 칭호", -// feeds = mockFeeds, -// totalFeedCount = mockFeeds.size, -// followerProfileImageUrls = mockFollowerImages -// ) -// } -// } -//} +@Preview +@Composable +private fun FeedOthersScreenPrev() { + val mockUserInfo = FeedUsersInfoResponse( + creatorId = 1, + profileImageUrl = "", + nickname = "김독서", + aliasName = "문학가", + aliasColor = "#A0F8E8", + followerCount = 120, + totalFeedCount = 5, + isFollowing = true, + latestFollowerProfileImageUrls = emptyList() + ) + val mockFeeds = List(5) { + FeedList( + feedId = it.toLong(), postDate = "1시간 전", isbn = "1234", + bookTitle = "미리보기 책 제목 ${it + 1}", bookAuthor = "작가", + contentBody = "미리보기 피드 내용입니다. 내용은 여기에 표시됩니다.", + contentUrls = emptyList(), likeCount = 10, commentCount = 2, + isPublic = true, isSaved = false, isLiked = true, isWriter = false + ) + } + + ThipTheme { + FeedOthersContent( + uiState = FeedOthersUiState( + isLoading = false, + userInfo = mockUserInfo, + feeds = mockFeeds + ), + onNavigateBack = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedOthersViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedOthersViewModel.kt index 6d9ae031..f8b4fd66 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedOthersViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedOthersViewModel.kt @@ -1,11 +1,14 @@ package com.texthip.thip.ui.feed.viewmodel +import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.feed.response.FeedList import com.texthip.thip.data.model.feed.response.FeedUsersInfoResponse import com.texthip.thip.data.repository.FeedRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -15,8 +18,7 @@ import javax.inject.Inject data class FeedOthersUiState( val isLoading: Boolean = true, val userInfo: FeedUsersInfoResponse? = null, - // TODO: 유저 피드 목록 불러오기 - // val feeds: List = emptyList(), + val feeds: List = emptyList(), val errorMessage: String? = null ) @@ -32,23 +34,34 @@ class FeedOthersViewModel @Inject constructor( val uiState = _uiState.asStateFlow() init { - fetchUserInfo() + fetchData() } - private fun fetchUserInfo() { + private fun fetchData() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } - feedRepository.getFeedUsersInfo(userId) - .onSuccess { data -> - _uiState.update { - it.copy(isLoading = false, userInfo = data) - } - } - .onFailure { exception -> - _uiState.update { - it.copy(isLoading = false, errorMessage = exception.message) - } - } + + val userInfoDeferred = async { feedRepository.getFeedUsersInfo(userId) } + val feedsDeferred = async { feedRepository.getFeedUsers(userId) } + + val userInfoResult = userInfoDeferred.await() + val feedsResult = feedsDeferred.await() + + val fetchedFeeds = feedsResult.getOrNull()?.feedList ?: emptyList() + + // ✅ 로그를 추가하여 유저 정보와 피드 개수를 확인 + Log.d("FeedOthersViewModel", "User Info Result: ${userInfoResult.getOrNull()}") + Log.d("FeedOthersViewModel", "Fetched Feeds Count: ${fetchedFeeds.size}") + + _uiState.update { + it.copy( + isLoading = false, + userInfo = userInfoResult.getOrNull(), + feeds = fetchedFeeds, + errorMessage = userInfoResult.exceptionOrNull()?.message + ?: feedsResult.exceptionOrNull()?.message + ) + } } } } \ No newline at end of file From c0de2aaa63d4cf5664799ad02aa624a827905dd3 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:52:38 +0900 Subject: [PATCH 26/51] =?UTF-8?q?[ui]:=20horizontal=20divider=20color=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt | 2 +- .../com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt | 4 ++-- .../main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt | 6 +++--- .../thip/ui/group/room/screen/GroupRoomChatScreen.kt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt index 4df9988c..7c3b30f0 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt @@ -189,7 +189,7 @@ fun FeedCommentScreen( } } } - HorizontalDivider(color = colors.DarkGrey02, thickness = 10.dp) + HorizontalDivider(color = colors.DarkGrey03, thickness = 10.dp) } } //댓글이 없는 경우 diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt index c44ba053..152dd30b 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt @@ -106,7 +106,7 @@ fun FeedOthersContent( .padding(bottom = 12.dp, start = 20.dp) ) HorizontalDivider( - color = colors.DarkGrey02, + color = colors.DarkGrey03, thickness = 1.dp, modifier = Modifier.padding(horizontal = 20.dp) ) @@ -140,7 +140,7 @@ fun FeedOthersContent( Spacer(modifier = Modifier.height(40.dp)) if (index < uiState.feeds.lastIndex) { HorizontalDivider( - color = colors.DarkGrey02, + color = colors.DarkGrey03, thickness = 10.dp ) } diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt index 79088d90..83e18a55 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt @@ -196,7 +196,7 @@ fun FeedScreen( .padding(bottom = 12.dp, start = 20.dp) ) HorizontalDivider( - color = colors.DarkGrey02, + color = colors.DarkGrey03, thickness = 1.dp, modifier = Modifier.padding(horizontal = 20.dp) ) @@ -234,7 +234,7 @@ fun FeedScreen( Spacer(modifier = Modifier.height(40.dp)) if (index != feeds.lastIndex) { HorizontalDivider( - color = colors.DarkGrey02, + color = colors.DarkGrey03, thickness = 10.dp ) } @@ -272,7 +272,7 @@ fun FeedScreen( ) if (index != feeds.lastIndex) { HorizontalDivider( - color = colors.DarkGrey02, + color = colors.DarkGrey03, thickness = 10.dp ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt index 693aa802..a70fd91b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt @@ -104,7 +104,7 @@ fun GroupRoomChatScreen() { ) { if (isNewDate) { HorizontalDivider( - color = colors.DarkGrey02, + color = colors.DarkGrey03, thickness = 10.dp ) CountingBar( From 789a505b3faf7d7b6b6009a16f2206c917a430f9 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:36:41 +0900 Subject: [PATCH 27/51] =?UTF-8?q?[feat]:=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=ED=95=9C=EB=A7=88=EB=94=94=20=EC=9E=91=EC=84=B1=20request,=20r?= =?UTF-8?q?esponse=20=EC=83=9D=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/rooms/request/RoomsDailyGreetingRequest.kt | 8 ++++++++ .../model/rooms/response/RoomsDailyGreetingResponse.kt | 10 ++++++++++ 2 files changed, 18 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsDailyGreetingRequest.kt create mode 100644 app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDailyGreetingResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsDailyGreetingRequest.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsDailyGreetingRequest.kt new file mode 100644 index 00000000..22b080fc --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsDailyGreetingRequest.kt @@ -0,0 +1,8 @@ +package com.texthip.thip.data.model.rooms.request + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsDailyGreetingRequest( + val content: String, +) diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDailyGreetingResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDailyGreetingResponse.kt new file mode 100644 index 00000000..04e567ea --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDailyGreetingResponse.kt @@ -0,0 +1,10 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsDailyGreetingResponse( + val attendanceCheckId: Long, + val roomId: Long, + val isFirstWrite: Boolean, +) From f8e5a8a0429e3a46ed8ef2bf32b15c296cc37e96 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:36:59 +0900 Subject: [PATCH 28/51] =?UTF-8?q?[feat]:=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=ED=95=9C=EB=A7=88=EB=94=94=20=EC=9E=91=EC=84=B1=20service,=20r?= =?UTF-8?q?epository=20=EC=9E=91=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/data/repository/RoomsRepository.kt | 13 +++++++++++++ .../com/texthip/thip/data/service/RoomsService.kt | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt index edd5c191..a26df35b 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt @@ -8,6 +8,7 @@ import com.texthip.thip.data.model.rooms.request.CreateRoomRequest import com.texthip.thip.data.model.rooms.request.RoomJoinRequest import com.texthip.thip.data.model.rooms.request.RoomSecretRoomRequest import com.texthip.thip.data.model.rooms.request.RoomsCreateVoteRequest +import com.texthip.thip.data.model.rooms.request.RoomsDailyGreetingRequest import com.texthip.thip.data.model.rooms.request.RoomsPostsLikesRequest import com.texthip.thip.data.model.rooms.request.RoomsRecordRequest import com.texthip.thip.data.model.rooms.request.RoomsVoteRequest @@ -246,4 +247,16 @@ class RoomsRepository @Inject constructor( ) ).handleBaseResponse().getOrThrow() } + + suspend fun postRoomsDailyGreeting( + roomId: Int, + content: String + ) = runCatching { + roomsService.postRoomsDailyGreeting( + roomId = roomId, + request = RoomsDailyGreetingRequest( + content = content + ) + ).handleBaseResponse().getOrThrow() + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt b/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt index 74819b06..f851efbd 100644 --- a/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt @@ -5,6 +5,7 @@ import com.texthip.thip.data.model.rooms.request.CreateRoomRequest import com.texthip.thip.data.model.rooms.request.RoomJoinRequest import com.texthip.thip.data.model.rooms.request.RoomSecretRoomRequest import com.texthip.thip.data.model.rooms.request.RoomsCreateVoteRequest +import com.texthip.thip.data.model.rooms.request.RoomsDailyGreetingRequest import com.texthip.thip.data.model.rooms.request.RoomsPostsLikesRequest import com.texthip.thip.data.model.rooms.request.RoomsRecordRequest import com.texthip.thip.data.model.rooms.request.RoomsVoteRequest @@ -18,6 +19,7 @@ import com.texthip.thip.data.model.rooms.response.RoomRecruitingResponse import com.texthip.thip.data.model.rooms.response.RoomSecretRoomResponse import com.texthip.thip.data.model.rooms.response.RoomsBookPageResponse import com.texthip.thip.data.model.rooms.response.RoomsCreateVoteResponse +import com.texthip.thip.data.model.rooms.response.RoomsDailyGreetingResponse import com.texthip.thip.data.model.rooms.response.RoomsDeleteRecordResponse import com.texthip.thip.data.model.rooms.response.RoomsDeleteVoteResponse import com.texthip.thip.data.model.rooms.response.RoomsPlayingResponse @@ -150,4 +152,10 @@ interface RoomsService { @Path("postId") postId: Int, @Body request: RoomsPostsLikesRequest ): BaseResponse + + @POST("rooms/{roomId}/daily-greeting") + suspend fun postRoomsDailyGreeting( + @Path("roomId") roomId: Int, + @Body request: RoomsDailyGreetingRequest + ): BaseResponse } \ No newline at end of file From e19c792287b89d2f7ae1e474aafe1eeab6a55308 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:38:15 +0900 Subject: [PATCH 29/51] =?UTF-8?q?[feat]:=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=ED=95=9C=EB=A7=88=EB=94=94=20=EC=9E=91=EC=84=B1=20viewmodel=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EB=B0=8F=20screen,=20navigation=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/room/component/GroupRoomBody.kt | 5 +- .../group/room/screen/GroupRoomChatScreen.kt | 72 +++++++++++++++---- .../ui/group/room/screen/GroupRoomScreen.kt | 4 ++ .../room/viewmodel/GroupRoomChatViewModel.kt | 52 ++++++++++++++ .../extensions/GroupNavigationExtensions.kt | 5 ++ .../navigator/navigations/GroupNavigation.kt | 11 +++ .../thip/ui/navigator/routes/GroupRoutes.kt | 3 + 7 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt b/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt index 1e25e71d..b505fc51 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt @@ -25,6 +25,7 @@ fun GroupRoomBody( userPercentage: Double, currentVotes: List, onNavigateToNote: () -> Unit = {}, + onNavigateToChat: () -> Unit = {}, onVoteClick: (CurrentVote) -> Unit = {} ) { Column( @@ -46,7 +47,9 @@ fun GroupRoomBody( CardChat( title = stringResource(R.string.group_room_chat), subtitle = stringResource(R.string.group_room_chat_description) - ) {} + ) { + onNavigateToChat() + } CardVote( voteData = currentVotes, diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt index a70fd91b..76c32cfd 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt @@ -1,5 +1,6 @@ package com.texthip.thip.ui.group.room.screen +import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -10,6 +11,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.HorizontalDivider 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 @@ -17,9 +19,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R import com.texthip.thip.ui.common.bottomsheet.MenuBottomSheet import com.texthip.thip.ui.common.cards.CardCommentGroup @@ -29,17 +33,55 @@ import com.texthip.thip.ui.common.view.CountingBar import com.texthip.thip.ui.group.room.mock.GroupRoomChatData import com.texthip.thip.ui.group.room.mock.MenuBottomSheetItem import com.texthip.thip.ui.group.room.mock.mockMessages +import com.texthip.thip.ui.group.room.viewmodel.GroupRoomChatEvent +import com.texthip.thip.ui.group.room.viewmodel.GroupRoomChatViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography +import kotlinx.coroutines.flow.collectLatest @Composable -fun GroupRoomChatScreen() { -// val mockMessages = emptyList() +fun GroupRoomChatScreen( + onBackClick: () -> Unit, + viewModel: GroupRoomChatViewModel = hiltViewModel() +) { + var inputText by remember { mutableStateOf("") } + val context = LocalContext.current + // val uiState by viewModel.uiState.collectAsState() + val chatMessages = emptyList() - var input by remember { mutableStateOf("") } - var replyTo by remember { mutableStateOf(null) } + LaunchedEffect(key1 = Unit) { + viewModel.eventFlow.collectLatest { event -> + when(event) { + is GroupRoomChatEvent.ShowToast -> { + Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() + } + is GroupRoomChatEvent.SubmissionSuccess -> { + } + } + } + } + GroupRoomChatContent( + chatMessages = chatMessages, + inputText = inputText, + onInputTextChanged = { newText -> inputText = newText }, + onSendClick = { + viewModel.postDailyGreeting(inputText) + inputText = "" + }, + onNavigateBack = onBackClick + ) +} + +@Composable +fun GroupRoomChatContent( + chatMessages: List, + inputText: String, + onInputTextChanged: (String) -> Unit, + onSendClick: () -> Unit, + onNavigateBack: () -> Unit +) { var isBottomSheetVisible by remember { mutableStateOf(false) } var selectedMessage by remember { mutableStateOf(null) } @@ -58,7 +100,7 @@ fun GroupRoomChatScreen() { ) { DefaultTopAppBar( title = stringResource(R.string.group_room_chat), - onLeftClick = {}, + onLeftClick = onNavigateBack, ) if (mockMessages.isEmpty()) { @@ -126,15 +168,10 @@ fun GroupRoomChatScreen() { } CommentTextField( - input = input, + input = inputText, hint = stringResource(R.string.group_room_chat_hint), - onInputChange = { input = it }, - onSendClick = { - input = "" - replyTo = null - }, - replyTo = replyTo, - onCancelReply = { replyTo = null } + onInputChange = onInputTextChanged, + onSendClick = onSendClick ) } } @@ -186,6 +223,13 @@ fun GroupRoomChatScreen() { @Composable private fun GroupRoomChatScreenPreview() { ThipTheme { - GroupRoomChatScreen() + var inputText by remember { mutableStateOf("") } + GroupRoomChatContent( + chatMessages = emptyList(), + inputText = inputText, + onInputTextChanged = { newText -> inputText = newText }, + onSendClick = {}, + onNavigateBack = {} + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomScreen.kt index 19ccfba6..01b1b996 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomScreen.kt @@ -51,6 +51,7 @@ fun GroupRoomScreen( roomId: Int, onBackClick: () -> Unit = {}, onNavigateToMates: () -> Unit = {}, + onNavigateToChat: () -> Unit = {}, onNavigateToNote: (page: Int?, isOverview: Boolean?) -> Unit = { _, _ -> }, viewModel: GroupRoomViewModel = hiltViewModel() ) { @@ -75,6 +76,7 @@ fun GroupRoomScreen( roomDetails = state.roomsPlaying, onBackClick = onBackClick, onNavigateToMates = onNavigateToMates, + onNavigateToChat = onNavigateToChat, onNavigateToNote = onNavigateToNote ) } @@ -93,6 +95,7 @@ fun GroupRoomContent( roomDetails: RoomsPlayingResponse, onBackClick: () -> Unit = {}, onNavigateToMates: () -> Unit = {}, + onNavigateToChat: () -> Unit = {}, onNavigateToNote: (page: Int?, isOverview: Boolean?) -> Unit = { _, _ -> }, ) { val scrollState = rememberScrollState() @@ -178,6 +181,7 @@ fun GroupRoomContent( currentVotes = roomDetails.currentVotes, // 일반 노트 카드 클릭 시 필터 없이 이동 onNavigateToNote = { onNavigateToNote(null, null) }, + onNavigateToChat = onNavigateToChat, // 투표 카드 클릭 시 필터 값과 함께 이동 onVoteClick = { vote: CurrentVote -> if (vote.isOverview) { diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt new file mode 100644 index 00000000..84e2066f --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomChatViewModel.kt @@ -0,0 +1,52 @@ +package com.texthip.thip.ui.group.room.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.repository.RoomsRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class GroupRoomChatUiState( + val isSubmitting: Boolean = false, + val error: String? = null +) + +sealed interface GroupRoomChatEvent { + data class ShowToast(val message: String) : GroupRoomChatEvent + data object SubmissionSuccess : GroupRoomChatEvent +} + +@HiltViewModel +class GroupRoomChatViewModel @Inject constructor( + private val roomsRepository: RoomsRepository, + savedStateHandle: SavedStateHandle +) : ViewModel() { + private val roomId: Int = requireNotNull(savedStateHandle["roomId"]) + + // TODO: 오늘의 한마디 조회 연결 + // private val _uiState = MutableStateFlow(GroupRoomChatUiState()) + // val uiState = _uiState.asStateFlow() + + private val _eventFlow = MutableSharedFlow() + val eventFlow = _eventFlow.asSharedFlow() + + fun postDailyGreeting(content: String) { + if (content.isBlank()) return + + viewModelScope.launch { + roomsRepository.postRoomsDailyGreeting( + roomId = roomId, + content = content + ).onSuccess { + _eventFlow.emit(GroupRoomChatEvent.SubmissionSuccess) + _eventFlow.emit(GroupRoomChatEvent.ShowToast("오늘의 한마디가 등록되었어요!")) + }.onFailure { + _eventFlow.emit(GroupRoomChatEvent.ShowToast(it.message ?: "등록에 실패했습니다.")) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt index 7812cf71..4de4f1b4 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt @@ -80,6 +80,11 @@ fun NavHostController.navigateToGroupRoomMates(roomId: Int) { navigate(GroupRoutes.RoomMates(roomId)) } +// 오늘의 한마디 회면으로 이동 +fun NavHostController.navigateToGroupRoomChat(roomId: Int) { + navigate(GroupRoutes.RoomChat(roomId)) +} + // 기록장 화면으로 이동 fun NavHostController.navigateToGroupNote( roomId: Int, diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index 2fd20e47..398c020b 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -20,6 +20,7 @@ import com.texthip.thip.ui.group.note.screen.GroupNoteCreateScreen import com.texthip.thip.ui.group.note.screen.GroupNoteScreen import com.texthip.thip.ui.group.note.screen.GroupVoteCreateScreen import com.texthip.thip.ui.group.note.viewmodel.GroupNoteViewModel +import com.texthip.thip.ui.group.room.screen.GroupRoomChatScreen import com.texthip.thip.ui.group.room.screen.GroupRoomMatesScreen import com.texthip.thip.ui.group.room.screen.GroupRoomRecruitScreen import com.texthip.thip.ui.group.room.screen.GroupRoomScreen @@ -37,6 +38,7 @@ import com.texthip.thip.ui.navigator.extensions.navigateToGroupNote import com.texthip.thip.ui.navigator.extensions.navigateToGroupNoteCreate import com.texthip.thip.ui.navigator.extensions.navigateToGroupRecruit import com.texthip.thip.ui.navigator.extensions.navigateToGroupRoom +import com.texthip.thip.ui.navigator.extensions.navigateToGroupRoomChat import com.texthip.thip.ui.navigator.extensions.navigateToGroupRoomMates import com.texthip.thip.ui.navigator.extensions.navigateToGroupRoomUnlock import com.texthip.thip.ui.navigator.extensions.navigateToGroupSearch @@ -271,6 +273,9 @@ fun NavGraphBuilder.groupNavigation( onNavigateToMates = { navController.navigateToGroupRoomMates(roomId) }, + onNavigateToChat = { + navController.navigateToGroupRoomChat(roomId) + }, onNavigateToNote = { page, isOverview -> navController.navigateToGroupNote(roomId, page, isOverview) }, @@ -293,6 +298,12 @@ fun NavGraphBuilder.groupNavigation( ) } + composable { + GroupRoomChatScreen( + onBackClick = { navigateBack() }, + ) + } + // Group Note 화면 composable { backStackEntry -> val route = backStackEntry.toRoute() diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt b/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt index 1cc8056f..bb7ec57c 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt @@ -36,6 +36,9 @@ sealed class GroupRoutes : Routes() { @Serializable data class RoomMates(val roomId: Int) : GroupRoutes() + @Serializable + data class RoomChat(val roomId: Int) : GroupRoutes() + @Serializable data class Note(val roomId: Int, val page: Int? = null, val isOverview: Boolean? = null) : GroupRoutes() From 0ffce8bfbd8a1a2b128c072dd89a67a1049a90ae Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:51:19 +0900 Subject: [PATCH 30/51] =?UTF-8?q?[feat]:=20=EC=A7=84=ED=96=89=EC=A4=91?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EC=9E=84=EB=B0=A9=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=B1=85=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=A1=9C=20=EB=84=98=EC=96=B4=EA=B0=80=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/ui/group/room/component/GroupRoomBody.kt | 7 ++++++- .../texthip/thip/ui/group/room/screen/GroupRoomScreen.kt | 7 ++++++- .../thip/ui/navigator/navigations/GroupNavigation.kt | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt b/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt index b505fc51..9f3d1a10 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt @@ -21,9 +21,11 @@ fun GroupRoomBody( modifier: Modifier = Modifier, bookTitle: String, authorName: String, + isbn: String, currentPage: Int, userPercentage: Double, currentVotes: List, + onNavigateToBookDetail: (isbn: String) -> Unit = {}, onNavigateToNote: () -> Unit = {}, onNavigateToChat: () -> Unit = {}, onVoteClick: (CurrentVote) -> Unit = {} @@ -35,7 +37,9 @@ fun GroupRoomBody( ActionBookButton( bookTitle = bookTitle, bookAuthor = authorName - ) {} + ) { + onNavigateToBookDetail(isbn) + } CardNote( currentPage = currentPage, @@ -64,6 +68,7 @@ private fun GroupRoomBodyPreview() { GroupRoomBody( bookTitle = "책 제목", authorName = "저자 이름", + isbn = "1234567890", currentPage = 100, userPercentage = 50.0, currentVotes = listOf( diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomScreen.kt index 01b1b996..11060209 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomScreen.kt @@ -53,6 +53,7 @@ fun GroupRoomScreen( onNavigateToMates: () -> Unit = {}, onNavigateToChat: () -> Unit = {}, onNavigateToNote: (page: Int?, isOverview: Boolean?) -> Unit = { _, _ -> }, + onNavigateToBookDetail: (isbn: String) -> Unit = {}, viewModel: GroupRoomViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -77,7 +78,8 @@ fun GroupRoomScreen( onBackClick = onBackClick, onNavigateToMates = onNavigateToMates, onNavigateToChat = onNavigateToChat, - onNavigateToNote = onNavigateToNote + onNavigateToNote = onNavigateToNote, + onNavigateToBookDetail = onNavigateToBookDetail ) } @@ -97,6 +99,7 @@ fun GroupRoomContent( onNavigateToMates: () -> Unit = {}, onNavigateToChat: () -> Unit = {}, onNavigateToNote: (page: Int?, isOverview: Boolean?) -> Unit = { _, _ -> }, + onNavigateToBookDetail: (isbn: String) -> Unit = {}, ) { val scrollState = rememberScrollState() @@ -176,9 +179,11 @@ fun GroupRoomContent( GroupRoomBody( bookTitle = roomDetails.bookTitle, authorName = roomDetails.authorName, + isbn = roomDetails.isbn, currentPage = roomDetails.currentPage, userPercentage = roomDetails.userPercentage, currentVotes = roomDetails.currentVotes, + onNavigateToBookDetail = onNavigateToBookDetail, // 일반 노트 카드 클릭 시 필터 없이 이동 onNavigateToNote = { onNavigateToNote(null, null) }, onNavigateToChat = onNavigateToChat, diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index 398c020b..a37c60a2 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -279,6 +279,9 @@ fun NavGraphBuilder.groupNavigation( onNavigateToNote = { page, isOverview -> navController.navigateToGroupNote(roomId, page, isOverview) }, + onNavigateToBookDetail = { isbn -> + navController.navigateToBookDetail(isbn) + } ) } From 2554e08f4c7b03829751962ee1bae4a2277463b1 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:54:15 +0900 Subject: [PATCH 31/51] =?UTF-8?q?[ui]:=20tooltip=20=EB=9C=A8=EB=8A=94=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/ui/group/note/screen/GroupNoteCreateScreen.kt | 2 +- .../texthip/thip/ui/group/note/screen/GroupVoteCreateScreen.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteCreateScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteCreateScreen.kt index bf69ed75..806d20ff 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteCreateScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteCreateScreen.kt @@ -113,7 +113,7 @@ fun GroupNoteCreateContent( } if (showTooltip && iconCoordinates.value != null) { val yOffsetDp = with(density) { - iconCoordinates.value!!.positionInRoot().y.toDp() + 32.dp + iconCoordinates.value!!.positionInRoot().y.toDp() } Box( diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupVoteCreateScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupVoteCreateScreen.kt index 832bda37..b4a613b3 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupVoteCreateScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupVoteCreateScreen.kt @@ -126,7 +126,7 @@ fun GroupVoteCreateContent( if (showTooltip && iconCoordinates.value != null) { val yOffsetDp = with(density) { - iconCoordinates.value!!.positionInRoot().y.toDp() + 32.dp + iconCoordinates.value!!.positionInRoot().y.toDp() } Box( From 8372dfbef33d7c1e3b917117f0f49f8f0fd8d85f Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 19:18:59 +0900 Subject: [PATCH 32/51] =?UTF-8?q?[refactor]:=20=ED=88=AC=ED=91=9C=ED=95=9C?= =?UTF-8?q?=20=ED=95=AD=EB=AA=A9=EB=A7=8C=20=EB=B0=94=EB=80=8C=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/rooms/response/RoomsVoteResponse.kt | 6 +- .../thip/ui/group/note/mock/CommentData.kt | 121 ------------------ .../note/viewmodel/GroupNoteViewModel.kt | 46 ++++++- 3 files changed, 44 insertions(+), 129 deletions(-) delete mode 100644 app/src/main/java/com/texthip/thip/ui/group/note/mock/CommentData.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsVoteResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsVoteResponse.kt index 1fd74ff9..1d90a17f 100644 --- a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsVoteResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsVoteResponse.kt @@ -4,7 +4,5 @@ import kotlinx.serialization.Serializable @Serializable data class RoomsVoteResponse( - val voteItemId: Int, - val roomId: Int, - val type: Boolean, -) + val voteItems: List, +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/mock/CommentData.kt b/app/src/main/java/com/texthip/thip/ui/group/note/mock/CommentData.kt deleted file mode 100644 index 5f7cbcd8..00000000 --- a/app/src/main/java/com/texthip/thip/ui/group/note/mock/CommentData.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.texthip.thip.ui.group.note.mock - -sealed class GroupNoteItem { -// abstract val postDate: String - abstract val postDate: String - abstract val page: Int - abstract val userId: Int - abstract val nickName: String - abstract val profileImageUrl: String - abstract val content: String - abstract val likeCount: Int - abstract val commentCount: Int - abstract val isLiked: Boolean - abstract val isWriter: Boolean - abstract val isLocked: Boolean -} - -data class GroupNoteRecord( - override val postDate: String, - override val page: Int, - override val userId: Int, - override val nickName: String, - override val profileImageUrl: String, - override val content: String, - override val likeCount: Int, - override val commentCount: Int, - override val isLiked: Boolean, - override val isWriter: Boolean, - override val isLocked: Boolean, - val recordId: Int -) : GroupNoteItem() - -data class GroupNoteVote( - override val postDate: String, - override val page: Int, - override val userId: Int, - override val nickName: String, - override val profileImageUrl: String, - override val content: String, - override val likeCount: Int, - override val commentCount: Int, - override val isLiked: Boolean, - override val isWriter: Boolean, - val voteId: Int, - override val isLocked: Boolean, - val voteItems: List -) : GroupNoteItem() - -data class VoteItem( - val voteItemId: Int, - val itemName: String, - val percentage: Int, - val isVoted: Boolean -) - -val mockGroupNoteItems: List = listOf( - GroupNoteRecord( - page = 132, - postDate = "12시간 전", - userId = 1, - nickName = "user.01", - profileImageUrl = "https://example.com/profile.jpg", - content = "내 생각에 이 부분이 가장 어려운 것 같다. 비유도 난해하고 잘 이해가 가지 않는데 다른 메이트들은 어떻게 읽었나요?", - likeCount = 123, - commentCount = 123, - isLiked = true, - isWriter = false, - isLocked = false, - recordId = 1 - ), - GroupNoteVote( - page = 12, - postDate = "12시간 전", - userId = 1, - nickName = "user.01", - profileImageUrl = "https://example.com/profile.jpg", - content = "3연에 나오는 심장은 무엇을 의미하는 걸까요?", - likeCount = 123, - commentCount = 123, - isLiked = false, - isWriter = false, - isLocked = true, - voteId = 1, - voteItems = listOf( - VoteItem(1, "김땡땡", 90, false), - VoteItem(2, "김땡땡", 10, false) - ) - ), - GroupNoteRecord( - page = 132, - postDate = "12시간 전", - userId = 1, - nickName = "user.01", - profileImageUrl = "https://example.com/profile.jpg", - content = "공백 포함 글자 입력입니다.", - likeCount = 123, - commentCount = 123, - isLiked = false, - isWriter = true, - isLocked = false, - recordId = 1 - ), - GroupNoteVote( - page = 12, - postDate = "12시간 전", - userId = 1, - nickName = "user.01", - profileImageUrl = "https://example.com/profile.jpg", - content = "3연에 나오는 심장은 무엇을 의미하는 걸까요?", - likeCount = 123, - commentCount = 123, - isLiked = true, - isWriter = true, - isLocked = false, - voteId = 1, - voteItems = listOf( - VoteItem(1, "김땡땡", 90, false), - VoteItem(2, "김땡땡", 10, false) - ) - ) -) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt index ff1bcae1..da491654 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt @@ -204,16 +204,54 @@ class GroupNoteViewModel @Inject constructor( } private fun vote(postId: Int, voteItemId: Int, type: Boolean) { + val originalPosts = _uiState.value.posts + val postIndex = originalPosts.indexOfFirst { it.postId == postId } + if (postIndex == -1) return + val postToUpdate = originalPosts[postIndex] + + val optimisticVoteItems = postToUpdate.voteItems.map { voteItem -> + voteItem.copy(isVoted = if (voteItem.voteItemId == voteItemId) type else false) + } + + val optimisticPosts = originalPosts.toMutableList().apply { + this[postIndex] = postToUpdate.copy(voteItems = optimisticVoteItems) + } + + _uiState.update { it.copy(posts = optimisticPosts) } + viewModelScope.launch { roomsRepository.postRoomsVote( roomId = roomId, voteId = postId, voteItemId = voteItemId, type = type - ).onSuccess { - loadPosts(isRefresh = true) - }.onFailure { throwable -> - _uiState.update { it.copy(error = throwable.message) } + ).onSuccess { voteResponse -> + if (voteResponse != null) { + val serverVoteItems = voteResponse.voteItems + + // 현재 UI가 가지고 있는 포스트 목록을 가져오기 + val currentPosts = _uiState.value.posts + val postIndex = currentPosts.indexOfFirst { it.postId == postId } + if (postIndex == -1) return@onSuccess + + val postToUpdate = currentPosts[postIndex] + + // 기존 순서는 유지하고 내용만 업데이트 + val updatedVoteItems = postToUpdate.voteItems.map { originalItem -> + val newItem = serverVoteItems.find { it.voteItemId == originalItem.voteItemId } + newItem ?: originalItem + } + + // 순서가 유지된 목록으로 최종 업데이트 + val finalPosts = currentPosts.toMutableList().apply { + this[postIndex] = postToUpdate.copy(voteItems = updatedVoteItems) + } + _uiState.update { it.copy(posts = finalPosts) } + } else { + loadPosts(isRefresh = true) + } + }.onFailure { + _uiState.update { it.copy(posts = originalPosts) } } } } From 940b89dfb626c7973336d5bf3bdd2b42f69fa0de Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 19:26:50 +0900 Subject: [PATCH 33/51] =?UTF-8?q?[ui]:=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=ED=95=9C=EB=A7=88=EB=94=94=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=97=90=EC=84=9C=20textfield=EB=8F=84=20=EC=9C=84=EB=A1=9C=20?= =?UTF-8?q?=EC=98=AC=EB=9D=BC=EC=98=A4=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt index 76c32cfd..fa771b18 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt @@ -38,6 +38,7 @@ import com.texthip.thip.ui.group.room.viewmodel.GroupRoomChatViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography +import com.texthip.thip.utils.rooms.advancedImePadding import kotlinx.coroutines.flow.collectLatest @Composable @@ -97,6 +98,7 @@ fun GroupRoomChatContent( Column( modifier = Modifier .fillMaxSize() + .advancedImePadding() ) { DefaultTopAppBar( title = stringResource(R.string.group_room_chat), From 55b30b20e35b7e064147a167607080f907b443c4 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 19:31:29 +0900 Subject: [PATCH 34/51] =?UTF-8?q?[chore]:=20=ED=8D=BC=EC=84=BC=ED=8A=B8=20?= =?UTF-8?q?=EC=86=8C=EC=88=98=EC=A0=90=20=EB=B0=98=EC=98=AC=EB=A6=BC?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/texthip/thip/ui/common/cards/CardNote.kt | 4 ++-- .../com/texthip/thip/ui/group/room/component/GroupRoomBody.kt | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardNote.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardNote.kt index 018263bd..e6a8473b 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardNote.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardNote.kt @@ -27,7 +27,7 @@ import com.texthip.thip.ui.theme.ThipTheme.typography @Composable fun CardNote( currentPage: Int, - percentage: Double, + percentage: Int, onClick: () -> Unit = { } ) { Column( @@ -98,7 +98,7 @@ fun CardNote( private fun CardNotePreview() { CardNote( currentPage = 50, - percentage = 30.0, + percentage = 30, onClick = { } ) } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt b/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt index 9f3d1a10..1a33072a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomBody.kt @@ -15,6 +15,7 @@ import com.texthip.thip.ui.common.buttons.ActionBookButton import com.texthip.thip.ui.common.cards.CardChat import com.texthip.thip.ui.common.cards.CardNote import com.texthip.thip.ui.common.cards.CardVote +import kotlin.math.roundToInt @Composable fun GroupRoomBody( @@ -43,7 +44,7 @@ fun GroupRoomBody( CardNote( currentPage = currentPage, - percentage = userPercentage + percentage = userPercentage.roundToInt(), ) { onNavigateToNote() } From 172b43c6ceba42611f2846ad764627274965db2d Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 20:46:29 +0900 Subject: [PATCH 35/51] =?UTF-8?q?[feat]:=20=EA=B8=B0=EB=A1=9D=20=ED=95=80?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20response,=20service,=20repository=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/rooms/response/RoomsRecordsPinResponse.kt | 11 +++++++++++ .../texthip/thip/data/repository/RoomsRepository.kt | 10 ++++++++++ .../com/texthip/thip/data/service/RoomsService.kt | 7 +++++++ 3 files changed, 28 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsRecordsPinResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsRecordsPinResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsRecordsPinResponse.kt new file mode 100644 index 00000000..14b51994 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsRecordsPinResponse.kt @@ -0,0 +1,11 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsRecordsPinResponse( + val bookTitle: String, + val authorName: String, + val bookImageUrl: String, + val isbn: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt index a26df35b..c8539668 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt @@ -259,4 +259,14 @@ class RoomsRepository @Inject constructor( ) ).handleBaseResponse().getOrThrow() } + + suspend fun getRoomsRecordsPin( + roomId: Int, + recordId: Int + ) = runCatching { + roomsService.getRoomsRecordsPin( + roomId = roomId, + recordId = recordId + ).handleBaseResponse().getOrThrow() + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt b/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt index f851efbd..80a9bedd 100644 --- a/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt @@ -26,6 +26,7 @@ import com.texthip.thip.data.model.rooms.response.RoomsPlayingResponse import com.texthip.thip.data.model.rooms.response.RoomsPostsLikesResponse import com.texthip.thip.data.model.rooms.response.RoomsPostsResponse import com.texthip.thip.data.model.rooms.response.RoomsRecordResponse +import com.texthip.thip.data.model.rooms.response.RoomsRecordsPinResponse import com.texthip.thip.data.model.rooms.response.RoomsUsersResponse import com.texthip.thip.data.model.rooms.response.RoomsVoteResponse import retrofit2.http.Body @@ -158,4 +159,10 @@ interface RoomsService { @Path("roomId") roomId: Int, @Body request: RoomsDailyGreetingRequest ): BaseResponse + + @GET("rooms/{roomId}/records/{recordId}/pin") + suspend fun getRoomsRecordsPin( + @Path("roomId") roomId: Int, + @Path("recordId") recordId: Int + ): BaseResponse } \ No newline at end of file From 401426c0d573bf6d45ad665ec94a180735ac5a21 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sat, 16 Aug 2025 20:48:16 +0900 Subject: [PATCH 36/51] =?UTF-8?q?[feat]:=20=EA=B8=B0=EB=A1=9D=20=ED=95=80?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20viewmodel=20=EC=9E=91=EC=84=B1=20=EB=B0=8F?= =?UTF-8?q?=20screen,=20navigation=20=EC=97=B0=EA=B2=B0=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/feed/viewmodel/FeedWriteViewModel.kt | 22 ++++++++++++ .../group/note/component/VoteCommentCard.kt | 5 --- .../ui/group/note/screen/GroupNoteScreen.kt | 31 ++++++++++++++-- .../note/viewmodel/GroupNoteViewModel.kt | 33 +++++++++++++++++ .../extensions/FeedNavigationExtensions.kt | 16 +++++++-- .../navigator/navigations/FeedNavigation.kt | 35 +++++++++++++++---- .../navigator/navigations/GroupNavigation.kt | 10 ++++++ .../thip/ui/navigator/routes/FeedRoutes.kt | 9 ++++- 8 files changed, 143 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt index b6fc4b6b..7a84240f 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt @@ -39,6 +39,28 @@ class FeedWriteViewModel @Inject constructor( loadFeedWriteInfo() } + fun setPinnedRecord( + isbn: String, + bookTitle: String, + bookAuthor: String, + bookImageUrl: String, + recordContent: String + ) { + val pinnedBook = BookData( + title = bookTitle, + imageUrl = bookImageUrl, + author = bookAuthor, + isbn = isbn + ) + updateState { + it.copy( + selectedBook = pinnedBook, + isBookPreselected = true, + feedContent = recordContent + ) + } + } + private fun loadFeedWriteInfo() { viewModelScope.launch { updateState { it.copy(isLoadingCategories = true) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/VoteCommentCard.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/VoteCommentCard.kt index 8149929c..04405d8c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/VoteCommentCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/VoteCommentCard.kt @@ -28,7 +28,6 @@ fun VoteCommentCard( onVote: (postId: Int, voteItemId: Int, type: Boolean) -> Unit = { _, _, _ -> }, onCommentClick: () -> Unit = {}, onLongPress: () -> Unit = {}, - onPinClick: () -> Unit = {} ) { val selectedIndex = data.voteItems.indexOfFirst { it.isVoted }.takeIf { it != -1 } val hasVoted = selectedIndex != null @@ -93,15 +92,11 @@ fun VoteCommentCard( isLiked = data.isLiked, likeCount = data.likeCount, commentCount = data.commentCount, - isPinVisible = isWriter, onLikeClick = { if (!isLocked) onLikeClick(data.postId, data.postType) }, onCommentClick = { if (!isLocked) onCommentClick() - }, - onPinClick = { - if (!isLocked) onPinClick() } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt index 817cb231..f256aa78 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt @@ -44,6 +44,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.texthip.thip.R import com.texthip.thip.data.model.rooms.response.PostList +import com.texthip.thip.data.model.rooms.response.RoomsRecordsPinResponse import com.texthip.thip.ui.common.bottomsheet.MenuBottomSheet import com.texthip.thip.ui.common.buttons.ExpandableFloatingButton import com.texthip.thip.ui.common.buttons.FabMenuItem @@ -59,6 +60,7 @@ import com.texthip.thip.ui.group.note.component.VoteCommentCard import com.texthip.thip.ui.group.note.viewmodel.CommentsEvent import com.texthip.thip.ui.group.note.viewmodel.CommentsViewModel import com.texthip.thip.ui.group.note.viewmodel.GroupNoteEvent +import com.texthip.thip.ui.group.note.viewmodel.GroupNoteSideEffect import com.texthip.thip.ui.group.note.viewmodel.GroupNoteUiState import com.texthip.thip.ui.group.note.viewmodel.GroupNoteViewModel import com.texthip.thip.ui.group.room.mock.MenuBottomSheetItem @@ -76,6 +78,7 @@ fun GroupNoteScreen( onBackClick: () -> Unit = {}, onCreateNoteClick: (recentPage: Int, totalPage: Int, isOverviewPossible: Boolean) -> Unit, onCreateVoteClick: (recentPage: Int, totalPage: Int, isOverviewPossible: Boolean) -> Unit, + onNavigateToFeedWrite: (pinInfo: RoomsRecordsPinResponse, recordContent: String) -> Unit, resultTabIndex: Int? = null, onResultConsumed: () -> Unit = {}, initialPage: Int? = null, @@ -126,6 +129,16 @@ fun GroupNoteScreen( } } + LaunchedEffect(viewModel.sideEffect) { + viewModel.sideEffect.collect { effect -> + when (effect) { + is GroupNoteSideEffect.NavigateToFeedWrite -> { + onNavigateToFeedWrite(effect.pinInfo, effect.recordContent) + } + } + } + } + GroupNoteContent( uiState = uiState, onEvent = viewModel::onEvent, @@ -160,6 +173,7 @@ fun GroupNoteContent( var selectedPostForMenu by remember { mutableStateOf(null) } var showDeleteDialog by remember { mutableStateOf(false) } var isPinDialogVisible by remember { mutableStateOf(false) } + var postToPin by remember { mutableStateOf(null) } var showToast by remember { mutableStateOf(false) } val isOverlayVisible = isCommentBottomSheetVisible || selectedPostForMenu != null || isPinDialogVisible || showDeleteDialog @@ -370,7 +384,10 @@ fun GroupNoteContent( isCommentBottomSheetVisible = true }, onLongPress = { selectedPostForMenu = post }, - onPinClick = { isPinDialogVisible = true }, + onPinClick = { + postToPin = post + isPinDialogVisible = true + }, onLikeClick = { postId, postType -> onEvent(GroupNoteEvent.OnLikeRecord(postId, postType)) } @@ -384,7 +401,6 @@ fun GroupNoteContent( isCommentBottomSheetVisible = true }, onLongPress = { selectedPostForMenu = post }, - onPinClick = { isPinDialogVisible = true }, onVote = { postId, voteItemId, type -> onEvent(GroupNoteEvent.OnVote(postId, voteItemId, type)) }, @@ -569,11 +585,20 @@ fun GroupNoteContent( title = stringResource(R.string.pin_modal_title), description = stringResource(R.string.pin_modal_content), onConfirm = { - // 핀하기 로직 + postToPin?.let { post -> + onEvent( + GroupNoteEvent.OnPinRecord( + recordId = post.postId, + content = post.content + ) + ) + } isPinDialogVisible = false + postToPin = null }, onCancel = { isPinDialogVisible = false + postToPin = null } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt index da491654..e401fdb7 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt @@ -5,12 +5,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.model.rooms.request.RoomsPostsRequestParams import com.texthip.thip.data.model.rooms.response.PostList +import com.texthip.thip.data.model.rooms.response.RoomsRecordsPinResponse import com.texthip.thip.data.repository.RoomsRepository import com.texthip.thip.utils.type.SortType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -39,6 +42,13 @@ data class GroupNoteUiState( val totalEnabled: Boolean = false ) +sealed interface GroupNoteSideEffect { + data class NavigateToFeedWrite( + val pinInfo: RoomsRecordsPinResponse, + val recordContent: String + ) : GroupNoteSideEffect +} + sealed interface GroupNoteEvent { data class OnTabSelected(val index: Int) : GroupNoteEvent data class OnSortSelected(val sortType: SortType) : GroupNoteEvent @@ -50,6 +60,7 @@ sealed interface GroupNoteEvent { data class OnVote(val postId: Int, val voteItemId: Int, val type: Boolean) : GroupNoteEvent data class OnDeleteRecord(val postId: Int, val postType: String) : GroupNoteEvent data class OnLikeRecord(val postId: Int, val postType: String) : GroupNoteEvent + data class OnPinRecord(val recordId: Int, val content: String) : GroupNoteEvent data object RefreshPosts : GroupNoteEvent } @@ -62,6 +73,9 @@ class GroupNoteViewModel @Inject constructor( private val _uiState = MutableStateFlow(GroupNoteUiState()) val uiState = _uiState.asStateFlow() + private val _sideEffect = MutableSharedFlow() + val sideEffect = _sideEffect.asSharedFlow() + private var nextCursor: String? = null private var roomId: Int = -1 @@ -149,12 +163,31 @@ class GroupNoteViewModel @Inject constructor( is GroupNoteEvent.OnDeleteRecord -> deletePost(event.postId, event.postType) is GroupNoteEvent.OnLikeRecord -> likeRecord(event.postId, event.postType) is GroupNoteEvent.RefreshPosts -> loadPosts(isRefresh = true) + is GroupNoteEvent.OnPinRecord -> pinRecord(event.recordId, event.content) else -> { Log.w("GroupNoteViewModel", "Unhandled event received: $event") } } } + private fun pinRecord(recordId: Int, content: String) { + viewModelScope.launch { + roomsRepository.getRoomsRecordsPin(roomId = roomId, recordId = recordId) + .onSuccess { pinInfo -> + if (pinInfo != null) { + _sideEffect.emit( + GroupNoteSideEffect.NavigateToFeedWrite( + pinInfo = pinInfo, + recordContent = content + ) + ) + } + } + .onFailure { + } + } + } + private fun likeRecord(postId: Int, postType: String) { val currentPosts = _uiState.value.posts val postIndex = currentPosts.indexOfFirst { it.postId == postId } diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt index 5f5f7f46..f4d82e6e 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt @@ -15,8 +15,20 @@ fun NavHostController.navigateToMySubscription() { } // 피드 작성으로 -fun NavHostController.navigateToFeedWrite() { - navigate(FeedRoutes.Write) +fun NavHostController.navigateToFeedWrite( + isbn: String? = null, + bookTitle: String? = null, + bookAuthor: String? = null, + bookImageUrl: String? = null, + recordContent: String? = null +) { + navigate(FeedRoutes.Write( + isbn = isbn, + bookTitle = bookTitle, + bookAuthor = bookAuthor, + bookImageUrl = bookImageUrl, + recordContent = recordContent + )) } // 유저 프로필(피드)로 diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt index 7212d086..16635c0b 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt @@ -1,12 +1,16 @@ package com.texthip.thip.ui.navigator.navigations +import androidx.compose.runtime.LaunchedEffect +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable +import androidx.navigation.toRoute import com.texthip.thip.ui.feed.screen.FeedOthersScreen import com.texthip.thip.ui.feed.screen.FeedScreen import com.texthip.thip.ui.feed.screen.FeedWriteScreen import com.texthip.thip.ui.feed.screen.MySubscriptionScreen +import com.texthip.thip.ui.feed.viewmodel.FeedWriteViewModel import com.texthip.thip.ui.navigator.extensions.navigateToFeedWrite import com.texthip.thip.ui.navigator.extensions.navigateToMySubscription import com.texthip.thip.ui.navigator.routes.FeedRoutes @@ -46,17 +50,34 @@ fun NavGraphBuilder.feedNavigation(navController: NavHostController, navigateBac } ) } - composable { + composable { backStackEntry -> + val route = backStackEntry.toRoute() + val viewModel: FeedWriteViewModel = hiltViewModel() + + LaunchedEffect(route) { + if (route.isbn != null && + route.bookTitle != null && + route.bookAuthor != null && + route.bookImageUrl != null && + route.recordContent != null) { + viewModel.setPinnedRecord( + isbn = route.isbn, + bookTitle = route.bookTitle, + bookAuthor = route.bookAuthor, + bookImageUrl = route.bookImageUrl, + recordContent = route.recordContent + ) + } + } + FeedWriteScreen( - onNavigateBack = { - navigateBack() - }, + viewModel = viewModel, + onNavigateBack = { navigateBack() }, onFeedCreated = { feedId -> // 피드 생성 성공 시 결과를 저장하고 피드 목록으로 돌아가기 navController.getBackStackEntry(MainTabRoutes.Feed) - .savedStateHandle - .set("feedId", feedId) - navController.popBackStack() + .savedStateHandle["feedId"] = feedId + navController.popBackStack(MainTabRoutes.Feed, inclusive = false) } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index a37c60a2..28320fff 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -31,6 +31,7 @@ import com.texthip.thip.ui.group.search.screen.GroupSearchScreen import com.texthip.thip.ui.group.viewmodel.GroupViewModel import com.texthip.thip.ui.navigator.extensions.navigateToAlarm import com.texthip.thip.ui.navigator.extensions.navigateToBookDetail +import com.texthip.thip.ui.navigator.extensions.navigateToFeedWrite import com.texthip.thip.ui.navigator.extensions.navigateToGroupDone import com.texthip.thip.ui.navigator.extensions.navigateToGroupMakeRoom import com.texthip.thip.ui.navigator.extensions.navigateToGroupMy @@ -345,6 +346,15 @@ fun NavGraphBuilder.groupNavigation( isOverviewPossible = isOverviewPossible ) }, + onNavigateToFeedWrite = { pinInfo, recordContent -> + navController.navigateToFeedWrite( + isbn = pinInfo.isbn, + bookTitle = pinInfo.bookTitle, + bookAuthor = pinInfo.authorName, + bookImageUrl = pinInfo.bookImageUrl, + recordContent = recordContent + ) + }, viewModel = viewModel ) } diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt b/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt index c91b26b9..3ed5e23b 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt @@ -5,7 +5,14 @@ import kotlinx.serialization.Serializable @Serializable sealed class FeedRoutes : Routes() { @Serializable data object MySubscription : FeedRoutes() - @Serializable data object Write : FeedRoutes() + @Serializable + data class Write( + val isbn: String? = null, + val bookTitle: String? = null, + val bookAuthor: String? = null, + val bookImageUrl: String? = null, + val recordContent: String? = null + ) : FeedRoutes() @Serializable data class Others(val userId: Long) : FeedRoutes() } \ No newline at end of file From df5f9c557c7f409e1fe0c17481b93771d60a4eb3 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 15:04:38 +0900 Subject: [PATCH 37/51] =?UTF-8?q?[feat]:=20=ED=94=BC=EB=93=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20dto,=20service,=20repository=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/feeds/response/AllFeedResponse.kt | 33 +++++++++++++++++++ .../model/feeds/response/MyFeedResponse.kt | 27 +++++++++++++++ .../thip/data/repository/FeedRepository.kt | 16 +++++++++ .../texthip/thip/data/service/FeedService.kt | 15 +++++++++ 4 files changed, 91 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/feeds/response/AllFeedResponse.kt create mode 100644 app/src/main/java/com/texthip/thip/data/model/feeds/response/MyFeedResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/feeds/response/AllFeedResponse.kt b/app/src/main/java/com/texthip/thip/data/model/feeds/response/AllFeedResponse.kt new file mode 100644 index 00000000..e9bda179 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/feeds/response/AllFeedResponse.kt @@ -0,0 +1,33 @@ +package com.texthip.thip.data.model.feeds.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class AllFeedResponse( + @SerialName("feedList") val feedList: List, + @SerialName("nextCursor") val nextCursor: String?, + @SerialName("isLast") val isLast: Boolean +) + +@Serializable +data class AllFeedItem( + @SerialName("feedId") val feedId: Int, + @SerialName("creatorId") val creatorId: Int, + @SerialName("creatorNickname") val creatorNickname: String, + @SerialName("creatorProfileImageUrl") val creatorProfileImageUrl: String?, + @SerialName("aliasName") val aliasName: String, + @SerialName("aliasColor") val aliasColor: String, + @SerialName("postDate") val postDate: String, + @SerialName("isbn") val isbn: String, + @SerialName("bookTitle") val bookTitle: String, + @SerialName("bookAuthor") val bookAuthor: String, + @SerialName("contentBody") val contentBody: String, + @SerialName("contentUrls") val contentUrls: List, + @SerialName("likeCount") val likeCount: Int, + @SerialName("commentCount") val commentCount: Int, + @SerialName("isSaved") val isSaved: Boolean, + @SerialName("isLiked") val isLiked: Boolean, + @SerialName("isWriter") val isWriter: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/feeds/response/MyFeedResponse.kt b/app/src/main/java/com/texthip/thip/data/model/feeds/response/MyFeedResponse.kt new file mode 100644 index 00000000..dae8e309 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/feeds/response/MyFeedResponse.kt @@ -0,0 +1,27 @@ +package com.texthip.thip.data.model.feeds.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class MyFeedResponse( + @SerialName("feedList") val feedList: List, + @SerialName("nextCursor") val nextCursor: String?, + @SerialName("isLast") val isLast: Boolean +) + +@Serializable +data class MyFeedItem( + @SerialName("feedId") val feedId: Int, + @SerialName("postDate") val postDate: String, + @SerialName("isbn") val isbn: String, + @SerialName("bookTitle") val bookTitle: String, + @SerialName("bookAuthor") val bookAuthor: String, + @SerialName("contentBody") val contentBody: String, + @SerialName("contentUrls") val contentUrls: List, + @SerialName("likeCount") val likeCount: Int, + @SerialName("commentCount") val commentCount: Int, + @SerialName("isPublic") val isPublic: Boolean, + @SerialName("isWriter") val isWriter: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt index 0a8dc1ae..9364c5c7 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt @@ -6,6 +6,8 @@ import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.feed.request.CreateFeedRequest import com.texthip.thip.data.model.feed.response.CreateFeedResponse import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse +import com.texthip.thip.data.model.feeds.response.AllFeedResponse +import com.texthip.thip.data.model.feeds.response.MyFeedResponse import com.texthip.thip.data.service.FeedService import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -129,6 +131,20 @@ class FeedRepository @Inject constructor( } } + /** 전체 피드 목록 조회 */ + suspend fun getAllFeeds(cursor: String? = null): Result = runCatching { + feedService.getAllFeeds(cursor) + .handleBaseResponse() + .getOrThrow() + } + + /** 내 피드 목록 조회 */ + suspend fun getMyFeeds(cursor: String? = null): Result = runCatching { + feedService.getMyFeeds(cursor) + .handleBaseResponse() + .getOrThrow() + } + /** 임시 파일들을 정리하는 함수 */ private fun cleanupTempFiles(tempFiles: List) { tempFiles.forEach { file -> diff --git a/app/src/main/java/com/texthip/thip/data/service/FeedService.kt b/app/src/main/java/com/texthip/thip/data/service/FeedService.kt index fc216f1e..bf69576b 100644 --- a/app/src/main/java/com/texthip/thip/data/service/FeedService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/FeedService.kt @@ -3,12 +3,15 @@ package com.texthip.thip.data.service import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.feed.response.CreateFeedResponse import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse +import com.texthip.thip.data.model.feeds.response.AllFeedResponse +import com.texthip.thip.data.model.feeds.response.MyFeedResponse import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.GET import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part +import retrofit2.http.Query interface FeedService { @@ -23,4 +26,16 @@ interface FeedService { @Part("request") request: RequestBody, @Part images: List? ): BaseResponse + + /** 전체 피드 목록 조회 */ + @GET("feeds") + suspend fun getAllFeeds( + @Query("cursor") cursor: String? = null + ): BaseResponse + + /** 내 피드 목록 조회 */ + @GET("feeds/mine") + suspend fun getMyFeeds( + @Query("cursor") cursor: String? = null + ): BaseResponse } \ No newline at end of file From a5c3f8618244a7e8a5ac23957151744eee919e8c Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 15:21:39 +0900 Subject: [PATCH 38/51] =?UTF-8?q?[feat]:=20=ED=94=BC=EB=93=9C=20ViewModel?= =?UTF-8?q?=EC=97=90=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/feed/viewmodel/FeedViewModel.kt | 224 +++++++++++++++++- 1 file changed, 214 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt index 04b3bdd5..3408bf11 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt @@ -2,48 +2,252 @@ package com.texthip.thip.ui.feed.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.feeds.response.AllFeedItem +import com.texthip.thip.data.model.feeds.response.MyFeedItem import com.texthip.thip.data.model.users.response.RecentWriterList +import com.texthip.thip.data.repository.FeedRepository import com.texthip.thip.data.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject data class FeedUiState( - val isLoading: Boolean = true, + val selectedTabIndex: Int = 0, + val allFeeds: List = emptyList(), + val myFeeds: List = emptyList(), val recentWriters: List = emptyList(), - val errorMessage: String? = null - //TODO 추후 피드 목록 등 다른 상태들 추가될 예정 -) + val isLoading: Boolean = false, + val isRefreshing: Boolean = false, + val isLoadingMore: Boolean = false, + val isLastPageAllFeeds: Boolean = false, + val isLastPageMyFeeds: Boolean = false, + val error: String? = null +) { + val canLoadMoreAllFeeds: Boolean get() = !isLoading && !isLoadingMore && !isLastPageAllFeeds + val canLoadMoreMyFeeds: Boolean get() = !isLoading && !isLoadingMore && !isLastPageMyFeeds + val currentTabFeeds: List get() = when (selectedTabIndex) { + 0 -> allFeeds + 1 -> myFeeds + else -> emptyList() + } + val canLoadMoreCurrentTab: Boolean get() = when (selectedTabIndex) { + 0 -> canLoadMoreAllFeeds + 1 -> canLoadMoreMyFeeds + else -> false + } +} @HiltViewModel class FeedViewModel @Inject constructor( + private val feedRepository: FeedRepository, private val userRepository: UserRepository ) : ViewModel() { - private val _uiState = MutableStateFlow(FeedUiState()) val uiState = _uiState.asStateFlow() + + private var allFeedsNextCursor: String? = null + private var myFeedsNextCursor: String? = null + private var isLoadingAllFeeds = false + private var isLoadingMyFeeds = false + + private fun updateState(update: (FeedUiState) -> FeedUiState) { + _uiState.value = update(_uiState.value) + } init { + loadAllFeeds() fetchRecentWriters() } + fun onTabSelected(index: Int) { + updateState { it.copy(selectedTabIndex = index) } + + when (index) { + 0 -> { + loadAllFeeds(isInitial = true) + } + 1 -> { + loadMyFeeds(isInitial = true) + } + } + } + + private fun loadAllFeeds(isInitial: Boolean = true) { + if (isLoadingAllFeeds && !isInitial) return + if (_uiState.value.isLastPageAllFeeds && !isInitial) return + + viewModelScope.launch { + try { + isLoadingAllFeeds = true + + if (isInitial) { + updateState { it.copy(isLoading = true, allFeeds = emptyList(), isLastPageAllFeeds = false) } + allFeedsNextCursor = null + } else { + updateState { it.copy(isLoadingMore = true) } + } + + val cursor = if (isInitial) null else allFeedsNextCursor + + feedRepository.getAllFeeds(cursor).onSuccess { response -> + response?.let { data -> + val currentList = if (isInitial) emptyList() else _uiState.value.allFeeds + updateState { + it.copy( + allFeeds = currentList + data.feedList, + error = null, + isLastPageAllFeeds = data.isLast + ) + } + allFeedsNextCursor = data.nextCursor + } ?: run { + updateState { it.copy(isLastPageAllFeeds = true) } + } + }.onFailure { exception -> + updateState { it.copy(error = exception.message) } + } + } finally { + isLoadingAllFeeds = false + updateState { it.copy(isLoading = false, isLoadingMore = false) } + } + } + } + + private fun loadMyFeeds(isInitial: Boolean = true) { + if (isLoadingMyFeeds && !isInitial) return + if (_uiState.value.isLastPageMyFeeds && !isInitial) return + + viewModelScope.launch { + try { + isLoadingMyFeeds = true + + if (isInitial) { + updateState { it.copy(isLoading = true, myFeeds = emptyList(), isLastPageMyFeeds = false) } + myFeedsNextCursor = null + } else { + updateState { it.copy(isLoadingMore = true) } + } + + val cursor = if (isInitial) null else myFeedsNextCursor + + feedRepository.getMyFeeds(cursor).onSuccess { response -> + response?.let { data -> + val currentList = if (isInitial) emptyList() else _uiState.value.myFeeds + updateState { + it.copy( + myFeeds = currentList + data.feedList, + error = null, + isLastPageMyFeeds = data.isLast + ) + } + myFeedsNextCursor = data.nextCursor + } ?: run { + updateState { it.copy(isLastPageMyFeeds = true) } + } + }.onFailure { exception -> + updateState { it.copy(error = exception.message) } + } + } finally { + isLoadingMyFeeds = false + updateState { it.copy(isLoading = false, isLoadingMore = false) } + } + } + } + + fun refreshCurrentTab() { + viewModelScope.launch { + updateState { it.copy(isRefreshing = true) } + + when (_uiState.value.selectedTabIndex) { + 0 -> refreshAllFeeds() + 1 -> refreshMyFeeds() + } + } + } + + private suspend fun refreshAllFeeds() { + allFeedsNextCursor = null + + feedRepository.getAllFeeds().onSuccess { response -> + response?.let { data -> + allFeedsNextCursor = data.nextCursor + updateState { + it.copy( + allFeeds = data.feedList, + isRefreshing = false, + isLastPageAllFeeds = data.isLast, + error = null + ) + } + } ?: updateState { + it.copy( + allFeeds = emptyList(), + isRefreshing = false, + isLastPageAllFeeds = true + ) + } + }.onFailure { exception -> + updateState { + it.copy( + isRefreshing = false, + error = exception.message + ) + } + } + } + + private suspend fun refreshMyFeeds() { + myFeedsNextCursor = null + + feedRepository.getMyFeeds().onSuccess { response -> + response?.let { data -> + myFeedsNextCursor = data.nextCursor + updateState { + it.copy( + myFeeds = data.feedList, + isRefreshing = false, + isLastPageMyFeeds = data.isLast, + error = null + ) + } + } ?: updateState { + it.copy( + myFeeds = emptyList(), + isRefreshing = false, + isLastPageMyFeeds = true + ) + } + }.onFailure { exception -> + updateState { + it.copy( + isRefreshing = false, + error = exception.message + ) + } + } + } + + fun loadMoreFeeds() { + when (_uiState.value.selectedTabIndex) { + 0 -> loadAllFeeds(isInitial = false) + 1 -> loadMyFeeds(isInitial = false) + } + } + private fun fetchRecentWriters() { viewModelScope.launch { - _uiState.update { it.copy(isLoading = true) } userRepository.getRecentWriters() .onSuccess { data -> - _uiState.update { + updateState { it.copy( - isLoading = false, recentWriters = data?.recentWriters ?: emptyList() ) } } .onFailure { exception -> - _uiState.update { it.copy(isLoading = false, errorMessage = exception.message) } + updateState { it.copy(error = exception.message) } } } } From d12fd87291b322d0c989b511854432cc6d598341 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 15:24:18 +0900 Subject: [PATCH 39/51] =?UTF-8?q?[refactor]:=20FeedItem=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/feed/component/ImageViewerModal.kt | 36 +++++++++---------- .../thip/ui/feed/screen/FeedCommentScreen.kt | 29 ++++++++------- .../thip/ui/feed/screen/FeedOthersScreen.kt | 4 +-- .../texthip/thip/ui/mypage/mock/FeedItem.kt | 4 +-- .../ui/mypage/viewmodel/SavedFeedViewModel.kt | 16 ++++----- 5 files changed, 43 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/component/ImageViewerModal.kt b/app/src/main/java/com/texthip/thip/ui/feed/component/ImageViewerModal.kt index 89dcb56e..166af14b 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/component/ImageViewerModal.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/component/ImageViewerModal.kt @@ -1,6 +1,5 @@ package com.texthip.thip.ui.feed.component -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -18,13 +17,13 @@ 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.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @@ -32,19 +31,18 @@ import com.texthip.thip.ui.theme.ThipTheme.typography @Composable fun ImageViewerModal( - images: List, + imageUrls: List, initialIndex: Int = 0, onDismiss: () -> Unit ) { val pagerState = rememberPagerState( initialPage = initialIndex, - pageCount = { images.size } + pageCount = { imageUrls.size } ) Box( modifier = Modifier .fillMaxSize() - .background(colors.Black) .clickable { onDismiss() } ) { // 닫기 버튼 @@ -71,8 +69,8 @@ fun ImageViewerModal( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - Image( - painter = images[page], + AsyncImage( + model = imageUrls[page], contentDescription = null, contentScale = ContentScale.Fit, // 원본 비율 유지하면서 화면에 맞춤 modifier = Modifier.fillMaxSize() @@ -81,14 +79,14 @@ fun ImageViewerModal( } // 페이지 인디케이터 (이미지가 2개 이상일 때만 표시) - if (images.size > 1) { + if (imageUrls.size > 1) { Row( modifier = Modifier .align(Alignment.BottomCenter) .padding(20.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - repeat(images.size) { index -> + repeat(imageUrls.size) { index -> Box( modifier = Modifier .size(8.dp) @@ -103,9 +101,9 @@ fun ImageViewerModal( } // 이미지 카운터 (예: 1/3) - if (images.size > 1) { + if (imageUrls.size > 1) { Text( - text = stringResource(id = R.string.tag_count, images.size, 3), + text = stringResource(id = R.string.tag_count, pagerState.currentPage + 1, imageUrls.size), style = typography.copy_r400_s14, color = colors.White, modifier = Modifier @@ -126,12 +124,12 @@ fun ImageViewerModal( @Composable private fun ImageViewerModalSingleImagePreview() { ThipTheme { - val mockImages = listOf( - painterResource(R.drawable.img_book_cover_sample) + val mockImageUrls = listOf( + "https://example.com/image1.jpg" ) ImageViewerModal( - images = mockImages, + imageUrls = mockImageUrls, initialIndex = 0, onDismiss = {} ) @@ -142,14 +140,14 @@ private fun ImageViewerModalSingleImagePreview() { @Composable private fun ImageViewerModalMultipleImagesPreview() { ThipTheme { - val mockImages = listOf( - painterResource(R.drawable.character_art), - painterResource(R.drawable.character_literature), - painterResource(R.drawable.character_sociology) + val mockImageUrls = listOf( + "https://example.com/image1.jpg", + "https://example.com/image2.jpg", + "https://example.com/image3.jpg" ) ImageViewerModal( - images = mockImages, + imageUrls = mockImageUrls, initialIndex = 1, onDismiss = { } ) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt index 4df9988c..c8e68bec 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt @@ -1,6 +1,5 @@ package com.texthip.thip.ui.feed.screen -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -36,10 +35,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.common.bottomsheet.MenuBottomSheet import com.texthip.thip.ui.common.buttons.ActionBookButton @@ -84,7 +83,7 @@ fun FeedCommentScreen( val feed = remember { mutableStateOf(feedItem) } val justNow = stringResource(R.string.just_a_moment_ago) - val images = feedItem.imageUrls.orEmpty().map { painterResource(id = it) } + val images = feedItem.imageUrls var showImageViewer by remember { mutableStateOf(false) } var selectedImageIndex by remember { mutableStateOf(0) } @@ -157,9 +156,9 @@ fun FeedCommentScreen( .padding(start = 20.dp, bottom = 16.dp), verticalAlignment = Alignment.CenterVertically ) { - itemsIndexed(images.take(3)) { index, image -> - Image( - painter = image, + itemsIndexed(images.take(3)) { index, imageUrl -> + AsyncImage( + model = imageUrl, contentDescription = null, modifier = Modifier .padding(end = 16.dp) @@ -457,7 +456,7 @@ fun FeedCommentScreen( if (showImageViewer && images.isNotEmpty()) { ImageViewerModal( - images = images.take(3), + imageUrls = images.take(3), initialIndex = selectedImageIndex, onDismiss = { showImageViewer = false } ) @@ -470,7 +469,7 @@ private fun FeedCommentScreenWithMockComments() { ThipTheme { val mockFeedItem = FeedItem( id = 1, - userProfileImage = R.drawable.character_literature, + userProfileImage = "https://example.com/profile.jpg", userName = "문학소녀", userRole = "문학 칭호", bookTitle = "채식주의자", @@ -483,9 +482,9 @@ private fun FeedCommentScreenWithMockComments() { isSaved = true, isLocked = true, imageUrls = listOf( - R.drawable.img_book_cover_sample, - R.drawable.img_book_cover_sample, - R.drawable.img_book_cover_sample + "https://example.com/image1.jpg", + "https://example.com/image2.jpg", + "https://example.com/image3.jpg" ), tags = listOf("에세이", "문학", "힐링") ) @@ -513,7 +512,7 @@ private fun FeedCommentScreenPrev() { ThipTheme { val mockFeedItem = FeedItem( id = 1, - userProfileImage = R.drawable.character_literature, + userProfileImage = "https://example.com/profile.jpg", userName = "문학소녀", userRole = "문학 칭호", bookTitle = "채식주의자", @@ -526,9 +525,9 @@ private fun FeedCommentScreenPrev() { isSaved = true, isLocked = false, imageUrls = listOf( - R.drawable.img_book_cover_sample, - R.drawable.img_book_cover_sample, - R.drawable.img_book_cover_sample + "https://example.com/image1.jpg", + "https://example.com/image2.jpg", + "https://example.com/image3.jpg" ), // bookImage = painterResource(R.drawable.img_book_cover_sample), // profileImage = "https://example.com/image1.jpg", diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt index 5c4c15d1..fe32a12e 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedOthersScreen.kt @@ -139,7 +139,7 @@ private fun FeedOthersScreenPrev() { val mockFeeds = List(5) { FeedItem( id = it + 1, - userProfileImage = R.drawable.character_literature, + userProfileImage = "https://example.com/profile$it.jpg", userName = "user.$it", userRole = "문학 칭호", bookTitle = "책 제목 ", @@ -151,7 +151,7 @@ private fun FeedOthersScreenPrev() { isLiked = false, isSaved = false, isLocked = it % 2 == 0, - imageUrls = listOf(R.drawable.img_book_cover_sample) + imageUrls = listOf("https://example.com/image$it.jpg") ) } val mockFollowerImages = listOf( diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/mock/FeedItem.kt b/app/src/main/java/com/texthip/thip/ui/mypage/mock/FeedItem.kt index 99009098..32c75942 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/mock/FeedItem.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/mock/FeedItem.kt @@ -2,7 +2,7 @@ package com.texthip.thip.ui.mypage.mock data class FeedItem( val id: Int, - val userProfileImage: Int? = null, + val userProfileImage: String? = null, val userName: String, val userRole: String, val bookTitle: String, @@ -15,6 +15,6 @@ data class FeedItem( val isSaved: Boolean, val isLocked: Boolean = false, val tags: List = emptyList(), - val imageUrls: List? = emptyList() + val imageUrls: List = emptyList() ) diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedFeedViewModel.kt b/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedFeedViewModel.kt index 056d05fe..7cb2229c 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedFeedViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedFeedViewModel.kt @@ -11,7 +11,7 @@ open class SavedFeedViewModel: ViewModel() { listOf( FeedItem( id = 1, - userProfileImage = R.drawable.character_art, + userProfileImage = "https://example.com/profile.jpg", userName = "user", userRole = "학생", bookTitle = "라랄ㄹ라라", @@ -25,7 +25,7 @@ open class SavedFeedViewModel: ViewModel() { ), FeedItem( id = 2, - userProfileImage = R.drawable.character_art, + userProfileImage = "https://example.com/profile.jpg", userName = "user", userRole = "학생", bookTitle = "라랄ㄹ라라", @@ -36,11 +36,11 @@ open class SavedFeedViewModel: ViewModel() { commentCount = 4, isLiked = false, isSaved = true, - imageUrls = null + imageUrls = emptyList() ), FeedItem( id = 3, - userProfileImage = R.drawable.character_art, + userProfileImage = "https://example.com/profile.jpg", userName = "user", userRole = "학생", bookTitle = "라랄ㄹ라라", @@ -51,11 +51,11 @@ open class SavedFeedViewModel: ViewModel() { commentCount = 4, isLiked = false, isSaved = true, - imageUrls = listOf(R.drawable.img_book_cover_sample) + imageUrls = listOf("https://example.com/image.jpg") ), FeedItem( id = 4, - userProfileImage = R.drawable.character_art, + userProfileImage = "https://example.com/profile.jpg", userName = "user", userRole = "학생", bookTitle = "책이름책이름", @@ -66,11 +66,11 @@ open class SavedFeedViewModel: ViewModel() { commentCount = 4, isLiked = false, isSaved = true, - imageUrls = listOf(R.drawable.img_book_cover_sample) + imageUrls = listOf("https://example.com/image.jpg") ), FeedItem( id = 5, - userProfileImage = R.drawable.character_art, + userProfileImage = "https://example.com/profile.jpg", userName = "user", userRole = "학생", bookTitle = "책이름책이름", From c3b2350d39468853e3a1323e270c16dfb071f7ca Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 15:24:51 +0900 Subject: [PATCH 40/51] =?UTF-8?q?[Feat]:=20FeedScreen=EC=97=90=20API?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20=EB=AC=B4=ED=95=9C=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EC=97=B0=EA=B2=B0=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/ui/feed/screen/FeedScreen.kt | 152 ++++++++++++++---- 1 file changed, 117 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt index f6bf43b4..d95169af 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt @@ -16,12 +16,17 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf @@ -54,10 +59,12 @@ import com.texthip.thip.ui.mypage.mock.FeedItem import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography +import com.texthip.thip.utils.color.hexToColor import kotlinx.coroutines.delay import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) @Composable fun FeedScreen( onNavigateToMySubscription: () -> Unit = {}, @@ -68,23 +75,46 @@ fun FeedScreen( totalFeedCount: Int = 0, selectedTabIndex: Int = 0, followerProfileImageUrls: List = emptyList(), - feedViewModel: FeedViewModel = hiltViewModel(), resultFeedId: Int? = null, onResultConsumed: () -> Unit = {}, + feedViewModel: FeedViewModel = hiltViewModel(), mySubscriptionViewModel: MySubscriptionViewModel = hiltViewModel() ) { val feedUiState by feedViewModel.uiState.collectAsState() - val selectedIndex = rememberSaveable { mutableIntStateOf(selectedTabIndex) } + val selectedIndex = rememberSaveable { mutableIntStateOf(feedUiState.selectedTabIndex) } val feedStateList = remember { mutableStateListOf().apply { addAll(feeds) } } val scope = rememberCoroutineScope() - var showProgressBar by remember { mutableStateOf(false) } val progress = remember { Animatable(0f) } + // 무한 스크롤 로직 + val listState = rememberLazyListState() + + // 무한 스크롤 로직 + val shouldLoadMore by remember(feedUiState.canLoadMoreCurrentTab, feedUiState.isLoadingMore) { + derivedStateOf { + val layoutInfo = listState.layoutInfo + val totalItems = layoutInfo.totalItemsCount + val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + + feedUiState.canLoadMoreCurrentTab && + !feedUiState.isLoadingMore && + feedUiState.currentTabFeeds.isNotEmpty() && + totalItems > 0 && + lastVisibleIndex >= totalItems - 3 + } + } + + LaunchedEffect(shouldLoadMore) { + if (shouldLoadMore) { + feedViewModel.loadMoreFeeds() + } + } + LaunchedEffect(resultFeedId) { if (resultFeedId != null) { onResultConsumed() @@ -103,6 +133,7 @@ fun FeedScreen( } } } + val mySubscriptions = listOf( MySubscriptionData( profileImageUrl = "https://example.com/image1.jpg", @@ -155,9 +186,13 @@ fun FeedScreen( ) val subscriptionUiState by mySubscriptionViewModel.uiState.collectAsState() Box(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier.fillMaxSize() + PullToRefreshBox( + isRefreshing = feedUiState.isRefreshing, + onRefresh = { feedViewModel.refreshCurrentTab() } ) { + Column( + modifier = Modifier.fillMaxSize() + ) { LogoTopAppBar( leftIcon = painterResource(R.drawable.ic_plusfriend), hasNotification = false, @@ -167,12 +202,16 @@ fun FeedScreen( Spacer(modifier = Modifier.height(32.dp)) HeaderMenuBarTab( titles = listOf("피드", "내 피드"), - selectedTabIndex = selectedIndex.value, - onTabSelected = { selectedIndex.value = it } + selectedTabIndex = feedUiState.selectedTabIndex, + onTabSelected = { + selectedIndex.intValue = it + feedViewModel.onTabSelected(it) + } ) // 스크롤 영역 전체 LazyColumn( + state = listState, modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp) ) { @@ -235,7 +274,7 @@ fun FeedScreen( ) Spacer(modifier = Modifier.height(40.dp)) Text( - text = stringResource(R.string.whole_num, totalFeedCount), + text = stringResource(R.string.whole_num, feedUiState.myFeeds.size), style = typography.menu_m500_s14_h24, color = colors.Grey, modifier = Modifier @@ -249,7 +288,7 @@ fun FeedScreen( ) } - if (totalFeedCount == 0) { + if (feedUiState.myFeeds.isEmpty()) { item { Box( modifier = Modifier @@ -265,21 +304,35 @@ fun FeedScreen( } } } else { - itemsIndexed(feedStateList, key = { _, item -> item.id }) { index, feed -> + itemsIndexed(feedUiState.myFeeds, key = { _, item -> item.feedId }) { index, myFeed -> Spacer(modifier = Modifier.height(if (index == 0) 20.dp else 40.dp)) + + // MyFeedItem을 FeedItem으로 변환 + val feedItem = FeedItem( + id = myFeed.feedId, + userProfileImage = null, + userName = "", // 내 피드이므로 고정값 + userRole = "", // 내 피드이므로 고정값 + bookTitle = myFeed.bookTitle, + authName = myFeed.bookAuthor, + timeAgo = myFeed.postDate, + content = myFeed.contentBody, + likeCount = myFeed.likeCount, + commentCount = myFeed.commentCount, + isLiked = false, // 내 피드는 좋아요 개념 없음 + isSaved = false, // 내 피드는 저장 개념 없음 + isLocked = !myFeed.isPublic, // isPublic의 반대값 + tags = emptyList(), + imageUrls = myFeed.contentUrls + ) + MyFeedCard( - feedItem = feed, - onLikeClick = { - val updated = feed.copy( - isLiked = !feed.isLiked, - likeCount = if (feed.isLiked) feed.likeCount - 1 else feed.likeCount + 1 - ) - feedStateList[index] = updated - }, - onContentClick = {} //TODO FeedCommentScreen으로 + feedItem = feedItem, + onLikeClick = {}, + onContentClick = {} ) Spacer(modifier = Modifier.height(40.dp)) - if (index != feeds.lastIndex) { + if (index != feedUiState.myFeeds.lastIndex) { HorizontalDivider( color = colors.DarkGrey02, thickness = 10.dp @@ -307,26 +360,38 @@ fun FeedScreen( onClick = onNavigateToMySubscription ) } - itemsIndexed(feedStateList, key = { _, item -> item.id }) { index, feed -> - val profileImage = feed.userProfileImage?.let { painterResource(it) } + itemsIndexed(feedUiState.allFeeds, key = { _, item -> item.feedId }) { index, allFeed -> + // AllFeedItem을 FeedItem으로 변환 + val feedItem = FeedItem( + id = allFeed.feedId, + userProfileImage = allFeed.creatorProfileImageUrl, + userName = allFeed.creatorNickname, + userRole = allFeed.aliasName, + bookTitle = allFeed.bookTitle, + authName = allFeed.bookAuthor, + timeAgo = allFeed.postDate, + content = allFeed.contentBody, + likeCount = allFeed.likeCount, + commentCount = allFeed.commentCount, + isLiked = allFeed.isLiked, + isSaved = allFeed.isSaved, + isLocked = false, + tags = emptyList(), + imageUrls = allFeed.contentUrls + ) SavedFeedCard( - feedItem = feed, + feedItem = feedItem, + bottomTextColor = hexToColor(allFeed.aliasColor), onBookmarkClick = { - val updated = feed.copy(isSaved = !feed.isSaved) - feedStateList[index] = updated + // TODO: API 호출로 북마크 상태 변경 }, onLikeClick = { - val updated = feed.copy( - isLiked = !feed.isLiked, - likeCount = if (feed.isLiked) feed.likeCount - 1 else feed.likeCount + 1 - ) - feedStateList[index] = updated + // TODO: API 호출로 좋아요 상태 변경 }, - onContentClick = {} //FeedCommentScreen으로 - + onContentClick = {} ) - if (index != feeds.lastIndex) { + if (index != feedUiState.allFeeds.lastIndex) { HorizontalDivider( color = colors.DarkGrey02, thickness = 10.dp @@ -334,6 +399,23 @@ fun FeedScreen( } } } + + // 무한 스크롤 로딩 인디케이터 + if (feedUiState.isLoadingMore) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + color = colors.White + ) + } + } + } + } } } FloatingButton( @@ -350,7 +432,7 @@ private fun FeedScreenPreview() { val mockFeeds = List(5) { FeedItem( id = it + 1, - userProfileImage = R.drawable.character_literature, + userProfileImage = "https://example.com/profile$it.jpg", userName = "user.$it", userRole = "문학 칭호", bookTitle = "책 제목 ", @@ -362,7 +444,7 @@ private fun FeedScreenPreview() { isLiked = false, isSaved = false, isLocked = it % 2 == 0, - imageUrls = listOf(R.drawable.img_book_cover_sample) + imageUrls = listOf("https://example.com/image$it.jpg") ) } val mockFollowerImages = listOf( From a06cd7a6739732ab4e46de0a78e3dbadc3020f70 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 15:25:09 +0900 Subject: [PATCH 41/51] =?UTF-8?q?[Feat]:=20FeedCard=EC=9D=98=20Clickable?= =?UTF-8?q?=20=EC=98=81=EC=97=AD=20=EC=88=98=EC=A0=95=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/feed/component/MyFeedCard.kt | 71 ++++++++-------- .../thip/ui/mypage/component/SavedFeedCard.kt | 81 ++++++++++--------- 2 files changed, 81 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt b/app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt index dee0a7ab..82b19623 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt @@ -1,7 +1,7 @@ package com.texthip.thip.ui.feed.component -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -19,6 +19,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.ActionBookButton import com.texthip.thip.ui.mypage.mock.FeedItem @@ -32,8 +33,7 @@ fun MyFeedCard( onLikeClick: () -> Unit = {}, onContentClick: () -> Unit = {} ) { - val images = feedItem.imageUrls.orEmpty().map { painterResource(id = it) } - val hasImages = images.isNotEmpty() + val hasImages = feedItem.imageUrls.isNotEmpty() val maxLines = if (hasImages) 3 else 8 Column( @@ -47,33 +47,39 @@ fun MyFeedCard( onClick = {} ) - Text( - text = feedItem.content, - style = typography.feedcopy_r400_s14_h20, - color = colors.White, - maxLines = maxLines, + Column ( modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) - .clickable { onContentClick() } - ) - - if (hasImages) { - Row( + .clickable { onContentClick() }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = feedItem.content, + style = typography.feedcopy_r400_s14_h20, + color = colors.White, + maxLines = maxLines, modifier = Modifier .fillMaxWidth() - .padding(bottom = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - images.take(3).forEach { image -> - Image( - painter = image, - contentDescription = null, - modifier = Modifier - .padding(end = 10.dp) - .size(100.dp), - contentScale = ContentScale.Crop - ) + .padding(vertical = 16.dp) + ) + + if (hasImages) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + feedItem.imageUrls.take(3).forEach { imageUrl -> + AsyncImage( + model = imageUrl, + contentDescription = null, + modifier = Modifier + .padding(end = 10.dp) + .size(100.dp), + contentScale = ContentScale.Crop + ) + } } } } @@ -94,6 +100,7 @@ fun MyFeedCard( modifier = Modifier.padding(start = 5.dp, end = 12.dp) ) Icon( + modifier = Modifier.clickable { onContentClick() }, painter = painterResource(R.drawable.ic_comment), contentDescription = null, tint = colors.White @@ -121,7 +128,7 @@ fun MyFeedCard( private fun MyFeedCardPrev() { val feed1 = FeedItem( id = 1, - userProfileImage = R.drawable.character_literature, + userProfileImage = "https://example.com/profile1.jpg", userName = "user.01", userRole = stringResource(R.string.influencer), bookTitle = "책 제목", @@ -133,11 +140,11 @@ private fun MyFeedCardPrev() { isLiked = false, isSaved = true, isLocked = true, - imageUrls = null + imageUrls = emptyList() ) val feed2 = FeedItem( id = 2, - userProfileImage = R.drawable.character_art, + userProfileImage = "https://example.com/profile2.jpg", userName = "user.01", userRole = stringResource(R.string.influencer), bookTitle = "책 제목", @@ -149,7 +156,7 @@ private fun MyFeedCardPrev() { isLiked = false, isSaved = true, isLocked = false, - imageUrls = listOf(R.drawable.img_book_cover_sample, R.drawable.img_book_cover_sample) + imageUrls = listOf("https://example.com/image1.jpg", "https://example.com/image2.jpg") ) Column { @@ -160,6 +167,4 @@ private fun MyFeedCardPrev() { feedItem = feed2 ) } - - } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt b/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt index 53255ff0..ce70e41d 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt @@ -1,7 +1,7 @@ package com.texthip.thip.ui.mypage.component -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -16,12 +16,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.ActionBookButton import com.texthip.thip.ui.common.header.ProfileBar @@ -34,24 +34,24 @@ import com.texthip.thip.ui.theme.ThipTheme.typography fun SavedFeedCard( modifier: Modifier = Modifier, feedItem: FeedItem, + bottomTextColor: Color = colors.NeonGreen, onBookmarkClick: () -> Unit = {}, onLikeClick: () -> Unit = {}, onContentClick: () -> Unit = {} ) { - val images = feedItem.imageUrls.orEmpty().map { painterResource(id = it) } - val imagePainters = feedItem.imageUrls.orEmpty().map { painterResource(it) } - val hasImages = imagePainters.isNotEmpty() + val hasImages = feedItem.imageUrls.isNotEmpty() val maxLines = if (hasImages) 3 else 8 Column( modifier = modifier .fillMaxWidth() - .padding(horizontal = 20.dp) + .padding(20.dp) ) { ProfileBar( - profileImage = feedItem.userProfileImage.toString(), + profileImage = feedItem.userProfileImage ?: "https://example.com/image1.jpg", topText = feedItem.userName, bottomText = feedItem.userRole, + bottomTextColor = bottomTextColor, showSubscriberInfo = false, hoursAgo = feedItem.timeAgo ) @@ -66,35 +66,43 @@ fun SavedFeedCard( onClick = {} ) } - Text( - text = feedItem.content, - style = typography.feedcopy_r400_s14_h20, - color = colors.White, - maxLines = maxLines, + + Column ( modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) - .clickable { onContentClick() } - ) - if (images.isNotEmpty()) { - Row( + .clickable { onContentClick() }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = feedItem.content, + style = typography.feedcopy_r400_s14_h20, + color = colors.White, + maxLines = maxLines, modifier = Modifier .fillMaxWidth() - .padding(bottom = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - images.take(3).forEach { image -> - Image( - painter = image, - contentDescription = null, - modifier = Modifier - .padding(end = 10.dp) - .size(100.dp), - contentScale = ContentScale.Crop - ) + .padding(vertical = 16.dp) + ) + if (hasImages) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + feedItem.imageUrls.take(3).forEach { imageUrl -> + AsyncImage( + model = imageUrl, + contentDescription = null, + modifier = Modifier + .padding(end = 10.dp) + .size(100.dp), + contentScale = ContentScale.Crop + ) + } } } } + Row( verticalAlignment = Alignment.CenterVertically ) { @@ -111,6 +119,7 @@ fun SavedFeedCard( modifier = Modifier.padding(start = 5.dp, end = 12.dp) ) Icon( + modifier = Modifier.clickable { onContentClick() }, painter = painterResource(R.drawable.ic_comment), contentDescription = null, tint = colors.White @@ -137,7 +146,7 @@ fun SavedFeedCard( private fun SavedFeedCardPrev() { val feed1 = FeedItem( id = 1, - userProfileImage = R.drawable.character_literature, + userProfileImage = "https://example.com/profile1.jpg", userName = "user.01", userRole = stringResource(R.string.influencer), bookTitle = "책 제목", @@ -148,12 +157,12 @@ private fun SavedFeedCardPrev() { commentCount = 5, isLiked = false, isSaved = true, - imageUrls = null + imageUrls = emptyList() ) val feed2 = FeedItem( id = 2, - userProfileImage = R.drawable.character_art, + userProfileImage = "https://example.com/profile2.jpg", userName = "user.01", userRole = stringResource(R.string.influencer), bookTitle = "책 제목", @@ -165,11 +174,7 @@ private fun SavedFeedCardPrev() { commentCount = 5, isLiked = false, isSaved = true, - imageUrls = listOf( - R.drawable.img_book_cover_sample, - R.drawable.img_book_cover_sample, - R.drawable.img_book_cover_sample - ) + imageUrls = listOf("https://example.com/image1.jpg", "https://example.com/image2.jpg", "https://example.com/image3.jpg") ) val scrollState = rememberScrollState() From 9ccde48d05693b83ad35b3102c3d1c8510ddd184 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 15:43:21 +0900 Subject: [PATCH 42/51] =?UTF-8?q?[refactor]:=20runCatching=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/repository/FeedRepository.kt | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt index 9364c5c7..cab39132 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt @@ -72,22 +72,20 @@ class FeedRepository @Inject constructor( // 임시 파일 목록 추적 val tempFiles = mutableListOf() - try { - // 이미지 파일들을 MultipartBody.Part로 변환 - val imageParts = if (imageUris.isNotEmpty()) { - withContext(Dispatchers.IO) { - imageUris.mapNotNull { uri -> - try { - uriToMultipartBodyPart(uri, "images", tempFiles) - } catch (e: Exception) { - null - } - } + // 이미지 파일들을 MultipartBody.Part로 변환 + val imageParts = if (imageUris.isNotEmpty()) { + withContext(Dispatchers.IO) { + imageUris.mapNotNull { uri -> + runCatching { + uriToMultipartBodyPart(uri, "images", tempFiles) + }.getOrNull() } - } else { - null } + } else { + null + } + try { feedService.createFeed(requestBody, imageParts) .handleBaseResponse() .getOrThrow() @@ -98,7 +96,7 @@ class FeedRepository @Inject constructor( } private fun uriToMultipartBodyPart(uri: Uri, paramName: String, tempFiles: MutableList): MultipartBody.Part? { - return try { + return runCatching { // MIME 타입 확인 val mimeType = context.contentResolver.getType(uri) ?: "image/jpeg" val extension = when (mimeType) { @@ -120,15 +118,14 @@ class FeedRepository @Inject constructor( FileOutputStream(tempFile).use { outputStream -> inputStream.copyTo(outputStream) } - } ?: return null + } ?: throw IllegalStateException("Failed to open input stream for URI: $uri") // MultipartBody.Part 생성 val requestFile = tempFile.asRequestBody(mimeType.toMediaType()) MultipartBody.Part.createFormData(paramName, fileName, requestFile) - } catch (e: Exception) { + }.onFailure { e -> e.printStackTrace() - null - } + }.getOrNull() } /** 전체 피드 목록 조회 */ @@ -148,11 +145,11 @@ class FeedRepository @Inject constructor( /** 임시 파일들을 정리하는 함수 */ private fun cleanupTempFiles(tempFiles: List) { tempFiles.forEach { file -> - try { + runCatching { if (file.exists()) { file.delete() } - } catch (e: Exception) { + }.onFailure { e -> e.printStackTrace() } } From 337bc58e9dbcec16443d9c8f9fd8f242cc680359 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 16:14:15 +0900 Subject: [PATCH 43/51] =?UTF-8?q?[feat]:=20=ED=94=BC=EB=93=9C=20=EC=9E=90?= =?UTF-8?q?=EC=84=B8=ED=9E=88=EB=B3=B4=EA=B8=B0=20Response,=20Service,=20R?= =?UTF-8?q?epository=20=EA=B5=AC=ED=98=84=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/feed/response/FeedDetailResponse.kt | 26 +++++++++++++++++++ .../thip/data/repository/FeedRepository.kt | 8 ++++++ .../texthip/thip/data/service/FeedService.kt | 8 ++++++ 3 files changed, 42 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/feed/response/FeedDetailResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedDetailResponse.kt b/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedDetailResponse.kt new file mode 100644 index 00000000..9b03febd --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedDetailResponse.kt @@ -0,0 +1,26 @@ +package com.texthip.thip.data.model.feed.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class FeedDetailResponse( + @SerialName("feedId") val feedId: Int, + @SerialName("creatorId") val creatorId: Int, + @SerialName("creatorNickname") val creatorNickname: String, + @SerialName("creatorProfileImageUrl") val creatorProfileImageUrl: String?, + @SerialName("aliasName") val aliasName: String, + @SerialName("aliasColor") val aliasColor: String, + @SerialName("postDate") val postDate: String, + @SerialName("bookTitle") val bookTitle: String, + @SerialName("isbn") val isbn: String, + @SerialName("bookAuthor") val bookAuthor: String, + @SerialName("contentBody") val contentBody: String, + @SerialName("contentUrls") val contentUrls: List, + @SerialName("likeCount") val likeCount: Int, + @SerialName("commentCount") val commentCount: Int, + @SerialName("isSaved") val isSaved: Boolean, + @SerialName("isLiked") val isLiked: Boolean, + @SerialName("isWriter") val isWriter: Boolean, + @SerialName("tagList") val tagList: List +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt index cab39132..ebcb198f 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt @@ -5,6 +5,7 @@ import android.net.Uri import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.feed.request.CreateFeedRequest import com.texthip.thip.data.model.feed.response.CreateFeedResponse +import com.texthip.thip.data.model.feed.response.FeedDetailResponse import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse import com.texthip.thip.data.model.feeds.response.AllFeedResponse import com.texthip.thip.data.model.feeds.response.MyFeedResponse @@ -142,6 +143,13 @@ class FeedRepository @Inject constructor( .getOrThrow() } + /** 피드 상세 조회 */ + suspend fun getFeedDetail(feedId: Int): Result = runCatching { + feedService.getFeedDetail(feedId) + .handleBaseResponse() + .getOrThrow() + } + /** 임시 파일들을 정리하는 함수 */ private fun cleanupTempFiles(tempFiles: List) { tempFiles.forEach { file -> diff --git a/app/src/main/java/com/texthip/thip/data/service/FeedService.kt b/app/src/main/java/com/texthip/thip/data/service/FeedService.kt index bf69576b..421ee2bf 100644 --- a/app/src/main/java/com/texthip/thip/data/service/FeedService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/FeedService.kt @@ -2,6 +2,7 @@ package com.texthip.thip.data.service import com.texthip.thip.data.model.base.BaseResponse import com.texthip.thip.data.model.feed.response.CreateFeedResponse +import com.texthip.thip.data.model.feed.response.FeedDetailResponse import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse import com.texthip.thip.data.model.feeds.response.AllFeedResponse import com.texthip.thip.data.model.feeds.response.MyFeedResponse @@ -11,6 +12,7 @@ import retrofit2.http.GET import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part +import retrofit2.http.Path import retrofit2.http.Query interface FeedService { @@ -38,4 +40,10 @@ interface FeedService { suspend fun getMyFeeds( @Query("cursor") cursor: String? = null ): BaseResponse + + /** 피드 상세 조회 */ + @GET("feeds/{feedId}") + suspend fun getFeedDetail( + @Path("feedId") feedId: Int + ): BaseResponse } \ No newline at end of file From 2a1943dca1de49d57d4071217b1d7c28915b0f5d Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 16:14:51 +0900 Subject: [PATCH 44/51] =?UTF-8?q?[feat]:=20=ED=94=BC=EB=93=9C=20=EC=9E=90?= =?UTF-8?q?=EC=84=B8=ED=9E=88=EB=B3=B4=EA=B8=B0=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/ui/feed/screen/FeedScreen.kt | 25 +++++++++++++++++-- .../extensions/FeedNavigationExtensions.kt | 5 ++++ .../navigator/navigations/FeedNavigation.kt | 17 +++++++++++++ .../thip/ui/navigator/routes/FeedRoutes.kt | 1 + 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt index d95169af..cc4e2db2 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt @@ -14,6 +14,7 @@ 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.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -69,6 +70,7 @@ import kotlinx.coroutines.launch fun FeedScreen( onNavigateToMySubscription: () -> Unit = {}, onNavigateToFeedWrite: () -> Unit = {}, + onNavigateToFeedComment: (Int) -> Unit = {}, nickname: String = "", userRole: String = "", feeds: List = emptyList(), @@ -185,6 +187,21 @@ fun FeedScreen( ) ) val subscriptionUiState by mySubscriptionViewModel.uiState.collectAsState() + + // 초기 로딩 상태 처리 + if (feedUiState.isLoading && feedUiState.currentTabFeeds.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + color = colors.White, + modifier = Modifier.size(48.dp) + ) + } + return + } + Box(modifier = Modifier.fillMaxSize()) { PullToRefreshBox( isRefreshing = feedUiState.isRefreshing, @@ -329,7 +346,9 @@ fun FeedScreen( MyFeedCard( feedItem = feedItem, onLikeClick = {}, - onContentClick = {} + onContentClick = { + onNavigateToFeedComment(feedItem.id) + } ) Spacer(modifier = Modifier.height(40.dp)) if (index != feedUiState.myFeeds.lastIndex) { @@ -389,7 +408,9 @@ fun FeedScreen( onLikeClick = { // TODO: API 호출로 좋아요 상태 변경 }, - onContentClick = {} + onContentClick = { + onNavigateToFeedComment(feedItem.id) + } ) if (index != feedUiState.allFeeds.lastIndex) { HorizontalDivider( diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt index 02387a67..de67495e 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt @@ -17,4 +17,9 @@ fun NavHostController.navigateToMySubscription() { // 피드 작성으로 fun NavHostController.navigateToFeedWrite() { navigate(FeedRoutes.Write) +} + +// 피드 댓글으로 +fun NavHostController.navigateToFeedComment(feedId: Int) { + navigate(FeedRoutes.Comment(feedId)) } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt index 40df9757..387da17a 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt @@ -3,9 +3,11 @@ package com.texthip.thip.ui.navigator.navigations import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable +import com.texthip.thip.ui.feed.screen.FeedCommentScreen import com.texthip.thip.ui.feed.screen.FeedScreen import com.texthip.thip.ui.feed.screen.FeedWriteScreen import com.texthip.thip.ui.feed.screen.MySubscriptionScreen +import com.texthip.thip.ui.navigator.extensions.navigateToFeedComment import com.texthip.thip.ui.navigator.extensions.navigateToFeedWrite import com.texthip.thip.ui.navigator.extensions.navigateToMySubscription import com.texthip.thip.ui.navigator.routes.FeedRoutes @@ -32,6 +34,9 @@ fun NavGraphBuilder.feedNavigation(navController: NavHostController) { }, onNavigateToFeedWrite = { navController.navigateToFeedWrite() + }, + onNavigateToFeedComment = { feedId -> + navController.navigateToFeedComment(feedId) } ) } @@ -52,4 +57,16 @@ fun NavGraphBuilder.feedNavigation(navController: NavHostController) { } ) } + composable { backStackEntry -> + val route = backStackEntry.arguments?.let { + FeedRoutes.Comment(it.getInt("feedId")) + } ?: return@composable + + FeedCommentScreen( + feedId = route.feedId, + onNavigateBack = { + navController.popBackStack() + } + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt b/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt index c8c9eeb6..1f7bbd9f 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt @@ -7,4 +7,5 @@ sealed class FeedRoutes : Routes() { @Serializable data object MySubscription : FeedRoutes() @Serializable data object Write : FeedRoutes() + @Serializable data class Comment(val feedId: Int) : FeedRoutes() } \ No newline at end of file From b7cd436fde3d58ecbd35c0fec87d6ed5052d7fc8 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 16:15:01 +0900 Subject: [PATCH 45/51] =?UTF-8?q?[feat]:=20=ED=94=BC=EB=93=9C=20=EC=9E=90?= =?UTF-8?q?=EC=84=B8=ED=9E=88=EB=B3=B4=EA=B8=B0=20viewmodel=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/feed/viewmodel/FeedDetailViewModel.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedDetailViewModel.kt diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedDetailViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedDetailViewModel.kt new file mode 100644 index 00000000..60f0f47c --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedDetailViewModel.kt @@ -0,0 +1,61 @@ +package com.texthip.thip.ui.feed.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.feed.response.FeedDetailResponse +import com.texthip.thip.data.repository.FeedRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class FeedDetailUiState( + val isLoading: Boolean = false, + val feedDetail: FeedDetailResponse? = null, + val error: String? = null +) + +@HiltViewModel +class FeedDetailViewModel @Inject constructor( + private val feedRepository: FeedRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(FeedDetailUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private fun updateState(update: (FeedDetailUiState) -> FeedDetailUiState) { + _uiState.value = update(_uiState.value) + } + + fun loadFeedDetail(feedId: Int) { + viewModelScope.launch { + updateState { it.copy(isLoading = true, error = null) } + + feedRepository.getFeedDetail(feedId) + .onSuccess { response -> + updateState { + it.copy( + isLoading = false, + feedDetail = response, + error = null + ) + } + } + .onFailure { exception -> + updateState { + it.copy( + isLoading = false, + feedDetail = null, + error = exception.message ?: "알 수 없는 오류가 발생했습니다." + ) + } + } + } + } + + fun clearError() { + updateState { it.copy(error = null) } + } +} \ No newline at end of file From a61eaba4a0adaceabba0fdc5c0c823b89b3a316e Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 16:15:14 +0900 Subject: [PATCH 46/51] =?UTF-8?q?[feat]:=20=ED=94=BC=EB=93=9C=20=EC=9E=90?= =?UTF-8?q?=EC=84=B8=ED=9E=88=EB=B3=B4=EA=B8=B0=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EA=B3=BC=20=EC=97=B0=EA=B2=B0=20=EC=99=84=EB=A3=8C=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/feed/screen/FeedCommentScreen.kt | 153 +++++++++--------- 1 file changed, 74 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt index c8e68bec..1d674f86 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt @@ -19,9 +19,12 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf @@ -32,12 +35,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.common.bottomsheet.MenuBottomSheet @@ -48,10 +51,9 @@ import com.texthip.thip.ui.common.header.ProfileBar import com.texthip.thip.ui.common.modal.DialogPopup import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar import com.texthip.thip.ui.feed.component.ImageViewerModal -import com.texthip.thip.ui.feed.mock.FeedItemType +import com.texthip.thip.ui.feed.viewmodel.FeedDetailViewModel import com.texthip.thip.ui.group.note.mock.mockCommentList import com.texthip.thip.ui.group.room.mock.MenuBottomSheetItem -import com.texthip.thip.ui.mypage.mock.FeedItem import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -61,29 +63,73 @@ import com.texthip.thip.ui.group.note.mock.ReplyItem as FeedReplyItem @Composable fun FeedCommentScreen( modifier: Modifier = Modifier, - feedItem: FeedItem, - bookImage: Painter? = null, - profileImage: String, - feedType: FeedItemType, - currentUserId: Int, - currentUserName: String, - currentUserGenre: String, - currentUserProfileImageUrl: String, + feedId: Int, + onNavigateBack: () -> Unit = {}, + currentUserId: Int = 1, + currentUserName: String = "현재사용자", + currentUserGenre: String = "문학", + currentUserProfileImageUrl: String = "", onLikeClick: () -> Unit = {}, onCommentInputChange: (String) -> Unit = {}, onSendClick: () -> Unit = {}, - commentList: SnapshotStateList? = null + commentList: SnapshotStateList? = null, + viewModel: FeedDetailViewModel = hiltViewModel() ) { + val uiState by viewModel.uiState.collectAsState() + + LaunchedEffect(feedId) { + viewModel.loadFeedDetail(feedId) + } + + // 로딩 상태 처리 + if (uiState.isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + color = colors.White, + modifier = Modifier.size(48.dp) + ) + } + return + } + + // 에러 상태 처리 + if (uiState.error != null) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "오류가 발생했습니다", + style = typography.smalltitle_sb600_s18_h24, + color = colors.White + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = uiState.error!!, + style = typography.copy_r400_s14, + color = colors.Grey + ) + } + } + return + } + + // 피드 데이터가 없으면 리턴 + val feedDetail = uiState.feedDetail ?: return val CommentList = commentList ?: remember { mutableStateListOf() } var isBottomSheetVisible by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) } val commentInput = remember { mutableStateOf("") } val replyTo = remember { mutableStateOf(null) } - val feed = remember { mutableStateOf(feedItem) } + val feed = remember { mutableStateOf(feedDetail) } val justNow = stringResource(R.string.just_a_moment_ago) - val images = feedItem.imageUrls + val images = feedDetail.contentUrls var showImageViewer by remember { mutableStateOf(false) } var selectedImageIndex by remember { mutableStateOf(0) } @@ -109,7 +155,7 @@ fun FeedCommentScreen( DefaultTopAppBar( isRightIconVisible = true, isTitleVisible = false, - onLeftClick = {}, + onLeftClick = onNavigateBack, onRightClick = { isBottomSheetVisible = true }, ) @@ -124,11 +170,11 @@ fun FeedCommentScreen( Column { ProfileBar( modifier = Modifier.padding(20.dp), - profileImage = profileImage, - topText = feedItem.userName, - bottomText = feedItem.userRole, + profileImage = feedDetail.creatorProfileImageUrl ?: "", + topText = feedDetail.creatorNickname, + bottomText = feedDetail.aliasName, showSubscriberInfo = false, - hoursAgo = feedItem.timeAgo + hoursAgo = feedDetail.postDate ) Column( Modifier @@ -136,13 +182,13 @@ fun FeedCommentScreen( .padding(vertical = 16.dp, horizontal = 20.dp) ) { ActionBookButton( - bookTitle = feedItem.bookTitle, - bookAuthor = feedItem.authName, + bookTitle = feedDetail.bookTitle, + bookAuthor = feedDetail.bookAuthor, onClick = {} ) } Text( - text = feedItem.content, + text = feedDetail.contentBody, style = typography.feedcopy_r400_s14_h20, color = colors.White, modifier = Modifier @@ -172,16 +218,16 @@ fun FeedCommentScreen( } } } - if (feedItem.tags.isNotEmpty()) { + if (feedDetail.tagList.isNotEmpty()) { Row( Modifier .fillMaxWidth() .padding(bottom = 16.dp, start = 20.dp, end = 20.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - feedItem.tags.forEach { tag -> + feedDetail.tagList.forEach { tag -> OptionChipButton( - text = tag, + text = "#$tag", isFilled = false, isSelected = false, onClick = {}) @@ -355,7 +401,7 @@ fun FeedCommentScreen( CommentTextField( modifier = Modifier.align(Alignment.BottomCenter), input = commentInput.value, - hint = stringResource(R.string.feed_reply_to, feedItem.userName), + hint = stringResource(R.string.feed_reply_to, feedDetail.creatorNickname), onInputChange = { commentInput.value = it onCommentInputChange(it) @@ -467,36 +513,13 @@ fun FeedCommentScreen( @Composable private fun FeedCommentScreenWithMockComments() { ThipTheme { - val mockFeedItem = FeedItem( - id = 1, - userProfileImage = "https://example.com/profile.jpg", - userName = "문학소녀", - userRole = "문학 칭호", - bookTitle = "채식주의자", - authName = "한강", - timeAgo = "1시간 전", - content = "이 책은 인간의 본성과 억압에 대한 깊은 성찰을 담고 있어요.", - likeCount = 12, - commentCount = 3, - isLiked = true, - isSaved = true, - isLocked = true, - imageUrls = listOf( - "https://example.com/image1.jpg", - "https://example.com/image2.jpg", - "https://example.com/image3.jpg" - ), - tags = listOf("에세이", "문학", "힐링") - ) val commentList = remember { mutableStateListOf().apply { addAll(mockCommentList.commentData) } } FeedCommentScreen( - feedItem = mockFeedItem, - feedType = FeedItemType.SAVABLE, - profileImage = "https://example.com/image1.jpg", + feedId = 1, currentUserId = 999, currentUserName = "나", currentUserGenre = "문학", @@ -510,38 +533,10 @@ private fun FeedCommentScreenWithMockComments() { @Composable private fun FeedCommentScreenPrev() { ThipTheme { - val mockFeedItem = FeedItem( - id = 1, - userProfileImage = "https://example.com/profile.jpg", - userName = "문학소녀", - userRole = "문학 칭호", - bookTitle = "채식주의자", - authName = "한강", - timeAgo = "1시간 전", - content = "이 책은 인간의 본성과 억압에 대한 깊은 성찰을 담고 있어요.", - likeCount = 12, - commentCount = 3, - isLiked = true, - isSaved = true, - isLocked = false, - imageUrls = listOf( - "https://example.com/image1.jpg", - "https://example.com/image2.jpg", - "https://example.com/image3.jpg" - ), -// bookImage = painterResource(R.drawable.img_book_cover_sample), -// profileImage = "https://example.com/image1.jpg", -// onLikeClick = {}, -// onCommentInputChange = {}, -// onSendClick = {}, - tags = listOf("에세이", "문학", "힐링") - ) val commentList = remember { mutableStateListOf() } FeedCommentScreen( - feedItem = mockFeedItem, - feedType = FeedItemType.SAVABLE, - profileImage = "https://example.com/image1.jpg", + feedId = 1, currentUserId = 999, currentUserName = "나", currentUserGenre = "문학", From e02e98fc22fe0ea17eddc3b313db8202afbfa5f1 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 16:40:38 +0900 Subject: [PATCH 47/51] =?UTF-8?q?[refactor]:=20PR=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/ui/feed/screen/FeedScreen.kt | 10 +-- .../thip/ui/feed/viewmodel/FeedViewModel.kt | 64 ++++++++++--------- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt index cc4e2db2..8089d601 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt @@ -29,12 +29,10 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -83,7 +81,6 @@ fun FeedScreen( mySubscriptionViewModel: MySubscriptionViewModel = hiltViewModel() ) { val feedUiState by feedViewModel.uiState.collectAsState() - val selectedIndex = rememberSaveable { mutableIntStateOf(feedUiState.selectedTabIndex) } val feedStateList = remember { mutableStateListOf().apply { addAll(feeds) @@ -220,10 +217,7 @@ fun FeedScreen( HeaderMenuBarTab( titles = listOf("피드", "내 피드"), selectedTabIndex = feedUiState.selectedTabIndex, - onTabSelected = { - selectedIndex.intValue = it - feedViewModel.onTabSelected(it) - } + onTabSelected = feedViewModel::onTabSelected ) // 스크롤 영역 전체 @@ -270,7 +264,7 @@ fun FeedScreen( } } } - if (selectedIndex.value == 1) { + if (feedUiState.selectedTabIndex == 1) { // 내 피드 item { Spacer(modifier = Modifier.height(32.dp)) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt index 3408bf11..397c0b8f 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt @@ -92,17 +92,17 @@ class FeedViewModel @Inject constructor( val cursor = if (isInitial) null else allFeedsNextCursor feedRepository.getAllFeeds(cursor).onSuccess { response -> - response?.let { data -> + if (response != null) { val currentList = if (isInitial) emptyList() else _uiState.value.allFeeds updateState { it.copy( - allFeeds = currentList + data.feedList, + allFeeds = currentList + response.feedList, error = null, - isLastPageAllFeeds = data.isLast + isLastPageAllFeeds = response.isLast ) } - allFeedsNextCursor = data.nextCursor - } ?: run { + allFeedsNextCursor = response.nextCursor + } else { updateState { it.copy(isLastPageAllFeeds = true) } } }.onFailure { exception -> @@ -133,17 +133,17 @@ class FeedViewModel @Inject constructor( val cursor = if (isInitial) null else myFeedsNextCursor feedRepository.getMyFeeds(cursor).onSuccess { response -> - response?.let { data -> + if (response != null) { val currentList = if (isInitial) emptyList() else _uiState.value.myFeeds updateState { it.copy( - myFeeds = currentList + data.feedList, + myFeeds = currentList + response.feedList, error = null, - isLastPageMyFeeds = data.isLast + isLastPageMyFeeds = response.isLast ) } - myFeedsNextCursor = data.nextCursor - } ?: run { + myFeedsNextCursor = response.nextCursor + } else { updateState { it.copy(isLastPageMyFeeds = true) } } }.onFailure { exception -> @@ -171,22 +171,24 @@ class FeedViewModel @Inject constructor( allFeedsNextCursor = null feedRepository.getAllFeeds().onSuccess { response -> - response?.let { data -> - allFeedsNextCursor = data.nextCursor + if (response != null) { + allFeedsNextCursor = response.nextCursor updateState { it.copy( - allFeeds = data.feedList, + allFeeds = response.feedList, isRefreshing = false, - isLastPageAllFeeds = data.isLast, + isLastPageAllFeeds = response.isLast, error = null ) } - } ?: updateState { - it.copy( - allFeeds = emptyList(), - isRefreshing = false, - isLastPageAllFeeds = true - ) + } else { + updateState { + it.copy( + allFeeds = emptyList(), + isRefreshing = false, + isLastPageAllFeeds = true + ) + } } }.onFailure { exception -> updateState { @@ -202,22 +204,24 @@ class FeedViewModel @Inject constructor( myFeedsNextCursor = null feedRepository.getMyFeeds().onSuccess { response -> - response?.let { data -> - myFeedsNextCursor = data.nextCursor + if (response != null) { + myFeedsNextCursor = response.nextCursor updateState { it.copy( - myFeeds = data.feedList, + myFeeds = response.feedList, isRefreshing = false, - isLastPageMyFeeds = data.isLast, + isLastPageMyFeeds = response.isLast, error = null ) } - } ?: updateState { - it.copy( - myFeeds = emptyList(), - isRefreshing = false, - isLastPageMyFeeds = true - ) + } else { + updateState { + it.copy( + myFeeds = emptyList(), + isRefreshing = false, + isLastPageMyFeeds = true + ) + } } }.onFailure { exception -> updateState { From 39197cb16998aa494bb59c277d77b97552fed34c Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 17:32:49 +0900 Subject: [PATCH 48/51] =?UTF-8?q?[refactor]:=20ActionBarButton=EC=9D=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/ui/feed/screen/FeedScreen.kt | 3 ++ .../thip/ui/mypage/component/SavedFeedCard.kt | 53 +++++-------------- 2 files changed, 17 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt index 8089d601..cfa3a1a5 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt @@ -404,6 +404,9 @@ fun FeedScreen( }, onContentClick = { onNavigateToFeedComment(feedItem.id) + }, + onCommentClick = { + onNavigateToFeedComment(feedItem.id) } ) if (index != feedUiState.allFeeds.lastIndex) { diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt b/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt index ce70e41d..9f288a14 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt @@ -4,25 +4,23 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.texthip.thip.R +import com.texthip.thip.ui.common.buttons.ActionBarButton import com.texthip.thip.ui.common.buttons.ActionBookButton import com.texthip.thip.ui.common.header.ProfileBar import com.texthip.thip.ui.mypage.mock.FeedItem @@ -37,7 +35,8 @@ fun SavedFeedCard( bottomTextColor: Color = colors.NeonGreen, onBookmarkClick: () -> Unit = {}, onLikeClick: () -> Unit = {}, - onContentClick: () -> Unit = {} + onContentClick: () -> Unit = {}, + onCommentClick: () -> Unit = {} ) { val hasImages = feedItem.imageUrls.isNotEmpty() val maxLines = if (hasImages) 3 else 8 @@ -103,41 +102,17 @@ fun SavedFeedCard( } } - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - modifier = Modifier.clickable { onLikeClick() }, - painter = painterResource(if (feedItem.isLiked) R.drawable.ic_heart_filled else R.drawable.ic_heart), - contentDescription = null, - tint = Color.Unspecified - ) - Text( - text = feedItem.likeCount.toString(), - style = typography.feedcopy_r400_s14_h20, - color = colors.White, - modifier = Modifier.padding(start = 5.dp, end = 12.dp) - ) - Icon( - modifier = Modifier.clickable { onContentClick() }, - painter = painterResource(R.drawable.ic_comment), - contentDescription = null, - tint = colors.White - ) - Text( - text = feedItem.commentCount.toString(), - style = typography.feedcopy_r400_s14_h20, - color = colors.White, - modifier = Modifier.padding(start = 5.dp, end = 12.dp) - ) - Spacer(modifier = Modifier.weight(1f)) - Icon( - modifier = Modifier.clickable { onBookmarkClick() }, - painter = painterResource(if (feedItem.isSaved) R.drawable.ic_save_filled else R.drawable.ic_save), - contentDescription = null, - tint = Color.Unspecified - ) - } + ActionBarButton( + modifier = Modifier.padding(bottom = 20.dp), + isLiked = feedItem.isLiked, + likeCount = feedItem.likeCount, + commentCount = feedItem.commentCount, + isSaveVisible = true, + isSaved = feedItem.isSaved, + onLikeClick = onLikeClick, + onCommentClick = onCommentClick, + onBookmarkClick = onBookmarkClick + ) } } From deb4f03b8099b39b21ffefe6416d9202b2cbc30d Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 17:40:15 +0900 Subject: [PATCH 49/51] =?UTF-8?q?[refactor]:=20=ED=8C=A8=EB=94=A9=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=B0=A9=EC=8B=9D=20=ED=86=B5=EC=9D=BC=20?= =?UTF-8?q?(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/texthip/thip/ui/feed/screen/FeedScreen.kt | 7 +++++-- .../com/texthip/thip/ui/mypage/component/SavedFeedCard.kt | 3 +-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt index cfa3a1a5..aafdf001 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt @@ -348,7 +348,7 @@ fun FeedScreen( if (index != feedUiState.myFeeds.lastIndex) { HorizontalDivider( color = colors.DarkGrey02, - thickness = 10.dp + thickness = 6.dp ) } } @@ -393,6 +393,8 @@ fun FeedScreen( imageUrls = allFeed.contentUrls ) + Spacer(modifier = Modifier.height(if (index == 0) 20.dp else 40.dp)) + SavedFeedCard( feedItem = feedItem, bottomTextColor = hexToColor(allFeed.aliasColor), @@ -409,10 +411,11 @@ fun FeedScreen( onNavigateToFeedComment(feedItem.id) } ) + Spacer(modifier = Modifier.height(40.dp)) if (index != feedUiState.allFeeds.lastIndex) { HorizontalDivider( color = colors.DarkGrey02, - thickness = 10.dp + thickness = 6.dp ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt b/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt index 9f288a14..f7f5176e 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt @@ -44,7 +44,7 @@ fun SavedFeedCard( Column( modifier = modifier .fillMaxWidth() - .padding(20.dp) + .padding(horizontal = 20.dp) ) { ProfileBar( profileImage = feedItem.userProfileImage ?: "https://example.com/image1.jpg", @@ -103,7 +103,6 @@ fun SavedFeedCard( } ActionBarButton( - modifier = Modifier.padding(bottom = 20.dp), isLiked = feedItem.isLiked, likeCount = feedItem.likeCount, commentCount = feedItem.commentCount, From 46b98b683f55262d8ec401bba3b9b63696ebca35 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 17:42:22 +0900 Subject: [PATCH 50/51] =?UTF-8?q?[refactor]:=20=EB=AC=B4=ED=95=9C=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EC=A4=91=EC=B2=A9=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt index 397c0b8f..0c676ed0 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt @@ -25,8 +25,8 @@ data class FeedUiState( val isLastPageMyFeeds: Boolean = false, val error: String? = null ) { - val canLoadMoreAllFeeds: Boolean get() = !isLoading && !isLoadingMore && !isLastPageAllFeeds - val canLoadMoreMyFeeds: Boolean get() = !isLoading && !isLoadingMore && !isLastPageMyFeeds + val canLoadMoreAllFeeds: Boolean get() = !isLoading && !isLoadingMore && !isRefreshing && !isLastPageAllFeeds + val canLoadMoreMyFeeds: Boolean get() = !isLoading && !isLoadingMore && !isRefreshing && !isLastPageMyFeeds val currentTabFeeds: List get() = when (selectedTabIndex) { 0 -> allFeeds 1 -> myFeeds @@ -234,6 +234,8 @@ class FeedViewModel @Inject constructor( } fun loadMoreFeeds() { + if (!_uiState.value.canLoadMoreCurrentTab || _uiState.value.isRefreshing) return + when (_uiState.value.selectedTabIndex) { 0 -> loadAllFeeds(isInitial = false) 1 -> loadMyFeeds(isInitial = false) From ebfaa52742fb8fd279f98a1b979cc8a8ee9bb28a Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 17:46:42 +0900 Subject: [PATCH 51/51] =?UTF-8?q?[refactor]:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A4=84=20=EB=B0=94=EA=BF=88=20=EC=88=98=EC=A0=95=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/repository/FeedRepository.kt | 20 +- .../thip/ui/feed/component/MyFeedCard.kt | 2 +- .../texthip/thip/ui/feed/screen/FeedScreen.kt | 400 +++++++++--------- .../ui/feed/viewmodel/FeedDetailViewModel.kt | 2 +- .../thip/ui/feed/viewmodel/FeedViewModel.kt | 71 ++-- .../ui/feed/viewmodel/FeedWriteViewModel.kt | 21 +- .../thip/ui/mypage/component/SavedFeedCard.kt | 8 +- .../navigator/navigations/FeedNavigation.kt | 6 +- 8 files changed, 279 insertions(+), 251 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt index ebcb198f..7652b71a 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt @@ -35,7 +35,7 @@ class FeedRepository @Inject constructor( val response = feedService.getFeedWriteInfo() .handleBaseResponse() .getOrThrow() - + // 카테고리 순서 조정 val orderedCategories = response?.categoryList?.sortedBy { category -> when (category.category) { @@ -47,7 +47,7 @@ class FeedRepository @Inject constructor( else -> 999 } } ?: emptyList() - + response?.copy(categoryList = orderedCategories) } @@ -72,7 +72,7 @@ class FeedRepository @Inject constructor( // 임시 파일 목록 추적 val tempFiles = mutableListOf() - + // 이미지 파일들을 MultipartBody.Part로 변환 val imageParts = if (imageUris.isNotEmpty()) { withContext(Dispatchers.IO) { @@ -96,7 +96,11 @@ class FeedRepository @Inject constructor( } } - private fun uriToMultipartBodyPart(uri: Uri, paramName: String, tempFiles: MutableList): MultipartBody.Part? { + private fun uriToMultipartBodyPart( + uri: Uri, + paramName: String, + tempFiles: MutableList + ): MultipartBody.Part? { return runCatching { // MIME 타입 확인 val mimeType = context.contentResolver.getType(uri) ?: "image/jpeg" @@ -106,21 +110,21 @@ class FeedRepository @Inject constructor( "image/jpeg", "image/jpg" -> "jpg" else -> "jpg" // 기본값 } - + // 파일명 생성 val fileName = "feed_image_${System.currentTimeMillis()}.$extension" val tempFile = File(context.cacheDir, fileName) - + // 임시 파일 목록에 추가 tempFiles.add(tempFile) - + // InputStream을 use 블록으로 안전하게 관리 context.contentResolver.openInputStream(uri)?.use { inputStream -> FileOutputStream(tempFile).use { outputStream -> inputStream.copyTo(outputStream) } } ?: throw IllegalStateException("Failed to open input stream for URI: $uri") - + // MultipartBody.Part 생성 val requestFile = tempFile.asRequestBody(mimeType.toMediaType()) MultipartBody.Part.createFormData(paramName, fileName, requestFile) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt b/app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt index 82b19623..297e75f3 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/component/MyFeedCard.kt @@ -47,7 +47,7 @@ fun MyFeedCard( onClick = {} ) - Column ( + Column( modifier = Modifier .clickable { onContentClick() }, verticalArrangement = Arrangement.Center, diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt index aafdf001..d4f1d87a 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt @@ -89,7 +89,7 @@ fun FeedScreen( val scope = rememberCoroutineScope() var showProgressBar by remember { mutableStateOf(false) } val progress = remember { Animatable(0f) } - + // 무한 스크롤 로직 val listState = rememberLazyListState() @@ -113,11 +113,11 @@ fun FeedScreen( feedViewModel.loadMoreFeeds() } } - + LaunchedEffect(resultFeedId) { if (resultFeedId != null) { onResultConsumed() - + showProgressBar = true progress.snapTo(0f) scope.launch { @@ -184,7 +184,7 @@ fun FeedScreen( ) ) val subscriptionUiState by mySubscriptionViewModel.uiState.collectAsState() - + // 초기 로딩 상태 처리 if (feedUiState.isLoading && feedUiState.currentTabFeeds.isEmpty()) { Box( @@ -198,7 +198,7 @@ fun FeedScreen( } return } - + Box(modifier = Modifier.fillMaxSize()) { PullToRefreshBox( isRefreshing = feedUiState.isRefreshing, @@ -207,145 +207,216 @@ fun FeedScreen( Column( modifier = Modifier.fillMaxSize() ) { - LogoTopAppBar( - leftIcon = painterResource(R.drawable.ic_plusfriend), - hasNotification = false, - onLeftClick = {}, - onRightClick = {}, - ) - Spacer(modifier = Modifier.height(32.dp)) - HeaderMenuBarTab( - titles = listOf("피드", "내 피드"), - selectedTabIndex = feedUiState.selectedTabIndex, - onTabSelected = feedViewModel::onTabSelected - ) - - // 스크롤 영역 전체 - LazyColumn( - state = listState, - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - item { - AnimatedVisibility(visible = showProgressBar) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 20.dp, end = 20.dp, top = 32.dp), - ) { - Text( - modifier = Modifier.padding(bottom = 12.dp), - text = if (progress.value < 1.0f) { - stringResource(R.string.posting_in_progress_feed) - } else { - stringResource(R.string.posting_complete_feed) - }, - style = typography.view_m500_s14, - color = colors.NeonGreen - ) + LogoTopAppBar( + leftIcon = painterResource(R.drawable.ic_plusfriend), + hasNotification = false, + onLeftClick = {}, + onRightClick = {}, + ) + Spacer(modifier = Modifier.height(32.dp)) + HeaderMenuBarTab( + titles = listOf("피드", "내 피드"), + selectedTabIndex = feedUiState.selectedTabIndex, + onTabSelected = feedViewModel::onTabSelected + ) - Box( + // 스크롤 영역 전체 + LazyColumn( + state = listState, + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + item { + AnimatedVisibility(visible = showProgressBar) { + Column( modifier = Modifier .fillMaxWidth() - .height(8.dp) - .clip(RoundedCornerShape(12.dp)) - .background(color = colors.Grey02) // 트랙(배경) 색상 + .padding(start = 20.dp, end = 20.dp, top = 32.dp), ) { + Text( + modifier = Modifier.padding(bottom = 12.dp), + text = if (progress.value < 1.0f) { + stringResource(R.string.posting_in_progress_feed) + } else { + stringResource(R.string.posting_complete_feed) + }, + style = typography.view_m500_s14, + color = colors.NeonGreen + ) + Box( modifier = Modifier - .fillMaxWidth(fraction = progress.value) - .fillMaxHeight() - .background( - color = colors.NeonGreen, - shape = RoundedCornerShape(12.dp) - ) - ) + .fillMaxWidth() + .height(8.dp) + .clip(RoundedCornerShape(12.dp)) + .background(color = colors.Grey02) // 트랙(배경) 색상 + ) { + Box( + modifier = Modifier + .fillMaxWidth(fraction = progress.value) + .fillMaxHeight() + .background( + color = colors.NeonGreen, + shape = RoundedCornerShape(12.dp) + ) + ) + } } } } - } - if (feedUiState.selectedTabIndex == 1) { - // 내 피드 - item { - Spacer(modifier = Modifier.height(32.dp)) - AuthorHeader( - profileImage = null, - nickname = nickname, - badgeText = userRole, - buttonText = "", - buttonWidth = 60.dp, - showButton = false - ) - Spacer(modifier = Modifier.height(16.dp)) - FeedSubscribeBarlist( - modifier = Modifier.padding(horizontal = 20.dp), - followerProfileImageUrls = followerProfileImageUrls, - onClick = { - } - ) - Spacer(modifier = Modifier.height(40.dp)) - Text( - text = stringResource(R.string.whole_num, feedUiState.myFeeds.size), - style = typography.menu_m500_s14_h24, - color = colors.Grey, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp, start = 20.dp) - ) - HorizontalDivider( - color = colors.DarkGrey02, - thickness = 1.dp, - modifier = Modifier.padding(horizontal = 20.dp) - ) - } - - if (feedUiState.myFeeds.isEmpty()) { + if (feedUiState.selectedTabIndex == 1) { + // 내 피드 item { - Box( + Spacer(modifier = Modifier.height(32.dp)) + AuthorHeader( + profileImage = null, + nickname = nickname, + badgeText = userRole, + buttonText = "", + buttonWidth = 60.dp, + showButton = false + ) + Spacer(modifier = Modifier.height(16.dp)) + FeedSubscribeBarlist( + modifier = Modifier.padding(horizontal = 20.dp), + followerProfileImageUrls = followerProfileImageUrls, + onClick = { + } + ) + Spacer(modifier = Modifier.height(40.dp)) + Text( + text = stringResource(R.string.whole_num, feedUiState.myFeeds.size), + style = typography.menu_m500_s14_h24, + color = colors.Grey, modifier = Modifier .fillMaxWidth() - .padding(top = 244.dp), - contentAlignment = Alignment.TopCenter - ) { - Text( - text = stringResource(R.string.create_feed), - style = typography.smalltitle_sb600_s18_h24, - color = colors.White + .padding(bottom = 12.dp, start = 20.dp) + ) + HorizontalDivider( + color = colors.DarkGrey02, + thickness = 1.dp, + modifier = Modifier.padding(horizontal = 20.dp) + ) + } + + if (feedUiState.myFeeds.isEmpty()) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 244.dp), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = stringResource(R.string.create_feed), + style = typography.smalltitle_sb600_s18_h24, + color = colors.White + ) + } + } + } else { + itemsIndexed( + feedUiState.myFeeds, + key = { _, item -> item.feedId }) { index, myFeed -> + Spacer(modifier = Modifier.height(if (index == 0) 20.dp else 40.dp)) + + // MyFeedItem을 FeedItem으로 변환 + val feedItem = FeedItem( + id = myFeed.feedId, + userProfileImage = null, + userName = "", // 내 피드이므로 고정값 + userRole = "", // 내 피드이므로 고정값 + bookTitle = myFeed.bookTitle, + authName = myFeed.bookAuthor, + timeAgo = myFeed.postDate, + content = myFeed.contentBody, + likeCount = myFeed.likeCount, + commentCount = myFeed.commentCount, + isLiked = false, // 내 피드는 좋아요 개념 없음 + isSaved = false, // 내 피드는 저장 개념 없음 + isLocked = !myFeed.isPublic, // isPublic의 반대값 + tags = emptyList(), + imageUrls = myFeed.contentUrls ) + + MyFeedCard( + feedItem = feedItem, + onLikeClick = {}, + onContentClick = { + onNavigateToFeedComment(feedItem.id) + } + ) + Spacer(modifier = Modifier.height(40.dp)) + if (index != feedUiState.myFeeds.lastIndex) { + HorizontalDivider( + color = colors.DarkGrey02, + thickness = 6.dp + ) + } } } } else { - itemsIndexed(feedUiState.myFeeds, key = { _, item -> item.feedId }) { index, myFeed -> - Spacer(modifier = Modifier.height(if (index == 0) 20.dp else 40.dp)) - - // MyFeedItem을 FeedItem으로 변환 + //피드 + item { + Spacer(modifier = Modifier.height(20.dp)) + val subscriptionsForBar = feedUiState.recentWriters.map { user -> + MySubscriptionData( + profileImageUrl = user.profileImageUrl, + nickname = user.nickname, + role = "", + roleColor = colors.White, + subscriberCount = 0, + isSubscribed = true + ) + } + MySubscribeBarlist( + modifier = Modifier.padding(horizontal = 20.dp), + subscriptions = subscriptionsForBar, + onClick = onNavigateToMySubscription + ) + } + itemsIndexed( + feedUiState.allFeeds, + key = { _, item -> item.feedId }) { index, allFeed -> + // AllFeedItem을 FeedItem으로 변환 val feedItem = FeedItem( - id = myFeed.feedId, - userProfileImage = null, - userName = "", // 내 피드이므로 고정값 - userRole = "", // 내 피드이므로 고정값 - bookTitle = myFeed.bookTitle, - authName = myFeed.bookAuthor, - timeAgo = myFeed.postDate, - content = myFeed.contentBody, - likeCount = myFeed.likeCount, - commentCount = myFeed.commentCount, - isLiked = false, // 내 피드는 좋아요 개념 없음 - isSaved = false, // 내 피드는 저장 개념 없음 - isLocked = !myFeed.isPublic, // isPublic의 반대값 + id = allFeed.feedId, + userProfileImage = allFeed.creatorProfileImageUrl, + userName = allFeed.creatorNickname, + userRole = allFeed.aliasName, + bookTitle = allFeed.bookTitle, + authName = allFeed.bookAuthor, + timeAgo = allFeed.postDate, + content = allFeed.contentBody, + likeCount = allFeed.likeCount, + commentCount = allFeed.commentCount, + isLiked = allFeed.isLiked, + isSaved = allFeed.isSaved, + isLocked = false, tags = emptyList(), - imageUrls = myFeed.contentUrls + imageUrls = allFeed.contentUrls ) - - MyFeedCard( + + Spacer(modifier = Modifier.height(if (index == 0) 20.dp else 40.dp)) + + SavedFeedCard( feedItem = feedItem, - onLikeClick = {}, + bottomTextColor = hexToColor(allFeed.aliasColor), + onBookmarkClick = { + // TODO: API 호출로 북마크 상태 변경 + }, + onLikeClick = { + // TODO: API 호출로 좋아요 상태 변경 + }, onContentClick = { onNavigateToFeedComment(feedItem.id) + }, + onCommentClick = { + onNavigateToFeedComment(feedItem.id) } ) Spacer(modifier = Modifier.height(40.dp)) - if (index != feedUiState.myFeeds.lastIndex) { + if (index != feedUiState.allFeeds.lastIndex) { HorizontalDivider( color = colors.DarkGrey02, thickness = 6.dp @@ -353,91 +424,24 @@ fun FeedScreen( } } } - } else { - //피드 - item { - Spacer(modifier = Modifier.height(20.dp)) - val subscriptionsForBar = feedUiState.recentWriters.map { user -> - MySubscriptionData( - profileImageUrl = user.profileImageUrl, - nickname = user.nickname, - role = "", - roleColor = colors.White, - subscriberCount = 0, - isSubscribed = true - ) - } - MySubscribeBarlist( - modifier = Modifier.padding(horizontal = 20.dp), - subscriptions = subscriptionsForBar, - onClick = onNavigateToMySubscription - ) - } - itemsIndexed(feedUiState.allFeeds, key = { _, item -> item.feedId }) { index, allFeed -> - // AllFeedItem을 FeedItem으로 변환 - val feedItem = FeedItem( - id = allFeed.feedId, - userProfileImage = allFeed.creatorProfileImageUrl, - userName = allFeed.creatorNickname, - userRole = allFeed.aliasName, - bookTitle = allFeed.bookTitle, - authName = allFeed.bookAuthor, - timeAgo = allFeed.postDate, - content = allFeed.contentBody, - likeCount = allFeed.likeCount, - commentCount = allFeed.commentCount, - isLiked = allFeed.isLiked, - isSaved = allFeed.isSaved, - isLocked = false, - tags = emptyList(), - imageUrls = allFeed.contentUrls - ) - Spacer(modifier = Modifier.height(if (index == 0) 20.dp else 40.dp)) - - SavedFeedCard( - feedItem = feedItem, - bottomTextColor = hexToColor(allFeed.aliasColor), - onBookmarkClick = { - // TODO: API 호출로 북마크 상태 변경 - }, - onLikeClick = { - // TODO: API 호출로 좋아요 상태 변경 - }, - onContentClick = { - onNavigateToFeedComment(feedItem.id) - }, - onCommentClick = { - onNavigateToFeedComment(feedItem.id) + // 무한 스크롤 로딩 인디케이터 + if (feedUiState.isLoadingMore) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + color = colors.White + ) } - ) - Spacer(modifier = Modifier.height(40.dp)) - if (index != feedUiState.allFeeds.lastIndex) { - HorizontalDivider( - color = colors.DarkGrey02, - thickness = 6.dp - ) - } - } - } - - // 무한 스크롤 로딩 인디케이터 - if (feedUiState.isLoadingMore) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - color = colors.White - ) } } } } - } } FloatingButton( icon = painterResource(id = R.drawable.ic_write), diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedDetailViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedDetailViewModel.kt index 60f0f47c..204d3604 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedDetailViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedDetailViewModel.kt @@ -32,7 +32,7 @@ class FeedDetailViewModel @Inject constructor( fun loadFeedDetail(feedId: Int) { viewModelScope.launch { updateState { it.copy(isLoading = true, error = null) } - + feedRepository.getFeedDetail(feedId) .onSuccess { response -> updateState { diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt index 0c676ed0..d80977b5 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt @@ -27,16 +27,18 @@ data class FeedUiState( ) { val canLoadMoreAllFeeds: Boolean get() = !isLoading && !isLoadingMore && !isRefreshing && !isLastPageAllFeeds val canLoadMoreMyFeeds: Boolean get() = !isLoading && !isLoadingMore && !isRefreshing && !isLastPageMyFeeds - val currentTabFeeds: List get() = when (selectedTabIndex) { - 0 -> allFeeds - 1 -> myFeeds - else -> emptyList() - } - val canLoadMoreCurrentTab: Boolean get() = when (selectedTabIndex) { - 0 -> canLoadMoreAllFeeds - 1 -> canLoadMoreMyFeeds - else -> false - } + val currentTabFeeds: List + get() = when (selectedTabIndex) { + 0 -> allFeeds + 1 -> myFeeds + else -> emptyList() + } + val canLoadMoreCurrentTab: Boolean + get() = when (selectedTabIndex) { + 0 -> canLoadMoreAllFeeds + 1 -> canLoadMoreMyFeeds + else -> false + } } @HiltViewModel @@ -46,12 +48,12 @@ class FeedViewModel @Inject constructor( ) : ViewModel() { private val _uiState = MutableStateFlow(FeedUiState()) val uiState = _uiState.asStateFlow() - + private var allFeedsNextCursor: String? = null private var myFeedsNextCursor: String? = null private var isLoadingAllFeeds = false private var isLoadingMyFeeds = false - + private fun updateState(update: (FeedUiState) -> FeedUiState) { _uiState.value = update(_uiState.value) } @@ -63,11 +65,12 @@ class FeedViewModel @Inject constructor( fun onTabSelected(index: Int) { updateState { it.copy(selectedTabIndex = index) } - + when (index) { 0 -> { loadAllFeeds(isInitial = true) } + 1 -> { loadMyFeeds(isInitial = true) } @@ -77,20 +80,26 @@ class FeedViewModel @Inject constructor( private fun loadAllFeeds(isInitial: Boolean = true) { if (isLoadingAllFeeds && !isInitial) return if (_uiState.value.isLastPageAllFeeds && !isInitial) return - + viewModelScope.launch { try { isLoadingAllFeeds = true - + if (isInitial) { - updateState { it.copy(isLoading = true, allFeeds = emptyList(), isLastPageAllFeeds = false) } + updateState { + it.copy( + isLoading = true, + allFeeds = emptyList(), + isLastPageAllFeeds = false + ) + } allFeedsNextCursor = null } else { updateState { it.copy(isLoadingMore = true) } } - + val cursor = if (isInitial) null else allFeedsNextCursor - + feedRepository.getAllFeeds(cursor).onSuccess { response -> if (response != null) { val currentList = if (isInitial) emptyList() else _uiState.value.allFeeds @@ -118,20 +127,26 @@ class FeedViewModel @Inject constructor( private fun loadMyFeeds(isInitial: Boolean = true) { if (isLoadingMyFeeds && !isInitial) return if (_uiState.value.isLastPageMyFeeds && !isInitial) return - + viewModelScope.launch { try { isLoadingMyFeeds = true - + if (isInitial) { - updateState { it.copy(isLoading = true, myFeeds = emptyList(), isLastPageMyFeeds = false) } + updateState { + it.copy( + isLoading = true, + myFeeds = emptyList(), + isLastPageMyFeeds = false + ) + } myFeedsNextCursor = null } else { updateState { it.copy(isLoadingMore = true) } } - + val cursor = if (isInitial) null else myFeedsNextCursor - + feedRepository.getMyFeeds(cursor).onSuccess { response -> if (response != null) { val currentList = if (isInitial) emptyList() else _uiState.value.myFeeds @@ -159,7 +174,7 @@ class FeedViewModel @Inject constructor( fun refreshCurrentTab() { viewModelScope.launch { updateState { it.copy(isRefreshing = true) } - + when (_uiState.value.selectedTabIndex) { 0 -> refreshAllFeeds() 1 -> refreshMyFeeds() @@ -169,7 +184,7 @@ class FeedViewModel @Inject constructor( private suspend fun refreshAllFeeds() { allFeedsNextCursor = null - + feedRepository.getAllFeeds().onSuccess { response -> if (response != null) { allFeedsNextCursor = response.nextCursor @@ -202,7 +217,7 @@ class FeedViewModel @Inject constructor( private suspend fun refreshMyFeeds() { myFeedsNextCursor = null - + feedRepository.getMyFeeds().onSuccess { response -> if (response != null) { myFeedsNextCursor = response.nextCursor @@ -232,10 +247,10 @@ class FeedViewModel @Inject constructor( } } } - + fun loadMoreFeeds() { if (!_uiState.value.canLoadMoreCurrentTab || _uiState.value.isRefreshing) return - + when (_uiState.value.selectedTabIndex) { 0 -> loadAllFeeds(isInitial = false) 1 -> loadMyFeeds(isInitial = false) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt index b6fc4b6b..7907c66c 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt @@ -44,7 +44,7 @@ class FeedWriteViewModel @Inject constructor( updateState { it.copy(isLoadingCategories = true) } feedRepository.getFeedWriteInfo() .onSuccess { response -> - updateState { + updateState { it.copy( categories = response?.categoryList ?: emptyList(), isLoadingCategories = false @@ -52,7 +52,7 @@ class FeedWriteViewModel @Inject constructor( } } .onFailure { - updateState { + updateState { it.copy( categories = emptyList(), isLoadingCategories = false, @@ -177,9 +177,9 @@ class FeedWriteViewModel @Inject constructor( val currentState = _uiState.value val availableSlots = 3 - currentState.imageUris.size val imagesToAdd = newImageUris.take(availableSlots) - - updateState { - it.copy(imageUris = currentState.imageUris + imagesToAdd) + + updateState { + it.copy(imageUris = currentState.imageUris + imagesToAdd) } } @@ -196,7 +196,7 @@ class FeedWriteViewModel @Inject constructor( } fun selectCategory(index: Int) { - updateState { + updateState { it.copy( selectedCategoryIndex = index, selectedTags = emptyList() // 카테고리 변경 시 태그 초기화 @@ -220,8 +220,8 @@ class FeedWriteViewModel @Inject constructor( fun removeTag(tag: String) { val currentTags = _uiState.value.selectedTags - updateState { - it.copy(selectedTags = currentTags - tag) + updateState { + it.copy(selectedTags = currentTags - tag) } } @@ -250,7 +250,7 @@ class FeedWriteViewModel @Inject constructor( tagList = currentState.selectedTags, imageUris = currentState.imageUris ) - + result.onSuccess { response -> val feedId = response?.feedId if (feedId != null) { @@ -260,7 +260,8 @@ class FeedWriteViewModel @Inject constructor( } }.onFailure { exception -> onError( - exception.message ?: stringResourceProvider.getString(R.string.error_network_error) + exception.message + ?: stringResourceProvider.getString(R.string.error_network_error) ) } diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt b/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt index f7f5176e..b9032072 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt @@ -66,7 +66,7 @@ fun SavedFeedCard( ) } - Column ( + Column( modifier = Modifier .clickable { onContentClick() }, verticalArrangement = Arrangement.Center, @@ -148,7 +148,11 @@ private fun SavedFeedCardPrev() { commentCount = 5, isLiked = false, isSaved = true, - imageUrls = listOf("https://example.com/image1.jpg", "https://example.com/image2.jpg", "https://example.com/image3.jpg") + imageUrls = listOf( + "https://example.com/image1.jpg", + "https://example.com/image2.jpg", + "https://example.com/image3.jpg" + ) ) val scrollState = rememberScrollState() diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt index 387da17a..c668a4a7 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt @@ -17,7 +17,7 @@ import com.texthip.thip.ui.navigator.routes.MainTabRoutes fun NavGraphBuilder.feedNavigation(navController: NavHostController) { composable { backStackEntry -> val resultFeedId = backStackEntry.savedStateHandle.get("feedId") - + FeedScreen( nickname = "ThipUser01", userRole = "문학가", @@ -58,10 +58,10 @@ fun NavGraphBuilder.feedNavigation(navController: NavHostController) { ) } composable { backStackEntry -> - val route = backStackEntry.arguments?.let { + val route = backStackEntry.arguments?.let { FeedRoutes.Comment(it.getInt("feedId")) } ?: return@composable - + FeedCommentScreen( feedId = route.feedId, onNavigateBack = {