From df5f9c557c7f409e1fe0c17481b93771d60a4eb3 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Sun, 17 Aug 2025 15:04:38 +0900 Subject: [PATCH 01/15] =?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 02/15] =?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 03/15] =?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 04/15] =?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 05/15] =?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 06/15] =?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 07/15] =?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 08/15] =?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 09/15] =?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 10/15] =?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 11/15] =?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 12/15] =?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 13/15] =?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 14/15] =?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 15/15] =?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 = {