From 85d6b24da0f80faeb43e01596174db7da0864fcc Mon Sep 17 00:00:00 2001 From: chanmi Date: Thu, 7 May 2026 20:55:46 +0900 Subject: [PATCH 01/13] =?UTF-8?q?[fix]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EB=B0=B0?= =?UTF-8?q?=EA=B2=BD=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/CollectionCreateThumbnail.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt index 1991176c..0ee48897 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt @@ -63,11 +63,11 @@ private fun CollectionCreateEmptyThumbnail( contentDescription = null, ) -// Icon( -// painter = painterResource(R.drawable.ic_background_photo), -// contentDescription = null, -// tint = Color.Unspecified, -// ) + Icon( + painter = painterResource(R.drawable.ic_background_photo), + contentDescription = null, + tint = Color.Unspecified, + ) } } @@ -97,11 +97,11 @@ private fun CollectionCreateFillThumbnail( .background(FlintTheme.colors.imgBlur), ) -// Icon( -// painter = painterResource(R.drawable.ic_background_photo), -// contentDescription = null, -// tint = Color.Unspecified, -// ) + Icon( + painter = painterResource(R.drawable.ic_background_photo), + contentDescription = null, + tint = Color.Unspecified, + ) } } From de7c20965bb7b49cb9ff2875d630b3b7cf35ed59 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 12 May 2026 05:01:18 +0900 Subject: [PATCH 02/13] =?UTF-8?q?[feat]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20UI=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CollectionCreateContentItemList 제거 - CollectionCreateContentImage: HorizontalPager 기반 이미지 슬라이더 (최대 5개, 빈 리스트 시 미표시, 1개 시 인디케이터 숨김) - CollectionCreateContentReason: 선택 이유 텍스트필드 + 스포일러 토글 분리 - AddContentSelectItem으로 컴포넌트 네이밍 개선 - CollectionCreateScreen에서 세 컴포넌트 조합으로 교체 --- .../collectioncreate/AddContentScreen.kt | 6 +- .../CollectionCreateScreen.kt | 452 ++++++++++-------- .../CollectionCreateContentItemList.kt | 136 ------ .../CollectionCreateContentSection.kt | 13 +- .../CollectionCreateContentSelect.kt | 86 ---- .../flint/presentation/main/MainNavigator.kt | 2 +- 6 files changed, 278 insertions(+), 417 deletions(-) delete mode 100644 app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentItemList.kt delete mode 100644 app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSelect.kt diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/AddContentScreen.kt b/app/src/main/java/com/flint/presentation/collectioncreate/AddContentScreen.kt index 33418f1e..d6bde25d 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/AddContentScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/AddContentScreen.kt @@ -36,7 +36,7 @@ import com.flint.core.designsystem.theme.FlintTheme import com.flint.domain.model.search.SearchContentItemModel import com.flint.domain.model.search.SearchContentListModel import com.flint.presentation.collectioncreate.component.CollectionCreateContentDeleteModal -import com.flint.presentation.collectioncreate.component.CollectionCreateContentSelect +import com.flint.presentation.collectioncreate.component.AddContentSelectItem import kotlinx.collections.immutable.ImmutableList @Composable @@ -177,7 +177,7 @@ fun AddContentScreen( ) { content -> val isSelected = selectedContents.any { it.id == content.id } - CollectionCreateContentSelect( + AddContentSelectItem( onCheckClick = { if (isSelected){ if (uiState.isCancelModalVisible) { @@ -189,7 +189,7 @@ fun AddContentScreen( } else onToggleContent(content) }, isSelected = isSelected, - imageUrl = content.posterUrl, + posterImageUrl = content.posterUrl, title = content.title, director = content.author, createdYear = content.year, 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 caebd23f..cdf00ddd 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -1,6 +1,7 @@ package com.flint.presentation.collectioncreate import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -12,11 +13,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -27,6 +28,8 @@ 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.res.painterResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.ImeAction @@ -45,9 +48,12 @@ import com.flint.core.designsystem.theme.FlintTheme import com.flint.domain.model.search.SearchContentItemModel import com.flint.domain.model.search.SearchContentListModel import com.flint.presentation.collectioncreate.component.CollectionCreateContentDeleteModal -import com.flint.presentation.collectioncreate.component.CollectionCreateContentItemList +import com.flint.presentation.collectioncreate.component.CollectionCreateContentImage +import com.flint.presentation.collectioncreate.component.CollectionCreateContentReason +import com.flint.presentation.collectioncreate.component.CollectionCreateContentSection import com.flint.presentation.collectioncreate.component.CollectionCreateThumbnail import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList @Composable fun CollectionCreateRoute( @@ -76,16 +82,12 @@ fun CollectionCreateRoute( onTitleChanged = viewModel::updateTitle, onDescriptionChanged = viewModel::updateDescription, onPublicChanged = viewModel::updateIsPublic, - selectedContents = uiState.selectedContents, - contentDetailsMap = uiState.contentDetailsMap, onRemoveContent = viewModel::removeContent, onBackClick = navigateUp, onSpoilerChanged = viewModel::updateSpoiler, onReasonChanged = viewModel::updateReason, onAddContentClick = navigateToAddContent, - onFinishClick = { - viewModel.onClickFinish() - }, + onFinishClick = viewModel::onClickFinish, modifier = Modifier.padding(paddingValues), ) } @@ -96,8 +98,6 @@ fun CollectionCreateScreen( onTitleChanged: (String) -> Unit = {}, onDescriptionChanged: (String) -> Unit = {}, onPublicChanged: (Boolean?) -> Unit = {}, - selectedContents: ImmutableList, - contentDetailsMap: Map, onRemoveContent: (SearchContentItemModel) -> Unit, onBackClick: () -> Unit, onSpoilerChanged: (String, Boolean) -> Unit = { _, _ -> }, @@ -119,7 +119,7 @@ fun CollectionCreateScreen( LazyColumn( modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(28.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { // 썸네일 item { @@ -128,190 +128,66 @@ fun CollectionCreateScreen( onClick = { }, ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(20.dp)) } // 컬렉션 제목 item { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - Text( - text = "컬렉션 제목", - color = FlintTheme.colors.white, - style = FlintTheme.typography.head3M18, - ) - - Spacer(Modifier.height(16.dp)) - - CollectionInputTextField( - modifier = Modifier.fillMaxWidth(), - value = uiState.title, - placeholder = "컬렉션의 제목을 입력해주세요.", - onValueChanged = onTitleChanged, - maxLength = 20, - singleLine = true, - maxLines = 1, - isShowLengthTitle = true, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Next - ) - ) - } + CollectionTitle( + title = uiState.title, + onTitleChanged = onTitleChanged, + modifier = Modifier.padding(horizontal = (16).dp), + ) } // 컬렉션 소개 item { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - Text( - text = - buildAnnotatedString { - append("컬렉션 소개 ") - withStyle( - style = SpanStyle(color = FlintTheme.colors.gray300), - ) { - append("(선택)") - } - }, - color = FlintTheme.colors.white, - style = FlintTheme.typography.head3M18, - ) - - Spacer(Modifier.height(16.dp)) - - CollectionInputTextField( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 104.dp), - value = uiState.description, - placeholder = "컬렉션의 소개를 작성해주세요.", - onValueChanged = onDescriptionChanged, - maxLength = 200, - singleLine = false, - maxLines = Int.MAX_VALUE, - isShowLengthTitle = true, - keyboardActions = KeyboardActions( - onDone = {}, - ), - ) - } + CollectionDescription( + description = uiState.description, + onDescriptionChanged = onDescriptionChanged, + modifier = Modifier.padding(horizontal = (16).dp), + ) } // 컬렉션 공개 여부 item { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - Text( - text = "컬렉션 공개 여부", - color = FlintTheme.colors.white, - style = FlintTheme.typography.head3M18, - ) - - Spacer(Modifier.height(16.dp)) - - Row { - FlintIconButton( - text = "공개", - iconRes = R.drawable.ic_share, - state = when(uiState.isPublic){ - true -> FlintButtonState.ColorOutline - false -> FlintButtonState.Disable - else -> FlintButtonState.Outline - }, - - onClick = { onPublicChanged(true) }, - modifier = Modifier.weight(1f), - ) - - Spacer(Modifier.width(8.dp)) - - FlintIconButton( - text = "비공개", - iconRes = R.drawable.ic_lock, - state = when(uiState.isPublic){ - true -> FlintButtonState.Disable - false -> FlintButtonState.ColorOutline - else -> FlintButtonState.Outline - }, - onClick = { onPublicChanged(false) }, - modifier = Modifier.weight(1f), - contentPadding = PaddingValues(start = 8.dp, end = 12.dp, top= 10.dp, bottom = 10.dp) - ) - } - } + CollectionPublicSection( + isPublic = uiState.isPublic, + onPublicChanged = onPublicChanged, + modifier = Modifier.padding(horizontal = (16).dp), + ) + + Spacer(Modifier.height(20.dp)) } - // 작품 추가 헤더 + // 작품 추가 섹션 item { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - Text( - text = "작품 추가", - color = FlintTheme.colors.white, - style = FlintTheme.typography.head3M18, - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "작품을 2개 이상 추가해주세요.", - color = FlintTheme.colors.gray200, - style = FlintTheme.typography.body2R14, - ) - Text( - text = "${selectedContents.size}/10", - color = FlintTheme.colors.white, - style = FlintTheme.typography.body2R14, - ) - } - } - } + CollectionAddContentSection( + selectedContents = uiState.selectedContents, + contentDetailsMap = uiState.contentDetailsMap, + onDeleteRequest = { content -> + contentToDelete = content + isModalVisible = true + }, + onSpoilerChanged = onSpoilerChanged, + onReasonChanged = onReasonChanged, + onAddContentClick = onAddContentClick, + modifier = Modifier.padding(horizontal = (16).dp), + ) - // 작품 리스트 - items( - items = selectedContents, - key = { it.id }, - ) { content -> - val detail = contentDetailsMap[content.id] ?: ContentDetail() - - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - CollectionCreateContentItemList( - onCancelClick = { - contentToDelete = content - isModalVisible = true - }, - imageUrl = content.posterUrl, - title = content.title, - director = content.author, - createdYear = content.year, - isSpoiler = detail.isSpoiler, - onSpoilerChanged = { isSpoiler -> - onSpoilerChanged(content.id, isSpoiler) - }, - selectedReason = detail.reason, - onSelectedReasonChanged = { reason -> - onReasonChanged(content.id, reason) - }, - ) - } + Spacer(Modifier.height(20.dp)) } - // 작품 추가 버튼 item { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - FlintIconButton( - text = "작품 추가하기", - iconRes = R.drawable.ic_plus, - state = FlintButtonState.ColorOutline, - onClick = onAddContentClick, - modifier = - Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = 80.dp), - contentPadding = PaddingValues(vertical = 28.dp) - ) - - Spacer(Modifier.height(36.dp)) - } + Text( + text = "웹사이트의 모든 콘텐츠는 저작권법의 보호를 받습니다. 사전 서면 동의 없이 무단 전제, 복사, 배포 등을 엄격히 금지합니다. \n" + + "Copyright © [2026] [Flint]. All rights reserved.", + color = FlintTheme.colors.gray300, + style = FlintTheme.typography.body2R14, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(Modifier.height(12.dp)) } } @@ -345,23 +221,223 @@ fun CollectionCreateScreen( } } -@Preview @Composable -fun CollectionCreateScreenPreview() { +private fun CollectionTitle( + title: String, + onTitleChanged: (String) -> Unit, + modifier: Modifier = Modifier, +){ + Column(modifier = modifier) { + Text( + text = "컬렉션 제목", + color = FlintTheme.colors.white, + style = FlintTheme.typography.head3M18, + ) + + Spacer(Modifier.height(16.dp)) + + CollectionInputTextField( + modifier = Modifier.fillMaxWidth(), + value = title, + placeholder = "컬렉션의 제목을 입력해주세요.", + onValueChanged = onTitleChanged, + maxLength = 20, + singleLine = true, + maxLines = 1, + isShowLengthTitle = true, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ) + ) + } +} + +@Composable +private fun CollectionDescription( + description: String, + onDescriptionChanged: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = + buildAnnotatedString { + append("컬렉션 소개 ") + withStyle( + style = SpanStyle(color = FlintTheme.colors.gray300), + ) { + append("(선택)") + } + }, + color = FlintTheme.colors.white, + style = FlintTheme.typography.head3M18, + ) + + Spacer(Modifier.height(16.dp)) + + CollectionInputTextField( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 104.dp), + value = description, + placeholder = "컬렉션의 소개를 작성해주세요.", + onValueChanged = onDescriptionChanged, + maxLength = 45, + singleLine = false, + maxLines = Int.MAX_VALUE, + isShowLengthTitle = true, + ) + } +} + +@Composable +private fun CollectionPublicSection( + isPublic: Boolean?, + onPublicChanged: (Boolean?) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = "컬렉션 공개 여부", + color = FlintTheme.colors.white, + style = FlintTheme.typography.head3M18, + ) + + Spacer(Modifier.height(16.dp)) + + Row { + FlintIconButton( + text = "공개", + iconRes = R.drawable.ic_share, + state = when (isPublic) { + true -> FlintButtonState.ColorOutline + false -> FlintButtonState.Disable + else -> FlintButtonState.Outline + }, + onClick = { onPublicChanged(true) }, + modifier = Modifier.weight(1f), + ) + + Spacer(Modifier.width(8.dp)) + + FlintIconButton( + text = "비공개", + iconRes = R.drawable.ic_lock, + state = when (isPublic) { + true -> FlintButtonState.Disable + false -> FlintButtonState.ColorOutline + else -> FlintButtonState.Outline + }, + onClick = { onPublicChanged(false) }, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(start = 8.dp, end = 12.dp, top = 10.dp, bottom = 10.dp) + ) + } + } +} + +@Composable +private fun CollectionAddContentSection( + selectedContents: ImmutableList, + contentDetailsMap: Map, + onDeleteRequest: (SearchContentItemModel) -> Unit, + onSpoilerChanged: (String, Boolean) -> Unit, + onReasonChanged: (String, String) -> Unit, + onAddContentClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = "작품 추가", + color = FlintTheme.colors.white, + style = FlintTheme.typography.head3M18, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "작품을 2개 이상 추가해주세요.", + color = FlintTheme.colors.gray200, + style = FlintTheme.typography.body2R14, + ) + Text( + text = "${selectedContents.size}/10", + color = FlintTheme.colors.white, + style = FlintTheme.typography.body2R14, + ) + } + + selectedContents.forEach { content -> + val detail = contentDetailsMap[content.id] ?: ContentDetail() + + Spacer(Modifier.height(28.dp)) + + Icon( + painter = painterResource(R.drawable.ic_deselect_large_pri), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .align(Alignment.End) + .clickable(onClick = { onDeleteRequest(content) }) + .padding(vertical = 10.dp) + .size(28.dp), + ) + + CollectionCreateContentSection( + posterImageUrl = content.posterUrl, + title = content.title, + director = content.author, + createdYear = content.year, + ) + + CollectionCreateContentImage( + imageUrls = emptyList(), + onDeleteClick = { onDeleteRequest(content) }, + ) + + Spacer(Modifier.height(16.dp)) + + CollectionCreateContentReason( + selectedReason = detail.reason, + onSelectedReasonChanged = { reason -> onReasonChanged(content.id, reason) }, + onSelectImageClick = {}, + isSpoiler = detail.isSpoiler, + onSpoilerChanged = { isSpoiler -> onSpoilerChanged(content.id, isSpoiler) }, + ) + } + + Spacer(Modifier.height(28.dp)) + + FlintIconButton( + text = "작품 추가하기", + iconRes = R.drawable.ic_plus, + state = FlintButtonState.ColorOutline, + onClick = onAddContentClick, + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 80.dp), + contentPadding = PaddingValues(vertical = 28.dp) + ) + } +} + +@Preview() +@Composable +private fun CollectionAddContentSectionPreview() { + val fakeContents = SearchContentListModel.FakeList.take(2).toImmutableList() + FlintTheme { - CollectionCreateScreen( - uiState = CollectionCreateUiState(), - onTitleChanged = {}, - onDescriptionChanged = {}, - onPublicChanged = {}, - selectedContents = SearchContentListModel.FakeList, - contentDetailsMap = emptyMap(), - onRemoveContent = {}, - onBackClick = {}, + CollectionAddContentSection( + selectedContents = fakeContents, + contentDetailsMap = fakeContents.associate { it.id to ContentDetail() }, + onDeleteRequest = {}, onSpoilerChanged = { _, _ -> }, onReasonChanged = { _, _ -> }, onAddContentClick = {}, - onFinishClick = {}, + modifier = Modifier.padding(horizontal = 16.dp), ) } } diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentItemList.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentItemList.kt deleted file mode 100644 index a2b275a6..00000000 --- a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentItemList.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.flint.presentation.collectioncreate.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -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.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardActions -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.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.flint.R -import com.flint.core.designsystem.component.textfield.CollectionInputTextField -import com.flint.core.designsystem.component.toggle.FlintBasicToggle -import com.flint.core.designsystem.theme.FlintTheme - -@Composable -fun CollectionCreateContentItemList( - onCancelClick: () -> Unit, - imageUrl: String, - title: String, - director: String, - createdYear: Int, - isSpoiler: Boolean, - onSpoilerChanged: (Boolean) -> Unit = {}, - selectedReason: String, - onSelectedReasonChanged: (String) -> Unit = {}, -) { - Column( - modifier = Modifier.background(color = FlintTheme.colors.background), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_cancel), - contentDescription = null, - tint = Color.White, - modifier = - Modifier - .clickable(onClick = onCancelClick) - .padding(12.dp) - .size(24.dp), - ) - } - - Spacer(Modifier.height(16.dp)) - - CollectionCreateContentSection( - imageUrl = imageUrl, - title = title, - director = director, - createdYear = createdYear, - ) - - Spacer(Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = "이 작품을 선택한 이유", - color = FlintTheme.colors.white, - style = FlintTheme.typography.head3M18, - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "스포일러", - color = FlintTheme.colors.white, - style = FlintTheme.typography.caption1M12, - ) - - Spacer(Modifier.width(8.dp)) - - FlintBasicToggle( - isChecked = isSpoiler, - onCheckedChange = onSpoilerChanged, - ) - } - } - - Spacer(Modifier.height(4.dp)) - - CollectionInputTextField( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 104.dp), - value = selectedReason, - placeholder = "이 작품의 매력 포인트를 적어주세요.", - onValueChanged = onSelectedReasonChanged, - singleLine = false, - maxLength = Int.MAX_VALUE, - maxLines = Int.MAX_VALUE, - isShowLengthTitle = false - ) - } -} - -@Preview(showBackground = true, backgroundColor = 0xFF121212) -@Composable -private fun CollectionCreateContentItemListPreview() { - FlintTheme { - CollectionCreateContentItemList( - onCancelClick = {}, - imageUrl = "https://buly.kr/DEaVFRZ", - title = "해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔", - director = "메롱", - createdYear = 2005, - isSpoiler = false, - selectedReason = "더미 이유", - onSpoilerChanged = {}, - onSelectedReasonChanged = {}, - ) - } -} diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSection.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSection.kt index 93c4b49f..f39ad61a 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSection.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSection.kt @@ -1,24 +1,31 @@ package com.flint.presentation.collectioncreate.component +import androidx.compose.foundation.clickable 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 import androidx.compose.foundation.layout.width +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.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.flint.R import com.flint.core.designsystem.component.image.NetworkImage import com.flint.core.designsystem.theme.FlintTheme @Composable fun CollectionCreateContentSection( - imageUrl: String, + posterImageUrl: String, title: String, director: String, createdYear: Int, @@ -29,7 +36,7 @@ fun CollectionCreateContentSection( verticalAlignment = Alignment.CenterVertically, ) { CollectionCreateContentSectionImage( - imageUrl = imageUrl, + imageUrl = posterImageUrl, modifier = Modifier .height(150.dp) @@ -101,7 +108,7 @@ private fun CollectionCreateContentSectionImage( private fun CollectionCreateContentSectionPreview() { FlintTheme { CollectionCreateContentSection( - imageUrl = "https://buly.kr/DEaVFRZ", + posterImageUrl = "https://buly.kr/DEaVFRZ", title = "해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔", director = "메롱", createdYear = 2005, diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSelect.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSelect.kt deleted file mode 100644 index 44f97b5e..00000000 --- a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSelect.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.flint.presentation.collectioncreate.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -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.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.designsystem.theme.FlintTheme - -@Composable -fun CollectionCreateContentSelect( - onCheckClick: () -> Unit, - isSelected: Boolean, - imageUrl: String, - title: String, - director: String, - createdYear: Int, - modifier: Modifier = Modifier, -) { - Row( - modifier = - modifier - .fillMaxWidth() - .background(color = FlintTheme.colors.background), - verticalAlignment = Alignment.CenterVertically, - ) { - CollectionCreateContentSection( - imageUrl = imageUrl, - title = title, - director = director, - createdYear = createdYear, - modifier = Modifier.weight(1f), - ) - - CollectionCreateContentSelectTag( - isSelected = isSelected, - onClick = onCheckClick, - ) - } -} - -@Composable -fun CollectionCreateContentSelectTag( - isSelected: Boolean, - onClick: () -> Unit, -) { - Icon( - imageVector = ImageVector.vectorResource(if (isSelected) R.drawable.ic_check_fill else R.drawable.ic_check_empty), - contentDescription = null, - tint = Color.Unspecified, - modifier = - Modifier - .size(48.dp) - .clickable(onClick = onClick), - ) -} - -@Preview -@Composable -private fun CollectionCreateContentSectionPreview() { - FlintTheme { - var isSelected by remember { mutableStateOf(false) } - CollectionCreateContentSelect( - onCheckClick = { isSelected = !isSelected }, - isSelected = isSelected, - imageUrl = "https://buly.kr/DEaVFRZ", - title = "해리포터 불의 잔", - director = "메롱", - createdYear = 2005, - ) - } -} 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 57d6cf88..6fca4fd1 100644 --- a/app/src/main/java/com/flint/presentation/main/MainNavigator.kt +++ b/app/src/main/java/com/flint/presentation/main/MainNavigator.kt @@ -33,7 +33,7 @@ class MainNavigator( val navController: NavHostController, coroutineScope: CoroutineScope, ) { - val startDestination = Route.Splash + val startDestination = Route.CollectionCreateGraph // NavController의 Flow를 관찰하여 현재 Destination을 StateFlow로 변환 private val currentDestination = From b21c76539ae58ceb85e89d9a7f320ebd6bdd7968 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 12 May 2026 05:03:33 +0900 Subject: [PATCH 03/13] =?UTF-8?q?[feat]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=B6=94=EA=B0=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AddContentSelectItem: 콘텐츠 선택 항목 (체크 아이콘 포함) - CollectionCreateContentImage: 이미지 페이저 (삭제 버튼, 페이지 인디케이터 포함) - CollectionCreateContentReason: 선택 이유 입력 (이미지 첨부, 스포일러 토글 포함) --- .../component/AddContentSelectItem.kt | 85 ++++++++++++ .../component/CollectionCreateContentImage.kt | 100 ++++++++++++++ .../CollectionCreateContentReason.kt | 123 ++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 app/src/main/java/com/flint/presentation/collectioncreate/component/AddContentSelectItem.kt create mode 100644 app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentImage.kt create mode 100644 app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentReason.kt diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/AddContentSelectItem.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/AddContentSelectItem.kt new file mode 100644 index 00000000..1ab791c3 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/AddContentSelectItem.kt @@ -0,0 +1,85 @@ +package com.flint.presentation.collectioncreate.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +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.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.designsystem.theme.FlintTheme + +@Composable +fun AddContentSelectItem( + onCheckClick: () -> Unit, + isSelected: Boolean, + posterImageUrl: String, + title: String, + director: String, + createdYear: Int, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .background(color = FlintTheme.colors.background), + verticalAlignment = Alignment.CenterVertically, + ) { + CollectionCreateContentSection( + posterImageUrl = posterImageUrl, + title = title, + director = director, + createdYear = createdYear, + modifier = Modifier.weight(1f), + ) + + AddContentSelectItemTag( + isSelected = isSelected, + onClick = onCheckClick, + ) + } +} + +@Composable +fun AddContentSelectItemTag( + isSelected: Boolean, + onClick: () -> Unit, +) { + Icon( + imageVector = ImageVector.vectorResource(if (isSelected) R.drawable.ic_check_fill else R.drawable.ic_check_empty), + contentDescription = null, + tint = Color.Unspecified, + modifier = + Modifier + .size(48.dp) + .clickable(onClick = onClick), + ) +} + +@Preview +@Composable +private fun AddContentSelectItemPreview() { + FlintTheme { + var isSelected by remember { mutableStateOf(false) } + AddContentSelectItem( + onCheckClick = { isSelected = !isSelected }, + isSelected = isSelected, + posterImageUrl = "https://buly.kr/DEaVFRZ", + title = "해리포터 불의 잔", + director = "메롱", + createdYear = 2005, + ) + } +} 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 new file mode 100644 index 00000000..faab6412 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentImage.kt @@ -0,0 +1,100 @@ +package com.flint.presentation.collectioncreate.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.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.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.designsystem.component.image.NetworkImage +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun CollectionCreateContentImage( + imageUrls: List, + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val pagerState = rememberPagerState { imageUrls.size } + + Column(modifier = modifier) { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth(), + ) { page -> + Box { + NetworkImage( + imageUrl = imageUrls[page], + modifier = Modifier + .fillMaxWidth() + .aspectRatio(4f / 3f), + ) + Icon( + painter = painterResource(R.drawable.ic_deselect_large_gray), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .align(Alignment.TopEnd) + .clickable(onClick = onDeleteClick) + .padding(all = 28.dp) + .size(24.dp), + ) + } + } + + if (imageUrls.size > 1) { + Spacer(Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + ) { + repeat(imageUrls.size) { index -> + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background( + if (index == pagerState.currentPage) FlintTheme.colors.secondary400 + else FlintTheme.colors.gray500 + ), + ) + } + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun CollectionCreateContentImagePreview() { + FlintTheme { + CollectionCreateContentImage( + imageUrls = listOf( + "https://buly.kr/DEaVFRZ", + "https://buly.kr/DEaVFRZ", + "https://buly.kr/DEaVFRZ", + ), + onDeleteClick = {}, + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentReason.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentReason.kt new file mode 100644 index 00000000..5fc3a4d7 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentReason.kt @@ -0,0 +1,123 @@ +package com.flint.presentation.collectioncreate.component + +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.foundation.layout.Column +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.designsystem.component.textfield.CollectionInputTextField +import com.flint.core.designsystem.component.toggle.FlintBasicToggle +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun CollectionCreateContentReason( + selectedReason: String, + onSelectedReasonChanged: (String) -> Unit, + onSelectImageClick: () -> Unit, + isSpoiler: Boolean, + onSpoilerChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = "이 작품을 선택한 이유", + color = FlintTheme.colors.white, + style = FlintTheme.typography.head3M18, + ) + + Spacer(Modifier.height(16.dp)) + + CollectionInputTextField( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 104.dp), + value = selectedReason, + placeholder = "이 작품의 매력 포인트를 적어주세요.", + onValueChanged = onSelectedReasonChanged, + singleLine = false, + maxLength = Int.MAX_VALUE, + maxLines = Int.MAX_VALUE, + isShowLengthTitle = false + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(width = 48.dp, height = 28.dp) + .border(1.dp, Color(0xFF1ABFF2), RoundedCornerShape(60.dp)) + .clip(RoundedCornerShape(60.dp)) + .background(Color(0xFF21242C)) + .clickable(onClick = onSelectImageClick), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(R.drawable.ic_background_photo), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.size(14.dp), + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "스포일러", + color = FlintTheme.colors.white, + style = FlintTheme.typography.caption1M12, + ) + + Spacer(Modifier.width(8.dp)) + + FlintBasicToggle( + isChecked = isSpoiler, + onCheckedChange = onSpoilerChanged, + ) + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun CollectionCreateContentReasonPreview() { + FlintTheme { + var reason by remember { mutableStateOf("") } + var isSpoiler by remember { mutableStateOf(false) } + CollectionCreateContentReason( + selectedReason = reason, + onSelectedReasonChanged = { reason = it }, + onSelectImageClick = {}, + isSpoiler = isSpoiler, + onSpoilerChanged = { isSpoiler = it }, + ) + } +} + From 02c2a2176adee49385e74463dec1463286da92a7 Mon Sep 17 00:00:00 2001 From: chanmi Date: Thu, 7 May 2026 20:55:46 +0900 Subject: [PATCH 04/13] =?UTF-8?q?[fix]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EB=B0=B0?= =?UTF-8?q?=EA=B2=BD=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/CollectionCreateThumbnail.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt index 1991176c..0ee48897 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt @@ -63,11 +63,11 @@ private fun CollectionCreateEmptyThumbnail( contentDescription = null, ) -// Icon( -// painter = painterResource(R.drawable.ic_background_photo), -// contentDescription = null, -// tint = Color.Unspecified, -// ) + Icon( + painter = painterResource(R.drawable.ic_background_photo), + contentDescription = null, + tint = Color.Unspecified, + ) } } @@ -97,11 +97,11 @@ private fun CollectionCreateFillThumbnail( .background(FlintTheme.colors.imgBlur), ) -// Icon( -// painter = painterResource(R.drawable.ic_background_photo), -// contentDescription = null, -// tint = Color.Unspecified, -// ) + Icon( + painter = painterResource(R.drawable.ic_background_photo), + contentDescription = null, + tint = Color.Unspecified, + ) } } From be8cb8fdfefd6ab2001c2841f4d89e1cfb228613 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 12 May 2026 05:01:18 +0900 Subject: [PATCH 05/13] =?UTF-8?q?[feat]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20UI=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CollectionCreateContentItemList 제거 - CollectionCreateContentImage: HorizontalPager 기반 이미지 슬라이더 (최대 5개, 빈 리스트 시 미표시, 1개 시 인디케이터 숨김) - CollectionCreateContentReason: 선택 이유 텍스트필드 + 스포일러 토글 분리 - AddContentSelectItem으로 컴포넌트 네이밍 개선 - CollectionCreateScreen에서 세 컴포넌트 조합으로 교체 --- .../collectioncreate/AddContentScreen.kt | 6 +- .../CollectionCreateScreen.kt | 452 ++++++++++-------- .../CollectionCreateContentItemList.kt | 136 ------ .../CollectionCreateContentSection.kt | 13 +- .../CollectionCreateContentSelect.kt | 86 ---- .../flint/presentation/main/MainNavigator.kt | 2 +- 6 files changed, 278 insertions(+), 417 deletions(-) delete mode 100644 app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentItemList.kt delete mode 100644 app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSelect.kt diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/AddContentScreen.kt b/app/src/main/java/com/flint/presentation/collectioncreate/AddContentScreen.kt index 33418f1e..d6bde25d 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/AddContentScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/AddContentScreen.kt @@ -36,7 +36,7 @@ import com.flint.core.designsystem.theme.FlintTheme import com.flint.domain.model.search.SearchContentItemModel import com.flint.domain.model.search.SearchContentListModel import com.flint.presentation.collectioncreate.component.CollectionCreateContentDeleteModal -import com.flint.presentation.collectioncreate.component.CollectionCreateContentSelect +import com.flint.presentation.collectioncreate.component.AddContentSelectItem import kotlinx.collections.immutable.ImmutableList @Composable @@ -177,7 +177,7 @@ fun AddContentScreen( ) { content -> val isSelected = selectedContents.any { it.id == content.id } - CollectionCreateContentSelect( + AddContentSelectItem( onCheckClick = { if (isSelected){ if (uiState.isCancelModalVisible) { @@ -189,7 +189,7 @@ fun AddContentScreen( } else onToggleContent(content) }, isSelected = isSelected, - imageUrl = content.posterUrl, + posterImageUrl = content.posterUrl, title = content.title, director = content.author, createdYear = content.year, 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 caebd23f..cdf00ddd 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -1,6 +1,7 @@ package com.flint.presentation.collectioncreate import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -12,11 +13,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -27,6 +28,8 @@ 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.res.painterResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.ImeAction @@ -45,9 +48,12 @@ import com.flint.core.designsystem.theme.FlintTheme import com.flint.domain.model.search.SearchContentItemModel import com.flint.domain.model.search.SearchContentListModel import com.flint.presentation.collectioncreate.component.CollectionCreateContentDeleteModal -import com.flint.presentation.collectioncreate.component.CollectionCreateContentItemList +import com.flint.presentation.collectioncreate.component.CollectionCreateContentImage +import com.flint.presentation.collectioncreate.component.CollectionCreateContentReason +import com.flint.presentation.collectioncreate.component.CollectionCreateContentSection import com.flint.presentation.collectioncreate.component.CollectionCreateThumbnail import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList @Composable fun CollectionCreateRoute( @@ -76,16 +82,12 @@ fun CollectionCreateRoute( onTitleChanged = viewModel::updateTitle, onDescriptionChanged = viewModel::updateDescription, onPublicChanged = viewModel::updateIsPublic, - selectedContents = uiState.selectedContents, - contentDetailsMap = uiState.contentDetailsMap, onRemoveContent = viewModel::removeContent, onBackClick = navigateUp, onSpoilerChanged = viewModel::updateSpoiler, onReasonChanged = viewModel::updateReason, onAddContentClick = navigateToAddContent, - onFinishClick = { - viewModel.onClickFinish() - }, + onFinishClick = viewModel::onClickFinish, modifier = Modifier.padding(paddingValues), ) } @@ -96,8 +98,6 @@ fun CollectionCreateScreen( onTitleChanged: (String) -> Unit = {}, onDescriptionChanged: (String) -> Unit = {}, onPublicChanged: (Boolean?) -> Unit = {}, - selectedContents: ImmutableList, - contentDetailsMap: Map, onRemoveContent: (SearchContentItemModel) -> Unit, onBackClick: () -> Unit, onSpoilerChanged: (String, Boolean) -> Unit = { _, _ -> }, @@ -119,7 +119,7 @@ fun CollectionCreateScreen( LazyColumn( modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(28.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { // 썸네일 item { @@ -128,190 +128,66 @@ fun CollectionCreateScreen( onClick = { }, ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(20.dp)) } // 컬렉션 제목 item { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - Text( - text = "컬렉션 제목", - color = FlintTheme.colors.white, - style = FlintTheme.typography.head3M18, - ) - - Spacer(Modifier.height(16.dp)) - - CollectionInputTextField( - modifier = Modifier.fillMaxWidth(), - value = uiState.title, - placeholder = "컬렉션의 제목을 입력해주세요.", - onValueChanged = onTitleChanged, - maxLength = 20, - singleLine = true, - maxLines = 1, - isShowLengthTitle = true, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Next - ) - ) - } + CollectionTitle( + title = uiState.title, + onTitleChanged = onTitleChanged, + modifier = Modifier.padding(horizontal = (16).dp), + ) } // 컬렉션 소개 item { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - Text( - text = - buildAnnotatedString { - append("컬렉션 소개 ") - withStyle( - style = SpanStyle(color = FlintTheme.colors.gray300), - ) { - append("(선택)") - } - }, - color = FlintTheme.colors.white, - style = FlintTheme.typography.head3M18, - ) - - Spacer(Modifier.height(16.dp)) - - CollectionInputTextField( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 104.dp), - value = uiState.description, - placeholder = "컬렉션의 소개를 작성해주세요.", - onValueChanged = onDescriptionChanged, - maxLength = 200, - singleLine = false, - maxLines = Int.MAX_VALUE, - isShowLengthTitle = true, - keyboardActions = KeyboardActions( - onDone = {}, - ), - ) - } + CollectionDescription( + description = uiState.description, + onDescriptionChanged = onDescriptionChanged, + modifier = Modifier.padding(horizontal = (16).dp), + ) } // 컬렉션 공개 여부 item { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - Text( - text = "컬렉션 공개 여부", - color = FlintTheme.colors.white, - style = FlintTheme.typography.head3M18, - ) - - Spacer(Modifier.height(16.dp)) - - Row { - FlintIconButton( - text = "공개", - iconRes = R.drawable.ic_share, - state = when(uiState.isPublic){ - true -> FlintButtonState.ColorOutline - false -> FlintButtonState.Disable - else -> FlintButtonState.Outline - }, - - onClick = { onPublicChanged(true) }, - modifier = Modifier.weight(1f), - ) - - Spacer(Modifier.width(8.dp)) - - FlintIconButton( - text = "비공개", - iconRes = R.drawable.ic_lock, - state = when(uiState.isPublic){ - true -> FlintButtonState.Disable - false -> FlintButtonState.ColorOutline - else -> FlintButtonState.Outline - }, - onClick = { onPublicChanged(false) }, - modifier = Modifier.weight(1f), - contentPadding = PaddingValues(start = 8.dp, end = 12.dp, top= 10.dp, bottom = 10.dp) - ) - } - } + CollectionPublicSection( + isPublic = uiState.isPublic, + onPublicChanged = onPublicChanged, + modifier = Modifier.padding(horizontal = (16).dp), + ) + + Spacer(Modifier.height(20.dp)) } - // 작품 추가 헤더 + // 작품 추가 섹션 item { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - Text( - text = "작품 추가", - color = FlintTheme.colors.white, - style = FlintTheme.typography.head3M18, - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "작품을 2개 이상 추가해주세요.", - color = FlintTheme.colors.gray200, - style = FlintTheme.typography.body2R14, - ) - Text( - text = "${selectedContents.size}/10", - color = FlintTheme.colors.white, - style = FlintTheme.typography.body2R14, - ) - } - } - } + CollectionAddContentSection( + selectedContents = uiState.selectedContents, + contentDetailsMap = uiState.contentDetailsMap, + onDeleteRequest = { content -> + contentToDelete = content + isModalVisible = true + }, + onSpoilerChanged = onSpoilerChanged, + onReasonChanged = onReasonChanged, + onAddContentClick = onAddContentClick, + modifier = Modifier.padding(horizontal = (16).dp), + ) - // 작품 리스트 - items( - items = selectedContents, - key = { it.id }, - ) { content -> - val detail = contentDetailsMap[content.id] ?: ContentDetail() - - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - CollectionCreateContentItemList( - onCancelClick = { - contentToDelete = content - isModalVisible = true - }, - imageUrl = content.posterUrl, - title = content.title, - director = content.author, - createdYear = content.year, - isSpoiler = detail.isSpoiler, - onSpoilerChanged = { isSpoiler -> - onSpoilerChanged(content.id, isSpoiler) - }, - selectedReason = detail.reason, - onSelectedReasonChanged = { reason -> - onReasonChanged(content.id, reason) - }, - ) - } + Spacer(Modifier.height(20.dp)) } - // 작품 추가 버튼 item { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - FlintIconButton( - text = "작품 추가하기", - iconRes = R.drawable.ic_plus, - state = FlintButtonState.ColorOutline, - onClick = onAddContentClick, - modifier = - Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = 80.dp), - contentPadding = PaddingValues(vertical = 28.dp) - ) - - Spacer(Modifier.height(36.dp)) - } + Text( + text = "웹사이트의 모든 콘텐츠는 저작권법의 보호를 받습니다. 사전 서면 동의 없이 무단 전제, 복사, 배포 등을 엄격히 금지합니다. \n" + + "Copyright © [2026] [Flint]. All rights reserved.", + color = FlintTheme.colors.gray300, + style = FlintTheme.typography.body2R14, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(Modifier.height(12.dp)) } } @@ -345,23 +221,223 @@ fun CollectionCreateScreen( } } -@Preview @Composable -fun CollectionCreateScreenPreview() { +private fun CollectionTitle( + title: String, + onTitleChanged: (String) -> Unit, + modifier: Modifier = Modifier, +){ + Column(modifier = modifier) { + Text( + text = "컬렉션 제목", + color = FlintTheme.colors.white, + style = FlintTheme.typography.head3M18, + ) + + Spacer(Modifier.height(16.dp)) + + CollectionInputTextField( + modifier = Modifier.fillMaxWidth(), + value = title, + placeholder = "컬렉션의 제목을 입력해주세요.", + onValueChanged = onTitleChanged, + maxLength = 20, + singleLine = true, + maxLines = 1, + isShowLengthTitle = true, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ) + ) + } +} + +@Composable +private fun CollectionDescription( + description: String, + onDescriptionChanged: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = + buildAnnotatedString { + append("컬렉션 소개 ") + withStyle( + style = SpanStyle(color = FlintTheme.colors.gray300), + ) { + append("(선택)") + } + }, + color = FlintTheme.colors.white, + style = FlintTheme.typography.head3M18, + ) + + Spacer(Modifier.height(16.dp)) + + CollectionInputTextField( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 104.dp), + value = description, + placeholder = "컬렉션의 소개를 작성해주세요.", + onValueChanged = onDescriptionChanged, + maxLength = 45, + singleLine = false, + maxLines = Int.MAX_VALUE, + isShowLengthTitle = true, + ) + } +} + +@Composable +private fun CollectionPublicSection( + isPublic: Boolean?, + onPublicChanged: (Boolean?) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = "컬렉션 공개 여부", + color = FlintTheme.colors.white, + style = FlintTheme.typography.head3M18, + ) + + Spacer(Modifier.height(16.dp)) + + Row { + FlintIconButton( + text = "공개", + iconRes = R.drawable.ic_share, + state = when (isPublic) { + true -> FlintButtonState.ColorOutline + false -> FlintButtonState.Disable + else -> FlintButtonState.Outline + }, + onClick = { onPublicChanged(true) }, + modifier = Modifier.weight(1f), + ) + + Spacer(Modifier.width(8.dp)) + + FlintIconButton( + text = "비공개", + iconRes = R.drawable.ic_lock, + state = when (isPublic) { + true -> FlintButtonState.Disable + false -> FlintButtonState.ColorOutline + else -> FlintButtonState.Outline + }, + onClick = { onPublicChanged(false) }, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(start = 8.dp, end = 12.dp, top = 10.dp, bottom = 10.dp) + ) + } + } +} + +@Composable +private fun CollectionAddContentSection( + selectedContents: ImmutableList, + contentDetailsMap: Map, + onDeleteRequest: (SearchContentItemModel) -> Unit, + onSpoilerChanged: (String, Boolean) -> Unit, + onReasonChanged: (String, String) -> Unit, + onAddContentClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = "작품 추가", + color = FlintTheme.colors.white, + style = FlintTheme.typography.head3M18, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "작품을 2개 이상 추가해주세요.", + color = FlintTheme.colors.gray200, + style = FlintTheme.typography.body2R14, + ) + Text( + text = "${selectedContents.size}/10", + color = FlintTheme.colors.white, + style = FlintTheme.typography.body2R14, + ) + } + + selectedContents.forEach { content -> + val detail = contentDetailsMap[content.id] ?: ContentDetail() + + Spacer(Modifier.height(28.dp)) + + Icon( + painter = painterResource(R.drawable.ic_deselect_large_pri), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .align(Alignment.End) + .clickable(onClick = { onDeleteRequest(content) }) + .padding(vertical = 10.dp) + .size(28.dp), + ) + + CollectionCreateContentSection( + posterImageUrl = content.posterUrl, + title = content.title, + director = content.author, + createdYear = content.year, + ) + + CollectionCreateContentImage( + imageUrls = emptyList(), + onDeleteClick = { onDeleteRequest(content) }, + ) + + Spacer(Modifier.height(16.dp)) + + CollectionCreateContentReason( + selectedReason = detail.reason, + onSelectedReasonChanged = { reason -> onReasonChanged(content.id, reason) }, + onSelectImageClick = {}, + isSpoiler = detail.isSpoiler, + onSpoilerChanged = { isSpoiler -> onSpoilerChanged(content.id, isSpoiler) }, + ) + } + + Spacer(Modifier.height(28.dp)) + + FlintIconButton( + text = "작품 추가하기", + iconRes = R.drawable.ic_plus, + state = FlintButtonState.ColorOutline, + onClick = onAddContentClick, + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 80.dp), + contentPadding = PaddingValues(vertical = 28.dp) + ) + } +} + +@Preview() +@Composable +private fun CollectionAddContentSectionPreview() { + val fakeContents = SearchContentListModel.FakeList.take(2).toImmutableList() + FlintTheme { - CollectionCreateScreen( - uiState = CollectionCreateUiState(), - onTitleChanged = {}, - onDescriptionChanged = {}, - onPublicChanged = {}, - selectedContents = SearchContentListModel.FakeList, - contentDetailsMap = emptyMap(), - onRemoveContent = {}, - onBackClick = {}, + CollectionAddContentSection( + selectedContents = fakeContents, + contentDetailsMap = fakeContents.associate { it.id to ContentDetail() }, + onDeleteRequest = {}, onSpoilerChanged = { _, _ -> }, onReasonChanged = { _, _ -> }, onAddContentClick = {}, - onFinishClick = {}, + modifier = Modifier.padding(horizontal = 16.dp), ) } } diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentItemList.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentItemList.kt deleted file mode 100644 index a2b275a6..00000000 --- a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentItemList.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.flint.presentation.collectioncreate.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -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.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.KeyboardActions -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.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.flint.R -import com.flint.core.designsystem.component.textfield.CollectionInputTextField -import com.flint.core.designsystem.component.toggle.FlintBasicToggle -import com.flint.core.designsystem.theme.FlintTheme - -@Composable -fun CollectionCreateContentItemList( - onCancelClick: () -> Unit, - imageUrl: String, - title: String, - director: String, - createdYear: Int, - isSpoiler: Boolean, - onSpoilerChanged: (Boolean) -> Unit = {}, - selectedReason: String, - onSelectedReasonChanged: (String) -> Unit = {}, -) { - Column( - modifier = Modifier.background(color = FlintTheme.colors.background), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_cancel), - contentDescription = null, - tint = Color.White, - modifier = - Modifier - .clickable(onClick = onCancelClick) - .padding(12.dp) - .size(24.dp), - ) - } - - Spacer(Modifier.height(16.dp)) - - CollectionCreateContentSection( - imageUrl = imageUrl, - title = title, - director = director, - createdYear = createdYear, - ) - - Spacer(Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = "이 작품을 선택한 이유", - color = FlintTheme.colors.white, - style = FlintTheme.typography.head3M18, - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "스포일러", - color = FlintTheme.colors.white, - style = FlintTheme.typography.caption1M12, - ) - - Spacer(Modifier.width(8.dp)) - - FlintBasicToggle( - isChecked = isSpoiler, - onCheckedChange = onSpoilerChanged, - ) - } - } - - Spacer(Modifier.height(4.dp)) - - CollectionInputTextField( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 104.dp), - value = selectedReason, - placeholder = "이 작품의 매력 포인트를 적어주세요.", - onValueChanged = onSelectedReasonChanged, - singleLine = false, - maxLength = Int.MAX_VALUE, - maxLines = Int.MAX_VALUE, - isShowLengthTitle = false - ) - } -} - -@Preview(showBackground = true, backgroundColor = 0xFF121212) -@Composable -private fun CollectionCreateContentItemListPreview() { - FlintTheme { - CollectionCreateContentItemList( - onCancelClick = {}, - imageUrl = "https://buly.kr/DEaVFRZ", - title = "해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔", - director = "메롱", - createdYear = 2005, - isSpoiler = false, - selectedReason = "더미 이유", - onSpoilerChanged = {}, - onSelectedReasonChanged = {}, - ) - } -} diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSection.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSection.kt index 93c4b49f..f39ad61a 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSection.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSection.kt @@ -1,24 +1,31 @@ package com.flint.presentation.collectioncreate.component +import androidx.compose.foundation.clickable 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 import androidx.compose.foundation.layout.width +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.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.flint.R import com.flint.core.designsystem.component.image.NetworkImage import com.flint.core.designsystem.theme.FlintTheme @Composable fun CollectionCreateContentSection( - imageUrl: String, + posterImageUrl: String, title: String, director: String, createdYear: Int, @@ -29,7 +36,7 @@ fun CollectionCreateContentSection( verticalAlignment = Alignment.CenterVertically, ) { CollectionCreateContentSectionImage( - imageUrl = imageUrl, + imageUrl = posterImageUrl, modifier = Modifier .height(150.dp) @@ -101,7 +108,7 @@ private fun CollectionCreateContentSectionImage( private fun CollectionCreateContentSectionPreview() { FlintTheme { CollectionCreateContentSection( - imageUrl = "https://buly.kr/DEaVFRZ", + posterImageUrl = "https://buly.kr/DEaVFRZ", title = "해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔 해리포터 불의 잔", director = "메롱", createdYear = 2005, diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSelect.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSelect.kt deleted file mode 100644 index 44f97b5e..00000000 --- a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentSelect.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.flint.presentation.collectioncreate.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -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.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.designsystem.theme.FlintTheme - -@Composable -fun CollectionCreateContentSelect( - onCheckClick: () -> Unit, - isSelected: Boolean, - imageUrl: String, - title: String, - director: String, - createdYear: Int, - modifier: Modifier = Modifier, -) { - Row( - modifier = - modifier - .fillMaxWidth() - .background(color = FlintTheme.colors.background), - verticalAlignment = Alignment.CenterVertically, - ) { - CollectionCreateContentSection( - imageUrl = imageUrl, - title = title, - director = director, - createdYear = createdYear, - modifier = Modifier.weight(1f), - ) - - CollectionCreateContentSelectTag( - isSelected = isSelected, - onClick = onCheckClick, - ) - } -} - -@Composable -fun CollectionCreateContentSelectTag( - isSelected: Boolean, - onClick: () -> Unit, -) { - Icon( - imageVector = ImageVector.vectorResource(if (isSelected) R.drawable.ic_check_fill else R.drawable.ic_check_empty), - contentDescription = null, - tint = Color.Unspecified, - modifier = - Modifier - .size(48.dp) - .clickable(onClick = onClick), - ) -} - -@Preview -@Composable -private fun CollectionCreateContentSectionPreview() { - FlintTheme { - var isSelected by remember { mutableStateOf(false) } - CollectionCreateContentSelect( - onCheckClick = { isSelected = !isSelected }, - isSelected = isSelected, - imageUrl = "https://buly.kr/DEaVFRZ", - title = "해리포터 불의 잔", - director = "메롱", - createdYear = 2005, - ) - } -} 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 57d6cf88..6fca4fd1 100644 --- a/app/src/main/java/com/flint/presentation/main/MainNavigator.kt +++ b/app/src/main/java/com/flint/presentation/main/MainNavigator.kt @@ -33,7 +33,7 @@ class MainNavigator( val navController: NavHostController, coroutineScope: CoroutineScope, ) { - val startDestination = Route.Splash + val startDestination = Route.CollectionCreateGraph // NavController의 Flow를 관찰하여 현재 Destination을 StateFlow로 변환 private val currentDestination = From ac049adc99625c07f3f8154c5e41dd138fa738d7 Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 12 May 2026 05:03:33 +0900 Subject: [PATCH 06/13] =?UTF-8?q?[feat]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=B6=94=EA=B0=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AddContentSelectItem: 콘텐츠 선택 항목 (체크 아이콘 포함) - CollectionCreateContentImage: 이미지 페이저 (삭제 버튼, 페이지 인디케이터 포함) - CollectionCreateContentReason: 선택 이유 입력 (이미지 첨부, 스포일러 토글 포함) --- .../component/AddContentSelectItem.kt | 85 ++++++++++++ .../component/CollectionCreateContentImage.kt | 100 ++++++++++++++ .../CollectionCreateContentReason.kt | 123 ++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 app/src/main/java/com/flint/presentation/collectioncreate/component/AddContentSelectItem.kt create mode 100644 app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentImage.kt create mode 100644 app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentReason.kt diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/AddContentSelectItem.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/AddContentSelectItem.kt new file mode 100644 index 00000000..1ab791c3 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/AddContentSelectItem.kt @@ -0,0 +1,85 @@ +package com.flint.presentation.collectioncreate.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +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.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.designsystem.theme.FlintTheme + +@Composable +fun AddContentSelectItem( + onCheckClick: () -> Unit, + isSelected: Boolean, + posterImageUrl: String, + title: String, + director: String, + createdYear: Int, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .background(color = FlintTheme.colors.background), + verticalAlignment = Alignment.CenterVertically, + ) { + CollectionCreateContentSection( + posterImageUrl = posterImageUrl, + title = title, + director = director, + createdYear = createdYear, + modifier = Modifier.weight(1f), + ) + + AddContentSelectItemTag( + isSelected = isSelected, + onClick = onCheckClick, + ) + } +} + +@Composable +fun AddContentSelectItemTag( + isSelected: Boolean, + onClick: () -> Unit, +) { + Icon( + imageVector = ImageVector.vectorResource(if (isSelected) R.drawable.ic_check_fill else R.drawable.ic_check_empty), + contentDescription = null, + tint = Color.Unspecified, + modifier = + Modifier + .size(48.dp) + .clickable(onClick = onClick), + ) +} + +@Preview +@Composable +private fun AddContentSelectItemPreview() { + FlintTheme { + var isSelected by remember { mutableStateOf(false) } + AddContentSelectItem( + onCheckClick = { isSelected = !isSelected }, + isSelected = isSelected, + posterImageUrl = "https://buly.kr/DEaVFRZ", + title = "해리포터 불의 잔", + director = "메롱", + createdYear = 2005, + ) + } +} 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 new file mode 100644 index 00000000..faab6412 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentImage.kt @@ -0,0 +1,100 @@ +package com.flint.presentation.collectioncreate.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.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.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.designsystem.component.image.NetworkImage +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun CollectionCreateContentImage( + imageUrls: List, + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val pagerState = rememberPagerState { imageUrls.size } + + Column(modifier = modifier) { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth(), + ) { page -> + Box { + NetworkImage( + imageUrl = imageUrls[page], + modifier = Modifier + .fillMaxWidth() + .aspectRatio(4f / 3f), + ) + Icon( + painter = painterResource(R.drawable.ic_deselect_large_gray), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .align(Alignment.TopEnd) + .clickable(onClick = onDeleteClick) + .padding(all = 28.dp) + .size(24.dp), + ) + } + } + + if (imageUrls.size > 1) { + Spacer(Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + ) { + repeat(imageUrls.size) { index -> + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background( + if (index == pagerState.currentPage) FlintTheme.colors.secondary400 + else FlintTheme.colors.gray500 + ), + ) + } + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun CollectionCreateContentImagePreview() { + FlintTheme { + CollectionCreateContentImage( + imageUrls = listOf( + "https://buly.kr/DEaVFRZ", + "https://buly.kr/DEaVFRZ", + "https://buly.kr/DEaVFRZ", + ), + onDeleteClick = {}, + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentReason.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentReason.kt new file mode 100644 index 00000000..5fc3a4d7 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentReason.kt @@ -0,0 +1,123 @@ +package com.flint.presentation.collectioncreate.component + +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.foundation.layout.Column +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.designsystem.component.textfield.CollectionInputTextField +import com.flint.core.designsystem.component.toggle.FlintBasicToggle +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun CollectionCreateContentReason( + selectedReason: String, + onSelectedReasonChanged: (String) -> Unit, + onSelectImageClick: () -> Unit, + isSpoiler: Boolean, + onSpoilerChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = "이 작품을 선택한 이유", + color = FlintTheme.colors.white, + style = FlintTheme.typography.head3M18, + ) + + Spacer(Modifier.height(16.dp)) + + CollectionInputTextField( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 104.dp), + value = selectedReason, + placeholder = "이 작품의 매력 포인트를 적어주세요.", + onValueChanged = onSelectedReasonChanged, + singleLine = false, + maxLength = Int.MAX_VALUE, + maxLines = Int.MAX_VALUE, + isShowLengthTitle = false + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(width = 48.dp, height = 28.dp) + .border(1.dp, Color(0xFF1ABFF2), RoundedCornerShape(60.dp)) + .clip(RoundedCornerShape(60.dp)) + .background(Color(0xFF21242C)) + .clickable(onClick = onSelectImageClick), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(R.drawable.ic_background_photo), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.size(14.dp), + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "스포일러", + color = FlintTheme.colors.white, + style = FlintTheme.typography.caption1M12, + ) + + Spacer(Modifier.width(8.dp)) + + FlintBasicToggle( + isChecked = isSpoiler, + onCheckedChange = onSpoilerChanged, + ) + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun CollectionCreateContentReasonPreview() { + FlintTheme { + var reason by remember { mutableStateOf("") } + var isSpoiler by remember { mutableStateOf(false) } + CollectionCreateContentReason( + selectedReason = reason, + onSelectedReasonChanged = { reason = it }, + onSelectImageClick = {}, + isSpoiler = isSpoiler, + onSpoilerChanged = { isSpoiler = it }, + ) + } +} + From 48db9af1bcf0f54e15488efc058dc60cb712c595 Mon Sep 17 00:00:00 2001 From: chanmi Date: Sat, 6 Jun 2026 16:31:18 +0900 Subject: [PATCH 07/13] =?UTF-8?q?[feat]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=8A=AC?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=8D=94=20=EB=B0=8F=20=ED=94=84=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ContentDetail에 imageUrls 필드 추가 - CollectionCreateContentImage HorizontalPager 무한 스크롤 적용 - 인디케이터 도트 currentIndex 기준으로 수정 - CollectionCreateScreen 프리뷰로 교체 (더미 데이터 포함) - 저작권 문구 수정 --- .../CollectionCreateScreen.kt | 36 ++++++++++++------- .../component/CollectionCreateContentImage.kt | 12 ++++--- .../uistate/CollectionCreateUiState.kt | 3 +- 3 files changed, 34 insertions(+), 17 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 cdf00ddd..079f653c 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -180,10 +180,9 @@ fun CollectionCreateScreen( item { Text( - text = "웹사이트의 모든 콘텐츠는 저작권법의 보호를 받습니다. 사전 서면 동의 없이 무단 전제, 복사, 배포 등을 엄격히 금지합니다. \n" + - "Copyright © [2026] [Flint]. All rights reserved.", + text = "Flint에서 제공하는 영화 · 드라마를 포함한 모든 콘텐츠의 저작권은 각 권리자에게 있으며, 관련 법령에 따라 보호됩니다. 컬렉션 이용 시 저작권을 준수해 주세요.", color = FlintTheme.colors.gray300, - style = FlintTheme.typography.body2R14, + style = FlintTheme.typography.caption1R12, modifier = Modifier.padding(horizontal = 16.dp) ) @@ -393,8 +392,10 @@ private fun CollectionAddContentSection( createdYear = content.year, ) + Spacer(Modifier.height(16.dp)) + CollectionCreateContentImage( - imageUrls = emptyList(), + imageUrls = detail.imageUrls, onDeleteClick = { onDeleteRequest(content) }, ) @@ -426,18 +427,29 @@ private fun CollectionAddContentSection( @Preview() @Composable -private fun CollectionAddContentSectionPreview() { +private fun CollectionCreateScreenPreview() { val fakeContents = SearchContentListModel.FakeList.take(2).toImmutableList() FlintTheme { - CollectionAddContentSection( - selectedContents = fakeContents, - contentDetailsMap = fakeContents.associate { it.id to ContentDetail() }, - onDeleteRequest = {}, - onSpoilerChanged = { _, _ -> }, - onReasonChanged = { _, _ -> }, + CollectionCreateScreen( + uiState = CollectionCreateUiState( + title = "내 컬렉션", + description = "컬렉션 소개입니다.", + isPublic = true, + selectedContents = fakeContents, + contentDetailsMap = fakeContents.associate { + it.id to ContentDetail( + imageUrls = listOf( + "https://buly.kr/DEaVFRZ", + "https://buly.kr/DEaVFRZ", + ) + ) + }, + ), + onRemoveContent = {}, + onBackClick = {}, onAddContentClick = {}, - modifier = Modifier.padding(horizontal = 16.dp), + onFinishClick = {}, ) } } 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 faab6412..89d1bdf4 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 @@ -34,16 +34,20 @@ fun CollectionCreateContentImage( onDeleteClick: () -> Unit, modifier: Modifier = Modifier, ) { - val pagerState = rememberPagerState { imageUrls.size } + val pageCount = if (imageUrls.size > 1) Int.MAX_VALUE else imageUrls.size + val initialPage = if (imageUrls.size > 1) Int.MAX_VALUE / 2 else 0 + val pagerState = rememberPagerState(initialPage = initialPage) { pageCount } + val currentIndex = if (imageUrls.isEmpty()) 0 else pagerState.currentPage % imageUrls.size Column(modifier = modifier) { HorizontalPager( state = pagerState, modifier = Modifier.fillMaxWidth(), ) { page -> + val index = page % imageUrls.size Box { NetworkImage( - imageUrl = imageUrls[page], + imageUrl = imageUrls[index], modifier = Modifier .fillMaxWidth() .aspectRatio(4f / 3f), @@ -55,7 +59,7 @@ fun CollectionCreateContentImage( modifier = Modifier .align(Alignment.TopEnd) .clickable(onClick = onDeleteClick) - .padding(all = 28.dp) + .padding(all = 16.dp) .size(24.dp), ) } @@ -74,7 +78,7 @@ fun CollectionCreateContentImage( .size(8.dp) .clip(CircleShape) .background( - if (index == pagerState.currentPage) FlintTheme.colors.secondary400 + if (index == currentIndex) FlintTheme.colors.secondary400 else FlintTheme.colors.gray500 ), ) 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 ee09df82..ff9b0cee 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 @@ -28,5 +28,6 @@ data class CollectionCreateUiState( @Immutable data class ContentDetail( val isSpoiler: Boolean = false, - val reason: String = "" + val reason: String = "", + val imageUrls: List = emptyList(), ) \ No newline at end of file From 865a64db911a88e91b71833d6567a4569bbb3c9a Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 9 Jun 2026 17:08:22 +0900 Subject: [PATCH 08/13] =?UTF-8?q?[feat]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20S3=20=EC=97=85=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 커버 사진 삭제 버튼 동작 연결 - 콘텐츠 이미지 Uri 로컬 저장 및 S3 업로드 플로우 구현 - 콘텐츠 이미지 1장씩 추가, 최대 5장 제한 - 이미지 표시 방식 Crop → Fit(4:3)으로 변경 - S3 업로드 로직 공통 함수로 리팩터링 --- .../request/CollectionCreateRequestDto.kt | 2 + .../collection/CollectionCreateMapper.kt | 1 + .../model/collection/CollectionCreateModel.kt | 1 + .../CollectionCreateScreen.kt | 96 +++++++++++++++--- .../CollectionCreateViewModel.kt | 97 +++++++++++++++++-- .../component/CollectionCreateContentImage.kt | 31 +++--- .../component/CollectionCreateThumbnail.kt | 6 +- .../uistate/CollectionCreateUiState.kt | 5 +- .../flint/presentation/main/MainNavigator.kt | 2 +- 9 files changed, 199 insertions(+), 42 deletions(-) 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 63c64bce..a0791eec 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,5 +24,7 @@ data class CollectionCreateRequestDto( val isSpoiler: Boolean, @SerialName("reason") val reason: String, + @SerialName("imageUrls") + val imageUrls: List = emptyList(), ) } diff --git a/app/src/main/java/com/flint/domain/mapper/collection/CollectionCreateMapper.kt b/app/src/main/java/com/flint/domain/mapper/collection/CollectionCreateMapper.kt index 4080489c..0c60de21 100644 --- a/app/src/main/java/com/flint/domain/mapper/collection/CollectionCreateMapper.kt +++ b/app/src/main/java/com/flint/domain/mapper/collection/CollectionCreateMapper.kt @@ -20,6 +20,7 @@ private fun CollectionCreateContentModel.toDto(): CollectionCreateRequestDto.Con contentId = contentId, isSpoiler = isSpoiler, reason = reason, + imageUrls = imageUrls, ) fun CollectionCreateResponseDto.toModel(): CollectionCreateModel = diff --git a/app/src/main/java/com/flint/domain/model/collection/CollectionCreateModel.kt b/app/src/main/java/com/flint/domain/model/collection/CollectionCreateModel.kt index aa54cbee..41b38e29 100644 --- a/app/src/main/java/com/flint/domain/model/collection/CollectionCreateModel.kt +++ b/app/src/main/java/com/flint/domain/model/collection/CollectionCreateModel.kt @@ -12,6 +12,7 @@ data class CollectionCreateContentModel( val contentId: String, val isSpoiler: Boolean, val reason: String, + val imageUrls: List = emptyList(), ) data class CollectionCreateModel( 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 079f653c..0bf7f550 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -1,5 +1,8 @@ package com.flint.presentation.collectioncreate +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -19,6 +22,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -39,6 +43,8 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.flint.R import com.flint.core.common.util.UiState +import com.flint.core.designsystem.component.bottomsheet.MenuBottomSheet +import com.flint.core.designsystem.component.bottomsheet.MenuBottomSheetData import com.flint.core.designsystem.component.button.FlintButtonState import com.flint.core.designsystem.component.button.FlintIconButton import com.flint.core.designsystem.component.button.FlintLargeButton @@ -77,6 +83,21 @@ fun CollectionCreateRoute( } } + val galleryLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + ) { uri: Uri? -> + viewModel.updateThumbnailImageUri(uri) + } + + var pendingContentId by remember { mutableStateOf(null) } + val contentImageLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + ) { uri: Uri? -> + uri ?: return@rememberLauncherForActivityResult + pendingContentId?.let { contentId -> viewModel.addContentImageUri(contentId, uri) } + pendingContentId = null + } + CollectionCreateScreen( uiState = uiState, onTitleChanged = viewModel::updateTitle, @@ -88,10 +109,21 @@ fun CollectionCreateRoute( onReasonChanged = viewModel::updateReason, onAddContentClick = navigateToAddContent, onFinishClick = viewModel::onClickFinish, + onGalleryClick = { galleryLauncher.launch("image/*") }, + onThumbnailDelete = { viewModel.updateThumbnailImageUri(null) }, + onSelectContentImage = { contentId -> + val currentCount = uiState.contentDetailsMap[contentId]?.contentImageUris?.size ?: 0 + if (currentCount < 5) { + pendingContentId = contentId + contentImageLauncher.launch("image/*") + } + }, + onRemoveContentImage = viewModel::removeContentImageUri, modifier = Modifier.padding(paddingValues), ) } +@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class) @Composable fun CollectionCreateScreen( uiState: CollectionCreateUiState, @@ -104,10 +136,15 @@ fun CollectionCreateScreen( onReasonChanged: (String, String) -> Unit = { _, _ -> }, onAddContentClick: () -> Unit, onFinishClick: () -> Unit, + onGalleryClick: () -> Unit = {}, + onThumbnailDelete: () -> Unit = {}, + onSelectContentImage: (contentId: String) -> Unit = {}, + onRemoveContentImage: (contentId: String, index: Int) -> Unit = { _, _ -> }, modifier: Modifier = Modifier, ) { var isModalVisible by remember { mutableStateOf(false) } var contentToDelete by remember { mutableStateOf(null) } + var isThumbnailBottomSheetVisible by remember { mutableStateOf(false) } Column( modifier = @@ -124,8 +161,8 @@ fun CollectionCreateScreen( // 썸네일 item { CollectionCreateThumbnail( - imageUrl = "", - onClick = { }, + imageUrl = uiState.thumbnailImageUri, + onClick = { isThumbnailBottomSheetVisible = true }, ) Spacer(Modifier.height(20.dp)) @@ -172,6 +209,8 @@ fun CollectionCreateScreen( onSpoilerChanged = onSpoilerChanged, onReasonChanged = onReasonChanged, onAddContentClick = onAddContentClick, + onSelectContentImage = onSelectContentImage, + onRemoveContentImage = onRemoveContentImage, modifier = Modifier.padding(horizontal = (16).dp), ) @@ -180,7 +219,7 @@ fun CollectionCreateScreen( item { Text( - text = "Flint에서 제공하는 영화 · 드라마를 포함한 모든 콘텐츠의 저작권은 각 권리자에게 있으며, 관련 법령에 따라 보호됩니다. 컬렉션 이용 시 저작권을 준수해 주세요.", + text = "Flint에서 제공하는 영화 · 드라마를 포함한 모든 콘텐츠의 저작권은 각 권리자에게 있으며, 관련 법령에 따라 보호됩니다. \n컬렉션 이용 시 저작권을 준수해 주세요.", color = FlintTheme.colors.gray300, style = FlintTheme.typography.caption1R12, modifier = Modifier.padding(horizontal = 16.dp) @@ -218,6 +257,29 @@ fun CollectionCreateScreen( }, ) } + + if (isThumbnailBottomSheetVisible) { + val menuBottomSheetDataList = + listOf( + MenuBottomSheetData( + label = "갤러리에서 선택", + clickAction = onGalleryClick, + ), + MenuBottomSheetData( + label = "커버 사진 삭제", + color = FlintTheme.colors.error500, + clickAction = onThumbnailDelete, + ), + ) + + val sheetState = rememberModalBottomSheetState() + + MenuBottomSheet( + menuBottomSheetDataList = menuBottomSheetDataList, + onDismiss = { isThumbnailBottomSheetVisible = false }, + sheetState = sheetState, + ) + } } @Composable @@ -343,6 +405,8 @@ private fun CollectionAddContentSection( onSpoilerChanged: (String, Boolean) -> Unit, onReasonChanged: (String, String) -> Unit, onAddContentClick: () -> Unit, + onSelectContentImage: (contentId: String) -> Unit, + onRemoveContentImage: (contentId: String, index: Int) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -394,17 +458,19 @@ private fun CollectionAddContentSection( Spacer(Modifier.height(16.dp)) - CollectionCreateContentImage( - imageUrls = detail.imageUrls, - onDeleteClick = { onDeleteRequest(content) }, - ) + if (detail.contentImageUris.isNotEmpty()) { + CollectionCreateContentImage( + imageUris = detail.contentImageUris, + onDeleteClick = { index -> onRemoveContentImage(content.id, index) }, + ) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(16.dp)) + } CollectionCreateContentReason( selectedReason = detail.reason, onSelectedReasonChanged = { reason -> onReasonChanged(content.id, reason) }, - onSelectImageClick = {}, + onSelectImageClick = { onSelectContentImage(content.id) }, isSpoiler = detail.isSpoiler, onSpoilerChanged = { isSpoiler -> onSpoilerChanged(content.id, isSpoiler) }, ) @@ -438,13 +504,13 @@ private fun CollectionCreateScreenPreview() { isPublic = true, selectedContents = fakeContents, contentDetailsMap = fakeContents.associate { - it.id to ContentDetail( - imageUrls = listOf( - "https://buly.kr/DEaVFRZ", - "https://buly.kr/DEaVFRZ", + it.id to ContentDetail( + contentImageUris = listOf( + Uri.parse("https://example.com/1"), + Uri.parse("https://example.com/2"), + ) ) - ) - }, + }, ), onRemoveContent = {}, onBackClick = {}, 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 ea922fa5..04773773 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt @@ -1,18 +1,21 @@ package com.flint.presentation.collectioncreate +import android.content.Context +import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.flint.core.common.util.UiState -import com.flint.data.dto.collection.request.CollectionCreateRequestDto import com.flint.domain.mapper.collection.toDto import com.flint.domain.model.collection.CollectionCreateContentModel import com.flint.domain.model.collection.CollectionCreateRequestModel import com.flint.domain.model.search.SearchContentItemModel -import com.flint.domain.model.search.SearchContentListModel import com.flint.domain.repository.CollectionRepository import com.flint.domain.repository.SearchRepository -import com.kakao.sdk.common.model.Description +import com.flint.domain.repository.StorageRepository +import com.flint.domain.type.FileExtension +import com.flint.domain.type.StoragePathType import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -20,19 +23,23 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import timber.log.Timber import javax.inject.Inject @HiltViewModel class CollectionCreateViewModel @Inject constructor( + @ApplicationContext private val context: Context, private val collectionRepository: CollectionRepository, - private val searchRepository: SearchRepository -) - : ViewModel() { + private val searchRepository: SearchRepository, + private val storageRepository: StorageRepository, +) : ViewModel() { private val _uiState = MutableStateFlow(CollectionCreateUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -53,8 +60,10 @@ class CollectionCreateViewModel @Inject constructor( private fun postCollectionCreate() { viewModelScope.launch { + val imageKey = uploadImageIfNeeded(_uiState.value.thumbnailImageUri, StoragePathType.COLLECTION_THUMBNAIL) ?: "" + val contentImageKeysMap = uploadContentImagesIfNeeded() val requestModel = CollectionCreateRequestModel( - imageUrl = "", + imageUrl = imageKey, title = uiState.value.title, description = uiState.value.description.ifBlank { "" }, isPublic = uiState.value.isPublic ?: true, @@ -64,6 +73,7 @@ class CollectionCreateViewModel @Inject constructor( contentId = content.id, isSpoiler = detail.isSpoiler, reason = detail.reason.ifBlank { "" }, + imageUrls = contentImageKeysMap[content.id] ?: emptyList(), ) }, ) @@ -198,4 +208,77 @@ class CollectionCreateViewModel @Inject constructor( } } } + + fun updateThumbnailImageUri(uri: Uri?) { + _uiState.update { it.copy(thumbnailImageUri = uri) } + } + + fun addContentImageUri(contentId: String, uri: Uri) { + _uiState.update { state -> + val current = state.contentDetailsMap[contentId] ?: ContentDetail() + if (current.contentImageUris.size >= 5) return@update state + val updated = current.copy(contentImageUris = current.contentImageUris + uri) + 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 + val updated = current.copy( + contentImageUris = current.contentImageUris.toMutableList().also { it.removeAt(index) } + ) + state.copy(contentDetailsMap = state.contentDetailsMap + (contentId to updated)) + } + } + + private suspend fun uploadContentImagesIfNeeded(): Map> { + val result = mutableMapOf>() + for ((contentId, detail) in _uiState.value.contentDetailsMap) { + val keys = detail.contentImageUris.mapNotNull { uri -> + uploadImageIfNeeded(uri, StoragePathType.COLLECTION_CONTENT) + } + if (keys.isNotEmpty()) result[contentId] = keys + } + return result + } + + private suspend fun uploadImageIfNeeded(uri: Uri?, pathType: StoragePathType): String? { + uri ?: return null + + val mimeType = withContext(Dispatchers.IO) { + context.contentResolver.getType(uri) + } ?: "image/jpeg" + val extension = mimeTypeToFileExtension(mimeType) + + val presignedUrl = storageRepository.getPresignedUrl( + pathType = pathType, + extension = extension, + ).getOrElse { error -> + Timber.e(error, "Failed to get presigned URL") + return null + } + + val imageBytes = withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(uri)?.use { it.readBytes() } + } ?: return null + + storageRepository.uploadToS3( + uploadUrl = presignedUrl.uploadUrl, + imageBytes = imageBytes, + mimeType = mimeType, + ).getOrElse { error -> + Timber.e(error, "Failed to upload image to S3") + return null + } + + return presignedUrl.key + } + + private fun mimeTypeToFileExtension(mimeType: String): FileExtension = when (mimeType) { + "image/png" -> FileExtension.PNG + "image/gif" -> FileExtension.GIF + "image/webp" -> FileExtension.WEBP + else -> FileExtension.JPEG + } } 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 89d1bdf4..9d82fab5 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 @@ -1,5 +1,6 @@ package com.flint.presentation.collectioncreate.component +import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -21,6 +22,7 @@ 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.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -30,24 +32,25 @@ import com.flint.core.designsystem.theme.FlintTheme @Composable fun CollectionCreateContentImage( - imageUrls: List, - onDeleteClick: () -> Unit, + imageUris: List, + onDeleteClick: (index: Int) -> Unit, modifier: Modifier = Modifier, ) { - val pageCount = if (imageUrls.size > 1) Int.MAX_VALUE else imageUrls.size - val initialPage = if (imageUrls.size > 1) Int.MAX_VALUE / 2 else 0 + val pageCount = if (imageUris.size > 1) Int.MAX_VALUE else imageUris.size + val initialPage = if (imageUris.size > 1) Int.MAX_VALUE / 2 else 0 val pagerState = rememberPagerState(initialPage = initialPage) { pageCount } - val currentIndex = if (imageUrls.isEmpty()) 0 else pagerState.currentPage % imageUrls.size + val currentIndex = if (imageUris.isEmpty()) 0 else pagerState.currentPage % imageUris.size Column(modifier = modifier) { HorizontalPager( state = pagerState, modifier = Modifier.fillMaxWidth(), ) { page -> - val index = page % imageUrls.size + val index = page % imageUris.size Box { NetworkImage( - imageUrl = imageUrls[index], + imageUrl = imageUris[index], + contentScale = ContentScale.Fit, modifier = Modifier .fillMaxWidth() .aspectRatio(4f / 3f), @@ -58,21 +61,21 @@ fun CollectionCreateContentImage( tint = Color.Unspecified, modifier = Modifier .align(Alignment.TopEnd) - .clickable(onClick = onDeleteClick) + .clickable { onDeleteClick(index) } .padding(all = 16.dp) .size(24.dp), ) } } - if (imageUrls.size > 1) { + if (imageUris.size > 1) { Spacer(Modifier.height(12.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), ) { - repeat(imageUrls.size) { index -> + repeat(imageUris.size) { index -> Box( modifier = Modifier .size(8.dp) @@ -93,10 +96,10 @@ fun CollectionCreateContentImage( private fun CollectionCreateContentImagePreview() { FlintTheme { CollectionCreateContentImage( - imageUrls = listOf( - "https://buly.kr/DEaVFRZ", - "https://buly.kr/DEaVFRZ", - "https://buly.kr/DEaVFRZ", + imageUris = listOf( + Uri.parse("https://example.com/1"), + Uri.parse("https://example.com/2"), + Uri.parse("https://example.com/3"), ), onDeleteClick = {}, ) diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt index 0ee48897..0a6959c0 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt @@ -24,11 +24,11 @@ import com.flint.core.designsystem.theme.FlintTheme @Composable fun CollectionCreateThumbnail( - imageUrl: String?, + imageUrl: Any?, onClick: () -> Unit, modifier: Modifier = Modifier, ) { - if (imageUrl.isNullOrBlank()) { + if (imageUrl == null || (imageUrl is String && imageUrl.isBlank())) { CollectionCreateEmptyThumbnail( onClick = onClick, modifier = modifier, @@ -73,7 +73,7 @@ private fun CollectionCreateEmptyThumbnail( @Composable private fun CollectionCreateFillThumbnail( - imageUrl: String, + imageUrl: Any, onClick: () -> Unit, modifier: Modifier = Modifier, ) { 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 ff9b0cee..dbad0116 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 @@ -1,5 +1,6 @@ package com.flint.presentation.collectioncreate +import android.net.Uri import androidx.compose.runtime.Immutable import com.flint.domain.model.search.SearchContentItemModel import kotlinx.collections.immutable.ImmutableList @@ -7,6 +8,7 @@ import kotlinx.collections.immutable.persistentListOf @Immutable data class CollectionCreateUiState( + val thumbnailImageUri: Uri? = null, val title: String = "", val description: String = "", val isPublic: Boolean? = null, @@ -14,7 +16,6 @@ data class CollectionCreateUiState( val contentDetailsMap: Map = emptyMap(), val contents: ImmutableList = persistentListOf(), val searchText: String = "", - ) { val isFinishButtonEnabled: Boolean = title.isNotBlank() && @@ -29,5 +30,5 @@ data class CollectionCreateUiState( data class ContentDetail( val isSpoiler: Boolean = false, val reason: String = "", - val imageUrls: List = emptyList(), + val contentImageUris: List = emptyList(), ) \ No newline at end of file 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 6fca4fd1..57d6cf88 100644 --- a/app/src/main/java/com/flint/presentation/main/MainNavigator.kt +++ b/app/src/main/java/com/flint/presentation/main/MainNavigator.kt @@ -33,7 +33,7 @@ class MainNavigator( val navController: NavHostController, coroutineScope: CoroutineScope, ) { - val startDestination = Route.CollectionCreateGraph + val startDestination = Route.Splash // NavController의 Flow를 관찰하여 현재 Destination을 StateFlow로 변환 private val currentDestination = From be55ed34b5d45be1fc9210a5be679fb26cbfa21b Mon Sep 17 00:00:00 2001 From: chanmi Date: Tue, 9 Jun 2026 17:36:22 +0900 Subject: [PATCH 09/13] =?UTF-8?q?[feat]=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EB=B7=B0=EC=96=B4=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미지 추가 시 새 사진으로 자동 스크롤 - 이미지 삭제 시 이전/다음 사진으로 자연스럽게 이동 - 사진 1장일 때 페이저 스크롤 비활성화 - 이미지 스케일 FillBounds로 변경 (전체 영역 채우기) --- .../component/CollectionCreateContentImage.kt | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) 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 9d82fab5..66e9a064 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 @@ -20,6 +20,11 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -36,21 +41,45 @@ fun CollectionCreateContentImage( onDeleteClick: (index: Int) -> Unit, modifier: Modifier = Modifier, ) { - val pageCount = if (imageUris.size > 1) Int.MAX_VALUE else imageUris.size - val initialPage = if (imageUris.size > 1) Int.MAX_VALUE / 2 else 0 - val pagerState = rememberPagerState(initialPage = initialPage) { pageCount } + val pagerState = rememberPagerState(initialPage = Int.MAX_VALUE / 2) { Int.MAX_VALUE } val currentIndex = if (imageUris.isEmpty()) 0 else pagerState.currentPage % imageUris.size + var prevSize by remember { mutableIntStateOf(imageUris.size) } + var deletedIndex by remember { mutableIntStateOf(-1) } + + LaunchedEffect(imageUris.size) { + if (imageUris.isEmpty()) { + prevSize = 0 + return@LaunchedEffect + } + val isAdded = imageUris.size > prevSize + val targetIndex = if (isAdded) { + imageUris.size - 1 + } else { + minOf(deletedIndex, imageUris.size - 1) + } + prevSize = imageUris.size + // 새로운 size 기준으로 targetIndex에 해당하는 가장 가까운 절대 페이지 계산 + val base = (pagerState.currentPage / imageUris.size) * imageUris.size + val targetPage = base + targetIndex + if (isAdded) { + pagerState.animateScrollToPage(targetPage) + } else { + pagerState.scrollToPage(targetPage) + } + } + Column(modifier = modifier) { HorizontalPager( state = pagerState, + userScrollEnabled = imageUris.size > 1, modifier = Modifier.fillMaxWidth(), ) { page -> val index = page % imageUris.size Box { NetworkImage( imageUrl = imageUris[index], - contentScale = ContentScale.Fit, + contentScale = ContentScale.FillBounds, modifier = Modifier .fillMaxWidth() .aspectRatio(4f / 3f), @@ -61,7 +90,10 @@ fun CollectionCreateContentImage( tint = Color.Unspecified, modifier = Modifier .align(Alignment.TopEnd) - .clickable { onDeleteClick(index) } + .clickable { + deletedIndex = index + onDeleteClick(index) + } .padding(all = 16.dp) .size(24.dp), ) From 9e7ec6369e483a3a6831acad7d1d76cdeb65542d Mon Sep 17 00:00:00 2001 From: chanmi Date: Fri, 12 Jun 2026 00:11:51 +0900 Subject: [PATCH 10/13] =?UTF-8?q?[fix]=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 갤러리 선택 취소 시 기존 썸네일이 삭제되던 문제 수정 - contentResolver 호출 시 발생할 수 있는 예외(SecurityException 등) 처리 - 이미지 업로드 실패 시 생성 요청을 중단하고 실패 상태를 반환하도록 변경 - 콘텐츠 이미지가 비어있을 때 발생할 수 있는 0으로 나누기 예외 방지 --- .../CollectionCreateScreen.kt | 1 + .../CollectionCreateViewModel.kt | 51 +++++++++++++------ .../component/CollectionCreateContentImage.kt | 4 +- 3 files changed, 39 insertions(+), 17 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 0bf7f550..f5d2a0d9 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -86,6 +86,7 @@ fun CollectionCreateRoute( val galleryLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent(), ) { uri: Uri? -> + uri ?: return@rememberLauncherForActivityResult viewModel.updateThumbnailImageUri(uri) } 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 04773773..6c49e0de 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt @@ -60,10 +60,18 @@ class CollectionCreateViewModel @Inject constructor( private fun postCollectionCreate() { viewModelScope.launch { - val imageKey = uploadImageIfNeeded(_uiState.value.thumbnailImageUri, StoragePathType.COLLECTION_THUMBNAIL) ?: "" + val thumbnailKey = uploadImageIfNeeded(_uiState.value.thumbnailImageUri, StoragePathType.COLLECTION_THUMBNAIL) + .getOrElse { + _createSuccess.emit(UiState.Failure) + return@launch + } val contentImageKeysMap = uploadContentImagesIfNeeded() + .getOrElse { + _createSuccess.emit(UiState.Failure) + return@launch + } val requestModel = CollectionCreateRequestModel( - imageUrl = imageKey, + imageUrl = thumbnailKey ?: "", title = uiState.value.title, description = uiState.value.description.ifBlank { "" }, isPublic = uiState.value.isPublic ?: true, @@ -232,22 +240,28 @@ class CollectionCreateViewModel @Inject constructor( } } - private suspend fun uploadContentImagesIfNeeded(): Map> { + private suspend fun uploadContentImagesIfNeeded(): Result>> { val result = mutableMapOf>() for ((contentId, detail) in _uiState.value.contentDetailsMap) { - val keys = detail.contentImageUris.mapNotNull { uri -> - uploadImageIfNeeded(uri, StoragePathType.COLLECTION_CONTENT) + val keys = mutableListOf() + for (uri in detail.contentImageUris) { + val key = uploadImageIfNeeded(uri, StoragePathType.COLLECTION_CONTENT) + .getOrElse { return Result.failure(it) } + if (key != null) keys.add(key) } if (keys.isNotEmpty()) result[contentId] = keys } - return result + return Result.success(result) } - private suspend fun uploadImageIfNeeded(uri: Uri?, pathType: StoragePathType): String? { - uri ?: return null + private suspend fun uploadImageIfNeeded(uri: Uri?, pathType: StoragePathType): Result { + uri ?: return Result.success(null) - val mimeType = withContext(Dispatchers.IO) { - context.contentResolver.getType(uri) + val mimeType = runCatching { + withContext(Dispatchers.IO) { context.contentResolver.getType(uri) } + }.getOrElse { error -> + Timber.e(error, "Failed to resolve mimeType") + return Result.failure(error) } ?: "image/jpeg" val extension = mimeTypeToFileExtension(mimeType) @@ -256,12 +270,17 @@ class CollectionCreateViewModel @Inject constructor( extension = extension, ).getOrElse { error -> Timber.e(error, "Failed to get presigned URL") - return null + return Result.failure(error) } - val imageBytes = withContext(Dispatchers.IO) { - context.contentResolver.openInputStream(uri)?.use { it.readBytes() } - } ?: return null + val imageBytes = runCatching { + withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(uri)?.use { it.readBytes() } + } + }.getOrElse { error -> + Timber.e(error, "Failed to read image bytes") + return Result.failure(error) + } ?: return Result.failure(IllegalStateException("Failed to open image stream: $uri")) storageRepository.uploadToS3( uploadUrl = presignedUrl.uploadUrl, @@ -269,10 +288,10 @@ class CollectionCreateViewModel @Inject constructor( mimeType = mimeType, ).getOrElse { error -> Timber.e(error, "Failed to upload image to S3") - return null + return Result.failure(error) } - return presignedUrl.key + return Result.success(presignedUrl.key) } private fun mimeTypeToFileExtension(mimeType: String): FileExtension = when (mimeType) { 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 66e9a064..5ea92a00 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 @@ -41,8 +41,10 @@ fun CollectionCreateContentImage( onDeleteClick: (index: Int) -> Unit, modifier: Modifier = Modifier, ) { + if (imageUris.isEmpty()) return + val pagerState = rememberPagerState(initialPage = Int.MAX_VALUE / 2) { Int.MAX_VALUE } - val currentIndex = if (imageUris.isEmpty()) 0 else pagerState.currentPage % imageUris.size + val currentIndex = pagerState.currentPage % imageUris.size var prevSize by remember { mutableIntStateOf(imageUris.size) } var deletedIndex by remember { mutableIntStateOf(-1) } From 29dd3c397f0237a14261f03b99040f913320a767 Mon Sep 17 00:00:00 2001 From: chanmi Date: Fri, 12 Jun 2026 00:18:20 +0900 Subject: [PATCH 11/13] =?UTF-8?q?[fix]=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - removeContentImageUri에서 유효 범위 밖 인덱스로 인한 IndexOutOfBoundsException 방지 --- .../presentation/collectioncreate/CollectionCreateViewModel.kt | 1 + 1 file changed, 1 insertion(+) 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 6c49e0de..4b4a0def 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt @@ -233,6 +233,7 @@ class CollectionCreateViewModel @Inject constructor( fun removeContentImageUri(contentId: String, index: Int) { _uiState.update { state -> val current = state.contentDetailsMap[contentId] ?: return@update state + if (index !in current.contentImageUris.indices) return@update state val updated = current.copy( contentImageUris = current.contentImageUris.toMutableList().also { it.removeAt(index) } ) From 1d683055a3b0ce3489c53f8135887a6daa235a0b Mon Sep 17 00:00:00 2001 From: chanmi Date: Fri, 12 Jun 2026 00:56:32 +0900 Subject: [PATCH 12/13] =?UTF-8?q?[fix]=20=EC=A2=85=EC=9A=B0=ED=82=B4?= =?UTF-8?q?=EC=BD=94=EB=A6=AC=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 갤러리 선택 중 프로세스/Activity 재생성 시 pendingContentId 유실 방지 (rememberSaveable 적용) - 콘텐츠 이미지 페이저가 초기 진입 시 마지막 이미지로 표시되던 문제 수정 --- .../collectioncreate/CollectionCreateScreen.kt | 3 ++- .../component/CollectionCreateContentImage.kt | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 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 f5d2a0d9..5055cd2f 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -90,7 +91,7 @@ fun CollectionCreateRoute( viewModel.updateThumbnailImageUri(uri) } - var pendingContentId by remember { mutableStateOf(null) } + var pendingContentId by rememberSaveable { mutableStateOf(null) } val contentImageLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent(), ) { uri: Uri? -> 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 5ea92a00..127b0647 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 @@ -43,13 +43,20 @@ fun CollectionCreateContentImage( ) { if (imageUris.isEmpty()) return - val pagerState = rememberPagerState(initialPage = Int.MAX_VALUE / 2) { Int.MAX_VALUE } + val pageCount = Int.MAX_VALUE + val pagerState = rememberPagerState( + initialPage = pageCount / 2 - (pageCount / 2) % imageUris.size, + ) { pageCount } val currentIndex = pagerState.currentPage % imageUris.size - var prevSize by remember { mutableIntStateOf(imageUris.size) } + var prevSize by remember { mutableIntStateOf(-1) } var deletedIndex by remember { mutableIntStateOf(-1) } LaunchedEffect(imageUris.size) { + if (prevSize == -1) { + prevSize = imageUris.size + return@LaunchedEffect + } if (imageUris.isEmpty()) { prevSize = 0 return@LaunchedEffect From b95923f4d63e39b3c6213beb69f775baf285cecf Mon Sep 17 00:00:00 2001 From: chanmi Date: Fri, 12 Jun 2026 01:00:51 +0900 Subject: [PATCH 13/13] =?UTF-8?q?[fix]=20=EC=A2=85=EC=9A=B0=ED=82=B4?= =?UTF-8?q?=EC=BD=94=EB=A6=AC=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isLoading 플래그를 추가해 업로드~생성 API 호출 중 완료 버튼을 비활성화하고 onClickFinish 재호출을 차단 --- .../CollectionCreateViewModel.kt | 70 ++++++++++--------- .../uistate/CollectionCreateUiState.kt | 2 + 2 files changed, 40 insertions(+), 32 deletions(-) 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 4b4a0def..df4a9c5b 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt @@ -55,44 +55,50 @@ class CollectionCreateViewModel @Inject constructor( } fun onClickFinish() { + if (_uiState.value.isLoading) return postCollectionCreate() } private fun postCollectionCreate() { viewModelScope.launch { - val thumbnailKey = uploadImageIfNeeded(_uiState.value.thumbnailImageUri, StoragePathType.COLLECTION_THUMBNAIL) - .getOrElse { - _createSuccess.emit(UiState.Failure) - return@launch - } - val contentImageKeysMap = uploadContentImagesIfNeeded() - .getOrElse { - _createSuccess.emit(UiState.Failure) - return@launch - } - val requestModel = CollectionCreateRequestModel( - imageUrl = thumbnailKey ?: "", - 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(), - ) - }, - ) + _uiState.update { it.copy(isLoading = true) } + try { + val thumbnailKey = uploadImageIfNeeded(_uiState.value.thumbnailImageUri, StoragePathType.COLLECTION_THUMBNAIL) + .getOrElse { + _createSuccess.emit(UiState.Failure) + return@launch + } + val contentImageKeysMap = uploadContentImagesIfNeeded() + .getOrElse { + _createSuccess.emit(UiState.Failure) + return@launch + } + val requestModel = CollectionCreateRequestModel( + imageUrl = thumbnailKey ?: "", + 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 - .postCollectionCreate(requestModel.toDto()) - .onSuccess { - println("컬렉션 생성 성공") - _createSuccess.emit(UiState.Success(it.collectionId)) - } - .onFailure { e -> println("컬렉션 생성 실패: ${e.message}") } + collectionRepository + .postCollectionCreate(requestModel.toDto()) + .onSuccess { + println("컬렉션 생성 성공") + _createSuccess.emit(UiState.Success(it.collectionId)) + } + .onFailure { e -> println("컬렉션 생성 실패: ${e.message}") } + } finally { + _uiState.update { it.copy(isLoading = false) } + } } } 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 dbad0116..de80c34b 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 @@ -16,8 +16,10 @@ data class CollectionCreateUiState( val contentDetailsMap: Map = emptyMap(), val contents: ImmutableList = persistentListOf(), val searchText: String = "", + val isLoading: Boolean = false, ) { val isFinishButtonEnabled: Boolean = + !isLoading && title.isNotBlank() && isPublic != null && selectedContents.size >= 2