diff --git a/app/src/main/java/com/texthip/thip/ui/common/buttons/ActionBookButton.kt b/app/src/main/java/com/texthip/thip/ui/common/buttons/ActionBookButton.kt index 1a109d7e..794d9ef4 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/buttons/ActionBookButton.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/buttons/ActionBookButton.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon @@ -33,7 +32,10 @@ fun ActionBookButton( ) { Box( modifier = Modifier - .background(color = colors.DarkGrey02, shape = RoundedCornerShape(12.dp)) + .background( + color = colors.DarkGrey, + shape = RoundedCornerShape(12.dp) + ) .clickable { onClick() } diff --git a/app/src/main/java/com/texthip/thip/ui/feed/component/RecommendFeedCard.kt b/app/src/main/java/com/texthip/thip/ui/feed/component/RecommendFeedCard.kt new file mode 100644 index 00000000..6e798a7f --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/feed/component/RecommendFeedCard.kt @@ -0,0 +1,183 @@ +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.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +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.data.model.feed.response.AllFeedItem +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.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 + +@Composable +fun RecommendFeedCard( + feedItem: AllFeedItem, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.clickable { onClick() }, + shape = RoundedCornerShape(18.dp), + colors = androidx.compose.material3.CardDefaults.cardColors( + containerColor = colors.DarkGrey02 + ) + ) { + val hasImages = feedItem.contentUrls.isNotEmpty() + val maxTextLines = 3 + + var isTextTruncated by remember { mutableStateOf(false) } + + // 추천글은 항상 3줄로 고정 + val processedText = remember(feedItem.contentBody) { + val lines = feedItem.contentBody.split("\n") + if (lines.size <= maxTextLines) { + feedItem.contentBody + } else { + lines.take(maxTextLines).joinToString("\n") + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + ) { + ProfileBar( + profileImage = feedItem.creatorProfileImageUrl ?: "", + topText = feedItem.creatorNickname, + bottomText = feedItem.aliasName, + bottomTextColor = hexToColor(feedItem.aliasColor), + showSubscriberInfo = false, + hoursAgo = feedItem.postDate, + onClick = onClick + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) { + ActionBookButton( + bookTitle = feedItem.bookTitle, + bookAuthor = feedItem.bookAuthor, + onClick = onClick + ) + } + + Column( + modifier = Modifier.clickable { onClick() }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box { + Text( + text = processedText, + style = typography.feedcopy_r400_s14_h20, + color = colors.White, + maxLines = maxTextLines, + modifier = Modifier.fillMaxWidth(), + onTextLayout = { textLayoutResult -> + isTextTruncated = textLayoutResult.hasVisualOverflow + } + ) + + // 텍스트가 잘린 경우에 + if (isTextTruncated) { + Image( + painter = painterResource(id = R.drawable.ic_text_more_darkgrey), + contentDescription = null, + modifier = Modifier.align(Alignment.BottomEnd) + ) + } + } + + if (hasImages) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + feedItem.contentUrls.take(3).forEach { imageUrl -> + AsyncImage( + model = imageUrl, + contentDescription = null, + modifier = Modifier + .padding(end = 10.dp) + .size(100.dp), + contentScale = ContentScale.Crop + ) + } + } + } + } + + ActionBarButton( + modifier = Modifier.padding(top = 16.dp), + isLiked = feedItem.isLiked, + likeCount = feedItem.likeCount, + commentCount = feedItem.commentCount, + isSaveVisible = false, + isSaved = feedItem.isSaved, + isLockIcon = false, + onLikeClick = { /* 카드 전체 클릭만 처리 */ }, + onCommentClick = onClick, + onBookmarkClick = { /* 카드 전체 클릭만 처리 */ } + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF000000) +@Composable +private fun RecommendedFeedCardPreview() { + ThipTheme { + RecommendFeedCard( + AllFeedItem( + feedId = 1, + creatorId = 123L, + creatorNickname = "user.01", + creatorProfileImageUrl = null, + aliasName = "공식 인플루언서", + aliasColor = "#97E4A3", + postDate = "2시간 전", + isbn = "9788983711892", + bookTitle = "코스모스", + bookAuthor = "칼 세이건", + contentBody = "이 책을 읽으면서 우주에 대한 새로운 시각을 갖게 되었습니다. 과학적 사실들이 아름다운 문장으로 표현되어 있어서 읽는 내내 감동받았어요. 이 책을 읽으면서 우주에 대한 새로운 시각을 갖게 되었습니다. 과학적 사실들이 아름다운 문장으로 표현되어 있어서 읽는 내내 감동받았어요.", + contentUrls = emptyList(), + likeCount = 42, + commentCount = 8, + isSaved = false, + isLiked = false, + isWriter = false + ), + onClick = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/feed/component/RecommendFeedCardSection.kt b/app/src/main/java/com/texthip/thip/ui/feed/component/RecommendFeedCardSection.kt new file mode 100644 index 00000000..4ba93006 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/feed/component/RecommendFeedCardSection.kt @@ -0,0 +1,226 @@ +package com.texthip.thip.ui.feed.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.texthip.thip.R +import com.texthip.thip.data.model.feed.response.AllFeedItem +import com.texthip.thip.ui.theme.ThipTheme +import com.texthip.thip.ui.theme.ThipTheme.colors +import com.texthip.thip.ui.theme.ThipTheme.typography + +@Composable +fun RecommendedFeedCarousel( + recommendedFeeds: List, + onFeedClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + if (recommendedFeeds.isEmpty()) return + + Column( + modifier = modifier.fillMaxWidth() + ) { + Text( + text = stringResource(R.string.recommended_feeds_title), + style = typography.title_b700_s20_h24, + color = colors.White, + modifier = Modifier.padding(horizontal = 20.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.recommended_feeds_subtitle), + style = typography.copy_m500_s14_h20, + color = colors.Grey, + modifier = Modifier.padding(horizontal = 20.dp) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // 카드 캐러셀 부분 + if (recommendedFeeds.size == 1) { + // 카드가 1개 + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + RecommendFeedCard( + feedItem = recommendedFeeds[0], + onClick = { onFeedClick(recommendedFeeds[0].feedId.toLong()) } + ) + } + } else { + val pagerState = rememberPagerState( + pageCount = { recommendedFeeds.size } + ) + + HorizontalPager( + state = pagerState, + contentPadding = PaddingValues(start = 20.dp, end = 20.dp), + pageSpacing = 12.dp, + modifier = Modifier.fillMaxWidth() + ) { page -> + Box( + modifier = Modifier + ) { + RecommendFeedCard( + feedItem = recommendedFeeds[page], + onClick = { onFeedClick(recommendedFeeds[page].feedId.toLong()) } + ) + } + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF000000) +@Composable +private fun RecommendedFeedCarouselPreview() { + ThipTheme { + RecommendedFeedCarousel( + recommendedFeeds = listOf( + AllFeedItem( + feedId = 1, + creatorId = 123L, + creatorNickname = "user.01", + creatorProfileImageUrl = null, + aliasName = "공식 인플루언서", + aliasColor = "#97E4A3", + postDate = "2시간 전", + isbn = "9788983711892", + bookTitle = "코스모스", + bookAuthor = "칼 세이건", + contentBody = "이 책을 읽으면서 우주에 대한 새로운 시각을 갖게 되었습니다. 과학적 사실들이 아름다운 문장으로 표현되어 있어서 읽는 내내 감동받았어요.", + contentUrls = emptyList(), + likeCount = 42, + commentCount = 8, + isSaved = false, + isLiked = false, + isWriter = false + ), + AllFeedItem( + feedId = 1, + creatorId = 123L, + creatorNickname = "user.01", + creatorProfileImageUrl = null, + aliasName = "공식 인플루언서", + aliasColor = "#97E4A3", + postDate = "2시간 전", + isbn = "9788983711892", + bookTitle = "코스모스", + bookAuthor = "칼 세이건", + contentBody = "이 책을 읽으면서 우주에 대한 새로운 시각을 갖게 되었습니다. 과학적 사실들이 아름다운 문장으로 표현되어 있어서 읽는 내내 감동받았어요.", + contentUrls = emptyList(), + likeCount = 42, + commentCount = 8, + isSaved = false, + isLiked = false, + isWriter = false + ), + AllFeedItem( + feedId = 1, + creatorId = 123L, + creatorNickname = "user.01", + creatorProfileImageUrl = null, + aliasName = "공식 인플루언서", + aliasColor = "#97E4A3", + postDate = "2시간 전", + isbn = "9788983711892", + bookTitle = "코스모스", + bookAuthor = "칼 세이건", + contentBody = "이 책을 읽으면서 우주에 대한 새로운 시각을 갖게 되었습니다. 과학적 사실들이 아름다운 문장으로 표현되어 있어서 읽는 내내 감동받았어요.", + contentUrls = emptyList(), + likeCount = 42, + commentCount = 8, + isSaved = false, + isLiked = false, + isWriter = false + ), + AllFeedItem( + feedId = 1, + creatorId = 123L, + creatorNickname = "user.01", + creatorProfileImageUrl = null, + aliasName = "공식 인플루언서", + aliasColor = "#97E4A3", + postDate = "2시간 전", + isbn = "9788983711892", + bookTitle = "코스모스", + bookAuthor = "칼 세이건", + contentBody = "이 책을 읽으면서 우주에 대한 새로운 시각을 갖게 되었습니다. 과학적 사실들이 아름다운 문장으로 표현되어 있어서 읽는 내내 감동받았어요.", + contentUrls = emptyList(), + likeCount = 42, + commentCount = 8, + isSaved = false, + isLiked = false, + isWriter = false + ), + AllFeedItem( + feedId = 1, + creatorId = 123L, + creatorNickname = "user.01", + creatorProfileImageUrl = null, + aliasName = "공식 인플루언서", + aliasColor = "#97E4A3", + postDate = "2시간 전", + isbn = "9788983711892", + bookTitle = "코스모스", + bookAuthor = "칼 세이건", + contentBody = "이 책을 읽으면서 우주에 대한 새로운 시각을 갖게 되었습니다. 과학적 사실들이 아름다운 문장으로 표현되어 있어서 읽는 내내 감동받았어요.", + contentUrls = emptyList(), + likeCount = 42, + commentCount = 8, + isSaved = false, + isLiked = false, + isWriter = false + ) + ), + onFeedClick = {} + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF000000) +@Composable +private fun RecommendedFeedCarouselSinglePreview() { + ThipTheme { + RecommendedFeedCarousel( + recommendedFeeds = listOf( + AllFeedItem( + feedId = 1, + creatorId = 123L, + creatorNickname = "책읽는사람", + creatorProfileImageUrl = null, + aliasName = "문학 애호가", + aliasColor = "#FF6B9D", + postDate = "2시간 전", + isbn = "9788983711892", + bookTitle = "코스모스", + bookAuthor = "칼 세이건", + contentBody = "이 책을 읽으면서 우주에 대한 새로운 시각을 갖게 되었습니다.", + contentUrls = emptyList(), + likeCount = 42, + commentCount = 8, + isSaved = false, + isLiked = true, + isWriter = false + ) + ), + onFeedClick = {} + ) + } +} 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 488868cf..e8fb09b3 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 @@ -54,6 +54,7 @@ import com.texthip.thip.ui.common.topappbar.LogoTopAppBar import com.texthip.thip.ui.feed.component.FeedSubscribeBarlist import com.texthip.thip.ui.feed.component.MyFeedCard import com.texthip.thip.ui.feed.component.MySubscribeBarlist +import com.texthip.thip.ui.feed.component.RecommendedFeedCarousel import com.texthip.thip.ui.feed.mock.FeedStateUpdateResult import com.texthip.thip.ui.feed.viewmodel.FeedUiState import com.texthip.thip.ui.feed.viewmodel.FeedViewModel @@ -513,10 +514,20 @@ private fun FeedContent( onClick = onNavigateToMySubscription ) } + + // 10번째 항목 후에 추천 섹션 삽입 itemsIndexed( feedUiState.allFeeds, - key = { _, item -> item.feedId }) { index, allFeed -> - // AllFeedItem을 FeedItem으로 변환 + key = { _, item -> item.feedId } + ) { index, allFeed -> + // 첫 항목 위에 여백 추가 + if (index == 0) { + Spacer(modifier = Modifier.height(20.dp)) + } else { + Spacer(modifier = Modifier.height(40.dp)) + } + + // 피드 카드 표시 val feedItem = FeedItem( id = allFeed.feedId.toLong(), userProfileImage = allFeed.creatorProfileImageUrl, @@ -534,37 +545,37 @@ private fun FeedContent( 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 = { - onChangeFeedSave(feedItem.id) - }, - onLikeClick = { - onChangeFeedLike(feedItem.id) - }, - onContentClick = { - onNavigateToFeedComment(feedItem.id) - }, - onCommentClick = { - onNavigateToFeedComment(feedItem.id) - }, - onBookClick = { - onNavigateToBookDetail(allFeed.isbn) - }, - onProfileClick = { - onNavigateToUserProfile(allFeed.creatorId) - } + onBookmarkClick = { onChangeFeedSave(feedItem.id) }, + onLikeClick = { onChangeFeedLike(feedItem.id) }, + onContentClick = { onNavigateToFeedComment(feedItem.id) }, + onCommentClick = { onNavigateToFeedComment(feedItem.id) }, + onBookClick = { onNavigateToBookDetail(allFeed.isbn) }, + onProfileClick = { onNavigateToUserProfile(allFeed.creatorId) } ) - Spacer(modifier = Modifier.height(40.dp)) - if (index != feedUiState.allFeeds.lastIndex) { - HorizontalDivider( - color = colors.DarkGrey02, - thickness = 6.dp + + // 10번째 피드 후에 추천 섹션 삽입 + if (index == 9 && feedUiState.recommendedFeeds.isNotEmpty()) { + Spacer(modifier = Modifier.height(40.dp)) + HorizontalDivider(color = colors.DarkGrey02, thickness = 6.dp) + Spacer(modifier = Modifier.height(40.dp)) + + RecommendedFeedCarousel( + recommendedFeeds = feedUiState.recommendedFeeds, + onFeedClick = { feedId -> onNavigateToFeedComment(feedId) } ) + + Spacer(modifier = Modifier.height(40.dp)) + HorizontalDivider(color = colors.DarkGrey02, thickness = 6.dp) + } else { + Spacer(modifier = Modifier.height(40.dp)) + // 마지막 항목이 아닐 때만 구분선 표시 + if (index != feedUiState.allFeeds.lastIndex) { + HorizontalDivider(color = colors.DarkGrey02, thickness = 6.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 901569f3..c18ea198 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 @@ -24,6 +24,7 @@ data class FeedUiState( val myFeeds: List = emptyList(), val recentWriters: List = emptyList(), val myFeedInfo: FeedMineInfoResponse? = null, + val recommendedFeeds: List = emptyList(), // 추천글 목록 val isLoading: Boolean = false, val isRefreshing: Boolean = false, // 탭 전환용 로딩 val isPullToRefreshing: Boolean = false, // Pull to refresh용 로딩 @@ -68,6 +69,7 @@ class FeedViewModel @Inject constructor( loadAllFeeds() fetchRecentWriters() fetchMyFeedInfo() + loadRecommendedFeeds() observeFeedUpdates() } @@ -104,11 +106,11 @@ class FeedViewModel @Inject constructor( } } - _uiState.update { + _uiState.update { it.copy( - allFeeds = updatedAllFeeds, + allFeeds = updatedAllFeeds, myFeeds = updatedMyFeeds - ) + ) } } } @@ -143,12 +145,12 @@ class FeedViewModel @Inject constructor( } } } - + fun refreshDataAndScrollToTop() { refreshData() // 스크롤 상단 이동은 Screen에서 처리 } - + fun refreshOnBottomNavReselect() { // 바텀 네비게이션에서 같은 탭 다시 클릭 시 (스크롤은 Screen에서 처리) refreshData() @@ -539,4 +541,12 @@ class FeedViewModel @Inject constructor( ) } } + + // 추천글 로드 + private fun loadRecommendedFeeds() { + viewModelScope.launch { + // TODO: 실제 추천글 API 연결 + updateState { it.copy(recommendedFeeds = emptyList()) } + } + } } \ 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 88e1a283..28ba21e1 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 @@ -49,18 +49,18 @@ fun SavedFeedCard( ) { val hasImages = feedItem.imageUrls.isNotEmpty() val maxTextLines = if (hasImages) 3 else 8 - + // 실제 텍스트 줄 수를 기준으로 표시할 텍스트 계산 val processedText = remember(feedItem.content, hasImages) { val lines = feedItem.content.split("\n") val nonEmptyLines = mutableListOf() // 실제 텍스트가 있는 줄의 인덱스 - + lines.forEachIndexed { index, line -> if (line.trim().isNotEmpty()) { nonEmptyLines.add(index) } } - + if (nonEmptyLines.size <= maxTextLines) { // 실제 텍스트 줄이 제한보다 적으면 전체 표시 feedItem.content @@ -70,9 +70,8 @@ fun SavedFeedCard( lines.take(lastAllowedLineIndex + 1).joinToString("\n") } } - - // 잘림 여부는 파생 값으로 계산 - val isTextTruncated = processedText != feedItem.content + + var isTextTruncated by remember { mutableStateOf(false) } Column( modifier = modifier @@ -111,9 +110,13 @@ fun SavedFeedCard( text = processedText, style = typography.feedcopy_r400_s14_h20, color = colors.White, + maxLines = maxTextLines, modifier = Modifier.fillMaxWidth(), + onTextLayout = { textLayoutResult -> + isTextTruncated = textLayoutResult.hasVisualOverflow + } ) - + // 텍스트가 잘린 경우에만 "...더보기" 표시 if (isTextTruncated) { Image( diff --git a/app/src/main/res/drawable/ic_text_more_darkgrey.xml b/app/src/main/res/drawable/ic_text_more_darkgrey.xml new file mode 100644 index 00000000..8416236d --- /dev/null +++ b/app/src/main/res/drawable/ic_text_more_darkgrey.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e23ee58a..85e51b57 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -456,6 +456,10 @@ 과학/IT + + 지금 뜨는 추천 글 + 비슷한 취향의 인플루언서, 작가가\n추천하는 도서를 만나보세요. + 211189590640-a2ul4bnsvislm12ov7k8eu61mtketl8d.apps.googleusercontent.com