diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsCreateVoteRequest.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsCreateVoteRequest.kt new file mode 100644 index 00000000..043e09aa --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsCreateVoteRequest.kt @@ -0,0 +1,16 @@ +package com.texthip.thip.data.model.rooms.request + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsCreateVoteRequest( + val page: Int, + val isOverview: Boolean, + val content: String, + val voteItemList: List +) + +@Serializable +data class VoteItem( + val itemName: String +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsPostsLikesRequest.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsPostsLikesRequest.kt new file mode 100644 index 00000000..6c3240c2 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsPostsLikesRequest.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.rooms.request + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsPostsLikesRequest( + val type: Boolean, + val roomPostType: String, +) diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsPostsRequestParams.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsPostsRequestParams.kt new file mode 100644 index 00000000..6b98cf0e --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsPostsRequestParams.kt @@ -0,0 +1,14 @@ +package com.texthip.thip.data.model.rooms.request + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsPostsRequestParams( + val type: String, + val sort: String? = null, + val pageStart: Int? = null, + val pageEnd: Int? = null, + val isOverview: Boolean? = null, + val isPageFilter: Boolean? = null, + val cursor: String? = null +) diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsRecordRequest.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsRecordRequest.kt new file mode 100644 index 00000000..649fcf96 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsRecordRequest.kt @@ -0,0 +1,10 @@ +package com.texthip.thip.data.model.rooms.request + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsRecordRequest( + val page: Int, + val isOverview: Boolean = false, + val content: String, +) diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsVoteRequest.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsVoteRequest.kt new file mode 100644 index 00000000..fa8c3823 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomsVoteRequest.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.rooms.request + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsVoteRequest( + val voteItemId: Int, + val type: Boolean +) diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsBookPageResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsBookPageResponse.kt new file mode 100644 index 00000000..2ed41c16 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsBookPageResponse.kt @@ -0,0 +1,11 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsBookPageResponse( + val totalBookPage: Int, + val recentBookPage: Int, + val isOverviewPossible: Boolean, + val roomId: Int, +) diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsCreateVoteResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsCreateVoteResponse.kt new file mode 100644 index 00000000..c68c122b --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsCreateVoteResponse.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsCreateVoteResponse( + val voteId: Int, + val roomId: Int +) diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDeleteRecordResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDeleteRecordResponse.kt new file mode 100644 index 00000000..66b46cd0 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsDeleteRecordResponse.kt @@ -0,0 +1,8 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsDeleteRecordResponse( + val roomId: Int, +) diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPostsLikesResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPostsLikesResponse.kt new file mode 100644 index 00000000..be694d64 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPostsLikesResponse.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsPostsLikesResponse( + val postId: Int, + val isLiked: Boolean, +) 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 new file mode 100644 index 00000000..7a77e1f8 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPostsResponse.kt @@ -0,0 +1,39 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsPostsResponse( + val postList: List, + val roomId: Int, + val isbn: String, + val isOverviewEnabled: Boolean, + val nextCursor: String?, + val isLast: Boolean, +) + +@Serializable +data class PostList( + val postId: Int, + val postDate: String, + val postType: String, + val page: Int, + val userId: Int, + val nickName: String, + val profileImageUrl: String?, + val content: String, + val likeCount: Int, + val commentCount: Int, + val isLiked: Boolean, + val isWriter: Boolean, + val isLocked: Boolean, + val voteItems: List, +) + +@Serializable +data class VoteItems( + val voteItemId: Int, + val itemName: String, + val percentage: Int, + val isVoted: Boolean, +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsRecordResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsRecordResponse.kt new file mode 100644 index 00000000..003c1e71 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsRecordResponse.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsRecordResponse( + val recordId: Int, + val roomId: Int, +) diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsUsersResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsUsersResponse.kt index 604d8062..b1c3e442 100644 --- a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsUsersResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsUsersResponse.kt @@ -12,6 +12,6 @@ data class UserList( val userId: Int, val nickname: String, val imageUrl: String, - val alias: String, + val aliasName: String, val followerCount: Int, ) \ No newline at end of file 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 new file mode 100644 index 00000000..1fd74ff9 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsVoteResponse.kt @@ -0,0 +1,10 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsVoteResponse( + val voteItemId: Int, + val roomId: Int, + val type: Boolean, +) 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 141a40ed..8b16c7a2 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 @@ -1,6 +1,11 @@ package com.texthip.thip.data.repository import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.model.rooms.request.RoomsCreateVoteRequest +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 +import com.texthip.thip.data.model.rooms.request.VoteItem import com.texthip.thip.data.service.RoomsService import javax.inject.Inject import javax.inject.Singleton @@ -24,4 +29,108 @@ class RoomsRepository @Inject constructor( roomId = roomId ).handleBaseResponse().getOrThrow() } + + suspend fun getRoomsPosts( + roomId: Int, + type: String = "group", + sort: String? = "latest", + pageStart: Int? = null, + pageEnd: Int? = null, + isOverview: Boolean? = false, + isPageFilter: Boolean? = false, + cursor: String? = null, + ) = runCatching { + roomsService.getRoomsPosts( + roomId = roomId, + type = type, + sort = sort, + pageStart = pageStart, + pageEnd = pageEnd, + isOverview = isOverview, + isPageFilter = isPageFilter, + cursor = cursor + ).handleBaseResponse().getOrThrow() + } + + suspend fun postRoomsRecord( + roomId: Int, + content: String, + isOverview: Boolean = false, + page: Int = 0 + ) = runCatching { + roomsService.postRoomsRecord( + roomId = roomId, + request = RoomsRecordRequest( + page = page, + isOverview = isOverview, + content = content + ) + ).handleBaseResponse().getOrThrow() + } + + suspend fun postRoomsCreateVote( + roomId: Int, + page: Int, + isOverview: Boolean, + content: String, + voteItemList: List + ) = runCatching { + roomsService.postRoomsCreateVote( + roomId = roomId, + request = RoomsCreateVoteRequest( + page = page, + isOverview = isOverview, + content = content, + voteItemList = voteItemList + ) + ).handleBaseResponse().getOrThrow() + } + + suspend fun getRoomsBookPage( + roomId: Int, + ) = runCatching { + roomsService.getRoomsBookPage( + roomId = roomId + ).handleBaseResponse().getOrThrow() + } + + suspend fun postRoomsVote( + roomId: Int, + voteId: Int, + voteItemId: Int, + type: Boolean + ) = runCatching { + roomsService.postRoomsVote( + roomId = roomId, + voteId = voteId, + request = RoomsVoteRequest( + voteItemId = voteItemId, + type = type + ) + ).handleBaseResponse().getOrThrow() + } + + suspend fun deleteRoomsRecord( + roomId: Int, + recordId: Int + ) = runCatching { + roomsService.deleteRoomsRecord( + roomId = roomId, + recordId = recordId + ).handleBaseResponse().getOrThrow() + } + + suspend fun postRoomsPostsLikes( + postId: Int, + type: Boolean, + roomPostType: String + ) = runCatching { + roomsService.postRoomsPostsLikes( + postId = postId, + request = RoomsPostsLikesRequest( + type = type, + roomPostType = roomPostType + ) + ).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 d493c456..3e8e05e0 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 @@ -1,10 +1,25 @@ package com.texthip.thip.data.service import com.texthip.thip.data.model.base.BaseResponse +import com.texthip.thip.data.model.rooms.request.RoomsCreateVoteRequest +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 +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.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.RoomsUsersResponse +import com.texthip.thip.data.model.rooms.response.RoomsVoteResponse +import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path +import retrofit2.http.Query interface RoomsService { @GET("rooms/{roomId}/playing") @@ -16,4 +31,52 @@ interface RoomsService { suspend fun getRoomsUsers( @Path("roomId") roomId: Int ): BaseResponse + + @GET("rooms/{roomId}/posts") + suspend fun getRoomsPosts( + @Path("roomId") roomId: Int, + @Query("type") type: String = "group", + @Query("sort") sort: String? = "latest", + @Query("pageStart") pageStart: Int? = null, + @Query("pageEnd") pageEnd: Int? = null, + @Query("isOverview") isOverview: Boolean? = false, + @Query("isPageFilter") isPageFilter: Boolean? = false, + @Query("cursor") cursor: String? = null, + ): BaseResponse + + @POST("rooms/{roomId}/record") + suspend fun postRoomsRecord( + @Path("roomId") roomId: Int, + @Body request: RoomsRecordRequest + ): BaseResponse + + @POST("rooms/{roomId}/vote") + suspend fun postRoomsCreateVote( + @Path("roomId") roomId: Int, + @Body request: RoomsCreateVoteRequest + ): BaseResponse + + @GET("rooms/{roomId}/book-page") + suspend fun getRoomsBookPage( + @Path("roomId") roomId: Int, + ): BaseResponse + + @POST("rooms/{roomId}/vote/{voteId}") + suspend fun postRoomsVote( + @Path("roomId") roomId: Int, + @Path("voteId") voteId: Int, + @Body request: RoomsVoteRequest + ): BaseResponse + + @DELETE("rooms/{roomId}/record/{recordId}") + suspend fun deleteRoomsRecord( + @Path("roomId") roomId: Int, + @Path("recordId") recordId: Int + ): BaseResponse + + @POST("room-posts/{postId}/likes") + suspend fun postRoomsPostsLikes( + @Path("postId") postId: Int, + @Body request: RoomsPostsLikesRequest + ): BaseResponse } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/common/buttons/GroupVoteButton.kt b/app/src/main/java/com/texthip/thip/ui/common/buttons/GroupVoteButton.kt index 4da40172..bc6b9e2d 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/buttons/GroupVoteButton.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/buttons/GroupVoteButton.kt @@ -23,9 +23,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.texthip.thip.ui.group.note.mock.VoteItem +import com.texthip.thip.data.model.rooms.response.VoteItems import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -33,7 +34,7 @@ import com.texthip.thip.ui.theme.ThipTheme.typography @Composable fun GroupVoteButton( modifier: Modifier = Modifier, - voteItems: List, + voteItems: List, selectedIndex: Int?, // 선택한 인덱스 hasVoted: Boolean = false, // 투표 여부 onOptionSelected: (Int?) -> Unit @@ -69,8 +70,9 @@ fun GroupVoteButton( Box( modifier = Modifier .fillMaxWidth() - .background(color = backgroundColor, shape = RoundedCornerShape(12.dp)) .height(44.dp) + .clip(RoundedCornerShape(12.dp)) + .background(color = backgroundColor) .clickable { if (isSelected) { onOptionSelected(null) @@ -84,10 +86,7 @@ fun GroupVoteButton( modifier = Modifier .fillMaxHeight() .fillMaxWidth(animatedPercent) - .background( - color = percentBarColor, - shape = RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp) - ) + .background(color = percentBarColor) ) } @@ -123,9 +122,9 @@ private fun GroupVoteButtonPreview() { var voteItems by remember { mutableStateOf( listOf( - VoteItem(1, "밥", 25, false), - VoteItem(2, "국수", 35, false), - VoteItem(3, "고기", 40, false) + VoteItems(1, "밥", 25, false), + VoteItems(2, "국수", 35, false), + VoteItems(3, "고기", 40, false) ) ) } 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 4902875f..77b256c8 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 @@ -48,6 +48,8 @@ import com.texthip.thip.ui.common.forms.CommentTextField 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.group.note.component.CommentItem +import com.texthip.thip.ui.group.note.component.ReplyItem import com.texthip.thip.ui.feed.component.ImageViewerModal import com.texthip.thip.ui.feed.mock.FeedItemType import com.texthip.thip.ui.group.note.component.CommentItem diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/component/FilterHeaderSection.kt b/app/src/main/java/com/texthip/thip/ui/group/note/component/FilterHeaderSection.kt index 33e37221..aff12d5a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/component/FilterHeaderSection.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/component/FilterHeaderSection.kt @@ -24,6 +24,7 @@ import com.texthip.thip.ui.theme.ThipTheme.typography @Composable fun FilterHeaderSection( + modifier: Modifier = Modifier, firstPage: String, lastPage: String, isTotalSelected: Boolean, @@ -31,13 +32,14 @@ fun FilterHeaderSection( onFirstPageChange: (String) -> Unit, onLastPageChange: (String) -> Unit, onTotalToggle: () -> Unit, - onDisabledClick: () -> Unit = { } + onDisabledClick: () -> Unit = { }, + onApplyPageFilter: () -> Unit ) { var isPageInputVisible by rememberSaveable { mutableStateOf(false) } val isPageFiltered = firstPage.isNotBlank() || lastPage.isNotBlank() Box( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(horizontal = 20.dp), contentAlignment = Alignment.Center @@ -76,6 +78,7 @@ fun FilterHeaderSection( lastPage = lastPage, onLastPageChange = onLastPageChange, onFinishClick = { + onApplyPageFilter() isPageInputVisible = false } ) @@ -97,6 +100,7 @@ private fun FilterHeaderSectionPreview() { onFirstPageChange = { firstPage = it }, onLastPageChange = { lastPage = it }, onTotalToggle = { isTotalSelected = !isTotalSelected }, - totalEnabled = true + totalEnabled = true, + onApplyPageFilter = {}, ) } \ No newline at end of file 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 1363f3e0..aa55b37c 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 @@ -6,10 +6,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding 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.Modifier import androidx.compose.ui.draw.blur import androidx.compose.ui.input.pointer.pointerInput @@ -17,21 +13,21 @@ 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.rooms.response.PostList import com.texthip.thip.ui.common.buttons.ActionBarButton import com.texthip.thip.ui.common.header.ProfileBar -import com.texthip.thip.ui.group.note.mock.GroupNoteRecord import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @Composable fun TextCommentCard( modifier: Modifier = Modifier, - data: GroupNoteRecord, + data: PostList, + onLikeClick: (postId: Int, postType: String) -> Unit = { _, _ -> }, onCommentClick: () -> Unit = {}, onLongPress: () -> Unit = {}, onPinClick: () -> Unit = {} ) { - var isLiked by remember { mutableStateOf(data.isLiked) } val isLocked = data.isLocked val isWriter = data.isWriter @@ -63,12 +59,12 @@ fun TextCommentCard( ) ActionBarButton( - isLiked = isLiked, + isLiked = data.isLiked, likeCount = data.likeCount, commentCount = data.commentCount, isPinVisible = isWriter, onLikeClick = { - if (!isLocked) isLiked = !isLiked + if (!isLocked) onLikeClick(data.postId, data.postType) }, onCommentClick = { if (!isLocked) onCommentClick() @@ -84,7 +80,9 @@ fun TextCommentCard( @Composable fun TextCommentCardPreview() { TextCommentCard( - data = GroupNoteRecord( + data = PostList( + postId = 1, + postType = "group", page = 132, postDate = "12시간 전", userId = 1, @@ -96,7 +94,7 @@ fun TextCommentCardPreview() { isLiked = true, isWriter = false, isLocked = false, - recordId = 1 + voteItems = emptyList() ) ) } \ No newline at end of file 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 1662f38f..9705ef67 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 @@ -6,10 +6,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding 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.Modifier import androidx.compose.ui.draw.blur import androidx.compose.ui.input.pointer.pointerInput @@ -17,26 +13,25 @@ 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.rooms.response.PostList import com.texthip.thip.ui.common.buttons.ActionBarButton import com.texthip.thip.ui.common.buttons.GroupVoteButton import com.texthip.thip.ui.common.header.ProfileBar -import com.texthip.thip.ui.group.note.mock.GroupNoteVote -import com.texthip.thip.ui.group.note.mock.VoteItem import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @Composable fun VoteCommentCard( modifier: Modifier = Modifier, - data: GroupNoteVote, + data: PostList, + onLikeClick: (postId: Int, postType: String) -> Unit = { _, _ -> }, + onVote: (postId: Int, voteItemId: Int, type: Boolean) -> Unit = { _, _, _ -> }, onCommentClick: () -> Unit = {}, onLongPress: () -> Unit = {}, onPinClick: () -> Unit = {} ) { - var isLiked by remember { mutableStateOf(data.isLiked) } - var selected by remember { mutableStateOf(null) } - var voteItems by remember { mutableStateOf(data.voteItems) } - val hasVoted = voteItems.any { it.isVoted } + val selectedIndex = data.voteItems.indexOfFirst { it.isVoted }.takeIf { it != -1 } + val hasVoted = selectedIndex != null val isLocked = data.isLocked val isWriter = data.isWriter @@ -69,19 +64,19 @@ fun VoteCommentCard( ) GroupVoteButton( - voteItems = voteItems, - selectedIndex = selected, + voteItems = data.voteItems, + selectedIndex = selectedIndex, hasVoted = hasVoted, - onOptionSelected = { + onOptionSelected = { index -> if (!isLocked) { - if (selected == it) { - selected = null - voteItems = voteItems.map { it.copy(isVoted = false) } - } else { - selected = it - voteItems = voteItems.mapIndexed { index, item -> - item.copy(isVoted = index == it) + if (index == null) { + selectedIndex?.let { + val votedItemId = data.voteItems[it].voteItemId + onVote(data.postId, votedItemId, false) // type: false (취소) } + } else { + val votedItemId = data.voteItems[index].voteItemId + onVote(data.postId, votedItemId, true) // type: true (투표) } } } @@ -89,12 +84,12 @@ fun VoteCommentCard( } ActionBarButton( - isLiked = isLiked, + isLiked = data.isLiked, likeCount = data.likeCount, commentCount = data.commentCount, isPinVisible = isWriter, onLikeClick = { - if (!isLocked) isLiked = !isLiked + if (!isLocked) onLikeClick(data.postId, data.postType) }, onCommentClick = { if (!isLocked) onCommentClick() @@ -110,23 +105,21 @@ fun VoteCommentCard( @Composable private fun VoteCommentCardPreview() { VoteCommentCard( - data = GroupNoteVote( + data = PostList( + postId = 1, + postType = "group", + page = 132, postDate = "12시간 전", - page = 12, userId = 1, nickName = "user.01", profileImageUrl = "https://example.com/profile.jpg", - content = "3연에 나오는 심장은 무엇을 의미하는 걸까요?", + content = "내 생각에 이 부분이 가장 어려운 것 같다. 비유도 난해하고 잘 이해가 가지 않는데 다른 메이트들은 어떻게 읽었나요?", likeCount = 123, commentCount = 123, isLiked = true, isWriter = false, isLocked = false, - voteId = 1, - voteItems = listOf( - VoteItem(1, "김땡땡", 90, false), - VoteItem(2, "김땡땡", 10, false), - ) + voteItems = emptyList() ) ) } \ No newline at end of file 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 083c01c5..bf69ed75 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 @@ -6,12 +6,15 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.positionInRoot @@ -20,39 +23,69 @@ 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 androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.texthip.thip.R import com.texthip.thip.ui.common.modal.ArrowPosition import com.texthip.thip.ui.common.modal.PopupModal import com.texthip.thip.ui.common.topappbar.InputTopAppBar import com.texthip.thip.ui.group.note.component.OpinionInputSection import com.texthip.thip.ui.group.note.component.PageInputSection +import com.texthip.thip.ui.group.note.viewmodel.GroupNoteCreateEvent +import com.texthip.thip.ui.group.note.viewmodel.GroupNoteCreateUiState +import com.texthip.thip.ui.group.note.viewmodel.GroupNoteCreateViewModel import com.texthip.thip.ui.theme.ThipTheme @Composable -fun GroupNoteCreateScreen() { - var pageText by rememberSaveable { mutableStateOf("") } - var isGeneralReview by rememberSaveable { mutableStateOf(false) } - var opinionText by rememberSaveable { mutableStateOf("") } +fun GroupNoteCreateScreen( + roomId: Int, + recentPage: Int, + totalPage: Int, + isOverviewPossible: Boolean, + onBackClick: () -> Unit, + onNavigateBackWithResult: () -> Unit, + viewModel: GroupNoteCreateViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val isFormFilled = pageText.isNotBlank() && opinionText.isNotBlank() + LaunchedEffect(key1 = Unit) { + viewModel.initialize(roomId, recentPage, totalPage, isOverviewPossible) + } + + LaunchedEffect(key1 = uiState.isSuccess) { + if (uiState.isSuccess) { + onNavigateBackWithResult() + } + } + GroupNoteCreateContent( + uiState = uiState, + onEvent = viewModel::onEvent, + onBackClick = onBackClick + ) +} + +@Composable +fun GroupNoteCreateContent( + uiState: GroupNoteCreateUiState, + onEvent: (GroupNoteCreateEvent) -> Unit, + onBackClick: () -> Unit +) { val density = LocalDensity.current var showTooltip by rememberSaveable { mutableStateOf(false) } // Tooltip 위치 측정용 state val iconCoordinates = remember { mutableStateOf(null) } - var isEligible by rememberSaveable { mutableStateOf(true) } // TODO: 서버 데이터? - Box( modifier = Modifier.fillMaxSize() ) { Column { InputTopAppBar( title = stringResource(R.string.write_record), - isRightButtonEnabled = isFormFilled, - onLeftClick = { /* 뒤로가기 동작 */ }, - onRightClick = { /* 완료 동작 */ } + isRightButtonEnabled = uiState.isFormFilled, + onLeftClick = onBackClick, + onRightClick = { onEvent(GroupNoteCreateEvent.CreateRecordClicked) } ) Column( @@ -61,19 +94,19 @@ fun GroupNoteCreateScreen() { verticalArrangement = Arrangement.spacedBy(32.dp), ) { PageInputSection( - pageText = pageText, - onPageTextChange = { pageText = it }, - isGeneralReview = isGeneralReview, - onGeneralReviewToggle = { isGeneralReview = it }, - isEligible = isEligible, - bookTotalPage = 600, + pageText = uiState.pageText, + onPageTextChange = { onEvent(GroupNoteCreateEvent.PageChanged(it)) }, + isGeneralReview = uiState.isGeneralReview, + onGeneralReviewToggle = { onEvent(GroupNoteCreateEvent.GeneralReviewToggled(it)) }, + isEligible = uiState.isOverviewPossible, + bookTotalPage = uiState.totalPage, onInfoClick = { showTooltip = true }, onInfoPositionCaptured = { iconCoordinates.value = it } ) OpinionInputSection( - text = opinionText, - onTextChange = { opinionText = it } + text = uiState.opinionText, + onTextChange = { onEvent(GroupNoteCreateEvent.OpinionChanged(it)) } ) } @@ -92,19 +125,31 @@ fun GroupNoteCreateScreen() { PopupModal( text = stringResource(R.string.condition_of_general_review), arrowPosition = ArrowPosition.RIGHT, - isEligible = isEligible, + isEligible = uiState.isOverviewPossible, onClose = { showTooltip = false } ) } } - } + if (uiState.isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } } @Preview @Composable private fun GroupNoteCreateScreenPreview() { ThipTheme { - GroupNoteCreateScreen() + GroupNoteCreateContent( + uiState = GroupNoteCreateUiState(pageText = "123", opinionText = "재미있었다."), + onEvent = {}, + onBackClick = {} + ) } } \ No newline at end of file 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 3e24b774..709e058d 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 @@ -1,6 +1,8 @@ package com.texthip.thip.ui.group.note.screen import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically @@ -9,30 +11,39 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight 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.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.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.rememberCoroutineScope 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.draw.clip 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 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.ui.common.bottomsheet.MenuBottomSheet import com.texthip.thip.ui.common.buttons.ExpandableFloatingButton import com.texthip.thip.ui.common.buttons.FabMenuItem @@ -45,56 +56,145 @@ import com.texthip.thip.ui.group.note.component.CommentBottomSheet import com.texthip.thip.ui.group.note.component.FilterHeaderSection import com.texthip.thip.ui.group.note.component.TextCommentCard import com.texthip.thip.ui.group.note.component.VoteCommentCard -import com.texthip.thip.ui.group.note.mock.GroupNoteRecord -import com.texthip.thip.ui.group.note.mock.GroupNoteVote import com.texthip.thip.ui.group.note.mock.mockComment -import com.texthip.thip.ui.group.note.mock.mockGroupNoteItems +import com.texthip.thip.ui.group.note.viewmodel.GroupNoteEvent +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 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.type.SortType +import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @Composable -fun GroupNoteScreen() { - val tabs = listOf(stringResource(R.string.group_record), stringResource(R.string.my_record)) - var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } +fun GroupNoteScreen( + roomId: Int, + onBackClick: () -> Unit = {}, + onCreateNoteClick: (recentPage: Int, totalPage: Int, isOverviewPossible: Boolean) -> Unit, + onCreateVoteClick: (recentPage: Int, totalPage: Int, isOverviewPossible: Boolean) -> Unit, + resultTabIndex: Int? = null, + onResultConsumed: () -> Unit = {}, + initialPage: Int? = null, + initialIsOverview: Boolean? = null, + viewModel: GroupNoteViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val scope = rememberCoroutineScope() - var firstPage by rememberSaveable { mutableStateOf("") } - var lastPage by rememberSaveable { mutableStateOf("") } - var isTotalSelected by rememberSaveable { mutableStateOf(false) } - var totalEnabled by rememberSaveable { mutableStateOf(false) } + var showProgressBar by remember { mutableStateOf(false) } + val progress = remember { Animatable(0f) } + var progressJob by remember { mutableStateOf(null) } - var selectedFilter by rememberSaveable { mutableStateOf("최신순") } - val filters = listOf("최신순", "인기순", "댓글 많은 순") + LaunchedEffect(resultTabIndex) { + if (resultTabIndex != null) { + viewModel.onEvent(GroupNoteEvent.OnTabSelected(resultTabIndex)) + onResultConsumed() - val filteredItems = when (selectedTabIndex) { - 0 -> mockGroupNoteItems // 전체 기록 - 1 -> mockGroupNoteItems.filter { it.isWriter } // 내 기록만 - else -> emptyList() + showProgressBar = true + progress.snapTo(0f) + progressJob = scope.launch { + progress.animateTo( + targetValue = 1f, + animationSpec = tween(durationMillis = 3000, easing = LinearEasing) + ) + delay(500) + if (showProgressBar) { + showProgressBar = false + } + } + } } - var isCommentBottomSheetVisible by rememberSaveable { mutableStateOf(false) } - var selectedNoteRecord by remember { mutableStateOf(null) } - var selectedNoteVote by remember { mutableStateOf(null) } - var selectedItemForMenu by remember { mutableStateOf(null) } + LaunchedEffect(uiState.isLoading) { + // 로딩이 끝났고, 프로그레스 바가 보이는 중이라면 + if (!uiState.isLoading && showProgressBar) { + progressJob?.cancel() // 진행 중인 3초 애니메이션 취소 + progress.snapTo(1f) // 즉시 100%로 변경 + delay(500) // 100% 상태를 잠시 보여줌 + showProgressBar = false // 프로그레스 바 숨기기 + } + } - var isMenuBottomSheetVisible by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(key1 = roomId) { + // 기록 생성 후 돌아온 경우가 아닐 때 (처음 진입 시) 초기화 + if (resultTabIndex == null) { + viewModel.initialize(roomId, initialPage, initialIsOverview) + } + } - var isPinDialogVisible by remember { mutableStateOf(false) } + GroupNoteContent( + uiState = uiState, + onEvent = viewModel::onEvent, + onBackClick = onBackClick, + onCreateNoteClick = { + uiState.let { s -> + onCreateNoteClick(s.recentBookPage, s.totalBookPage, s.isOverviewPossible) + } + }, + onCreateVoteClick = { + uiState.let { s -> + onCreateVoteClick(s.recentBookPage, s.totalBookPage, s.isOverviewPossible) + } + }, + showProgressBar = showProgressBar, + progress = progress.value + ) +} +@Composable +fun GroupNoteContent( + uiState: GroupNoteUiState, + onEvent: (GroupNoteEvent) -> Unit, + onBackClick: () -> Unit, + onCreateNoteClick: () -> Unit, + onCreateVoteClick: () -> Unit, + showProgressBar: Boolean, + progress: Float +) { + var isCommentBottomSheetVisible by remember { mutableStateOf(false) } + var selectedPostForComment by remember { mutableStateOf(null) } + var selectedPostForMenu by remember { mutableStateOf(null) } + var showDeleteDialog by remember { mutableStateOf(false) } + var isPinDialogVisible by remember { mutableStateOf(false) } var showToast by remember { mutableStateOf(false) } - // 토스트 3초 LaunchedEffect(showToast) { if (showToast) { - delay(6000) // 2초 등장, 4초 노출 - showToast = false // exit 에니메이션 2초 + delay(3000) + showToast = false + } + } + + 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) } + + val listState = rememberLazyListState() + + LaunchedEffect(uiState.selectedTabIndex) { + listState.scrollToItem(0) + } + + val isScrolledToEnd by remember { + derivedStateOf { + val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull() + // 마지막 아이템이 보이고, 전체 아이템 수와 일치하며, 마지막 페이지가 아닐 때 + lastVisibleItem != null && lastVisibleItem.index == listState.layoutInfo.totalItemsCount - 1 && !uiState.isLastPage + } + } + + LaunchedEffect(isScrolledToEnd) { + if (isScrolledToEnd) { + onEvent(GroupNoteEvent.LoadMorePosts) } } Box( - if (isCommentBottomSheetVisible || isMenuBottomSheetVisible || isPinDialogVisible) { + if (isCommentBottomSheetVisible || selectedPostForMenu != null || isPinDialogVisible) { Modifier .fillMaxSize() .blur(5.dp) @@ -126,19 +226,38 @@ fun GroupNoteScreen() { Column(modifier = Modifier.fillMaxSize()) { DefaultTopAppBar( title = stringResource(R.string.record_book), - onLeftClick = {} + onLeftClick = onBackClick ) HeaderMenuBarTab( titles = tabs, - selectedTabIndex = selectedTabIndex, - onTabSelected = { selectedTabIndex = it }, + selectedTabIndex = uiState.selectedTabIndex, + onTabSelected = { onEvent(GroupNoteEvent.OnTabSelected(it)) }, modifier = Modifier .fillMaxWidth() .padding(top = 20.dp) ) - if (filteredItems.isEmpty()) { + if (uiState.isLoading) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } else if (uiState.posts.isEmpty()) { + val noRecordTextTitle = if (uiState.isOverview) { + stringResource(R.string.no_overviews_yet) + } else { + stringResource(R.string.no_records_yet) + } + val noRecordTextContent = when (uiState.selectedTabIndex) { + 0 -> if (uiState.isOverview) { + stringResource(R.string.no_overview_subtext) + } else { + stringResource(R.string.no_group_record_subtext) + } + + 1 -> stringResource(R.string.no_my_record_subtext) + else -> "" + } // 기록이 없을 때 중앙에 메시지 Column( modifier = Modifier @@ -151,27 +270,27 @@ fun GroupNoteScreen() { ) ) { Text( - text = stringResource(R.string.no_records_yet), + text = noRecordTextTitle, style = typography.smalltitle_sb600_s18_h24, color = colors.White ) Text( - text = when (selectedTabIndex) { - 0 -> stringResource(R.string.no_group_record_subtext) - 1 -> stringResource(R.string.no_my_record_subtext) - else -> "" - }, + text = noRecordTextContent, style = typography.copy_r400_s14, color = colors.Grey ) } } else { // 피드 리스트 영역 - LazyColumn(modifier = Modifier.fillMaxSize()) { - if (selectedTabIndex == 0) { + LazyColumn(state = listState, modifier = Modifier.weight(1f)) { + if (uiState.selectedTabIndex == 0) { item { Row( - modifier = Modifier.padding(top = 76.dp, start = 20.dp, end = 20.dp), + modifier = Modifier.padding( + top = 76.dp, + start = 20.dp, + end = 20.dp + ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), ) { @@ -189,79 +308,136 @@ fun GroupNoteScreen() { } } } - itemsIndexed(filteredItems) { index, item -> - val isLast = index == filteredItems.lastIndex + 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 < 1.0f) { + stringResource(R.string.posting_in_progress) + } else { + stringResource(R.string.posting_complete) + }, + style = typography.view_m500_s14, + color = colors.NeonGreen + ) - val itemModifier = if (isLast) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .clip(RoundedCornerShape(12.dp)) + .background(color = colors.Grey02) // 트랙(배경) 색상 + ) { + Box( + modifier = Modifier + .fillMaxWidth(fraction = progress) + .fillMaxHeight() + .background( + color = colors.NeonGreen, + shape = RoundedCornerShape(12.dp) + ) + ) + } + } + } + } + itemsIndexed( + uiState.posts, + key = { _, post -> post.postId }) { index, post -> + val itemModifier = if (index == uiState.posts.lastIndex) { Modifier.padding(bottom = 20.dp) } else { Modifier } - when (item) { - is GroupNoteRecord -> TextCommentCard( - data = item, + when (post.postType) { + "RECORD" -> TextCommentCard( + data = post, modifier = itemModifier, - onCommentClick = { - selectedNoteRecord = item - isCommentBottomSheetVisible = true - }, - onLongPress = { - selectedItemForMenu = item - isMenuBottomSheetVisible = true - }, - onPinClick = { - isPinDialogVisible = true + onCommentClick = { isCommentBottomSheetVisible = true }, + onLongPress = { selectedPostForMenu = post }, + onPinClick = { isPinDialogVisible = true }, + onLikeClick = { postId, postType -> + onEvent(GroupNoteEvent.OnLikeRecord(postId, postType)) } ) - is GroupNoteVote -> VoteCommentCard( - data = item, + "VOTE" -> VoteCommentCard( + data = post, modifier = itemModifier, - onCommentClick = { - selectedNoteVote = item - isCommentBottomSheetVisible = true + onCommentClick = { isCommentBottomSheetVisible = true }, + onLongPress = { selectedPostForMenu = post }, + onPinClick = { isPinDialogVisible = true }, + onVote = { postId, voteItemId, type -> + onEvent(GroupNoteEvent.OnVote(postId, voteItemId, type)) }, - onLongPress = { - selectedItemForMenu = item - isMenuBottomSheetVisible = true - }, - onPinClick = { - isPinDialogVisible = true + onLikeClick = { postId, postType -> + onEvent(GroupNoteEvent.OnLikeRecord(postId, postType)) } ) } } + + if (uiState.isLoadingMore) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } } } } - if (selectedTabIndex == 0) { + if (uiState.selectedTabIndex == 0) { Box( modifier = Modifier .fillMaxWidth() - .padding(top = 119.dp) - .background(color = colors.Black) - .padding(top = 20.dp) + .padding(top = 118.dp) ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .background(color = colors.Black) + ) + FilterButton( modifier = Modifier .align(Alignment.CenterEnd) - .padding(end = 20.dp), - selectedOption = selectedFilter, - options = filters, - onOptionSelected = { selectedFilter = it } + .padding(top = 20.dp, end = 20.dp), + selectedOption = stringResource(uiState.selectedSort.displayNameRes), + options = sortDisplayStrings, + onOptionSelected = { selectedString -> + val selectedIndex = sortDisplayStrings.indexOf(selectedString) + if (selectedIndex != -1) { + val selectedSortType = SortType.entries[selectedIndex] + onEvent(GroupNoteEvent.OnSortSelected(selectedSortType)) + } + } ) FilterHeaderSection( - firstPage = firstPage, - lastPage = lastPage, - isTotalSelected = isTotalSelected, - totalEnabled = totalEnabled, - onFirstPageChange = { firstPage = it }, - onLastPageChange = { lastPage = it }, - onTotalToggle = { isTotalSelected = !isTotalSelected }, - onDisabledClick = { showToast = true } + modifier = Modifier.padding(top = 20.dp), + firstPage = uiState.pageStart, + lastPage = uiState.pageEnd, + isTotalSelected = uiState.isOverview, + totalEnabled = uiState.totalEnabled, + onFirstPageChange = { onEvent(GroupNoteEvent.OnPageStartChanged(it)) }, + onLastPageChange = { onEvent(GroupNoteEvent.OnPageEndChanged(it)) }, + onTotalToggle = { onEvent(GroupNoteEvent.OnOverviewToggled(!uiState.isOverview)) }, + onDisabledClick = { showToast = true }, + onApplyPageFilter = { onEvent(GroupNoteEvent.ApplyPageFilter) } ) } } @@ -271,26 +447,27 @@ fun GroupNoteScreen() { FabMenuItem( icon = painterResource(R.drawable.ic_write), text = stringResource(R.string.write_record), - onClick = { } + onClick = onCreateNoteClick ), FabMenuItem( icon = painterResource(R.drawable.ic_vote), text = stringResource(R.string.create_vote), - onClick = { } + onClick = onCreateVoteClick ) ) ) } } - if (isCommentBottomSheetVisible && (selectedNoteRecord != null || selectedNoteVote != null)) { + if (isCommentBottomSheetVisible && selectedPostForComment != null) { CommentBottomSheet( commentResponse = listOf(mockComment, mockComment, mockComment), // commentResponse = emptyList(), onDismiss = { isCommentBottomSheetVisible = false - selectedNoteRecord = null - selectedNoteVote = null +// selectedNoteRecord = null +// selectedNoteVote = null + selectedPostForComment = null }, onSendReply = { replyText, commentId, replyTo -> // 댓글 전송 로직 구현 @@ -298,12 +475,8 @@ fun GroupNoteScreen() { ) } - if (isMenuBottomSheetVisible && selectedItemForMenu != null) { - val isWriter = when (val item = selectedItemForMenu) { - is GroupNoteRecord -> item.isWriter - is GroupNoteVote -> item.isWriter - else -> false - } + if (selectedPostForMenu != null) { + val isWriter = selectedPostForMenu!!.isWriter val menuItems = if (isWriter) { listOf( @@ -311,9 +484,9 @@ fun GroupNoteScreen() { text = stringResource(R.string.delete), color = colors.Red, onClick = { - // TODO: 삭제 처리 - isMenuBottomSheetVisible = false - selectedItemForMenu = null + onEvent(GroupNoteEvent.OnDeleteRecord(selectedPostForMenu!!.postId)) + showDeleteDialog = false + selectedPostForMenu = null } ) ) @@ -324,8 +497,7 @@ fun GroupNoteScreen() { color = colors.Red, onClick = { // TODO: 신고 처리 - isMenuBottomSheetVisible = false - selectedItemForMenu = null + selectedPostForMenu = null } ) ) @@ -334,8 +506,7 @@ fun GroupNoteScreen() { MenuBottomSheet( items = menuItems, onDismiss = { - isMenuBottomSheetVisible = false - selectedItemForMenu = null + selectedPostForMenu = null } ) } @@ -365,6 +536,41 @@ fun GroupNoteScreen() { @Composable private fun GroupNoteScreenPreview() { ThipTheme { - GroupNoteScreen() + GroupNoteContent( + uiState = GroupNoteUiState( + posts = listOf( + PostList( + userId = 1, + profileImageUrl = "https://example.com/profile.jpg", + voteItems = emptyList(), + postId = 1, + postType = "RECORD", + page = 1, + postDate = "12시간 전", + nickName = "사용자1", + content = "첫 번째 기록입니다.", + isLiked = false, + likeCount = 10, + commentCount = 2, + isLocked = false, + isWriter = true + ) + ), + selectedTabIndex = 0, + selectedSort = SortType.LATEST, + isLoading = false, + isLoadingMore = false, + pageStart = "1", + pageEnd = "10", + isOverview = false, + totalEnabled = true + ), + onEvent = {}, + onBackClick = {}, + onCreateNoteClick = {}, + onCreateVoteClick = {}, + showProgressBar = true, + progress = 0.5f + ) } } \ No newline at end of file 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 f3b3953d..832bda37 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 @@ -6,13 +6,15 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.positionInRoot @@ -21,44 +23,65 @@ 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 androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.texthip.thip.R import com.texthip.thip.ui.common.modal.ArrowPosition import com.texthip.thip.ui.common.modal.PopupModal import com.texthip.thip.ui.common.topappbar.InputTopAppBar import com.texthip.thip.ui.group.note.component.PageInputSection import com.texthip.thip.ui.group.note.component.VoteInputSection +import com.texthip.thip.ui.group.note.viewmodel.GroupVoteCreateEvent +import com.texthip.thip.ui.group.note.viewmodel.GroupVoteCreateUiState +import com.texthip.thip.ui.group.note.viewmodel.GroupVoteCreateViewModel import com.texthip.thip.ui.theme.ThipTheme @Composable -fun GroupVoteCreateScreen() { - var pageText by rememberSaveable { mutableStateOf("") } - var isGeneralReview by rememberSaveable { mutableStateOf(false) } +fun GroupVoteCreateScreen( + roomId: Int, + recentPage: Int, + totalPage: Int, + isOverviewPossible: Boolean, + onBackClick: () -> Unit, + onNavigateBackWithResult: () -> Unit, + viewModel: GroupVoteCreateViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - var title by rememberSaveable { mutableStateOf("") } - val options = remember { mutableStateListOf("", "") } + LaunchedEffect(Unit) { + viewModel.initialize(roomId, recentPage, totalPage, isOverviewPossible) + } + + LaunchedEffect(uiState.isSuccess) { + if (uiState.isSuccess) { + onNavigateBackWithResult() + } + } + GroupVoteCreateContent( + uiState = uiState, + onEvent = viewModel::onEvent, + onBackClick = onBackClick + ) +} + +@Composable +fun GroupVoteCreateContent( + uiState: GroupVoteCreateUiState, + onEvent: (GroupVoteCreateEvent) -> Unit, + onBackClick: () -> Unit, +) { val density = LocalDensity.current var showTooltip by rememberSaveable { mutableStateOf(false) } - - // Tooltip 위치 측정용 state val iconCoordinates = remember { mutableStateOf(null) } - var isEligible by rememberSaveable { mutableStateOf(false) } // TODO: 서버 데이터? - - // 완료 버튼 활성화 조건 - val filledOptionsCount = options.count { it.isNotBlank() } - val isRightButtonEnabled = - (isGeneralReview || pageText.isNotBlank()) && - title.isNotBlank() && - filledOptionsCount >= 2 - Box(modifier = Modifier.fillMaxSize()) { Column { InputTopAppBar( title = stringResource(R.string.create_vote), - isRightButtonEnabled = isRightButtonEnabled, - onLeftClick = { /* 뒤로가기 동작 */ }, - onRightClick = { /* 완료 동작 */ } + isRightButtonEnabled = uiState.isFormFilled, + onLeftClick = onBackClick, + onRightClick = { onEvent(GroupVoteCreateEvent.CreateVoteClicked) } ) Column( @@ -67,35 +90,39 @@ fun GroupVoteCreateScreen() { verticalArrangement = Arrangement.spacedBy(32.dp), ) { PageInputSection( - pageText = pageText, - onPageTextChange = { pageText = it }, - isGeneralReview = isGeneralReview, - onGeneralReviewToggle = { isGeneralReview = it }, - isEligible = isEligible, - bookTotalPage = 600, + pageText = uiState.pageText, + onPageTextChange = { onEvent(GroupVoteCreateEvent.PageChanged(it)) }, + isGeneralReview = uiState.isGeneralReview, + onGeneralReviewToggle = { onEvent(GroupVoteCreateEvent.GeneralReviewToggled(it)) }, + isEligible = uiState.isGeneralReviewEnabled, + bookTotalPage = uiState.bookTotalPage, onInfoClick = { showTooltip = true }, onInfoPositionCaptured = { iconCoordinates.value = it } ) VoteInputSection( - title = title, - onTitleChange = { title = it }, - options = options, + title = uiState.title, + onTitleChange = { onEvent(GroupVoteCreateEvent.TitleChanged(it)) }, + options = uiState.options, onOptionChange = { index, newText -> - options[index] = newText - }, - onAddOption = { - if (options.size < 5) { - options.add("") - } + onEvent(GroupVoteCreateEvent.OptionChanged(index, newText)) }, + onAddOption = { onEvent(GroupVoteCreateEvent.AddOptionClicked) }, onRemoveOption = { index -> - options.removeAt(index) + onEvent(GroupVoteCreateEvent.RemoveOptionClicked(index)) } ) } } + if (uiState.isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } if (showTooltip && iconCoordinates.value != null) { val yOffsetDp = with(density) { @@ -111,7 +138,7 @@ fun GroupVoteCreateScreen() { PopupModal( text = stringResource(R.string.condition_of_general_review), arrowPosition = ArrowPosition.RIGHT, - isEligible = isEligible, + isEligible = uiState.isGeneralReviewEnabled, onClose = { showTooltip = false } ) } @@ -123,6 +150,16 @@ fun GroupVoteCreateScreen() { @Composable private fun GroupVoteCreateScreenPreview() { ThipTheme { - GroupVoteCreateScreen() + GroupVoteCreateContent( + uiState = GroupVoteCreateUiState( + pageText = "123", + title = "가장 인상깊은 구절은?", + options = listOf("1연 1행", "2연 3행", ""), + bookTotalPage = 600, + isGeneralReviewEnabled = true + ), + onEvent = {}, + onBackClick = {} + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteCreateViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteCreateViewModel.kt new file mode 100644 index 00000000..1788a565 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteCreateViewModel.kt @@ -0,0 +1,114 @@ +package com.texthip.thip.ui.group.note.viewmodel + +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.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class GroupNoteCreateUiState( + val pageText: String = "", + val opinionText: String = "", + val isGeneralReview: Boolean = false, + val isLoading: Boolean = false, + val isSuccess: Boolean = false, + val error: String? = null, + val totalPage: Int = 0, + val isOverviewPossible: Boolean = false +) { + // 입력 폼이 모두 채워졌는지 확인 + val isFormFilled: Boolean + get() = (pageText.isNotBlank() || isGeneralReview) && opinionText.isNotBlank() +} + +sealed interface GroupNoteCreateEvent { + data class PageChanged(val text: String) : GroupNoteCreateEvent + data class OpinionChanged(val text: String) : GroupNoteCreateEvent + data class GeneralReviewToggled(val isChecked: Boolean) : GroupNoteCreateEvent + data object CreateRecordClicked : GroupNoteCreateEvent +} + +@HiltViewModel +class GroupNoteCreateViewModel @Inject constructor( + private val roomsRepository: RoomsRepository +) : ViewModel() { + private val _uiState = MutableStateFlow(GroupNoteCreateUiState()) + val uiState = _uiState.asStateFlow() + + private var roomId: Int = -1 + + fun initialize( + roomId: Int, + recentPage: Int, + totalPage: Int, + isOverviewPossible: Boolean + ) { + this.roomId = roomId + _uiState.update { + it.copy( + pageText = recentPage.toString(), + totalPage = totalPage, + isOverviewPossible = isOverviewPossible + ) + } + } + + fun onEvent(event: GroupNoteCreateEvent) { + when (event) { + is GroupNoteCreateEvent.PageChanged -> { + if (!_uiState.value.isGeneralReview) { + _uiState.update { it.copy(pageText = event.text) } + } + } + + is GroupNoteCreateEvent.OpinionChanged -> { + _uiState.update { it.copy(opinionText = event.text) } + } + + is GroupNoteCreateEvent.GeneralReviewToggled -> { + _uiState.update { + val newPageText = if (event.isChecked) "" else it.pageText + it.copy(isGeneralReview = event.isChecked, pageText = newPageText) + } + } + + GroupNoteCreateEvent.CreateRecordClicked -> { + createRecord() + } + } + } + + private fun createRecord() { + val currentState = _uiState.value + val pageNumber = if (currentState.isGeneralReview) { + currentState.totalPage + } else { + currentState.pageText.toIntOrNull() + } + if (pageNumber == null || !currentState.isFormFilled) { + _uiState.update { it.copy(error = "페이지 번호를 정확히 입력해주세요.") } + return + } + + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + + roomsRepository.postRoomsRecord( + roomId = roomId, + content = currentState.opinionText, + isOverview = currentState.isGeneralReview, + page = pageNumber + ).onSuccess { + _uiState.update { it.copy(isLoading = false, isSuccess = true) } + }.onFailure { throwable -> + _uiState.update { + it.copy(isLoading = false, error = throwable.message) + } + } + } + } +} \ 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 new file mode 100644 index 00000000..e79db56a --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt @@ -0,0 +1,274 @@ +package com.texthip.thip.ui.group.note.viewmodel + +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.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.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class GroupNoteUiState( + // 데이터 로딩 상태 + val isLoading: Boolean = false, + val isLoadingMore: Boolean = false, + val error: String? = null, + val isLastPage: Boolean = false, + + // 화면 데이터 + val posts: List = emptyList(), + val recentBookPage: Int = 0, + val totalBookPage: Int = 0, + val isOverviewPossible: Boolean = false, + + // 필터 및 탭 상태 + val selectedTabIndex: Int = 0, + val selectedSort: SortType = SortType.LATEST, + val pageStart: String = "", + val pageEnd: String = "", + val isOverview: Boolean = false, + val isPageFilter: Boolean = false, + val totalEnabled: Boolean = false +) + +sealed interface GroupNoteEvent { + data class OnTabSelected(val index: Int) : GroupNoteEvent + data class OnSortSelected(val sortType: SortType) : GroupNoteEvent + data class OnPageStartChanged(val page: String) : GroupNoteEvent + data class OnPageEndChanged(val page: String) : GroupNoteEvent + data class OnOverviewToggled(val isSelected: Boolean) : 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 OnLikeRecord(val postId: Int, val postType: String) : GroupNoteEvent +} + + +@HiltViewModel +class GroupNoteViewModel @Inject constructor( + private val roomsRepository: RoomsRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(GroupNoteUiState()) + val uiState = _uiState.asStateFlow() + + private var nextCursor: String? = null + private var roomId: Int = -1 + + fun initialize( + roomId: Int, + initialPage: Int? = null, + initialIsOverview: Boolean? = null + ) { + this.roomId = roomId + + if (initialPage != null || initialIsOverview != null) { + _uiState.update { + it.copy( + pageStart = initialPage?.toString() ?: "", + pageEnd = initialPage?.toString() ?: "", + isOverview = initialIsOverview ?: false, + isPageFilter = initialPage != null + ) + } + } + + viewModelScope.launch { + val postsJob = async { loadPosts(isRefresh = true) } + val bookPageJob = async { loadBookPageInfo() } + awaitAll(postsJob, bookPageJob) + } + } + + private fun loadBookPageInfo() { + viewModelScope.launch { + roomsRepository.getRoomsBookPage(roomId) + .onSuccess { response -> + if (response != null) { + _uiState.update { + it.copy( + recentBookPage = response.recentBookPage, + totalBookPage = response.totalBookPage, + isOverviewPossible = response.isOverviewPossible + ) + } + } + } + .onFailure { throwable -> + _uiState.update { it.copy(error = throwable.message) } + } + } + } + + fun onEvent(event: GroupNoteEvent) { + when (event) { + is GroupNoteEvent.OnTabSelected -> { + _uiState.update { it.copy(selectedTabIndex = event.index) } + loadPosts(isRefresh = true) + } + + is GroupNoteEvent.OnSortSelected -> { + _uiState.update { it.copy(selectedSort = event.sortType) } + loadPosts(isRefresh = true) + } + + is GroupNoteEvent.OnPageStartChanged -> _uiState.update { it.copy(pageStart = event.page) } + is GroupNoteEvent.OnPageEndChanged -> _uiState.update { it.copy(pageEnd = event.page) } + is GroupNoteEvent.OnOverviewToggled -> { + _uiState.update { it.copy(isOverview = event.isSelected) } + loadPosts(isRefresh = true) + } + + GroupNoteEvent.ApplyPageFilter -> { + val currentState = _uiState.value + val isFilterActive = + currentState.pageStart.isNotBlank() || currentState.pageEnd.isNotBlank() + + _uiState.update { it.copy(isPageFilter = isFilterActive) } + loadPosts(isRefresh = true) + } + + GroupNoteEvent.LoadMorePosts -> loadPosts(isRefresh = false) + + is GroupNoteEvent.OnVote -> vote( + postId = event.postId, + voteItemId = event.voteItemId, + type = event.type + ) + + is GroupNoteEvent.OnDeleteRecord -> deleteRecord(event.postId) + is GroupNoteEvent.OnLikeRecord -> likeRecord(event.postId, event.postType) + } + } + + private fun likeRecord(postId: Int, postType: String) { + val currentPosts = _uiState.value.posts + val postIndex = currentPosts.indexOfFirst { it.postId == postId } + if (postIndex == -1) return + + val oldPost = currentPosts[postIndex] + + val newIsLiked = !oldPost.isLiked + val newLikeCount = if (newIsLiked) oldPost.likeCount + 1 else oldPost.likeCount - 1 + + val optimisticPost = oldPost.copy( + isLiked = newIsLiked, + likeCount = newLikeCount.coerceAtLeast(0) + ) + + val newPosts = currentPosts.toMutableList().apply { this[postIndex] = optimisticPost } + _uiState.update { it.copy(posts = newPosts) } + + viewModelScope.launch { + roomsRepository.postRoomsPostsLikes( + postId = postId, + type = newIsLiked, + roomPostType = postType + ) + .onFailure { + val rollbackPosts = currentPosts.toMutableList().apply { this[postIndex] = oldPost } + _uiState.update { it.copy(posts = rollbackPosts) } + } + } + } + + private fun deleteRecord(postId: Int) { + 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) } + } + } + } + + private fun vote(postId: Int, voteItemId: Int, type: Boolean) { + 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) } + } + } + } + + private fun loadPosts(isRefresh: Boolean = false) { + val currentState = _uiState.value + if (currentState.isLoading || currentState.isLoadingMore || (currentState.isLastPage && !isRefresh)) return + + viewModelScope.launch { + _uiState.update { + if (isRefresh) it.copy( + isLoading = true, + posts = emptyList(), + error = null, + isLastPage = false + ) + else it.copy(isLoadingMore = true, error = null) + } + + val cursor = if (isRefresh) null else nextCursor + val type = if (currentState.selectedTabIndex == 0) "group" else "mine" + + val params = if (type == "mine") { + // "mine" 탭일 경우 필수 파라미터만 채워 넣음 + RoomsPostsRequestParams(type = type, cursor = cursor) + } else { + // "group" 탭일 경우 모든 필터 파라미터 포함 + RoomsPostsRequestParams( + type = type, + sort = currentState.selectedSort.apiKey, + pageStart = currentState.pageStart.toIntOrNull(), + pageEnd = currentState.pageEnd.toIntOrNull(), + isOverview = currentState.isOverview, + isPageFilter = currentState.isPageFilter, + cursor = cursor + ) + } + + roomsRepository.getRoomsPosts( + roomId = roomId, + type = params.type, + sort = params.sort, + pageStart = params.pageStart, + pageEnd = params.pageEnd, + isOverview = params.isOverview, + isPageFilter = params.isPageFilter, + cursor = params.cursor + ).onSuccess { response -> + if (response != null) { + nextCursor = response.nextCursor + _uiState.update { + it.copy( + isLoading = false, + isLoadingMore = false, + posts = if (isRefresh) response.postList else it.posts + response.postList, + isLastPage = response.isLast, + totalEnabled = response.isOverviewEnabled + ) + } + } + }.onFailure { throwable -> + _uiState.update { + it.copy(isLoading = false, isLoadingMore = false, error = throwable.message) + } + } + } + } +} diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupVoteCreateViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupVoteCreateViewModel.kt new file mode 100644 index 00000000..cf758a4f --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupVoteCreateViewModel.kt @@ -0,0 +1,149 @@ +package com.texthip.thip.ui.group.note.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.rooms.request.VoteItem +import com.texthip.thip.data.repository.RoomsRepository +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 GroupVoteCreateUiState( + // 입력 값 + val pageText: String = "", + val title: String = "", + val options: List = listOf("", ""), // 옵션은 최소 2개로 시작 + val isGeneralReview: Boolean = false, + + // 상태 값 + val isLoading: Boolean = false, + val isSuccess: Boolean = false, + val error: String? = null, + + // 초기 설정 값 + val bookTotalPage: Int = 0, + val isGeneralReviewEnabled: Boolean = false, + + // 내부 관리용 + internal val savedPageText: String = "" +) { + // 완료 버튼 활성화 조건 + val isFormFilled: Boolean + get() { + val filledOptionsCount = options.count { it.isNotBlank() } + return (isGeneralReview || pageText.isNotBlank()) && + title.isNotBlank() && + filledOptionsCount >= 2 + } +} + +sealed interface GroupVoteCreateEvent { + data class PageChanged(val text: String) : GroupVoteCreateEvent + data class TitleChanged(val text: String) : GroupVoteCreateEvent + data class OptionChanged(val index: Int, val text: String) : GroupVoteCreateEvent + data class GeneralReviewToggled(val isChecked: Boolean) : GroupVoteCreateEvent + data object AddOptionClicked : GroupVoteCreateEvent + data class RemoveOptionClicked(val index: Int) : GroupVoteCreateEvent + data object CreateVoteClicked : GroupVoteCreateEvent +} + +@HiltViewModel +class GroupVoteCreateViewModel @Inject constructor( + private val roomsRepository: RoomsRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(GroupVoteCreateUiState()) + val uiState = _uiState.asStateFlow() + + private var roomId: Int = -1 + + fun initialize( + roomId: Int, + recentPage: Int, + totalPage: Int, + isOverviewPossible: Boolean + ) { + this.roomId = roomId + _uiState.update { + it.copy( + pageText = recentPage.toString(), + bookTotalPage = totalPage, + isGeneralReviewEnabled = isOverviewPossible + ) + } + } + + fun onEvent(event: GroupVoteCreateEvent) { + when (event) { + is GroupVoteCreateEvent.PageChanged -> if (!_uiState.value.isGeneralReview) { + _uiState.update { it.copy(pageText = event.text) } + } + + is GroupVoteCreateEvent.TitleChanged -> _uiState.update { it.copy(title = event.text) } + is GroupVoteCreateEvent.OptionChanged -> _uiState.update { + val newOptions = it.options.toMutableList() + newOptions[event.index] = event.text + it.copy(options = newOptions) + } + + is GroupVoteCreateEvent.GeneralReviewToggled -> _uiState.update { + if (event.isChecked) { + it.copy(isGeneralReview = true, savedPageText = it.pageText, pageText = "") + } else { + it.copy(isGeneralReview = false, pageText = it.savedPageText) + } + } + + GroupVoteCreateEvent.AddOptionClicked -> if (_uiState.value.options.size < 5) { + _uiState.update { it.copy(options = it.options + "") } + } + + is GroupVoteCreateEvent.RemoveOptionClicked -> if (_uiState.value.options.size > 2) { + _uiState.update { + val newOptions = it.options.toMutableList() + newOptions.removeAt(event.index) + it.copy(options = newOptions) + } + } + + GroupVoteCreateEvent.CreateVoteClicked -> createVote() + } + } + + private fun createVote() { + val currentState = _uiState.value + if (!currentState.isFormFilled) return + val pageNumber = if (currentState.isGeneralReview) { + currentState.bookTotalPage + } else { + currentState.pageText.toIntOrNull() + } + if (pageNumber == null) { + _uiState.update { it.copy(error = "페이지 번호를 정확히 입력해주세요.") } + return + } + + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + + val voteItems = currentState.options + .filter { it.isNotBlank() } + .map { VoteItem(itemName = it) } + + roomsRepository.postRoomsCreateVote( + roomId = roomId, + page = pageNumber, + isOverview = currentState.isGeneralReview, + content = currentState.title, + voteItemList = voteItems + ).onSuccess { + _uiState.update { it.copy(isLoading = false, isSuccess = true) } + }.onFailure { throwable -> + _uiState.update { it.copy(isLoading = false, error = throwable.message) } + } + } + } +} \ 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 ecfbf519..1e25e71d 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 @@ -23,7 +23,9 @@ fun GroupRoomBody( authorName: String, currentPage: Int, userPercentage: Double, - currentVotes: List + currentVotes: List, + onNavigateToNote: () -> Unit = {}, + onVoteClick: (CurrentVote) -> Unit = {} ) { Column( modifier = modifier.padding(horizontal = 20.dp), @@ -37,7 +39,9 @@ fun GroupRoomBody( CardNote( currentPage = currentPage, percentage = userPercentage - ) {} + ) { + onNavigateToNote() + } CardChat( title = stringResource(R.string.group_room_chat), @@ -45,7 +49,8 @@ fun GroupRoomBody( ) {} CardVote( - voteData = currentVotes + voteData = currentVotes, + onVoteClick = onVoteClick ) } } @@ -58,7 +63,7 @@ private fun GroupRoomBodyPreview() { authorName = "저자 이름", currentPage = 100, userPercentage = 50.0, - currentVotes = listOf( + currentVotes = listOf( CurrentVote( content = "3연에 나오는 심장은 무엇을 의미하는 걸까요?", page = 12, diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomMatesList.kt b/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomMatesList.kt index 0188acd7..af83361a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomMatesList.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/component/GroupRoomMatesList.kt @@ -30,7 +30,7 @@ fun GroupRoomMatesList( ProfileBar( profileImage = member.imageUrl, topText = member.nickname, - bottomText = member.alias, + bottomText = member.aliasName, // bottomTextColor = member.aliasColor, bottomTextColor = colors.ScienceIt, // TODO: 서버에서 보내주는 색상으로 수정 showSubscriberInfo = true, @@ -57,14 +57,14 @@ private fun GroupRoomMatesListPreview() { UserList( userId = 1, nickname = "김희용", - alias = "문학가", + aliasName = "문학가", imageUrl = "https://example.com/image1.jpg", followerCount = 100 ), UserList( userId = 2, nickname = "노성준", - alias = "문학가", + aliasName = "문학가", imageUrl = "https://example.com/image1.jpg", followerCount = 100 ), diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomMatesScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomMatesScreen.kt index a511d0a7..ce2dfa52 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomMatesScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomMatesScreen.kt @@ -103,14 +103,14 @@ private fun GroupRoomMatesScreenPreview() { UserList( userId = 1, nickname = "김희용", - alias = "문학가", + aliasName = "문학가", imageUrl = "https://example.com/image1.jpg", followerCount = 100 ), UserList( userId = 2, nickname = "노성준", - alias = "문학가", + aliasName = "문학가", imageUrl = "https://example.com/image1.jpg", followerCount = 100 ), 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 9a40056b..9ac9b3b8 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 @@ -1,6 +1,5 @@ package com.texthip.thip.ui.group.room.screen -import com.texthip.thip.data.model.rooms.response.RoomsPlayingResponse import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,6 +32,8 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.texthip.thip.R +import com.texthip.thip.data.model.rooms.response.CurrentVote +import com.texthip.thip.data.model.rooms.response.RoomsPlayingResponse import com.texthip.thip.ui.common.bottomsheet.MenuBottomSheet import com.texthip.thip.ui.common.modal.DialogPopup import com.texthip.thip.ui.common.topappbar.GradationTopAppBar @@ -50,6 +51,7 @@ fun GroupRoomScreen( roomId: Int, onBackClick: () -> Unit = {}, onNavigateToMates: () -> Unit = {}, + onNavigateToNote: (page: Int?, isOverview: Boolean?) -> Unit = { _, _ -> }, viewModel: GroupRoomViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -72,7 +74,8 @@ fun GroupRoomScreen( GroupRoomContent( roomDetails = state.roomsPlaying, onBackClick = onBackClick, - onNavigateToMates = onNavigateToMates + onNavigateToMates = onNavigateToMates, + onNavigateToNote = onNavigateToNote ) } @@ -89,7 +92,8 @@ fun GroupRoomScreen( fun GroupRoomContent( roomDetails: RoomsPlayingResponse, onBackClick: () -> Unit = {}, - onNavigateToMates: () -> Unit = {} + onNavigateToMates: () -> Unit = {}, + onNavigateToNote: (page: Int?, isOverview: Boolean?) -> Unit = { _, _ -> }, ) { val scrollState = rememberScrollState() @@ -170,7 +174,17 @@ fun GroupRoomContent( authorName = roomDetails.authorName, currentPage = roomDetails.currentPage, userPercentage = roomDetails.userPercentage, - currentVotes = roomDetails.currentVotes + currentVotes = roomDetails.currentVotes, + // 일반 노트 카드 클릭 시 필터 없이 이동 + onNavigateToNote = { onNavigateToNote(null, null) }, + // 투표 카드 클릭 시 필터 값과 함께 이동 + onVoteClick = { vote: CurrentVote -> + if (vote.isOverview) { + onNavigateToNote(null, true) + } else { + onNavigateToNote(vote.page, false) + } + } ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/BottomNavigationBar.kt b/app/src/main/java/com/texthip/thip/ui/navigator/BottomNavigationBar.kt index 3e25db24..d2fe0c45 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/BottomNavigationBar.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/BottomNavigationBar.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon @@ -27,8 +28,8 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import com.texthip.thip.ui.navigator.data.NavBarItems -import com.texthip.thip.ui.navigator.extensions.navigateToTab import com.texthip.thip.ui.navigator.extensions.isRoute +import com.texthip.thip.ui.navigator.extensions.navigateToTab import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -40,6 +41,7 @@ fun BottomNavigationBar(navController: NavHostController) { Box( modifier = Modifier .fillMaxWidth() + .navigationBarsPadding() .height(73.dp) .clip( RoundedCornerShape( 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 2a6151ca..481601e1 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 @@ -59,3 +59,46 @@ fun NavHostController.navigateToGroupRoom(roomId: Int) { fun NavHostController.navigateToGroupRoomMates(roomId: Int) { navigate(GroupRoutes.RoomMates(roomId)) } + +// 기록장 화면으로 이동 +fun NavHostController.navigateToGroupNote( + roomId: Int, + page: Int? = null, + isOverview: Boolean? = null +) { + navigate(GroupRoutes.Note(roomId = roomId, page = page, isOverview = isOverview)) +} + +// 기록 생성 화면으로 이동 +fun NavHostController.navigateToGroupNoteCreate( + roomId: Int, + recentBookPage: Int, + totalBookPage: Int, + isOverviewPossible: Boolean +) { + navigate( + GroupRoutes.NoteCreate( + roomId = roomId, + recentBookPage = recentBookPage, + totalBookPage = totalBookPage, + isOverviewPossible = isOverviewPossible + ) + ) +} + +// 투표 생성 화면으로 이동 +fun NavHostController.navigateToGroupVoteCreate( + roomId: Int, + recentPage: Int, + totalPage: Int, + isOverviewPossible: Boolean +) { + navigate( + GroupRoutes.VoteCreate( + roomId = roomId, + recentPage = recentPage, + totalPage = totalPage, + isOverviewPossible = isOverviewPossible + ) + ) +} \ No newline at end of file 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 9fe1a3e6..e8facc43 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 @@ -4,9 +4,8 @@ import android.annotation.SuppressLint import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable @@ -17,6 +16,10 @@ import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomViewModel import com.texthip.thip.ui.group.myroom.mock.RoomType import com.texthip.thip.ui.group.myroom.screen.GroupMyScreen import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel +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.GroupRoomMatesScreen import com.texthip.thip.ui.group.room.screen.GroupRoomRecruitScreen import com.texthip.thip.ui.group.room.screen.GroupRoomScreen @@ -27,10 +30,13 @@ import com.texthip.thip.ui.navigator.extensions.navigateToAlarm import com.texthip.thip.ui.navigator.extensions.navigateToGroupDone import com.texthip.thip.ui.navigator.extensions.navigateToGroupMakeRoom import com.texthip.thip.ui.navigator.extensions.navigateToGroupMy +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.navigateToGroupRoomMates import com.texthip.thip.ui.navigator.extensions.navigateToGroupSearch +import com.texthip.thip.ui.navigator.extensions.navigateToGroupVoteCreate import com.texthip.thip.ui.navigator.extensions.navigateToRecommendedGroupRecruit import com.texthip.thip.ui.navigator.routes.GroupRoutes import com.texthip.thip.ui.navigator.routes.MainTabRoutes @@ -173,19 +179,6 @@ fun NavGraphBuilder.groupNavigation( val route = backStackEntry.toRoute() val roomId = route.roomId - val parentEntry = remember(navController) { - try { - navController.getBackStackEntry(MainTabRoutes.Group) - } catch (e: Exception) { - null - } - } - val groupViewModel: GroupViewModel = if (parentEntry != null) { - viewModel(viewModelStoreOwner = parentEntry) - } else { - viewModel() - } - GroupRoomScreen( // roomId = roomId, roomId = 1, @@ -195,6 +188,9 @@ fun NavGraphBuilder.groupNavigation( onNavigateToMates = { navController.navigateToGroupRoomMates(roomId) }, + onNavigateToNote = { page, isOverview -> + navController.navigateToGroupNote(roomId, page, isOverview) + }, ) } @@ -214,4 +210,90 @@ fun NavGraphBuilder.groupNavigation( } ) } + + // Group Note 화면 + composable { backStackEntry -> + val route = backStackEntry.toRoute() + val roomId = route.roomId + val page = route.page + val isOverview = route.isOverview + + val result = backStackEntry.savedStateHandle.get("selected_tab_index") + + val viewModel: GroupNoteViewModel = hiltViewModel(backStackEntry) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + GroupNoteScreen( +// roomId = roomId, + roomId = 1, + resultTabIndex = result, + initialPage = page, + initialIsOverview = isOverview, + onResultConsumed = { + backStackEntry.savedStateHandle.remove("selected_tab_index") + }, + onBackClick = { navigateBack() }, + onCreateNoteClick = { recentPage, totalPage, isOverviewPossible -> + navController.navigateToGroupNoteCreate( + roomId = roomId, + recentBookPage = recentPage, + totalBookPage = totalPage, + isOverviewPossible = isOverviewPossible + ) + }, + // [수정] '투표 생성' 클릭 시 페이지 정보와 함께 내비게이션 + onCreateVoteClick = { recentPage, totalPage, isOverviewPossible -> + navController.navigateToGroupVoteCreate( + roomId = roomId, + recentPage = recentPage, + totalPage = totalPage, + isOverviewPossible = isOverviewPossible + ) + }, + viewModel = viewModel + ) + } + + // Group Note Create 화면 + composable { backStackEntry -> + val route = backStackEntry.toRoute() + val roomId = route.roomId + + GroupNoteCreateScreen( + roomId = 1, + recentPage = route.recentBookPage, + totalPage = route.totalBookPage, + isOverviewPossible = route.isOverviewPossible, + onBackClick = { + navigateBack() + }, + onNavigateBackWithResult = { + navController.previousBackStackEntry + ?.savedStateHandle + ?.set("selected_tab_index", 1) + navigateBack() + } + ) + } + + composable { backStackEntry -> + val route = backStackEntry.toRoute() + val roomId = route.roomId + + GroupVoteCreateScreen( +// roomId = roomId, + roomId = 1, + recentPage = route.recentPage, + totalPage = route.totalPage, + isOverviewPossible = route.isOverviewPossible, + onBackClick = { navigateBack() }, + onNavigateBackWithResult = { + // 투표 생성 후 '내 기록' 탭으로 이동 + navController.previousBackStackEntry + ?.savedStateHandle + ?.set("selected_tab_index", 1) + navigateBack() + } + ) + } } \ No newline at end of file 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 533bf084..ce599479 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 @@ -6,22 +6,42 @@ import kotlinx.serialization.Serializable sealed class GroupRoutes : Routes() { @Serializable data object MakeRoom : GroupRoutes() - + @Serializable data object Done : GroupRoutes() - + @Serializable data object Search : GroupRoutes() - + @Serializable data object My : GroupRoutes() - + @Serializable data class Recruit(val roomId: Int) : GroupRoutes() - + @Serializable data class Room(val roomId: Int) : GroupRoutes() @Serializable data class RoomMates(val roomId: Int) : GroupRoutes() + + @Serializable + data class Note(val roomId: Int, val page: Int? = null, val isOverview: Boolean? = null) : + GroupRoutes() + + @Serializable + data class NoteCreate( + val roomId: Int, + val recentBookPage: Int, + val totalBookPage: Int, + val isOverviewPossible: Boolean + ) + + @Serializable + data class VoteCreate( + val roomId: Int, + val recentPage: Int, + val totalPage: Int, + val isOverviewPossible: Boolean + ) } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt index 59acf186..849cf9d1 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt @@ -65,8 +65,8 @@ fun SearchBookDetailScreen( var selectedFilterOption by remember { mutableIntStateOf(0) } val filterOptions = listOf( - stringResource(R.string.search_filter_popular), - stringResource(R.string.search_filter_latest) + stringResource(R.string.sort_like), + stringResource(R.string.sort_latest) ) // 알림 5초간 노출 diff --git a/app/src/main/java/com/texthip/thip/utils/type/SortType.kt b/app/src/main/java/com/texthip/thip/utils/type/SortType.kt new file mode 100644 index 00000000..3b12aeb6 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/utils/type/SortType.kt @@ -0,0 +1,19 @@ +package com.texthip.thip.utils.type + +import androidx.annotation.StringRes +import com.texthip.thip.R + +enum class SortType ( + @StringRes val displayNameRes: Int, + val apiKey: String +) { + LATEST(R.string.sort_latest, "latest"), + LIKE(R.string.sort_like, "like"), + COMMENT(R.string.sort_comment, "comment"); + + companion object { + fun fromApiKey(key: String?): SortType { + return entries.find { it.apiKey == key } ?: LATEST + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dade4eff..39ec0b0d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -223,12 +223,17 @@ 항목 추가 독서 진행도 80% 이상부터 총평을 볼 수 있어요. 아직 기록이 없어요. + 아직 총평이 없어요. 우리 모임의 첫번째 기록을 남겨보세요 나의 첫번째 기록을 남겨보세요 + 책을 끝까지 읽고 든 생각을 남겨보세요 아직 댓글이 없어요. 첫번째 댓글을 남겨보세요 이 기록을 피드에 핀할까요? 핀하면 내 피드에 글을 옮길 수 있어요. + 댓글 많은 순 + 기록을 게시 중입니다... + 기록이 게시되었습니다! 피드 @@ -351,8 +356,8 @@ 피드 글 둘러보기 이 책으로 작성된 피드가 없어요. 첫번째 피드를 작성해보세요! - 인기순 - 최신순 + 인기순 + 최신순 소개 %1$s 저 · %2$s 인기순