From 3a93a246dd9fb74de722dad3f831b4ff19099f66 Mon Sep 17 00:00:00 2001 From: chanmi Date: Mon, 15 Jun 2026 01:26:49 +0900 Subject: [PATCH 01/19] =?UTF-8?q?[refactor]=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionDetailScreen.kt | 691 ++---------------- .../component/CollectionDetailDescription.kt | 138 ++++ .../component/CollectionDetailThumbnail.kt | 103 +++ .../collectiondetail/component/Content.kt | 215 ++++++ .../component/PeopleWhoSavedThisCollection.kt | 208 ++++++ 5 files changed, 721 insertions(+), 634 deletions(-) create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDescription.kt create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailThumbnail.kt create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/component/Content.kt create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt index a4eb4e72..4c07e3eb 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt @@ -1,32 +1,19 @@ package com.flint.presentation.collectiondetail -import androidx.compose.foundation.Image import androidx.compose.foundation.LocalOverscrollFactory import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background -import androidx.compose.foundation.border -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.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.defaultMinSize 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.layout.width import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -35,33 +22,19 @@ import androidx.compose.runtime.mutableIntStateOf 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.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInParent -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.flint.R -import com.flint.core.common.extension.noRippleClickable import com.flint.core.common.util.UiState -import com.flint.core.designsystem.component.button.FlintSaveDoneButton -import com.flint.core.designsystem.component.button.FlintSaveNoneButton import com.flint.core.designsystem.component.collection.PeopleBottomSheet -import com.flint.core.designsystem.component.collection.Spoiler -import com.flint.core.designsystem.component.image.NetworkImage -import com.flint.core.designsystem.component.image.ProfileImage import com.flint.core.designsystem.component.indicator.FlintLoadingIndicator import com.flint.core.designsystem.component.progressbar.UnderImageProgressBar import com.flint.core.designsystem.component.toast.ShowSaveToast @@ -73,6 +46,10 @@ import com.flint.domain.model.bookmark.CollectionBookmarkUsersModel import com.flint.domain.model.collection.CollectionDetailModelNew import com.flint.domain.model.content.ContentModelNew import com.flint.domain.type.UserRoleType +import com.flint.presentation.collectiondetail.component.CollectionDetailDescription +import com.flint.presentation.collectiondetail.component.CollectionDetailThumbnail +import com.flint.presentation.collectiondetail.component.Content +import com.flint.presentation.collectiondetail.component.PeopleWhoSavedThisCollection import com.flint.presentation.collectiondetail.sideeffect.CollectionDetailSideEffect import com.flint.presentation.collectiondetail.uistate.CollectionDetailUiState import kotlinx.collections.immutable.ImmutableList @@ -109,6 +86,7 @@ fun CollectionDetailRoute( CollectionDetailScreen( paddingValues = paddingValues, targetImageUrl = targetImageUrl, + thumbnailUrl = collectionDetail.thumbnailUrl, title = collectionDetail.title, isBookmarked = collectionDetail.isBookmarked, authorNickname = collectionDetail.author.nickname, @@ -215,6 +193,7 @@ fun CollectionDetailRoute( @Composable fun CollectionDetailScreen( paddingValues: PaddingValues, + thumbnailUrl: String, title: String, isBookmarked: Boolean, authorNickname: String, @@ -268,26 +247,29 @@ fun CollectionDetailScreen( } Column( - modifier = - Modifier - .fillMaxSize() - .padding(paddingValues) - .background(color = FlintTheme.colors.background), + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(color = FlintTheme.colors.background), ) { - FlintBackTopAppbar( - onClick = navigateUp, - backgroundColor = Color.Transparent, - ) +// FlintBackTopAppbar( +// onClick = navigateUp, +// backgroundColor = Color.Transparent, +// ) Box { + FlintBackTopAppbar( + onClick = navigateUp, + backgroundColor = Color.Transparent, + ) Column( - modifier = - Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding(bottom = 24.dp), + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(bottom = 24.dp), ) { - Thumbnail( + CollectionDetailThumbnail( + thumbnailImage = thumbnailUrl, title = title, isBookmarked = isBookmarked, onSaveDoneButtonClick = onSaveDoneButtonClick, @@ -352,571 +334,8 @@ fun CollectionDetailScreen( } } -@Composable -private fun PeopleWhoSavedThisCollection( - people: ImmutableList, - onMoreClick: () -> Unit, -) { - Column( - modifier = Modifier.padding(vertical = 10.dp), - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = "이 컬렉션을 저장한 사람들", - color = FlintTheme.colors.white, - style = FlintTheme.typography.head2Sb20, - modifier = - Modifier - .weight(1f) - .padding(horizontal = 16.dp), - ) - - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_more), - contentDescription = null, - modifier = - Modifier - .size(48.dp) - .clickable(onClick = onMoreClick) - .padding(12.dp), - tint = FlintTheme.colors.white, - ) - } - - Spacer(Modifier.height(24.dp)) - - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Row( - horizontalArrangement = Arrangement.spacedBy((-12).dp), - ) { - people.take(5).forEach { author: CollectionBookmarkUsersModel.User -> - ProfileImage( - imageUrl = author.profileImageUrl, - modifier = - Modifier - .size(56.dp) - .border(3.dp, FlintTheme.colors.background, CircleShape), - contentDescription = author.nickName, - ) - } - } - - Spacer(Modifier.width(8.dp)) - - if (people.size >= 6) { - Row { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_plus), - contentDescription = "그 외", - tint = FlintTheme.colors.white, - ) - - Text( - text = (people.size - 5).toString(), - color = FlintTheme.colors.gray50, - style = FlintTheme.typography.head2M20, - ) - } - } - } - } -} - -private class PeoplePreviewProvider : - PreviewParameterProvider> { - override val values: Sequence> = - sequenceOf( - persistentListOf( - CollectionBookmarkUsersModel.User( - userId = "1", - nickName = "유저1", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - ), - persistentListOf( - CollectionBookmarkUsersModel.User( - userId = "1", - nickName = "유저1", - profileImageUrl = "", - userRole = UserRoleType.ADMIN, - ), - CollectionBookmarkUsersModel.User( - userId = "2", - nickName = "유저2", - profileImageUrl = "", - userRole = UserRoleType.FLINER, - ), - CollectionBookmarkUsersModel.User( - userId = "3", - nickName = "유저3", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "4", - nickName = "유저4", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "5", - nickName = "유저5", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - ), - persistentListOf( - CollectionBookmarkUsersModel.User( - userId = "1", - nickName = "유저1", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "2", - nickName = "유저2", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "3", - nickName = "유저3", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "4", - nickName = "유저4", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "5", - nickName = "유저5", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "6", - nickName = "유저6", - profileImageUrl = "", - userRole = UserRoleType.FLINER, - ), - CollectionBookmarkUsersModel.User( - userId = "7", - nickName = "유저7", - profileImageUrl = "", - userRole = UserRoleType.ADMIN, - ), - ), - ) -} - -@Preview -@Composable -private fun PeopleWhoSavedThisCollectionPreview( - @PreviewParameter(PeoplePreviewProvider::class) people: ImmutableList, -) { - FlintTheme { - PeopleWhoSavedThisCollection( - people = people, - onMoreClick = {}, - ) - } -} - -@Composable -private fun Thumbnail( - title: String, - isBookmarked: Boolean, - onSaveDoneButtonClick: () -> Unit, - onSaveNoneButtonClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Box( - modifier = - modifier - .fillMaxWidth() - .aspectRatio(360f / 270f), - ) { - Image( - painter = painterResource(R.drawable.img_collection_bg2), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.FillBounds, - ) - - Column( - modifier = - Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) - .padding(top = 55.dp, bottom = 19.dp), - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = title, - color = FlintTheme.colors.white, - style = FlintTheme.typography.display2M28, - modifier = Modifier.fillMaxWidth(), - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - if (isBookmarked) { - FlintSaveDoneButton( - onClick = { - onSaveDoneButtonClick() - }, - ) - } else { - FlintSaveNoneButton( - onClick = { - onSaveNoneButtonClick() - }, - ) - } - } - } -} - -@Composable -private fun CollectionDetailDescription( - authorNickname: String, - authorUserRoleType: UserRoleType, - createdAt: String, - collectionContent: String, - onAuthorNicknameClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(24.dp), - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = authorNickname, - color = FlintTheme.colors.white, - style = FlintTheme.typography.head2Sb20, - modifier = Modifier.noRippleClickable( - onClick = { onAuthorNicknameClick() } - ) - ) - - if (authorUserRoleType == UserRoleType.FLINER) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_qualified), - contentDescription = "플리너", - tint = Color.Unspecified, - ) - } else { - Text( - "|", - color = FlintTheme.colors.gray200, - style = FlintTheme.typography.head3M18, - ) - } - - Text( - text = createdAt, - color = FlintTheme.colors.gray200, - style = FlintTheme.typography.body2M14, - ) - } - - Box( - modifier = - Modifier - .fillMaxWidth() - .height(1.dp) - .clip(CircleShape) - .background(color = FlintTheme.colors.gray300), - ) - - Text( - text = collectionContent, - color = FlintTheme.colors.gray100, - style = FlintTheme.typography.body1R16, - ) - } -} - -@Composable -private fun Content( - content: ContentModelNew, - onBookmarkIconClick: (contentId: String) -> Unit, - onSpoilClick: (contentId: String) -> Unit, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - NetworkImage( - imageUrl = content.imageUrl, - modifier = - Modifier - .fillMaxWidth() - .aspectRatio(360f / 480f), - contentScale = ContentScale.Crop, - ) - - Spacer(Modifier.height(32.dp)) - - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column { - Text( - content.title, - color = FlintTheme.colors.white, - style = FlintTheme.typography.head2Sb20, - ) - - Spacer(Modifier.height(12.dp)) - - Text( - content.year.toString(), - color = FlintTheme.colors.gray300, - style = FlintTheme.typography.body1R16, - ) - - Text( - content.director, - color = FlintTheme.colors.gray300, - style = FlintTheme.typography.body1R16, - ) - } - - Row( - modifier = - Modifier.noRippleClickable(onClick = { onBookmarkIconClick(content.id) }), - ) { - Column( - modifier = - Modifier - .padding(start = 24.dp) - .padding(vertical = 3.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - if (content.isBookmarked) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_bookmark_fill), - contentDescription = "저장됨", - tint = Color.Unspecified, - ) - } else { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_bookmark_empty), - contentDescription = "저장되지 않음", - tint = Color.White, - ) - } - - Text( - text = content.bookmarkCount.toString(), - color = FlintTheme.colors.white, - style = FlintTheme.typography.caption1M12, - ) - } - } - } - - Spacer(Modifier.height(24.dp)) - - Box( - modifier = - Modifier - .fillMaxWidth() - .height(1.dp) - .background(FlintTheme.colors.gray500), - ) - - Spacer(Modifier.height(24.dp)) - - if (content.isSpoiler) { - Spoiler( - onSpoilClick = { onSpoilClick(content.id) }, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = content.reason, - color = FlintTheme.colors.gray100, - style = FlintTheme.typography.body1R16, - modifier = - Modifier - .defaultMinSize(minHeight = 183.dp) - .fillMaxWidth(), - ) - } - } else { - Text( - text = content.reason, - color = FlintTheme.colors.gray100, - style = FlintTheme.typography.body1R16, - modifier = - Modifier - .defaultMinSize(minHeight = 183.dp) - .fillMaxWidth(), - ) - } - - Spacer(Modifier.height(64.dp)) - } - } -} - -private data class HeaderPreviewData( - val title: String, - val isMine: Boolean, - val isBookmarked: Boolean, -) - -private class HeaderPreviewProvider : PreviewParameterProvider { - override val values: Sequence = - sequenceOf( - HeaderPreviewData( - title = "한번 보면 못 빠져나오는 여운남는 사랑이야기".repeat(2), - isMine = false, - isBookmarked = true, - ), - HeaderPreviewData( - title = "한번 보면 못 빠져나오는 여운남는 사랑이야기", - isMine = false, - isBookmarked = false, - ), - ) -} - -@Preview -@Composable -private fun ThumbnailPreview( - @PreviewParameter(HeaderPreviewProvider::class) data: HeaderPreviewData, -) { - FlintTheme { - Thumbnail( - title = data.title, - isBookmarked = data.isBookmarked, - onSaveDoneButtonClick = {}, - onSaveNoneButtonClick = {} - ) - } -} - -private data class DescriptionPreviewData( - val authorNickname: String, - val authorUserRoleType: UserRoleType, - val createdAt: String, - val collectionContent: String, -) - -private class DescriptionPreviewProvider : PreviewParameterProvider { - override val values: Sequence = - sequenceOf( - DescriptionPreviewData( - authorNickname = "키카", - authorUserRoleType = UserRoleType.FLINER, - createdAt = "2026. 01. 07.", - collectionContent = "시간이 흘러도 빛이 바래지 않는,\n사랑의 미묘한 온도를 담은 제 최애 영화 모음집입니다", - ), - DescriptionPreviewData( - authorNickname = "일반유저", - authorUserRoleType = UserRoleType.FLING, - createdAt = "2026. 01. 15.", - collectionContent = "한글자 두글자 세글자 네글자 다섯글자 ".repeat(10), - ), - DescriptionPreviewData( - authorNickname = "관리자", - authorUserRoleType = UserRoleType.ADMIN, - createdAt = "2026. 01. 01.", - collectionContent = "공식 추천 컬렉션입니다", - ), - ) -} - -@Preview -@Composable -private fun CollectionDetailDescriptionPreview( - @PreviewParameter(DescriptionPreviewProvider::class) data: DescriptionPreviewData, -) { - FlintTheme { - CollectionDetailDescription( - authorNickname = data.authorNickname, - authorUserRoleType = data.authorUserRoleType, - createdAt = data.createdAt, - collectionContent = data.collectionContent, - onAuthorNicknameClick = {} - ) - } -} - -private class ContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence = - sequenceOf( - ContentModelNew( - id = "0", - title = "드라마 제목", - year = 2000, - imageUrl = "", - director = "가스 제닝스", - reason = "달라진 온도\n-\n같은 구도에 채도를 달리해 변해버린 사랑을 시각적으로 담아낸 장면들", - isSpoiler = false, - isBookmarked = false, - bookmarkCount = 42, - ), - ContentModelNew( - id = "0", - title = "스포일러 있는 영화", - year = 2024, - imageUrl = "", - director = "감독 이름", - reason = "이 내용은 스포일러가 포함되어 있습니다.", - isSpoiler = true, - isBookmarked = false, - bookmarkCount = 42, - ), - ContentModelNew( - id = "0", - title = "저장된 영화", - year = 2023, - imageUrl = "", - director = "다른 감독", - reason = "내가 저장한 영화입니다.", - isSpoiler = false, - isBookmarked = true, - bookmarkCount = 42, - ), - ) -} - - -@Preview -@Composable -private fun ContentPreview( - @PreviewParameter(ContentPreviewProvider::class) content: ContentModelNew, -) { - FlintTheme { - Content( - content = content, - onBookmarkIconClick = {}, - onSpoilClick = {} - ) - } -} - private data class ScreenPreviewData( + val thumbnailUrl: String, val title: String, val isBookmarked: Boolean, val authorNickname: String, @@ -964,6 +383,7 @@ private class ScreenPreviewProvider : PreviewParameterProvider = sequenceOf( ScreenPreviewData( + thumbnailUrl = "", title = "한번 보면 못 빠져나오는 여운남는 사랑이야기", isBookmarked = true, authorNickname = "키카", @@ -972,6 +392,7 @@ private class ScreenPreviewProvider : PreviewParameterProvider - CollectionDetailScreen( - paddingValues = paddingValues, - title = data.title, - isBookmarked = data.isBookmarked, - authorNickname = data.authorNickname, - authorUserRoleType = data.authorUserRoleType, - createdAt = "2026. 01. 07.", - description = "시간이 흘러도 빛이 바래지 않는,\n사랑의 미묘한 온도를 담은 제 최애 영화 모음집입니다", - contents = data.contents, - people = data.people, - onSaveDoneButtonClick = {}, - onSaveNoneButtonClick = {}, - navigateUp = {}, - onBookmarkIconClick = {}, - onSpoilClick = {}, - onAuthorClick = {}, - onAuthorNicknameClick = {} - ) - } - } -} +//@Preview +//@Composable +//private fun CollectionDetailScreenPreview( +// @PreviewParameter(ScreenPreviewProvider::class) data: ScreenPreviewData, +//) { +// FlintTheme { +// Scaffold { paddingValues: PaddingValues -> +// CollectionDetailScreen( +// paddingValues = paddingValues, +// title = data.title, +// isBookmarked = data.isBookmarked, +// authorNickname = data.authorNickname, +// authorUserRoleType = data.authorUserRoleType, +// createdAt = "2026. 01. 07.", +// description = "시간이 흘러도 빛이 바래지 않는,\n사랑의 미묘한 온도를 담은 제 최애 영화 모음집입니다", +// contents = data.contents, +// people = data.people, +// onSaveDoneButtonClick = {}, +// onSaveNoneButtonClick = {}, +// navigateUp = {}, +// onBookmarkIconClick = {}, +// onSpoilClick = {}, +// onAuthorClick = {}, +// onAuthorNicknameClick = {}, +// onAuthorClick = {}, +// targetImageUrl = {} +// ) +// } +// } +//} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDescription.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDescription.kt new file mode 100644 index 00000000..f8ee8160 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDescription.kt @@ -0,0 +1,138 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.background +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.height +import androidx.compose.foundation.shape.CircleShape +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.common.extension.noRippleClickable +import com.flint.core.designsystem.theme.FlintTheme +import com.flint.domain.type.UserRoleType + +@Composable +fun CollectionDetailDescription( + authorNickname: String, + authorUserRoleType: UserRoleType, + createdAt: String, + collectionContent: String, + onAuthorNicknameClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = authorNickname, + color = FlintTheme.colors.white, + style = FlintTheme.typography.head2Sb20, + modifier = Modifier.noRippleClickable( + onClick = { onAuthorNicknameClick() } + ) + ) + + if (authorUserRoleType == UserRoleType.FLINER) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_qualified), + contentDescription = "플리너", + tint = Color.Unspecified, + ) + } else { + Text( + "|", + color = FlintTheme.colors.gray200, + style = FlintTheme.typography.head3M18, + ) + } + + Text( + text = createdAt, + color = FlintTheme.colors.gray200, + style = FlintTheme.typography.body2M14, + ) + } + + Box( + modifier = + Modifier + .fillMaxWidth() + .height(1.dp) + .clip(CircleShape) + .background(color = FlintTheme.colors.gray300), + ) + + Text( + text = collectionContent, + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1R16, + ) + } +} + +private data class DescriptionPreviewData( + val authorNickname: String, + val authorUserRoleType: UserRoleType, + val createdAt: String, + val collectionContent: String, +) + +private class DescriptionPreviewProvider : PreviewParameterProvider { + override val values: Sequence = + sequenceOf( + DescriptionPreviewData( + authorNickname = "키카", + authorUserRoleType = UserRoleType.FLINER, + createdAt = "2026. 01. 07.", + collectionContent = "시간이 흘러도 빛이 바래지 않는,\n사랑의 미묘한 온도를 담은 제 최애 영화 모음집입니다", + ), + DescriptionPreviewData( + authorNickname = "일반유저", + authorUserRoleType = UserRoleType.FLING, + createdAt = "2026. 01. 15.", + collectionContent = "한글자 두글자 세글자 네글자 다섯글자 ".repeat(10), + ), + DescriptionPreviewData( + authorNickname = "관리자", + authorUserRoleType = UserRoleType.ADMIN, + createdAt = "2026. 01. 01.", + collectionContent = "공식 추천 컬렉션입니다", + ), + ) +} + +@Preview +@Composable +private fun CollectionDetailDescriptionPreview( + @PreviewParameter(DescriptionPreviewProvider::class) data: DescriptionPreviewData, +) { + FlintTheme { + CollectionDetailDescription( + authorNickname = data.authorNickname, + authorUserRoleType = data.authorUserRoleType, + createdAt = data.createdAt, + collectionContent = data.collectionContent, + onAuthorNicknameClick = {} + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailThumbnail.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailThumbnail.kt new file mode 100644 index 00000000..96ece0cd --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailThumbnail.kt @@ -0,0 +1,103 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.core.designsystem.component.button.FlintSaveDoneButton +import com.flint.core.designsystem.component.button.FlintSaveNoneButton +import com.flint.core.designsystem.component.image.NetworkImage +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun CollectionDetailThumbnail( + thumbnailImage: String, + title: String, + isBookmarked: Boolean, + onSaveDoneButtonClick: () -> Unit, + onSaveNoneButtonClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .aspectRatio(360f / 270f), + ) { + NetworkImage( + imageUrl = thumbnailImage, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.FillBounds, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .padding(top = 57.dp, bottom = 19.dp), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = title, + color = FlintTheme.colors.white, + style = FlintTheme.typography.display2M28, + modifier = Modifier.fillMaxWidth(), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + if (isBookmarked) { + FlintSaveDoneButton( + onClick = { + onSaveDoneButtonClick() + }, + ) + } else { + FlintSaveNoneButton( + onClick = { + onSaveNoneButtonClick() + }, + ) + } + } + } +} + +@Preview +@Composable +private fun ThumbnailPreview() { + FlintTheme { + Column { + CollectionDetailThumbnail( + thumbnailImage = "", + title = "한번 보면 못 빠져나오는 여운남는 사랑이야기".repeat(2), + isBookmarked = true, + onSaveDoneButtonClick = {}, + onSaveNoneButtonClick = {}, + ) + + Spacer(Modifier.height(20.dp)) + + CollectionDetailThumbnail( + thumbnailImage = "https://buly.kr/DEaVFRZ", + title = "한번 보면 못 빠져나오는 여운남는 사랑이야기", + isBookmarked = false, + onSaveDoneButtonClick = {}, + onSaveNoneButtonClick = {}, + ) + } + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/Content.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/Content.kt new file mode 100644 index 00000000..5438aaca --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/Content.kt @@ -0,0 +1,215 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.background +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.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +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.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.common.extension.noRippleClickable +import com.flint.core.designsystem.component.collection.Spoiler +import com.flint.core.designsystem.component.image.NetworkImage +import com.flint.core.designsystem.theme.FlintTheme +import com.flint.domain.model.content.ContentModelNew + +@Composable +fun Content( + content: ContentModelNew, + onBookmarkIconClick: (contentId: String) -> Unit, + onSpoilClick: (contentId: String) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + NetworkImage( + imageUrl = content.imageUrl, + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(360f / 480f), + contentScale = ContentScale.Crop, + ) + + Spacer(Modifier.height(32.dp)) + + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column { + Text( + content.title, + color = FlintTheme.colors.white, + style = FlintTheme.typography.head2Sb20, + ) + + Spacer(Modifier.height(12.dp)) + + Text( + content.year.toString(), + color = FlintTheme.colors.gray300, + style = FlintTheme.typography.body1R16, + ) + + Text( + content.director, + color = FlintTheme.colors.gray300, + style = FlintTheme.typography.body1R16, + ) + } + + Row( + modifier = + Modifier.noRippleClickable(onClick = { onBookmarkIconClick(content.id) }), + ) { + Column( + modifier = + Modifier + .padding(start = 24.dp) + .padding(vertical = 3.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + if (content.isBookmarked) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_bookmark_fill), + contentDescription = "저장됨", + tint = Color.Unspecified, + ) + } else { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_bookmark_empty), + contentDescription = "저장되지 않음", + tint = Color.White, + ) + } + + Text( + text = content.bookmarkCount.toString(), + color = FlintTheme.colors.white, + style = FlintTheme.typography.caption1M12, + ) + } + } + } + + Spacer(Modifier.height(24.dp)) + + Box( + modifier = + Modifier + .fillMaxWidth() + .height(1.dp) + .background(FlintTheme.colors.gray500), + ) + + Spacer(Modifier.height(24.dp)) + + if (content.isSpoiler) { + Spoiler( + onSpoilClick = { onSpoilClick(content.id) }, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = content.reason, + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1R16, + modifier = + Modifier + .defaultMinSize(minHeight = 183.dp) + .fillMaxWidth(), + ) + } + } else { + Text( + text = content.reason, + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1R16, + modifier = + Modifier + .defaultMinSize(minHeight = 183.dp) + .fillMaxWidth(), + ) + } + + Spacer(Modifier.height(64.dp)) + } + } +} + +private class ContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence = + sequenceOf( + ContentModelNew( + id = "0", + title = "드라마 제목", + year = 2000, + imageUrl = "", + director = "가스 제닝스", + reason = "달라진 온도\n-\n같은 구도에 채도를 달리해 변해버린 사랑을 시각적으로 담아낸 장면들", + isSpoiler = false, + isBookmarked = false, + bookmarkCount = 42, + ), + ContentModelNew( + id = "0", + title = "스포일러 있는 영화", + year = 2024, + imageUrl = "", + director = "감독 이름", + reason = "이 내용은 스포일러가 포함되어 있습니다.", + isSpoiler = true, + isBookmarked = false, + bookmarkCount = 42, + ), + ContentModelNew( + id = "0", + title = "저장된 영화", + year = 2023, + imageUrl = "", + director = "다른 감독", + reason = "내가 저장한 영화입니다.", + isSpoiler = false, + isBookmarked = true, + bookmarkCount = 42, + ), + ) +} + +@Preview +@Composable +private fun ContentPreview( + @PreviewParameter(ContentPreviewProvider::class) content: ContentModelNew, +) { + FlintTheme { + Content( + content = content, + onBookmarkIconClick = {}, + onSpoilClick = {} + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt new file mode 100644 index 00000000..5c82b14b --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt @@ -0,0 +1,208 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.border +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +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.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.designsystem.component.image.ProfileImage +import com.flint.core.designsystem.theme.FlintTheme +import com.flint.domain.model.bookmark.CollectionBookmarkUsersModel +import com.flint.domain.type.UserRoleType +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun PeopleWhoSavedThisCollection( + people: ImmutableList, + onMoreClick: () -> Unit, +) { + Column( + modifier = Modifier.padding(vertical = 10.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "이 컬렉션을 저장한 사람들", + color = FlintTheme.colors.white, + style = FlintTheme.typography.head2Sb20, + modifier = + Modifier + .weight(1f) + .padding(horizontal = 16.dp), + ) + + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_more), + contentDescription = null, + modifier = + Modifier + .size(48.dp) + .clickable(onClick = onMoreClick) + .padding(12.dp), + tint = FlintTheme.colors.white, + ) + } + + Spacer(Modifier.height(24.dp)) + + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy((-12).dp), + ) { + people.take(5).forEach { author: CollectionBookmarkUsersModel.User -> + ProfileImage( + imageUrl = author.profileImageUrl, + modifier = + Modifier + .size(56.dp) + .border(3.dp, FlintTheme.colors.background, CircleShape), + contentDescription = author.nickName, + ) + } + } + + Spacer(Modifier.width(8.dp)) + + if (people.size >= 6) { + Row { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_plus), + contentDescription = "그 외", + tint = FlintTheme.colors.white, + ) + + Text( + text = (people.size - 5).toString(), + color = FlintTheme.colors.gray50, + style = FlintTheme.typography.head2M20, + ) + } + } + } + } +} + +private class PeoplePreviewProvider : + PreviewParameterProvider> { + override val values: Sequence> = + sequenceOf( + persistentListOf( + CollectionBookmarkUsersModel.User( + userId = "1", + nickName = "유저1", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + ), + persistentListOf( + CollectionBookmarkUsersModel.User( + userId = "1", + nickName = "유저1", + profileImageUrl = "", + userRole = UserRoleType.ADMIN, + ), + CollectionBookmarkUsersModel.User( + userId = "2", + nickName = "유저2", + profileImageUrl = "", + userRole = UserRoleType.FLINER, + ), + CollectionBookmarkUsersModel.User( + userId = "3", + nickName = "유저3", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "4", + nickName = "유저4", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "5", + nickName = "유저5", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + ), + persistentListOf( + CollectionBookmarkUsersModel.User( + userId = "1", + nickName = "유저1", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "2", + nickName = "유저2", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "3", + nickName = "유저3", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "4", + nickName = "유저4", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "5", + nickName = "유저5", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "6", + nickName = "유저6", + profileImageUrl = "", + userRole = UserRoleType.FLINER, + ), + CollectionBookmarkUsersModel.User( + userId = "7", + nickName = "유저7", + profileImageUrl = "", + userRole = UserRoleType.ADMIN, + ), + ), + ) +} + +@Preview +@Composable +private fun PeopleWhoSavedThisCollectionPreview( + @PreviewParameter(PeoplePreviewProvider::class) people: ImmutableList, +) { + FlintTheme { + PeopleWhoSavedThisCollection( + people = people, + onMoreClick = {}, + ) + } +} From d79c50728fce22dff2ef1260fadc2d4581486e8d Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 03:13:25 +0900 Subject: [PATCH 02/19] =?UTF-8?q?[feat]=20CollectionCopyrightFooter=20?= =?UTF-8?q?=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/CollectionCopyrightFooter.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionCopyrightFooter.kt diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionCopyrightFooter.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionCopyrightFooter.kt new file mode 100644 index 00000000..0758a767 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionCopyrightFooter.kt @@ -0,0 +1,35 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun CollectionCopyrightFooter( + modifier: Modifier = Modifier, +) { + Text( + text = "Flint에서 제공하는 영화 · 드라마를 포함한 모든 콘텐츠의 저작권은 각 권리자에게 있으며, 관련 법령에 따라 보호됩니다.\n " + + "컬렉션 이용 시 저작권을 준수해 주세요.", + color = FlintTheme.colors.gray300, + style = FlintTheme.typography.body2R14, + modifier = modifier + .fillMaxWidth() + .background(color = FlintTheme.colors.gray800) + .padding(horizontal = 16.dp, vertical = 32.dp), + ) +} + +@Preview +@Composable +private fun CollectionCopyrightFooterPreview() { + FlintTheme { + CollectionCopyrightFooter() + } +} From 1f6ab2c4c6ca7d36f42554190857671f998b5ac4 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 03:13:39 +0900 Subject: [PATCH 03/19] =?UTF-8?q?[feat]=20PeopleWhoSavedThisCollection.kt?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/PeopleWhoSavedThisCollection.kt | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt index 5c82b14b..0f02413a 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt @@ -6,6 +6,7 @@ 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.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -36,45 +37,47 @@ fun PeopleWhoSavedThisCollection( onMoreClick: () -> Unit, ) { Column( - modifier = Modifier.padding(vertical = 10.dp), + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 24.dp, bottom = 32.dp), ) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { Text( text = "이 컬렉션을 저장한 사람들", color = FlintTheme.colors.white, style = FlintTheme.typography.head2Sb20, - modifier = - Modifier - .weight(1f) - .padding(horizontal = 16.dp), + modifier = Modifier, ) Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_more), contentDescription = null, - modifier = - Modifier - .size(48.dp) + modifier = Modifier + .size(24.dp) .clickable(onClick = onMoreClick) - .padding(12.dp), + .padding(vertical = 1.dp) + .padding(end = 3.dp), tint = FlintTheme.colors.white, ) } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(32.dp)) Row( - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Row( horizontalArrangement = Arrangement.spacedBy((-12).dp), ) { - people.take(5).forEach { author: CollectionBookmarkUsersModel.User -> + people.take(6).forEach { author: CollectionBookmarkUsersModel.User -> ProfileImage( imageUrl = author.profileImageUrl, - modifier = - Modifier + modifier = Modifier .size(56.dp) .border(3.dp, FlintTheme.colors.background, CircleShape), contentDescription = author.nickName, @@ -84,7 +87,7 @@ fun PeopleWhoSavedThisCollection( Spacer(Modifier.width(8.dp)) - if (people.size >= 6) { + if (people.size >= 7) { Row { Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_plus), @@ -93,7 +96,7 @@ fun PeopleWhoSavedThisCollection( ) Text( - text = (people.size - 5).toString(), + text = (people.size - 6).toString(), color = FlintTheme.colors.gray50, style = FlintTheme.typography.head2M20, ) From 6a94302f225f857429c6ca493488a1c8358af3d5 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 05:51:06 +0900 Subject: [PATCH 04/19] =?UTF-8?q?[feat]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=BC=80=EB=B0=A5=20=EB=A9=94=EB=89=B4(?= =?UTF-8?q?=EC=88=98=EC=A0=95/=EC=82=AD=EC=A0=9C/=EC=8B=A0=EA=B3=A0)=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=82=B4=20=EC=BB=AC=EB=A0=89?= =?UTF-8?q?=EC=85=98=20=EC=97=AC=EB=B6=80(isMine)=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionDetailScreen.kt | 141 ++++++++---------- .../CollectionDetailViewModel.kt | 13 +- .../CollectionDetailDropdownMenuItem.kt | 76 ++++++++++ .../component/CollectionDetailThumbnail.kt | 27 +++- .../component/CollectionDetailTopAppBar.kt | 125 ++++++++++++++++ .../uistate/CollectionDetailUiState.kt | 1 + app/src/main/res/drawable/ic_kebab.xml | 29 +++- 7 files changed, 326 insertions(+), 86 deletions(-) create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDropdownMenuItem.kt create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailTopAppBar.kt diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt index 4c07e3eb..049318d3 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt @@ -3,7 +3,6 @@ package com.flint.presentation.collectiondetail import androidx.compose.foundation.LocalOverscrollFactory import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -14,17 +13,15 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.tooling.preview.Preview @@ -39,13 +36,13 @@ import com.flint.core.designsystem.component.indicator.FlintLoadingIndicator import com.flint.core.designsystem.component.progressbar.UnderImageProgressBar import com.flint.core.designsystem.component.toast.ShowSaveToast import com.flint.core.designsystem.component.toast.ShowToast -import com.flint.core.designsystem.component.topappbar.FlintBackTopAppbar import com.flint.core.designsystem.theme.FlintTheme import com.flint.core.navigation.model.CollectionListRouteType import com.flint.domain.model.bookmark.CollectionBookmarkUsersModel import com.flint.domain.model.collection.CollectionDetailModelNew import com.flint.domain.model.content.ContentModelNew import com.flint.domain.type.UserRoleType +import com.flint.presentation.collectiondetail.component.CollectionCopyrightFooter import com.flint.presentation.collectiondetail.component.CollectionDetailDescription import com.flint.presentation.collectiondetail.component.CollectionDetailThumbnail import com.flint.presentation.collectiondetail.component.Content @@ -104,6 +101,16 @@ fun CollectionDetailRoute( onSpoilClick = viewModel::spoil, onAuthorNicknameClick = { navigateToProfile(collectionDetail.author.id) }, onAuthorClick = navigateToProfile, + isMine = uiState.data.isMine, + onEditClick = { + // TODO: 컬렉션 수정 화면 연결 + }, + onDeleteClick = { + // TODO: 컬렉션 삭제 로직 연결 + }, + onReportClick = { + // TODO: 신고(Route.CollectionReport) 화면 복구 후 navigateToCollectionReport(collectionDetail.id) 연결 + }, ) } @@ -209,6 +216,10 @@ fun CollectionDetailScreen( onSpoilClick: (String) -> Unit, onAuthorNicknameClick: () -> Unit, onAuthorClick: (authorId: String) -> Unit, + isMine: Boolean, + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + onReportClick: () -> Unit, targetImageUrl: String? = null, ) { CompositionLocalProvider( @@ -216,7 +227,6 @@ fun CollectionDetailScreen( ) { var showPeopleBottomSheet: Boolean by remember { mutableStateOf(false) } val scrollState: ScrollState = rememberScrollState() - var thumbnailHeight: Int by remember { mutableIntStateOf(0) } val contentPositions: MutableMap = remember { mutableMapOf() } val scrollProgress: Float = @@ -226,8 +236,6 @@ fun CollectionDetailScreen( 0f } - val isProgressBarSticky: Boolean = scrollState.value >= thumbnailHeight - LaunchedEffect(Unit) { if (targetImageUrl == null) return@LaunchedEffect val targetPosition: Int = contentPositions[targetImageUrl] ?: return@LaunchedEffect @@ -252,83 +260,64 @@ fun CollectionDetailScreen( .padding(paddingValues) .background(color = FlintTheme.colors.background), ) { -// FlintBackTopAppbar( -// onClick = navigateUp, -// backgroundColor = Color.Transparent, -// ) + // 썸네일 영역은 스크롤해도 화면 상단에 고정 + CollectionDetailThumbnail( + thumbnailImage = thumbnailUrl, + title = title, + isBookmarked = isBookmarked, + isMine = isMine, + onBackClick = navigateUp, + onSaveDoneButtonClick = onSaveDoneButtonClick, + onSaveNoneButtonClick = onSaveNoneButtonClick, + onEditClick = onEditClick, + onDeleteClick = onDeleteClick, + onReportClick = onReportClick, + ) - Box { - FlintBackTopAppbar( - onClick = navigateUp, - backgroundColor = Color.Transparent, + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .verticalScroll(scrollState), + ) { + UnderImageProgressBar(progress = scrollProgress) + + Spacer(Modifier.height(24.dp)) + + CollectionDetailDescription( + authorNickname = authorNickname, + authorUserRoleType = authorUserRoleType, + createdAt = createdAt, + collectionContent = description, + onAuthorNicknameClick = onAuthorNicknameClick, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), ) - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding(bottom = 24.dp), - ) { - CollectionDetailThumbnail( - thumbnailImage = thumbnailUrl, - title = title, - isBookmarked = isBookmarked, - onSaveDoneButtonClick = onSaveDoneButtonClick, - onSaveNoneButtonClick = onSaveNoneButtonClick, - modifier = Modifier.onGloballyPositioned { coordinates: LayoutCoordinates -> - thumbnailHeight = coordinates.size.height - }, - ) - if (!isProgressBarSticky) { - UnderImageProgressBar(progress = scrollProgress) - } else { - // sticky 상태일 때 공간 유지 - Spacer(Modifier.height(5.dp)) - } + Spacer(Modifier.height(48.dp)) - Spacer(Modifier.height(24.dp)) - - CollectionDetailDescription( - authorNickname = authorNickname, - authorUserRoleType = authorUserRoleType, - createdAt = createdAt, - collectionContent = description, - onAuthorNicknameClick = onAuthorNicknameClick, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + contents.forEach { content: ContentModelNew -> + Content( + content = content, + onBookmarkIconClick = onBookmarkIconClick, + onSpoilClick = onSpoilClick, + modifier = Modifier.onGloballyPositioned { coordinates -> + contentPositions[content.imageUrl] = + coordinates.positionInParent().y.toInt() + }, ) - - Spacer(Modifier.height(48.dp)) - - contents.forEach { content: ContentModelNew -> - Content( - content = content, - onBookmarkIconClick = onBookmarkIconClick, - onSpoilClick = onSpoilClick, - modifier = Modifier.onGloballyPositioned { coordinates -> - contentPositions[content.imageUrl] = - coordinates.positionInParent().y.toInt() - }, - ) - } - - if (people.isNotEmpty()) { - PeopleWhoSavedThisCollection( - people = people, - onMoreClick = { showPeopleBottomSheet = true }, - ) - } } - // Sticky ProgressBar - if (isProgressBarSticky) { - UnderImageProgressBar( - progress = scrollProgress, - modifier = Modifier.fillMaxWidth(), + if (people.isNotEmpty()) { + PeopleWhoSavedThisCollection( + people = people, + onMoreClick = { showPeopleBottomSheet = true }, ) } + + CollectionCopyrightFooter() } } } diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt index 2f115f4d..6fa1ef8d 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt @@ -4,8 +4,10 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.flint.core.common.util.DataStoreKey.USER_ID import com.flint.core.common.util.UiState import com.flint.core.navigation.Route +import com.flint.data.local.PreferencesManager import com.flint.domain.model.bookmark.CollectionBookmarkUsersModel import com.flint.domain.model.collection.CollectionDetailModelNew import com.flint.domain.model.content.ContentModelNew @@ -25,6 +27,7 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -34,6 +37,7 @@ class CollectionDetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val bookmarkRepository: BookmarkRepository, private val collectionRepository: CollectionRepository, + private val preferencesManager: PreferencesManager, ) : ViewModel() { init { val collectionId: String = savedStateHandle.toRoute().collectionId @@ -203,11 +207,16 @@ class CollectionDetailViewModel @Inject constructor( async { collectionRepository.getCollectionDetail(collectionId) } val collectionBookmarkUsers: Deferred> = async { bookmarkRepository.getCollectionBookmarkUsers(collectionId) } + val myUserId: Deferred = + async { preferencesManager.getString(USER_ID).first() } + + val collectionDetailResult: CollectionDetailModelNew = collectionDetail.await().getOrThrow() UiState.Success( CollectionDetailUiState( - collectionDetail = collectionDetail.await().getOrThrow(), - collectionBookmarkUsers = collectionBookmarkUsers.await().getOrThrow() + collectionDetail = collectionDetailResult, + collectionBookmarkUsers = collectionBookmarkUsers.await().getOrThrow(), + isMine = collectionDetailResult.author.id == myUserId.await(), ) ) }.onSuccess { newUiState: UiState.Success -> diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDropdownMenuItem.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDropdownMenuItem.kt new file mode 100644 index 00000000..3e970386 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDropdownMenuItem.kt @@ -0,0 +1,76 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.flint.core.common.extension.noRippleClickable +import com.flint.core.designsystem.theme.FlintTheme + +private val DropdownMenuItemShape = RoundedCornerShape(8.dp) +private val DropdownMenuItemPadding = Modifier.padding(horizontal = 24.dp, vertical = 6.dp) + +@Composable +fun CollectionReportDropdownMenuItem(onClick: () -> Unit) { + Box( + modifier = Modifier + .background(color = FlintTheme.colors.gray700, shape = DropdownMenuItemShape) + .noRippleClickable(onClick = onClick) + .then(DropdownMenuItemPadding), + contentAlignment = Alignment.Center, + ) { + Text( + text = "신고", + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1M16, + ) + } +} + +@Composable +fun CollectionEditDeleteDropdownMenuItem( + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, +) { + Column( + modifier = Modifier.background(color = FlintTheme.colors.gray700, shape = DropdownMenuItemShape), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable(onClick = onEditClick) + .then(DropdownMenuItemPadding), + contentAlignment = Alignment.Center, + ) { + Text( + text = "수정", + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1M16, + ) + } + + HorizontalDivider(color = FlintTheme.colors.gray400, thickness = 1.dp) + + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable(onClick = onDeleteClick) + .then(DropdownMenuItemPadding), + contentAlignment = Alignment.Center, + ) { + Text( + text = "삭제", + color = FlintTheme.colors.error500, + style = FlintTheme.typography.body1M16, + ) + } + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailThumbnail.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailThumbnail.kt index 96ece0cd..6548a3dd 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailThumbnail.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailThumbnail.kt @@ -13,6 +13,7 @@ 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.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -27,8 +28,13 @@ fun CollectionDetailThumbnail( thumbnailImage: String, title: String, isBookmarked: Boolean, + isMine: Boolean, + onBackClick: () -> Unit, onSaveDoneButtonClick: () -> Unit, onSaveNoneButtonClick: () -> Unit, + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + onReportClick: () -> Unit, modifier: Modifier = Modifier, ) { Box( @@ -43,11 +49,20 @@ fun CollectionDetailThumbnail( contentScale = ContentScale.FillBounds, ) + CollectionDetailTopAppBar( + isMine = isMine, + onBackClick = onBackClick, + onEditClick = onEditClick, + onDeleteClick = onDeleteClick, + onReportClick = onReportClick, + backgroundColor = Color.Transparent, + ) + Column( modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp) - .padding(top = 57.dp, bottom = 19.dp), + .padding(top = 57.dp, bottom = 22.dp), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.SpaceBetween, ) { @@ -85,8 +100,13 @@ private fun ThumbnailPreview() { thumbnailImage = "", title = "한번 보면 못 빠져나오는 여운남는 사랑이야기".repeat(2), isBookmarked = true, + isMine = true, + onBackClick = {}, onSaveDoneButtonClick = {}, onSaveNoneButtonClick = {}, + onEditClick = {}, + onDeleteClick = {}, + onReportClick = {}, ) Spacer(Modifier.height(20.dp)) @@ -95,8 +115,13 @@ private fun ThumbnailPreview() { thumbnailImage = "https://buly.kr/DEaVFRZ", title = "한번 보면 못 빠져나오는 여운남는 사랑이야기", isBookmarked = false, + isMine = false, + onBackClick = {}, onSaveDoneButtonClick = {}, onSaveNoneButtonClick = {}, + onEditClick = {}, + onDeleteClick = {}, + onReportClick = {}, ) } } diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailTopAppBar.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailTopAppBar.kt new file mode 100644 index 00000000..50f1b440 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailTopAppBar.kt @@ -0,0 +1,125 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.background +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.material3.DropdownMenu +import androidx.compose.material3.Icon +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.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.common.extension.noRippleClickable +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun CollectionDetailTopAppBar( + isMine: Boolean, + onBackClick: () -> Unit, + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + onReportClick: () -> Unit, + modifier: Modifier = Modifier, + backgroundColor: Color = FlintTheme.colors.background, +) { + var showSettingsMenu: Boolean by remember { mutableStateOf(false) } + + Row( + modifier = modifier + .fillMaxWidth() + .background(backgroundColor) + .padding(horizontal = 12.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier + .size(48.dp) + .noRippleClickable(onClick = onBackClick) + .padding(12.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_back), + contentDescription = null, + tint = FlintTheme.colors.white, + ) + + Box( + modifier = Modifier.size(48.dp), + ) { + Icon( + modifier = Modifier + .size(48.dp) + .noRippleClickable(onClick = { showSettingsMenu = true }), + imageVector = ImageVector.vectorResource(R.drawable.ic_kebab), + contentDescription = null, + tint = FlintTheme.colors.white, + ) + + DropdownMenu( + expanded = showSettingsMenu, + onDismissRequest = { showSettingsMenu = false }, + containerColor = Color.Transparent, + shape = RectangleShape, + tonalElevation = 0.dp, + shadowElevation = 0.dp, + ) { + if (isMine) { + CollectionEditDeleteDropdownMenuItem( + onEditClick = { + showSettingsMenu = false + onEditClick() + }, + onDeleteClick = { + showSettingsMenu = false + onDeleteClick() + }, + ) + } else { + CollectionReportDropdownMenuItem( + onClick = { + showSettingsMenu = false + onReportClick() + }, + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun CollectionDetailTopAppBarPreview() { + FlintTheme { + Column { + CollectionDetailTopAppBar( + isMine = true, + onBackClick = {}, + onEditClick = {}, + onDeleteClick = {}, + onReportClick = {}, + ) + CollectionDetailTopAppBar( + isMine = false, + onBackClick = {}, + onEditClick = {}, + onDeleteClick = {}, + onReportClick = {}, + ) + } + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/uistate/CollectionDetailUiState.kt b/app/src/main/java/com/flint/presentation/collectiondetail/uistate/CollectionDetailUiState.kt index f750228c..c515ce57 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/uistate/CollectionDetailUiState.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/uistate/CollectionDetailUiState.kt @@ -6,4 +6,5 @@ import com.flint.domain.model.collection.CollectionDetailModelNew data class CollectionDetailUiState( val collectionDetail: CollectionDetailModelNew, val collectionBookmarkUsers: CollectionBookmarkUsersModel, + val isMine: Boolean, ) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_kebab.xml b/app/src/main/res/drawable/ic_kebab.xml index 86ab0961..b1cdf068 100644 --- a/app/src/main/res/drawable/ic_kebab.xml +++ b/app/src/main/res/drawable/ic_kebab.xml @@ -1,15 +1,30 @@ + + android:width="48dp" + android:height="48dp" + android:viewportWidth="48" + android:viewportHeight="48"> From 3a072f0a59688088dc4d61bd9d0193c683be977e Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 07:21:27 +0900 Subject: [PATCH 05/19] =?UTF-8?q?[refactor]=20=EC=BB=AC=EB=A0=89=EC=85=98?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=BB=A8=ED=85=90=EC=B8=A0=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20LazyColumn=20=EC=A0=84=ED=99=98=20=EB=B0=8F=20?= =?UTF-8?q?=EB=AF=B8=EC=82=AC=EC=9A=A9=20=EC=A7=84=ED=96=89=EB=8F=84=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=EB=B0=94=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionDetailScreen.kt | 164 +++++++++--------- 1 file changed, 85 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt index 049318d3..69612d6d 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt @@ -1,7 +1,6 @@ package com.flint.presentation.collectiondetail import androidx.compose.foundation.LocalOverscrollFactory -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -10,8 +9,10 @@ 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.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable @@ -22,8 +23,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -33,7 +32,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flint.core.common.util.UiState import com.flint.core.designsystem.component.collection.PeopleBottomSheet import com.flint.core.designsystem.component.indicator.FlintLoadingIndicator -import com.flint.core.designsystem.component.progressbar.UnderImageProgressBar import com.flint.core.designsystem.component.toast.ShowSaveToast import com.flint.core.designsystem.component.toast.ShowToast import com.flint.core.designsystem.theme.FlintTheme @@ -45,7 +43,7 @@ import com.flint.domain.type.UserRoleType import com.flint.presentation.collectiondetail.component.CollectionCopyrightFooter import com.flint.presentation.collectiondetail.component.CollectionDetailDescription import com.flint.presentation.collectiondetail.component.CollectionDetailThumbnail -import com.flint.presentation.collectiondetail.component.Content +import com.flint.presentation.collectiondetail.component.CollectionDetailContent import com.flint.presentation.collectiondetail.component.PeopleWhoSavedThisCollection import com.flint.presentation.collectiondetail.sideeffect.CollectionDetailSideEffect import com.flint.presentation.collectiondetail.uistate.CollectionDetailUiState @@ -54,6 +52,7 @@ import kotlinx.collections.immutable.persistentListOf import java.time.format.DateTimeFormatter private const val DATE_FORMAT_TO_SHOW = "yyyy. MM. dd." +private const val CONTENT_LIST_HEADER_ITEM_COUNT = 1 @Composable fun CollectionDetailRoute( @@ -226,21 +225,14 @@ fun CollectionDetailScreen( LocalOverscrollFactory provides null, ) { var showPeopleBottomSheet: Boolean by remember { mutableStateOf(false) } - val scrollState: ScrollState = rememberScrollState() - val contentPositions: MutableMap = remember { mutableMapOf() } - - val scrollProgress: Float = - if (scrollState.maxValue > 0) { - scrollState.value.toFloat() / scrollState.maxValue - } else { - 0f - } + val lazyListState: LazyListState = rememberLazyListState() LaunchedEffect(Unit) { if (targetImageUrl == null) return@LaunchedEffect - val targetPosition: Int = contentPositions[targetImageUrl] ?: return@LaunchedEffect + val targetIndex: Int = contents.indexOfFirst { it.imageUrl == targetImageUrl } + if (targetIndex == -1) return@LaunchedEffect - scrollState.animateScrollTo(targetPosition) + lazyListState.animateScrollToItem(CONTENT_LIST_HEADER_ITEM_COUNT + targetIndex) } if (showPeopleBottomSheet) { @@ -274,50 +266,57 @@ fun CollectionDetailScreen( onReportClick = onReportClick, ) - Column( + LazyColumn( + state = lazyListState, modifier = Modifier .weight(1f) - .fillMaxWidth() - .verticalScroll(scrollState), + .fillMaxWidth(), ) { - UnderImageProgressBar(progress = scrollProgress) - - Spacer(Modifier.height(24.dp)) - - CollectionDetailDescription( - authorNickname = authorNickname, - authorUserRoleType = authorUserRoleType, - createdAt = createdAt, - collectionContent = description, - onAuthorNicknameClick = onAuthorNicknameClick, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - - Spacer(Modifier.height(48.dp)) + item { + Column { + Spacer(Modifier.height(24.dp)) + + CollectionDetailDescription( + authorNickname = authorNickname, + authorUserRoleType = authorUserRoleType, + createdAt = createdAt, + collectionContent = description, + onAuthorNicknameClick = onAuthorNicknameClick, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + } - contents.forEach { content: ContentModelNew -> - Content( + items( + items = contents, + key = { content: ContentModelNew -> content.id }, + ) { content: ContentModelNew -> + CollectionDetailContent( content = content, onBookmarkIconClick = onBookmarkIconClick, onSpoilClick = onSpoilClick, - modifier = Modifier.onGloballyPositioned { coordinates -> - contentPositions[content.imageUrl] = - coordinates.positionInParent().y.toInt() - }, ) } + item { + Spacer(Modifier.height(24.dp)) + } + if (people.isNotEmpty()) { - PeopleWhoSavedThisCollection( - people = people, - onMoreClick = { showPeopleBottomSheet = true }, - ) + item { + PeopleWhoSavedThisCollection( + people = people, + onMoreClick = { showPeopleBottomSheet = true }, + ) + } } - CollectionCopyrightFooter() + item { + CollectionCopyrightFooter() + } } } } @@ -331,6 +330,7 @@ private data class ScreenPreviewData( val authorUserRoleType: UserRoleType, val contents: ImmutableList, val people: ImmutableList, + val isMine: Boolean, ) private class ScreenPreviewProvider : PreviewParameterProvider { @@ -379,6 +379,7 @@ private class ScreenPreviewProvider : PreviewParameterProvider -// CollectionDetailScreen( -// paddingValues = paddingValues, -// title = data.title, -// isBookmarked = data.isBookmarked, -// authorNickname = data.authorNickname, -// authorUserRoleType = data.authorUserRoleType, -// createdAt = "2026. 01. 07.", -// description = "시간이 흘러도 빛이 바래지 않는,\n사랑의 미묘한 온도를 담은 제 최애 영화 모음집입니다", -// contents = data.contents, -// people = data.people, -// onSaveDoneButtonClick = {}, -// onSaveNoneButtonClick = {}, -// navigateUp = {}, -// onBookmarkIconClick = {}, -// onSpoilClick = {}, -// onAuthorClick = {}, -// onAuthorNicknameClick = {}, -// onAuthorClick = {}, -// targetImageUrl = {} -// ) -// } -// } -//} +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun CollectionDetailScreenPreview( + @PreviewParameter(ScreenPreviewProvider::class) data: ScreenPreviewData, +) { + FlintTheme { + Scaffold { paddingValues: PaddingValues -> + CollectionDetailScreen( + paddingValues = paddingValues, + thumbnailUrl = data.thumbnailUrl, + title = data.title, + isBookmarked = data.isBookmarked, + authorNickname = data.authorNickname, + authorUserRoleType = data.authorUserRoleType, + createdAt = "2026. 01. 07.", + description = "시간이 흘러도 빛이 바래지 않는,\n사랑의 미묘한 온도를 담은 제 최애 영화 모음집입니다", + contents = data.contents, + people = data.people, + onSaveDoneButtonClick = {}, + onSaveNoneButtonClick = {}, + navigateUp = {}, + onBookmarkIconClick = {}, + onSpoilClick = {}, + onAuthorNicknameClick = {}, + onAuthorClick = {}, + isMine = data.isMine, + onEditClick = {}, + onDeleteClick = {}, + onReportClick = {}, + ) + } + } +} From 2a0adf9d16e0902ff779892a09e4ba3cb66ff51c Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 07:21:56 +0900 Subject: [PATCH 06/19] =?UTF-8?q?[refactor]=20=EC=BB=AC=EB=A0=89=EC=85=98?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=BB=A8=ED=85=90=EC=B8=A0=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20LazyColumn=20=EC=A0=84=ED=99=98=20=EB=B0=8F=20?= =?UTF-8?q?=EB=AF=B8=EC=82=AC=EC=9A=A9=20=EC=A7=84=ED=96=89=EB=8F=84=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=EB=B0=94=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../progressbar/UnderImageProgressBar.kt | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 app/src/main/java/com/flint/core/designsystem/component/progressbar/UnderImageProgressBar.kt diff --git a/app/src/main/java/com/flint/core/designsystem/component/progressbar/UnderImageProgressBar.kt b/app/src/main/java/com/flint/core/designsystem/component/progressbar/UnderImageProgressBar.kt deleted file mode 100644 index 3a65d791..00000000 --- a/app/src/main/java/com/flint/core/designsystem/component/progressbar/UnderImageProgressBar.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.flint.core.designsystem.component.progressbar - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import com.flint.core.designsystem.theme.FlintTheme - -@Composable -fun UnderImageProgressBar( - progress: Float, - modifier: Modifier = Modifier, -) { - Box( - modifier = - modifier - .height(5.dp) - .background(FlintTheme.colors.gray700), - ) { - Box( - modifier = - Modifier - .fillMaxWidth(progress) - .fillMaxHeight() - .clip(CircleShape) - .background(FlintTheme.colors.secondary500), - ) - } -} - -@Preview -@Composable -private fun UnderImageProgressBarPreview( - @PreviewParameter(UnderImageProgressBarPreviewParameterProvider::class) progress: Float, -) { - FlintTheme { - UnderImageProgressBar( - progress = progress, - modifier = Modifier.fillMaxWidth(), - ) - } -} - -private class UnderImageProgressBarPreviewParameterProvider : PreviewParameterProvider { - override val values: Sequence = sequenceOf(0f, 0.25f, 0.5f, 0.75f, 1f) -} From 19059d605ce977b13422a410216bcf0a5fcd52e0 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 07:22:36 +0900 Subject: [PATCH 07/19] =?UTF-8?q?[refactor]=20Content.kt=20->=20Collection?= =?UTF-8?q?DetailContent.kt=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/CollectionDetailContent.kt | 266 ++++++++++++++++++ .../collectiondetail/component/Content.kt | 215 -------------- 2 files changed, 266 insertions(+), 215 deletions(-) create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt delete mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/component/Content.kt diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt new file mode 100644 index 00000000..43ada4a8 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt @@ -0,0 +1,266 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.background +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.Spacer +import androidx.compose.foundation.layout.aspectRatio +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.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.common.extension.noRippleClickable +import com.flint.core.designsystem.component.collection.Spoiler +import com.flint.core.designsystem.component.image.NetworkImage +import com.flint.core.designsystem.theme.FlintTheme +import com.flint.domain.model.content.ContentModelNew + +// 백엔드에서 스틸컷 이미지 목록을 제공하기 전까지, 동일 이미지를 반복해 캐러셀 UI만 우선 구성 +private const val MOCK_SCENE_IMAGE_COUNT = 5 + +@Composable +fun CollectionDetailContent( + content: ContentModelNew, + onBookmarkIconClick: (contentId: String) -> Unit, + onSpoilClick: (contentId: String) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Spacer(Modifier.height(48.dp)) + + CollectionDetailContentCarousel(content = content) + + Spacer(Modifier.height(32.dp)) + + CollectionDetailContentInfo( + content = content, + onBookmarkIconClick = onBookmarkIconClick, + onSpoilClick = onSpoilClick, + ) + } +} + +@Composable +private fun CollectionDetailContentCarousel(content: ContentModelNew) { + val pageCount = Int.MAX_VALUE + val pagerState = rememberPagerState( + initialPage = pageCount / 2 - (pageCount / 2) % MOCK_SCENE_IMAGE_COUNT, + ) { pageCount } + val currentIndex = pagerState.currentPage % MOCK_SCENE_IMAGE_COUNT + + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth(), + ) { + NetworkImage( + imageUrl = content.imageUrl, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(360f / 270f), + contentScale = ContentScale.Crop, + ) + } + + Spacer(Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + ) { + repeat(MOCK_SCENE_IMAGE_COUNT) { index -> + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background( + if (index == currentIndex) { + FlintTheme.colors.secondary400 + } else { + FlintTheme.colors.gray500 + }, + ), + ) + } + } +} + +@Composable +private fun CollectionDetailContentInfo( + content: ContentModelNew, + onBookmarkIconClick: (contentId: String) -> Unit, + onSpoilClick: (contentId: String) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row { + NetworkImage( + imageUrl = content.imageUrl, + modifier = Modifier.size(width = 60.dp, height = 90.dp), + contentScale = ContentScale.Crop, + ) + + Spacer(modifier = Modifier.width(14.dp)) + + Column { + Text( + text = content.title, + color = FlintTheme.colors.white, + style = FlintTheme.typography.head2Sb20, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = content.year.toString(), + color = FlintTheme.colors.gray300, + style = FlintTheme.typography.body1R16, + ) + + Text( + text = content.director, + color = FlintTheme.colors.gray300, + style = FlintTheme.typography.body1R16, + ) + } + } + + Column( + modifier = Modifier + .noRippleClickable(onClick = { onBookmarkIconClick(content.id) }) + .padding(top = 6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (content.isBookmarked) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_bookmark_fill), + contentDescription = "저장됨", + tint = Color.Unspecified, + ) + } else { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_bookmark_empty), + contentDescription = "저장되지 않음", + tint = Color.White, + ) + } + + Text( + text = content.bookmarkCount.toString(), + color = FlintTheme.colors.gray200, + style = FlintTheme.typography.caption1M12, + ) + } + } + + Spacer(Modifier.height(24.dp)) + + HorizontalDivider(color = FlintTheme.colors.gray500, thickness = 1.dp) + + Spacer(Modifier.height(24.dp)) + + if (content.isSpoiler) { + Spoiler( + onSpoilClick = { onSpoilClick(content.id) }, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = content.reason, + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1R16, + modifier = Modifier.fillMaxWidth(), + ) + } + } else { + Text( + text = content.reason, + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1R16, + modifier = Modifier.fillMaxWidth(), + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + } +} + +private class CollectionDetailContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence = + sequenceOf( + ContentModelNew( + id = "0", + title = "드라마 제목", + year = 2000, + imageUrl = "", + director = "가스 제닝스", + reason = "달라진 온도\n-\n같은 구도에 채도를 달리해 변해버린 사랑을 시각적으로 담아낸 장면들", + isSpoiler = false, + isBookmarked = false, + bookmarkCount = 42, + ), + ContentModelNew( + id = "0", + title = "스포일러 있는 영화", + year = 2024, + imageUrl = "", + director = "감독 이름", + reason = "이 내용은 스포일러가 포함되어 있습니다.", + isSpoiler = true, + isBookmarked = false, + bookmarkCount = 42, + ), + ContentModelNew( + id = "0", + title = "저장된 영화", + year = 2023, + imageUrl = "", + director = "다른 감독", + reason = "내가 저장한 영화입니다.", + isSpoiler = false, + isBookmarked = true, + bookmarkCount = 42, + ), + ) +} + +@Preview +@Composable +private fun CollectionDetailContentPreview( + @PreviewParameter(CollectionDetailContentPreviewProvider::class) content: ContentModelNew, +) { + FlintTheme { + CollectionDetailContent( + content = content, + onBookmarkIconClick = {}, + onSpoilClick = {} + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/Content.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/Content.kt deleted file mode 100644 index 5438aaca..00000000 --- a/app/src/main/java/com/flint/presentation/collectiondetail/component/Content.kt +++ /dev/null @@ -1,215 +0,0 @@ -package com.flint.presentation.collectiondetail.component - -import androidx.compose.foundation.background -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.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -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.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import com.flint.R -import com.flint.core.common.extension.noRippleClickable -import com.flint.core.designsystem.component.collection.Spoiler -import com.flint.core.designsystem.component.image.NetworkImage -import com.flint.core.designsystem.theme.FlintTheme -import com.flint.domain.model.content.ContentModelNew - -@Composable -fun Content( - content: ContentModelNew, - onBookmarkIconClick: (contentId: String) -> Unit, - onSpoilClick: (contentId: String) -> Unit, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - NetworkImage( - imageUrl = content.imageUrl, - modifier = - Modifier - .fillMaxWidth() - .aspectRatio(360f / 480f), - contentScale = ContentScale.Crop, - ) - - Spacer(Modifier.height(32.dp)) - - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column { - Text( - content.title, - color = FlintTheme.colors.white, - style = FlintTheme.typography.head2Sb20, - ) - - Spacer(Modifier.height(12.dp)) - - Text( - content.year.toString(), - color = FlintTheme.colors.gray300, - style = FlintTheme.typography.body1R16, - ) - - Text( - content.director, - color = FlintTheme.colors.gray300, - style = FlintTheme.typography.body1R16, - ) - } - - Row( - modifier = - Modifier.noRippleClickable(onClick = { onBookmarkIconClick(content.id) }), - ) { - Column( - modifier = - Modifier - .padding(start = 24.dp) - .padding(vertical = 3.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - if (content.isBookmarked) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_bookmark_fill), - contentDescription = "저장됨", - tint = Color.Unspecified, - ) - } else { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_bookmark_empty), - contentDescription = "저장되지 않음", - tint = Color.White, - ) - } - - Text( - text = content.bookmarkCount.toString(), - color = FlintTheme.colors.white, - style = FlintTheme.typography.caption1M12, - ) - } - } - } - - Spacer(Modifier.height(24.dp)) - - Box( - modifier = - Modifier - .fillMaxWidth() - .height(1.dp) - .background(FlintTheme.colors.gray500), - ) - - Spacer(Modifier.height(24.dp)) - - if (content.isSpoiler) { - Spoiler( - onSpoilClick = { onSpoilClick(content.id) }, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = content.reason, - color = FlintTheme.colors.gray100, - style = FlintTheme.typography.body1R16, - modifier = - Modifier - .defaultMinSize(minHeight = 183.dp) - .fillMaxWidth(), - ) - } - } else { - Text( - text = content.reason, - color = FlintTheme.colors.gray100, - style = FlintTheme.typography.body1R16, - modifier = - Modifier - .defaultMinSize(minHeight = 183.dp) - .fillMaxWidth(), - ) - } - - Spacer(Modifier.height(64.dp)) - } - } -} - -private class ContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence = - sequenceOf( - ContentModelNew( - id = "0", - title = "드라마 제목", - year = 2000, - imageUrl = "", - director = "가스 제닝스", - reason = "달라진 온도\n-\n같은 구도에 채도를 달리해 변해버린 사랑을 시각적으로 담아낸 장면들", - isSpoiler = false, - isBookmarked = false, - bookmarkCount = 42, - ), - ContentModelNew( - id = "0", - title = "스포일러 있는 영화", - year = 2024, - imageUrl = "", - director = "감독 이름", - reason = "이 내용은 스포일러가 포함되어 있습니다.", - isSpoiler = true, - isBookmarked = false, - bookmarkCount = 42, - ), - ContentModelNew( - id = "0", - title = "저장된 영화", - year = 2023, - imageUrl = "", - director = "다른 감독", - reason = "내가 저장한 영화입니다.", - isSpoiler = false, - isBookmarked = true, - bookmarkCount = 42, - ), - ) -} - -@Preview -@Composable -private fun ContentPreview( - @PreviewParameter(ContentPreviewProvider::class) content: ContentModelNew, -) { - FlintTheme { - Content( - content = content, - onBookmarkIconClick = {}, - onSpoilClick = {} - ) - } -} From f67aa944692124b2b862f9fac410d28da47af502 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 07:24:34 +0900 Subject: [PATCH 08/19] =?UTF-8?q?[delect]=20CollectionReport=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/flint/core/navigation/Route.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/flint/core/navigation/Route.kt b/app/src/main/java/com/flint/core/navigation/Route.kt index 66dd2a26..b1d935da 100644 --- a/app/src/main/java/com/flint/core/navigation/Route.kt +++ b/app/src/main/java/com/flint/core/navigation/Route.kt @@ -42,6 +42,11 @@ interface Route { val targetImageUrl: String? = null, ) : Route + @Serializable + data class CollectionReport( + val collectionId: String, + ) : Route + @Serializable data object CollectionCreate : Route From b13a7d6357434497ba5f82869fe59167a57ef97d Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 15:23:29 +0900 Subject: [PATCH 09/19] =?UTF-8?q?[feat]=20CollectionDetail=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=AA=A8=EB=8B=AC=20=EC=A0=9C=EC=9E=91=20=EB=B0=8F?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionDetailScreen.kt | 14 +++++++- .../component/CollectionDetailDeleteModal.kt | 35 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDeleteModal.kt diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt index 69612d6d..8bd7adca 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt @@ -41,6 +41,7 @@ import com.flint.domain.model.collection.CollectionDetailModelNew import com.flint.domain.model.content.ContentModelNew import com.flint.domain.type.UserRoleType import com.flint.presentation.collectiondetail.component.CollectionCopyrightFooter +import com.flint.presentation.collectiondetail.component.CollectionDetailDeleteModal import com.flint.presentation.collectiondetail.component.CollectionDetailDescription import com.flint.presentation.collectiondetail.component.CollectionDetailThumbnail import com.flint.presentation.collectiondetail.component.CollectionDetailContent @@ -68,6 +69,7 @@ fun CollectionDetailRoute( var showCollectionSaveToast: Boolean by remember { mutableStateOf(false) } var showContentSaveToast: Boolean by remember { mutableStateOf(false) } var showContentCancelToast: Boolean by remember { mutableStateOf(false) } + var showDeleteModal: Boolean by remember { mutableStateOf(false) } when (val uiState = uiState) { UiState.Loading -> { @@ -105,7 +107,7 @@ fun CollectionDetailRoute( // TODO: 컬렉션 수정 화면 연결 }, onDeleteClick = { - // TODO: 컬렉션 삭제 로직 연결 + showDeleteModal = true }, onReportClick = { // TODO: 신고(Route.CollectionReport) 화면 복구 후 navigateToCollectionReport(collectionDetail.id) 연결 @@ -116,6 +118,16 @@ fun CollectionDetailRoute( else -> {} } + if (showDeleteModal) { + CollectionDetailDeleteModal( + onConfirm = { + showDeleteModal = false + // TODO: 컬렉션 삭제 API 연결 + }, + onDismiss = { showDeleteModal = false }, + ) + } + if (showCollectionCancelToast) { ShowToast( text = "컬렉션 저장이 취소되었어요", diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDeleteModal.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDeleteModal.kt new file mode 100644 index 00000000..50df3af5 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDeleteModal.kt @@ -0,0 +1,35 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.flint.R +import com.flint.core.designsystem.component.modal.TwoButtonModal +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun CollectionDetailDeleteModal( + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + TwoButtonModal( + title = "컬렉션을 삭제할까요?", + message = "내가 작성한 컬렉션이 삭제돼요.", + cancelText = "취소", + confirmText = "삭제", + onConfirm = onConfirm, + onDismiss = onDismiss, + icon = R.drawable.ic_gradient_trash, + isDestructive = true, + ) +} + +@Preview(showBackground = true) +@Composable +private fun CollectionDetailDeleteModalPreview() { + FlintTheme { + CollectionDetailDeleteModal( + onConfirm = {}, + onDismiss = {}, + ) + } +} From e1ac1306df8c87523f625e101939500ea68c19c4 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 15:31:34 +0900 Subject: [PATCH 10/19] =?UTF-8?q?[feat]=20CollectionDetailDescription.kt?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionDetailScreen.kt | 3 +- .../component/CollectionDetailDescription.kt | 34 +++++++++---------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt index 8bd7adca..11eacbbd 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt @@ -294,8 +294,7 @@ fun CollectionDetailScreen( createdAt = createdAt, collectionContent = description, onAuthorNicknameClick = onAuthorNicknameClick, - modifier = - Modifier + modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), ) diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDescription.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDescription.kt index f8ee8160..716c1609 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDescription.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDescription.kt @@ -1,19 +1,15 @@ package com.flint.presentation.collectiondetail.component -import androidx.compose.foundation.background 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.height -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider 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.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource @@ -73,20 +69,18 @@ fun CollectionDetailDescription( ) } - Box( - modifier = - Modifier - .fillMaxWidth() - .height(1.dp) - .clip(CircleShape) - .background(color = FlintTheme.colors.gray300), + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + color = FlintTheme.colors.gray300, ) - Text( - text = collectionContent, - color = FlintTheme.colors.gray100, - style = FlintTheme.typography.body1R16, - ) + if (collectionContent.isNotBlank()) { + Text( + text = collectionContent, + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1R16, + ) + } } } @@ -118,6 +112,12 @@ private class DescriptionPreviewProvider : PreviewParameterProvider Date: Tue, 16 Jun 2026 15:36:57 +0900 Subject: [PATCH 11/19] =?UTF-8?q?[feat]=20PeopleWhoSavedThisCollection.kt?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=EC=82=AC=ED=95=AD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/PeopleWhoSavedThisCollection.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt index 0f02413a..ef6850cd 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt @@ -85,20 +85,22 @@ fun PeopleWhoSavedThisCollection( } } - Spacer(Modifier.width(8.dp)) + Spacer(Modifier.width(6.dp)) if (people.size >= 7) { - Row { + Row ( + verticalAlignment = Alignment.CenterVertically + ){ Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_plus), contentDescription = "그 외", - tint = FlintTheme.colors.white, + tint = FlintTheme.colors.gray200, ) Text( text = (people.size - 6).toString(), - color = FlintTheme.colors.gray50, - style = FlintTheme.typography.head2M20, + color = FlintTheme.colors.gray200, + style = FlintTheme.typography.head3M18, ) } } From 5e2631e267f45f70866b7b8ff3c91f059c704f88 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 23:39:54 +0900 Subject: [PATCH 12/19] =?UTF-8?q?[feat]=20CollectionDetail=20customImageUr?= =?UTF-8?q?ls=20/=20isPublic=20data=20=EB=A0=88=EC=9D=B4=EC=96=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/flint/data/api/CollectionApi.kt | 9 +++++++++ .../collection/response/CollectionDetailResponseDto.kt | 2 ++ .../collection/response/CollectionUpdateResponseDto.kt | 6 ++++++ .../domain/mapper/collection/CollectionDetailMapper.kt | 2 ++ .../domain/model/collection/CollectionDetailModel.kt | 1 + .../java/com/flint/domain/model/content/ContentModel.kt | 1 + .../com/flint/domain/repository/CollectionRepository.kt | 9 +++++++++ 7 files changed, 30 insertions(+) create mode 100644 app/src/main/java/com/flint/data/dto/collection/response/CollectionUpdateResponseDto.kt diff --git a/app/src/main/java/com/flint/data/api/CollectionApi.kt b/app/src/main/java/com/flint/data/api/CollectionApi.kt index 47bb9bba..df65ba89 100644 --- a/app/src/main/java/com/flint/data/api/CollectionApi.kt +++ b/app/src/main/java/com/flint/data/api/CollectionApi.kt @@ -4,11 +4,13 @@ import com.flint.data.dto.base.BaseResponse import com.flint.data.dto.collection.request.CollectionCreateRequestDto import com.flint.data.dto.collection.response.CollectionCreateResponseDto import com.flint.data.dto.collection.response.CollectionDetailResponseDto +import com.flint.data.dto.collection.response.CollectionUpdateResponseDto import com.flint.data.dto.collection.response.CollectionsResponseDto import com.flint.data.dto.collection.response.RecentCollectionListResponseDto import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST +import retrofit2.http.PUT import retrofit2.http.Path import retrofit2.http.Query @@ -32,6 +34,13 @@ interface CollectionApi { @Path("collectionId") collectionId: String, ): BaseResponse + // 컬렉션 수정 + @PUT("/api/v1/collections/{collectionId}") + suspend fun updateCollection( + @Path("collectionId") collectionId: String, + @Body requestDto: CollectionCreateRequestDto, + ): BaseResponse + // 최근 본 컬렉션 목록 조회 @GET("/api/v1/collections/recent") suspend fun getRecentCollectionList(): BaseResponse diff --git a/app/src/main/java/com/flint/data/dto/collection/response/CollectionDetailResponseDto.kt b/app/src/main/java/com/flint/data/dto/collection/response/CollectionDetailResponseDto.kt index c1ba8c3d..a92c54a8 100644 --- a/app/src/main/java/com/flint/data/dto/collection/response/CollectionDetailResponseDto.kt +++ b/app/src/main/java/com/flint/data/dto/collection/response/CollectionDetailResponseDto.kt @@ -12,6 +12,7 @@ data class CollectionDetailResponseDto( @SerialName("id") val id: String, @SerialName("thumbnailUrl") val thumbnailUrl: String, @SerialName("isBookmarked") val isBookmarked: Boolean, + @SerialName("isPublic") val isPublic: Boolean, @SerialName("title") val title: String, ) { @Serializable @@ -31,6 +32,7 @@ data class CollectionDetailResponseDto( @SerialName("isSpoiler") val isSpoiler: Boolean, @SerialName("reason") val reason: String, @SerialName("imageUrl") val imageUrl: String, + @SerialName("customImageUrls") val customImageUrls: List = emptyList(), @SerialName("title") val title: String, @SerialName("year") val year: Int, ) diff --git a/app/src/main/java/com/flint/data/dto/collection/response/CollectionUpdateResponseDto.kt b/app/src/main/java/com/flint/data/dto/collection/response/CollectionUpdateResponseDto.kt new file mode 100644 index 00000000..ed007d98 --- /dev/null +++ b/app/src/main/java/com/flint/data/dto/collection/response/CollectionUpdateResponseDto.kt @@ -0,0 +1,6 @@ +package com.flint.data.dto.collection.response + +import kotlinx.serialization.Serializable + +@Serializable +class CollectionUpdateResponseDto diff --git a/app/src/main/java/com/flint/domain/mapper/collection/CollectionDetailMapper.kt b/app/src/main/java/com/flint/domain/mapper/collection/CollectionDetailMapper.kt index f274d793..1d56bac5 100644 --- a/app/src/main/java/com/flint/domain/mapper/collection/CollectionDetailMapper.kt +++ b/app/src/main/java/com/flint/domain/mapper/collection/CollectionDetailMapper.kt @@ -17,6 +17,7 @@ fun CollectionDetailResponseDto.toModel(): CollectionDetailModelNew { id = id, thumbnailUrl = thumbnailUrl, isBookmarked = isBookmarked, + isPublic = isPublic, title = title, ) } @@ -39,6 +40,7 @@ private fun CollectionDetailResponseDto.Content.toModel(): ContentModelNew { isSpoiler = isSpoiler, reason = reason, imageUrl = imageUrl, + customImageUrls = customImageUrls, title = title, year = year, ) diff --git a/app/src/main/java/com/flint/domain/model/collection/CollectionDetailModel.kt b/app/src/main/java/com/flint/domain/model/collection/CollectionDetailModel.kt index 3810500b..4477bc43 100644 --- a/app/src/main/java/com/flint/domain/model/collection/CollectionDetailModel.kt +++ b/app/src/main/java/com/flint/domain/model/collection/CollectionDetailModel.kt @@ -13,5 +13,6 @@ data class CollectionDetailModelNew( val id: String, val thumbnailUrl: String, val isBookmarked: Boolean, + val isPublic: Boolean, val title: String, ) diff --git a/app/src/main/java/com/flint/domain/model/content/ContentModel.kt b/app/src/main/java/com/flint/domain/model/content/ContentModel.kt index 61048659..9951dac0 100644 --- a/app/src/main/java/com/flint/domain/model/content/ContentModel.kt +++ b/app/src/main/java/com/flint/domain/model/content/ContentModel.kt @@ -11,6 +11,7 @@ data class ContentModelNew( val isSpoiler: Boolean, val reason: String, val imageUrl: String, + val customImageUrls: List = emptyList(), val title: String, val year: Int, ) diff --git a/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt b/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt index 46c0ce2c..8c6b6db1 100644 --- a/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt +++ b/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt @@ -41,6 +41,15 @@ class CollectionRepository @Inject constructor( response.toModel() } + // 컬렉션 수정 + suspend fun updateCollection( + collectionId: String, + requestDto: CollectionCreateRequestDto, + ): Result = + suspendRunCatching { + apiService.updateCollection(collectionId, requestDto) + } + // 최근 본 컬렉션 목록 조회 suspend fun getRecentCollectionList(): Result = suspendRunCatching { apiService.getRecentCollectionList().data.toModel() } From c766b1cdd441e15593c0e7411b1a0f0b6384b42e Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 23:40:17 +0900 Subject: [PATCH 13/19] =?UTF-8?q?[feat]=20CollectionDetail=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=BA=90?= =?UTF-8?q?=EB=9F=AC=EC=85=80=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/flint/core/navigation/Route.kt | 5 +- .../CollectionDetailScreen.kt | 15 ++++- .../component/CollectionDetailContent.kt | 57 ++++++++++--------- .../navigation/CollectionDetailNavigation.kt | 8 +++ 4 files changed, 56 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/flint/core/navigation/Route.kt b/app/src/main/java/com/flint/core/navigation/Route.kt index b1d935da..927214cf 100644 --- a/app/src/main/java/com/flint/core/navigation/Route.kt +++ b/app/src/main/java/com/flint/core/navigation/Route.kt @@ -40,6 +40,7 @@ interface Route { data class CollectionDetail( val collectionId: String, val targetImageUrl: String? = null, + val showEditSuccessToast: Boolean = false, ) : Route @Serializable @@ -51,7 +52,9 @@ interface Route { data object CollectionCreate : Route @Serializable - data object CollectionCreateGraph : Route + data class CollectionCreateGraph( + val collectionId: String? = null, + ) : Route @Serializable data object SavedContentList : Route diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt index 11eacbbd..eac7c6c3 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt @@ -61,7 +61,9 @@ fun CollectionDetailRoute( navigateToCollectionList: (CollectionListRouteType) -> Unit, navigateToProfile: (authorId: String) -> Unit, navigateUp: () -> Unit, + navigateToCollectionEdit: (collectionId: String) -> Unit, targetImageUrl: String? = null, + showEditSuccessToast: Boolean = false, viewModel: CollectionDetailViewModel = hiltViewModel(), ) { val uiState: UiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -70,6 +72,7 @@ fun CollectionDetailRoute( var showContentSaveToast: Boolean by remember { mutableStateOf(false) } var showContentCancelToast: Boolean by remember { mutableStateOf(false) } var showDeleteModal: Boolean by remember { mutableStateOf(false) } + var showEditSuccessToastState: Boolean by remember { mutableStateOf(showEditSuccessToast) } when (val uiState = uiState) { UiState.Loading -> { @@ -104,7 +107,7 @@ fun CollectionDetailRoute( onAuthorClick = navigateToProfile, isMine = uiState.data.isMine, onEditClick = { - // TODO: 컬렉션 수정 화면 연결 + navigateToCollectionEdit(collectionDetail.id) }, onDeleteClick = { showDeleteModal = true @@ -128,6 +131,16 @@ fun CollectionDetailRoute( ) } + if (showEditSuccessToastState) { + ShowToast( + text = "컬렉션이 수정되었어요", + imageVector = null, + paddingValues = paddingValues, + yOffset = 12.dp, + hide = { showEditSuccessToastState = false }, + ) + } + if (showCollectionCancelToast) { ShowToast( text = "컬렉션 저장이 취소되었어요", diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt index 43ada4a8..a50172c6 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt @@ -37,9 +37,6 @@ import com.flint.core.designsystem.component.image.NetworkImage import com.flint.core.designsystem.theme.FlintTheme import com.flint.domain.model.content.ContentModelNew -// 백엔드에서 스틸컷 이미지 목록을 제공하기 전까지, 동일 이미지를 반복해 캐러셀 UI만 우선 구성 -private const val MOCK_SCENE_IMAGE_COUNT = 5 - @Composable fun CollectionDetailContent( content: ContentModelNew, @@ -50,9 +47,11 @@ fun CollectionDetailContent( Column(modifier = modifier) { Spacer(Modifier.height(48.dp)) - CollectionDetailContentCarousel(content = content) + if (content.customImageUrls.isNotEmpty()) { + CollectionDetailContentCarousel(content = content) - Spacer(Modifier.height(32.dp)) + Spacer(Modifier.height(32.dp)) + } CollectionDetailContentInfo( content = content, @@ -64,18 +63,20 @@ fun CollectionDetailContent( @Composable private fun CollectionDetailContentCarousel(content: ContentModelNew) { + val images = content.customImageUrls val pageCount = Int.MAX_VALUE val pagerState = rememberPagerState( - initialPage = pageCount / 2 - (pageCount / 2) % MOCK_SCENE_IMAGE_COUNT, + initialPage = pageCount / 2 - (pageCount / 2) % images.size, ) { pageCount } - val currentIndex = pagerState.currentPage % MOCK_SCENE_IMAGE_COUNT + val currentIndex = pagerState.currentPage % images.size HorizontalPager( state = pagerState, + userScrollEnabled = images.size > 1, modifier = Modifier.fillMaxWidth(), - ) { + ) { page -> NetworkImage( - imageUrl = content.imageUrl, + imageUrl = images[page % images.size], modifier = Modifier .fillMaxWidth() .aspectRatio(360f / 270f), @@ -83,25 +84,27 @@ private fun CollectionDetailContentCarousel(content: ContentModelNew) { ) } - Spacer(Modifier.height(8.dp)) + if (images.size > 1) { + Spacer(Modifier.height(8.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), - ) { - repeat(MOCK_SCENE_IMAGE_COUNT) { index -> - Box( - modifier = Modifier - .size(8.dp) - .clip(CircleShape) - .background( - if (index == currentIndex) { - FlintTheme.colors.secondary400 - } else { - FlintTheme.colors.gray500 - }, - ), - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + ) { + repeat(images.size) { index -> + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background( + if (index == currentIndex) { + FlintTheme.colors.secondary400 + } else { + FlintTheme.colors.gray500 + }, + ), + ) + } } } } diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt b/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt index 218b50e8..e12e2f48 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt @@ -8,17 +8,20 @@ import androidx.navigation.compose.composable import androidx.navigation.toRoute import com.flint.core.navigation.Route import com.flint.core.navigation.model.CollectionListRouteType +import com.flint.presentation.collectioncreate.navigation.navigateToCollectionEdit import com.flint.presentation.collectiondetail.CollectionDetailRoute fun NavController.navigateToCollectionDetail( collectionId: String, targetImageUrl: String? = null, + showEditSuccessToast: Boolean = false, navOptions: NavOptions? = null, ) { navigate( Route.CollectionDetail( collectionId = collectionId, targetImageUrl = targetImageUrl, + showEditSuccessToast = showEditSuccessToast, ), navOptions, ) @@ -29,15 +32,20 @@ fun NavGraphBuilder.collectionDetailNavGraph( navigateToCollectionList: (CollectionListRouteType) -> Unit, navigateUp: () -> Unit, navigateToProfile: (userId: String) -> Unit, + navController: NavController, ) { composable { backStackEntry -> val route = backStackEntry.toRoute() CollectionDetailRoute( paddingValues = paddingValues, targetImageUrl = route.targetImageUrl, + showEditSuccessToast = route.showEditSuccessToast, navigateToCollectionList = navigateToCollectionList, navigateUp = navigateUp, navigateToProfile = navigateToProfile, + navigateToCollectionEdit = { collectionId -> + navController.navigateToCollectionEdit(collectionId) + }, ) } } From 0a48b08b3883c185c881abdac74f73a435f2f226 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 23:40:38 +0900 Subject: [PATCH 14/19] =?UTF-8?q?[fix]=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=9A=94=EC=B2=AD=20SerialName?= =?UTF-8?q?=20imageUrls=20=E2=86=92=20customImages=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/dto/collection/request/CollectionCreateRequestDto.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/flint/data/dto/collection/request/CollectionCreateRequestDto.kt b/app/src/main/java/com/flint/data/dto/collection/request/CollectionCreateRequestDto.kt index a0791eec..2f04acee 100644 --- a/app/src/main/java/com/flint/data/dto/collection/request/CollectionCreateRequestDto.kt +++ b/app/src/main/java/com/flint/data/dto/collection/request/CollectionCreateRequestDto.kt @@ -24,7 +24,7 @@ data class CollectionCreateRequestDto( val isSpoiler: Boolean, @SerialName("reason") val reason: String, - @SerialName("imageUrls") + @SerialName("customImages") val imageUrls: List = emptyList(), ) } From 8ed015f50a62cefdb2b2d185f6a4bd5b637939f1 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 23:40:54 +0900 Subject: [PATCH 15/19] =?UTF-8?q?[feat]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EB=B0=8F=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionCreateScreen.kt | 10 ++++-- .../component/CollectionCreateContentImage.kt | 32 +++++++++++------- .../navigation/CollectionCreateNavigation.kt | 25 ++++++++++---- .../uistate/CollectionCreateUiState.kt | 33 +++++++++++++++++-- .../flint/presentation/main/MainNavHost.kt | 3 +- .../flint/presentation/main/MainNavigator.kt | 5 +++ 6 files changed, 85 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt index 5055cd2f..3369a129 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -120,6 +120,7 @@ fun CollectionCreateRoute( contentImageLauncher.launch("image/*") } }, + onRemoveExistingContentImage = viewModel::removeExistingContentImageUrl, onRemoveContentImage = viewModel::removeContentImageUri, modifier = Modifier.padding(paddingValues), ) @@ -141,6 +142,7 @@ fun CollectionCreateScreen( onGalleryClick: () -> Unit = {}, onThumbnailDelete: () -> Unit = {}, onSelectContentImage: (contentId: String) -> Unit = {}, + onRemoveExistingContentImage: (contentId: String, index: Int) -> Unit = { _, _ -> }, onRemoveContentImage: (contentId: String, index: Int) -> Unit = { _, _ -> }, modifier: Modifier = Modifier, ) { @@ -163,7 +165,7 @@ fun CollectionCreateScreen( // 썸네일 item { CollectionCreateThumbnail( - imageUrl = uiState.thumbnailImageUri, + imageUrl = uiState.thumbnailImageUri ?: uiState.existingThumbnailUrl, onClick = { isThumbnailBottomSheetVisible = true }, ) @@ -212,6 +214,7 @@ fun CollectionCreateScreen( onReasonChanged = onReasonChanged, onAddContentClick = onAddContentClick, onSelectContentImage = onSelectContentImage, + onRemoveExistingContentImage = onRemoveExistingContentImage, onRemoveContentImage = onRemoveContentImage, modifier = Modifier.padding(horizontal = (16).dp), ) @@ -408,6 +411,7 @@ private fun CollectionAddContentSection( onReasonChanged: (String, String) -> Unit, onAddContentClick: () -> Unit, onSelectContentImage: (contentId: String) -> Unit, + onRemoveExistingContentImage: (contentId: String, index: Int) -> Unit, onRemoveContentImage: (contentId: String, index: Int) -> Unit, modifier: Modifier = Modifier, ) { @@ -460,9 +464,11 @@ private fun CollectionAddContentSection( Spacer(Modifier.height(16.dp)) - if (detail.contentImageUris.isNotEmpty()) { + if (detail.existingImageUrls.isNotEmpty() || detail.contentImageUris.isNotEmpty()) { CollectionCreateContentImage( + existingImageUrls = detail.existingImageUrls, imageUris = detail.contentImageUris, + onDeleteExistingClick = { index -> onRemoveExistingContentImage(content.id, index) }, onDeleteClick = { index -> onRemoveContentImage(content.id, index) }, ) diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentImage.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentImage.kt index 127b0647..e1d797ca 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentImage.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentImage.kt @@ -37,17 +37,20 @@ import com.flint.core.designsystem.theme.FlintTheme @Composable fun CollectionCreateContentImage( + existingImageUrls: List, imageUris: List, + onDeleteExistingClick: (index: Int) -> Unit, onDeleteClick: (index: Int) -> Unit, modifier: Modifier = Modifier, ) { - if (imageUris.isEmpty()) return + val images: List = existingImageUrls + imageUris + if (images.isEmpty()) return val pageCount = Int.MAX_VALUE val pagerState = rememberPagerState( - initialPage = pageCount / 2 - (pageCount / 2) % imageUris.size, + initialPage = pageCount / 2 - (pageCount / 2) % images.size, ) { pageCount } - val currentIndex = pagerState.currentPage % imageUris.size + val currentIndex = pagerState.currentPage % images.size var prevSize by remember { mutableIntStateOf(-1) } var deletedIndex by remember { mutableIntStateOf(-1) } @@ -81,13 +84,13 @@ fun CollectionCreateContentImage( Column(modifier = modifier) { HorizontalPager( state = pagerState, - userScrollEnabled = imageUris.size > 1, + userScrollEnabled = images.size > 1, modifier = Modifier.fillMaxWidth(), ) { page -> - val index = page % imageUris.size + val index = page % images.size Box { NetworkImage( - imageUrl = imageUris[index], + imageUrl = images[index], contentScale = ContentScale.FillBounds, modifier = Modifier .fillMaxWidth() @@ -100,23 +103,27 @@ fun CollectionCreateContentImage( modifier = Modifier .align(Alignment.TopEnd) .clickable { - deletedIndex = index - onDeleteClick(index) - } + deletedIndex = index + if (index < existingImageUrls.size) { + onDeleteExistingClick(index) + } else { + onDeleteClick(index - existingImageUrls.size) + } + } .padding(all = 16.dp) .size(24.dp), ) } } - if (imageUris.size > 1) { + if (images.size > 1) { Spacer(Modifier.height(12.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), ) { - repeat(imageUris.size) { index -> + repeat(images.size) { index -> Box( modifier = Modifier .size(8.dp) @@ -137,11 +144,12 @@ fun CollectionCreateContentImage( private fun CollectionCreateContentImagePreview() { FlintTheme { CollectionCreateContentImage( + existingImageUrls = listOf("https://example.com/1"), imageUris = listOf( - Uri.parse("https://example.com/1"), Uri.parse("https://example.com/2"), Uri.parse("https://example.com/3"), ), + onDeleteExistingClick = {}, onDeleteClick = {}, ) } diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/navigation/CollectionCreateNavigation.kt b/app/src/main/java/com/flint/presentation/collectioncreate/navigation/CollectionCreateNavigation.kt index 27e4f19c..17ecfa87 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/navigation/CollectionCreateNavigation.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/navigation/CollectionCreateNavigation.kt @@ -21,6 +21,13 @@ fun NavController.navigateToCollectionCreate( navigate(Route.CollectionCreate, navOptions) } +fun NavController.navigateToCollectionEdit( + collectionId: String, + navOptions: NavOptions? = null, +) { + navigate(Route.CollectionCreateGraph(collectionId = collectionId), navOptions) +} + fun NavController.navigateToAddContent( navOptions: NavOptions? = null ) { @@ -41,13 +48,19 @@ fun NavGraphBuilder.collectionCreateNavGraph( paddingValues = paddingValues, navigateToAddContent = navController::navigateToAddContent, navigateUp = navController::navigateUp, - navigateToCollectionDetail = { - navController.navigateToCollectionDetail(it, - navOptions = navOptions { - popUpTo { - inclusive = true // 현재 화면을 백스택에서 제거 + navigateToCollectionDetail = { collectionId -> + if (viewModel.isEditMode) { + navController.navigateToCollectionDetail( + collectionId = collectionId, + showEditSuccessToast = true, + navOptions = navOptions { + popUpTo { inclusive = true } } - }) + ) + } else { + navController.popBackStack(inclusive = true) + navController.navigateToCollectionDetail(collectionId = collectionId) + } }, viewModel = viewModel ) diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/uistate/CollectionCreateUiState.kt b/app/src/main/java/com/flint/presentation/collectioncreate/uistate/CollectionCreateUiState.kt index de80c34b..9939addf 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/uistate/CollectionCreateUiState.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/uistate/CollectionCreateUiState.kt @@ -9,6 +9,7 @@ import kotlinx.collections.immutable.persistentListOf @Immutable data class CollectionCreateUiState( val thumbnailImageUri: Uri? = null, + val existingThumbnailUrl: String? = null, val title: String = "", val description: String = "", val isPublic: Boolean? = null, @@ -17,12 +18,39 @@ data class CollectionCreateUiState( val contents: ImmutableList = persistentListOf(), val searchText: String = "", val isLoading: Boolean = false, + // 수정 모드 원본값 (null이면 생성 모드) + val originalTitle: String? = null, + val originalDescription: String = "", + val originalIsPublic: Boolean? = null, + val originalThumbnailUrl: String? = null, + val originalContentIds: Set = emptySet(), + val originalContentDetails: Map> = emptyMap(), + val originalContentImageUrls: Map> = emptyMap(), ) { - val isFinishButtonEnabled: Boolean = + private val isEditMode: Boolean get() = originalTitle != null + + private val hasChanges: Boolean get() = isEditMode && ( + title != originalTitle || + description != originalDescription || + isPublic != originalIsPublic || + thumbnailImageUri != null || + existingThumbnailUrl != originalThumbnailUrl || + selectedContents.map { it.id }.toSet() != originalContentIds || + contentDetailsMap.any { (id, detail) -> + val original = originalContentDetails[id] + detail.isSpoiler != original?.first || + detail.reason != original?.second || + detail.contentImageUris.isNotEmpty() || + detail.existingImageUrls != (originalContentImageUrls[id] ?: emptyList()) + } + ) + + val isFinishButtonEnabled: Boolean get() = !isLoading && title.isNotBlank() && isPublic != null && - selectedContents.size >= 2 + selectedContents.size >= 2 && + (!isEditMode || hasChanges) val isCancelModalVisible: Boolean = contentDetailsMap.values.any { it.reason.isNotBlank() } @@ -32,5 +60,6 @@ data class CollectionCreateUiState( data class ContentDetail( val isSpoiler: Boolean = false, val reason: String = "", + val existingImageUrls: List = emptyList(), val contentImageUris: List = emptyList(), ) \ No newline at end of file diff --git a/app/src/main/java/com/flint/presentation/main/MainNavHost.kt b/app/src/main/java/com/flint/presentation/main/MainNavHost.kt index a4d5a26b..63ae40e9 100644 --- a/app/src/main/java/com/flint/presentation/main/MainNavHost.kt +++ b/app/src/main/java/com/flint/presentation/main/MainNavHost.kt @@ -77,7 +77,8 @@ fun MainNavHost( paddingValues = paddingValues, navigateToCollectionList = navigator::navigateToCollectionList, navigateUp = navigator::navigateUp, - navigateToProfile = navigator::navigateToProfile + navigateToProfile = navigator::navigateToProfile, + navController = navigator.navController, ) collectionCreateNavGraph( diff --git a/app/src/main/java/com/flint/presentation/main/MainNavigator.kt b/app/src/main/java/com/flint/presentation/main/MainNavigator.kt index a53f9d39..0ca0d58c 100644 --- a/app/src/main/java/com/flint/presentation/main/MainNavigator.kt +++ b/app/src/main/java/com/flint/presentation/main/MainNavigator.kt @@ -13,6 +13,7 @@ import androidx.navigation.navOptions import com.flint.core.navigation.Route import com.flint.core.navigation.model.CollectionListRouteType import com.flint.presentation.collectioncreate.navigation.navigateToCollectionCreate +import com.flint.presentation.collectioncreate.navigation.navigateToCollectionEdit import com.flint.presentation.collectiondetail.navigation.navigateToCollectionDetail import com.flint.presentation.collectionlist.navigation.navigateToCollectionList import com.flint.presentation.explore.navigation.navigateToExplore @@ -151,6 +152,10 @@ class MainNavigator( navController.navigateToCollectionCreate(navOptions) } + fun navigateToCollectionEdit(collectionId: String, navOptions: NavOptions? = null) { + navController.navigateToCollectionEdit(collectionId, navOptions) + } + fun navigateToSavedContent(navOptions: NavOptions? = null) { navController.navigateToSavedContentList(navOptions) } From 95383b2234296a5c5867efb9af20ed8079f19238 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 16 Jun 2026 23:41:06 +0900 Subject: [PATCH 16/19] =?UTF-8?q?[feat]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EB=B0=8F=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionCreateViewModel.kt | 124 +++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt index df4a9c5b..dda36913 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt @@ -2,6 +2,7 @@ package com.flint.presentation.collectioncreate import android.content.Context import android.net.Uri +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.flint.core.common.util.UiState @@ -35,11 +36,15 @@ import javax.inject.Inject @HiltViewModel class CollectionCreateViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, @ApplicationContext private val context: Context, private val collectionRepository: CollectionRepository, private val searchRepository: SearchRepository, private val storageRepository: StorageRepository, ) : ViewModel() { + + private val editingCollectionId: String? = savedStateHandle["collectionId"] + val isEditMode: Boolean = editingCollectionId != null private val _uiState = MutableStateFlow(CollectionCreateUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -52,11 +57,18 @@ class CollectionCreateViewModel @Inject constructor( init { observeSearchQuery() loadInitialContents() + if (editingCollectionId != null) { + loadCollectionForEdit(editingCollectionId) + } } fun onClickFinish() { if (_uiState.value.isLoading) return - postCollectionCreate() + if (editingCollectionId != null) { + putCollectionUpdate(editingCollectionId) + } else { + postCollectionCreate() + } } private fun postCollectionCreate() { @@ -102,6 +114,105 @@ class CollectionCreateViewModel @Inject constructor( } } + private fun loadCollectionForEdit(collectionId: String) { + viewModelScope.launch { + collectionRepository.getCollectionDetail(collectionId) + .onSuccess { detail -> + val selectedContents = detail.contents.map { content -> + SearchContentItemModel( + id = content.id, + title = content.title, + author = content.director, + posterUrl = content.imageUrl, + year = content.year, + ) + }.toImmutableList() + + val contentDetailsMap = detail.contents.associate { content -> + content.id to ContentDetail( + isSpoiler = content.isSpoiler, + reason = content.reason, + existingImageUrls = content.customImageUrls, + ) + } + + val thumbnailUrl = detail.thumbnailUrl.ifBlank { null } + val originalDetails = detail.contents.associate { content -> + content.id to Pair(content.isSpoiler, content.reason) + } + val originalImageUrls = detail.contents.associate { content -> + content.id to content.customImageUrls + } + + _uiState.update { + it.copy( + existingThumbnailUrl = thumbnailUrl, + title = detail.title, + description = detail.description, + isPublic = detail.isPublic, + selectedContents = selectedContents, + contentDetailsMap = contentDetailsMap, + originalTitle = detail.title, + originalDescription = detail.description, + originalIsPublic = detail.isPublic, + originalThumbnailUrl = thumbnailUrl, + originalContentIds = detail.contents.map { it.id }.toSet(), + originalContentDetails = originalDetails, + originalContentImageUrls = originalImageUrls, + ) + } + } + } + } + + private fun putCollectionUpdate(collectionId: String) { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + try { + val thumbnailKey = if (_uiState.value.thumbnailImageUri != null) { + uploadImageIfNeeded(_uiState.value.thumbnailImageUri, StoragePathType.COLLECTION_THUMBNAIL) + .getOrElse { + _createSuccess.emit(UiState.Failure) + return@launch + } + } else { + null + } + + val contentImageKeysMap = uploadContentImagesIfNeeded() + .getOrElse { + _createSuccess.emit(UiState.Failure) + return@launch + } + + val requestModel = CollectionCreateRequestModel( + imageUrl = thumbnailKey ?: _uiState.value.existingThumbnailUrl ?: "", + title = _uiState.value.title, + description = _uiState.value.description.ifBlank { "" }, + isPublic = _uiState.value.isPublic ?: true, + contentList = _uiState.value.selectedContents.map { content -> + val detail = _uiState.value.contentDetailsMap[content.id] ?: ContentDetail() + CollectionCreateContentModel( + contentId = content.id, + isSpoiler = detail.isSpoiler, + reason = detail.reason.ifBlank { "" }, + imageUrls = contentImageKeysMap[content.id] ?: emptyList(), + ) + }, + ) + + collectionRepository + .updateCollection(collectionId, requestModel.toDto()) + .onSuccess { + _createSuccess.emit(UiState.Success(collectionId)) + } + .onFailure { e -> Timber.e(e, "컬렉션 수정 실패") } + } finally { + _uiState.update { it.copy(isLoading = false) } + } + } + } + fun resetCreateSuccess() = viewModelScope.launch { _createSuccess.emit(UiState.Empty) } @@ -236,6 +347,17 @@ class CollectionCreateViewModel @Inject constructor( } } + fun removeExistingContentImageUrl(contentId: String, index: Int) { + _uiState.update { state -> + val current = state.contentDetailsMap[contentId] ?: return@update state + if (index !in current.existingImageUrls.indices) return@update state + val updated = current.copy( + existingImageUrls = current.existingImageUrls.toMutableList().also { it.removeAt(index) } + ) + state.copy(contentDetailsMap = state.contentDetailsMap + (contentId to updated)) + } + } + fun removeContentImageUri(contentId: String, index: Int) { _uiState.update { state -> val current = state.contentDetailsMap[contentId] ?: return@update state From 4d5aeba566a41037df03d39be1c1dc6ad0d59c26 Mon Sep 17 00:00:00 2001 From: chanmi Date: Wed, 17 Jun 2026 00:17:41 +0900 Subject: [PATCH 17/19] =?UTF-8?q?[feat]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20API=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=99=84=EB=A3=8C=20=ED=86=A0=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DELETE /api/v1/collections/{collectionId} API 연결 - 삭제 성공 시 이전 화면으로 이동 및 "컬렉션을 삭제했어요" 토스트 표시 - BaseEmptyResponse로 data 필드 없는 응답 처리 --- .../java/com/flint/data/api/CollectionApi.kt | 11 ++++++++-- .../domain/repository/CollectionRepository.kt | 6 +++++ .../CollectionCreateViewModel.kt | 2 +- .../CollectionDetailScreen.kt | 9 +++++++- .../navigation/CollectionDetailNavigation.kt | 8 +++++++ .../flint/presentation/main/MainNavHost.kt | 22 +++++++++++++++++++ 6 files changed, 54 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/flint/data/api/CollectionApi.kt b/app/src/main/java/com/flint/data/api/CollectionApi.kt index df65ba89..ce09beea 100644 --- a/app/src/main/java/com/flint/data/api/CollectionApi.kt +++ b/app/src/main/java/com/flint/data/api/CollectionApi.kt @@ -1,13 +1,14 @@ package com.flint.data.api +import com.flint.data.dto.base.BaseEmptyResponse import com.flint.data.dto.base.BaseResponse import com.flint.data.dto.collection.request.CollectionCreateRequestDto import com.flint.data.dto.collection.response.CollectionCreateResponseDto import com.flint.data.dto.collection.response.CollectionDetailResponseDto -import com.flint.data.dto.collection.response.CollectionUpdateResponseDto import com.flint.data.dto.collection.response.CollectionsResponseDto import com.flint.data.dto.collection.response.RecentCollectionListResponseDto import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.PUT @@ -39,7 +40,13 @@ interface CollectionApi { suspend fun updateCollection( @Path("collectionId") collectionId: String, @Body requestDto: CollectionCreateRequestDto, - ): BaseResponse + ): BaseEmptyResponse + + // 컬렉션 삭제 + @DELETE("/api/v1/collections/{collectionId}") + suspend fun deleteCollection( + @Path("collectionId") collectionId: String, + ): BaseEmptyResponse // 최근 본 컬렉션 목록 조회 @GET("/api/v1/collections/recent") diff --git a/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt b/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt index 8c6b6db1..30805a08 100644 --- a/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt +++ b/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt @@ -50,6 +50,12 @@ class CollectionRepository @Inject constructor( apiService.updateCollection(collectionId, requestDto) } + // 컬렉션 삭제 + suspend fun deleteCollection(collectionId: String): Result = + suspendRunCatching { + apiService.deleteCollection(collectionId) + } + // 최근 본 컬렉션 목록 조회 suspend fun getRecentCollectionList(): Result = suspendRunCatching { apiService.getRecentCollectionList().data.toModel() } diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt index dda36913..651cabec 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt @@ -196,7 +196,7 @@ class CollectionCreateViewModel @Inject constructor( contentId = content.id, isSpoiler = detail.isSpoiler, reason = detail.reason.ifBlank { "" }, - imageUrls = contentImageKeysMap[content.id] ?: emptyList(), + imageUrls = detail.existingImageUrls + (contentImageKeysMap[content.id] ?: emptyList()), ) }, ) diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt index eac7c6c3..94e59db4 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt @@ -61,6 +61,7 @@ fun CollectionDetailRoute( navigateToCollectionList: (CollectionListRouteType) -> Unit, navigateToProfile: (authorId: String) -> Unit, navigateUp: () -> Unit, + navigateUpWithDeleteSuccess: () -> Unit, navigateToCollectionEdit: (collectionId: String) -> Unit, targetImageUrl: String? = null, showEditSuccessToast: Boolean = false, @@ -125,7 +126,7 @@ fun CollectionDetailRoute( CollectionDetailDeleteModal( onConfirm = { showDeleteModal = false - // TODO: 컬렉션 삭제 API 연결 + viewModel.deleteCollection() }, onDismiss = { showDeleteModal = false }, ) @@ -215,6 +216,12 @@ fun CollectionDetailRoute( showContentSaveToast = false } } + + CollectionDetailSideEffect.DeleteCollectionSuccess -> navigateUpWithDeleteSuccess() + + CollectionDetailSideEffect.DeleteCollectionFailure -> { + // TODO: 삭제 실패 다이얼로그 + } } } } diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt b/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt index e12e2f48..ab328e11 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt @@ -27,6 +27,8 @@ fun NavController.navigateToCollectionDetail( ) } +const val KEY_SHOW_DELETE_SUCCESS_TOAST = "showDeleteSuccessToast" + fun NavGraphBuilder.collectionDetailNavGraph( paddingValues: PaddingValues, navigateToCollectionList: (CollectionListRouteType) -> Unit, @@ -42,6 +44,12 @@ fun NavGraphBuilder.collectionDetailNavGraph( showEditSuccessToast = route.showEditSuccessToast, navigateToCollectionList = navigateToCollectionList, navigateUp = navigateUp, + navigateUpWithDeleteSuccess = { + navController.previousBackStackEntry + ?.savedStateHandle + ?.set(KEY_SHOW_DELETE_SUCCESS_TOAST, true) + navController.navigateUp() + }, navigateToProfile = navigateToProfile, navigateToCollectionEdit = { collectionId -> navController.navigateToCollectionEdit(collectionId) diff --git a/app/src/main/java/com/flint/presentation/main/MainNavHost.kt b/app/src/main/java/com/flint/presentation/main/MainNavHost.kt index 63ae40e9..8c86c6ce 100644 --- a/app/src/main/java/com/flint/presentation/main/MainNavHost.kt +++ b/app/src/main/java/com/flint/presentation/main/MainNavHost.kt @@ -5,10 +5,15 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost +import androidx.navigation.compose.currentBackStackEntryAsState +import com.flint.core.designsystem.component.toast.ShowToast import com.flint.core.designsystem.theme.FlintTheme import com.flint.presentation.collectioncreate.navigation.collectionCreateNavGraph +import com.flint.presentation.collectiondetail.navigation.KEY_SHOW_DELETE_SUCCESS_TOAST import com.flint.presentation.collectiondetail.navigation.collectionDetailNavGraph import com.flint.presentation.collectionlist.navigation.collectionListNavGraph import com.flint.presentation.explore.navigation.exploreNavGraph @@ -30,6 +35,11 @@ fun MainNavHost( paddingValues: PaddingValues, modifier: Modifier = Modifier, ) { + val currentBackStackEntry by navigator.navController.currentBackStackEntryAsState() + val showDeleteSuccessToast = currentBackStackEntry + ?.savedStateHandle + ?.get(KEY_SHOW_DELETE_SUCCESS_TOAST) ?: false + Box( modifier = modifier @@ -132,5 +142,17 @@ fun MainNavHost( navigateToLogin = navigator::navigateToLogin, ) } + + if (showDeleteSuccessToast) { + ShowToast( + text = "컬렉션을 삭제했어요", + imageVector = null, + paddingValues = paddingValues, + yOffset = 12.dp, + hide = { + currentBackStackEntry?.savedStateHandle?.set(KEY_SHOW_DELETE_SUCCESS_TOAST, false) + }, + ) + } } } From 7297b4c62b800a89e888b30f7d5cb4f601cecac8 Mon Sep 17 00:00:00 2001 From: chanmi Date: Wed, 17 Jun 2026 00:17:56 +0900 Subject: [PATCH 18/19] =?UTF-8?q?[feat]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20API=20=EC=97=B0=EA=B2=B0=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionDetailViewModel.kt | 16 ++++++++++++++++ .../sideeffect/CollectionDetailSideEffect.kt | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt index 6fa1ef8d..f74fc814 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt @@ -16,6 +16,7 @@ import com.flint.domain.repository.CollectionRepository import com.flint.presentation.collectiondetail.sideeffect.CollectionDetailSideEffect import com.flint.presentation.collectiondetail.uistate.CollectionDetailUiState import dagger.hilt.android.lifecycle.HiltViewModel +import timber.log.Timber import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job @@ -159,6 +160,21 @@ class CollectionDetailViewModel @Inject constructor( } } + fun deleteCollection() { + val collectionId = (_uiState.value as? UiState.Success)?.data?.collectionDetail?.id ?: return + viewModelScope.launch { + collectionRepository.deleteCollection(collectionId) + .onSuccess { + Timber.d("DELETE SUCCESS collectionId=$collectionId") + _sideEffect.emit(CollectionDetailSideEffect.DeleteCollectionSuccess) + } + .onFailure { + Timber.e(it, "DELETE FAILURE collectionId=$collectionId") + _sideEffect.emit(CollectionDetailSideEffect.DeleteCollectionFailure) + } + } + } + fun spoil(contentId: String) { _uiState.update { uiState: UiState -> if (uiState !is UiState.Success) return@update uiState diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/sideeffect/CollectionDetailSideEffect.kt b/app/src/main/java/com/flint/presentation/collectiondetail/sideeffect/CollectionDetailSideEffect.kt index c8d4f65c..0bfedc1e 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/sideeffect/CollectionDetailSideEffect.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/sideeffect/CollectionDetailSideEffect.kt @@ -6,4 +6,8 @@ sealed interface CollectionDetailSideEffect { object ToggleCollectionBookmarkFailure : CollectionDetailSideEffect class ToggleContentBookmarkSuccess(val isBookmarked: Boolean) : CollectionDetailSideEffect + + object DeleteCollectionSuccess : CollectionDetailSideEffect + + object DeleteCollectionFailure : CollectionDetailSideEffect } From 5f108c9e68486694bf958cf57cd759747daf9db1 Mon Sep 17 00:00:00 2001 From: chanmi Date: Wed, 17 Jun 2026 00:36:48 +0900 Subject: [PATCH 19/19] =?UTF-8?q?[refactor]=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EC=BD=94=EB=A6=AC=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../collection/response/CollectionUpdateResponseDto.kt | 6 ------ .../flint/domain/repository/CollectionRepository.kt | 2 ++ .../collectioncreate/CollectionCreateViewModel.kt | 1 + .../collectiondetail/CollectionDetailViewModel.kt | 10 +++++++--- .../component/CollectionDetailTopAppBar.kt | 5 +++-- app/src/main/res/values/strings.xml | 2 ++ 6 files changed, 15 insertions(+), 11 deletions(-) delete mode 100644 app/src/main/java/com/flint/data/dto/collection/response/CollectionUpdateResponseDto.kt diff --git a/app/src/main/java/com/flint/data/dto/collection/response/CollectionUpdateResponseDto.kt b/app/src/main/java/com/flint/data/dto/collection/response/CollectionUpdateResponseDto.kt deleted file mode 100644 index ed007d98..00000000 --- a/app/src/main/java/com/flint/data/dto/collection/response/CollectionUpdateResponseDto.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.flint.data.dto.collection.response - -import kotlinx.serialization.Serializable - -@Serializable -class CollectionUpdateResponseDto diff --git a/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt b/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt index 30805a08..2a0e315c 100644 --- a/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt +++ b/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt @@ -48,12 +48,14 @@ class CollectionRepository @Inject constructor( ): Result = suspendRunCatching { apiService.updateCollection(collectionId, requestDto) + Unit } // 컬렉션 삭제 suspend fun deleteCollection(collectionId: String): Result = suspendRunCatching { apiService.deleteCollection(collectionId) + Unit } // 최근 본 컬렉션 목록 조회 diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt index 651cabec..8f0be3e2 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt @@ -162,6 +162,7 @@ class CollectionCreateViewModel @Inject constructor( ) } } + .onFailure { e -> Timber.e(e, "컬렉션 편집 로드 실패") } } } diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt index f74fc814..f7bed4f2 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt @@ -223,8 +223,12 @@ class CollectionDetailViewModel @Inject constructor( async { collectionRepository.getCollectionDetail(collectionId) } val collectionBookmarkUsers: Deferred> = async { bookmarkRepository.getCollectionBookmarkUsers(collectionId) } - val myUserId: Deferred = - async { preferencesManager.getString(USER_ID).first() } + val myUserId: String = + runCatching { preferencesManager.getString(USER_ID).first() } + .getOrElse { + Timber.w(it, "USER_ID 조회 실패. isMine=false로 처리") + "" + } val collectionDetailResult: CollectionDetailModelNew = collectionDetail.await().getOrThrow() @@ -232,7 +236,7 @@ class CollectionDetailViewModel @Inject constructor( CollectionDetailUiState( collectionDetail = collectionDetailResult, collectionBookmarkUsers = collectionBookmarkUsers.await().getOrThrow(), - isMine = collectionDetailResult.author.id == myUserId.await(), + isMine = myUserId.isNotBlank() && collectionDetailResult.author.id == myUserId, ) ) }.onSuccess { newUiState: UiState.Success -> diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailTopAppBar.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailTopAppBar.kt index 50f1b440..58289736 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailTopAppBar.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailTopAppBar.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -53,7 +54,7 @@ fun CollectionDetailTopAppBar( .noRippleClickable(onClick = onBackClick) .padding(12.dp), imageVector = ImageVector.vectorResource(R.drawable.ic_back), - contentDescription = null, + contentDescription = stringResource(R.string.cd_back), tint = FlintTheme.colors.white, ) @@ -65,7 +66,7 @@ fun CollectionDetailTopAppBar( .size(48.dp) .noRippleClickable(onClick = { showSettingsMenu = true }), imageVector = ImageVector.vectorResource(R.drawable.ic_kebab), - contentDescription = null, + contentDescription = stringResource(R.string.cd_more_options), tint = FlintTheme.colors.white, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f72dc375..54676e38 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ Flint + 뒤로 가기 + 더 보기 \ No newline at end of file