From ea9c218db76921dc53d12527872a2f9a790a7801 Mon Sep 17 00:00:00 2001 From: chanmi1125 Date: Tue, 18 Aug 2026 09:04:00 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[#31]=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=B0?= =?UTF-8?q?=EA=B2=BD=EC=83=89=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/flint/core/designsystem/theme/Color.kt | 7 +++++++ .../collectiondetail/component/CollectionDetailContent.kt | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/flint/core/designsystem/theme/Color.kt b/app/src/main/java/com/flint/core/designsystem/theme/Color.kt index c9e99655..722369b7 100644 --- a/app/src/main/java/com/flint/core/designsystem/theme/Color.kt +++ b/app/src/main/java/com/flint/core/designsystem/theme/Color.kt @@ -55,6 +55,7 @@ data class Colors( val gray900: Color, val white: Color, val background: Color, + val subBackground: Color, val error200: Color, val error500: Color, val error700: Color, @@ -119,6 +120,7 @@ val FlintColors = gray900 = Color(0xFF151821), white = Color(0xFFFFFFFF), background = Color(0xFF121212), + subBackground = Color(0xFF18191B), error200 = Color(0xFFFFD7DB), error500 = Color(0xFFFF4D62), error700 = Color(0xFFB53746), @@ -405,6 +407,11 @@ private fun FlintColorsPreview() { .background(color = FlintColors.background) .size(100.dp), ) + Box( + Modifier + .background(color = FlintColors.subBackground) + .size(100.dp), + ) Box( Modifier .background(color = FlintColors.error200) diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt index 9ea31d85..2e5562c3 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt @@ -44,7 +44,11 @@ fun CollectionDetailContent( onSpoilClick: (contentId: String) -> Unit, modifier: Modifier = Modifier, ) { - Column(modifier = modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .background(FlintTheme.colors.subBackground), + ) { Spacer(Modifier.height(48.dp)) if (content.customImageUrls.isNotEmpty()) { From 3e2e021f0d3869fa792b9c443c1e167800ba31dd Mon Sep 17 00:00:00 2001 From: chanmi1125 Date: Tue, 18 Aug 2026 09:17:55 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[#31]=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/button/FlintButtonState.kt | 17 +++++++++++++++++ .../textfield/CollectionInputTextField.kt | 2 ++ .../component/textfield/FlintBasicTextField.kt | 5 +++-- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/flint/core/designsystem/component/button/FlintButtonState.kt b/app/src/main/java/com/flint/core/designsystem/component/button/FlintButtonState.kt index 641e2302..879003e9 100644 --- a/app/src/main/java/com/flint/core/designsystem/component/button/FlintButtonState.kt +++ b/app/src/main/java/com/flint/core/designsystem/component/button/FlintButtonState.kt @@ -78,6 +78,23 @@ enum class FlintButtonState() { get() = BorderStroke(2.dp, FlintTheme.colors.primary400) }, + Error { + override val background: Brush + @Composable + @ReadOnlyComposable + get() = SolidColor(FlintTheme.colors.gray800) + + override val contentColor: Color + @Composable + @ReadOnlyComposable + get() = FlintTheme.colors.white + + override val border: BorderStroke + @Composable + @ReadOnlyComposable + get() = BorderStroke(2.dp, FlintTheme.colors.error500) + }, + Destructive { override val background: Brush @Composable diff --git a/app/src/main/java/com/flint/core/designsystem/component/textfield/CollectionInputTextField.kt b/app/src/main/java/com/flint/core/designsystem/component/textfield/CollectionInputTextField.kt index dd71d34d..e4ff694e 100644 --- a/app/src/main/java/com/flint/core/designsystem/component/textfield/CollectionInputTextField.kt +++ b/app/src/main/java/com/flint/core/designsystem/component/textfield/CollectionInputTextField.kt @@ -34,6 +34,7 @@ fun CollectionInputTextField( maxLines: Int, isShowLengthTitle: Boolean, modifier: Modifier = Modifier, + isError: Boolean = false, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default ) { @@ -48,6 +49,7 @@ fun CollectionInputTextField( maxLength = maxLength, singleLine = singleLine, maxLines = maxLines, + isError = isError, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, paddingValues = PaddingValues(horizontal = 12.dp, vertical = 10.dp), diff --git a/app/src/main/java/com/flint/core/designsystem/component/textfield/FlintBasicTextField.kt b/app/src/main/java/com/flint/core/designsystem/component/textfield/FlintBasicTextField.kt index a3ebcfb7..2374274d 100644 --- a/app/src/main/java/com/flint/core/designsystem/component/textfield/FlintBasicTextField.kt +++ b/app/src/main/java/com/flint/core/designsystem/component/textfield/FlintBasicTextField.kt @@ -50,6 +50,7 @@ fun FlintBasicTextField( textStyle: TextStyle = FlintTheme.typography.body1R16, backgroundColor: Color = FlintTheme.colors.gray800, borderColor: Color = Color.Unspecified, + isError: Boolean = false, paddingValues: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp), keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, @@ -65,8 +66,8 @@ fun FlintBasicTextField( .clip(RoundedCornerShape(radius)) .background(backgroundColor) .border( - width = 1.dp, - color = borderColor, + width = if (isError) 2.dp else 1.dp, + color = if (isError) FlintTheme.colors.error500 else borderColor, shape = RoundedCornerShape(radius), ) .clickable( From d63fdd5cb8565dbecbdfa59a57224ee521181d5d Mon Sep 17 00:00:00 2001 From: chanmi1125 Date: Tue, 18 Aug 2026 10:51:22 +0900 Subject: [PATCH 3/9] =?UTF-8?q?[#31]=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=97=90=EB=9F=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designsystem/component/toast/ShowToast.kt | 9 +- .../CollectionCreateScreen.kt | 209 ++++++++++++------ .../CollectionCreateContentReason.kt | 4 +- .../uistate/CollectionCreateUiState.kt | 9 +- app/src/main/res/drawable/ic_toast_error.xml | 15 ++ 5 files changed, 172 insertions(+), 74 deletions(-) create mode 100644 app/src/main/res/drawable/ic_toast_error.xml diff --git a/app/src/main/java/com/flint/core/designsystem/component/toast/ShowToast.kt b/app/src/main/java/com/flint/core/designsystem/component/toast/ShowToast.kt index 8810d988..760b5796 100644 --- a/app/src/main/java/com/flint/core/designsystem/component/toast/ShowToast.kt +++ b/app/src/main/java/com/flint/core/designsystem/component/toast/ShowToast.kt @@ -1,9 +1,12 @@ package com.flint.core.designsystem.component.toast import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -23,6 +26,7 @@ import com.flint.core.designsystem.theme.FlintTheme import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.seconds +@OptIn(ExperimentalLayoutApi::class) @Composable fun ShowToast( text: String, @@ -30,12 +34,15 @@ fun ShowToast( paddingValues: PaddingValues, yOffset: Dp, hide: () -> Unit, + imeYOffset: Dp = yOffset, ) { LaunchedEffect(Unit) { delay(2.seconds) hide() } + val bottomOffset = if (WindowInsets.isImeVisible) imeYOffset else yOffset + Box( modifier = Modifier .fillMaxSize() @@ -47,7 +54,7 @@ fun ShowToast( imageVector = imageVector, modifier = Modifier .padding(paddingValues) - .padding(bottom = yOffset), + .padding(bottom = bottomOffset), ) } } 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 e6aaae4a..fdbace19 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -21,6 +21,9 @@ 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.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -32,13 +35,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.ImeAction @@ -54,6 +60,7 @@ import com.flint.core.designsystem.component.button.FlintButtonState import com.flint.core.designsystem.component.button.FlintIconButton import com.flint.core.designsystem.component.button.FlintLargeButton import com.flint.core.designsystem.component.textfield.CollectionInputTextField +import com.flint.core.designsystem.component.toast.ShowToast import com.flint.core.designsystem.component.topappbar.FlintBackTopAppbar import com.flint.core.designsystem.theme.FlintTheme import com.flint.domain.model.search.SearchContentItemModel @@ -65,6 +72,7 @@ import com.flint.presentation.collectioncreate.component.CollectionCreateContent import com.flint.presentation.collectioncreate.component.CollectionCreateThumbnail import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.launch @Composable fun CollectionCreateRoute( @@ -183,6 +191,16 @@ fun CollectionCreateScreen( var isModalVisible by remember { mutableStateOf(false) } var contentToDelete by remember { mutableStateOf(null) } var isThumbnailBottomSheetVisible by remember { mutableStateOf(false) } + var showValidationErrors by rememberSaveable { mutableStateOf(false) } + var showRequiredFieldsToast by remember { mutableStateOf(false) } + val lazyListState = rememberLazyListState() + val coroutineScope = rememberCoroutineScope() + + // LazyColumn 아이템 순서가 고정이므로, 인덱스로 필수 항목 섹션을 가리켜 에러 발생 시 스크롤한다. + val titleItemIndex = 1 + val publicItemIndex = 3 + val addContentHeaderIndex = 4 + val firstContentItemIndex = addContentHeaderIndex + 1 Column( modifier = @@ -193,6 +211,7 @@ fun CollectionCreateScreen( FlintBackTopAppbar(onClick = onBackClick) LazyColumn( + state = lazyListState, modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp), ) { @@ -211,6 +230,7 @@ fun CollectionCreateScreen( CollectionTitle( title = uiState.title, onTitleChanged = onTitleChanged, + isError = showValidationErrors && uiState.title.isBlank(), modifier = Modifier.padding(horizontal = (16).dp), ) } @@ -229,32 +249,29 @@ fun CollectionCreateScreen( CollectionPublicSection( isPublic = uiState.isPublic, onPublicChanged = onPublicChanged, + isError = showValidationErrors && uiState.isPublic == null, modifier = Modifier.padding(horizontal = (16).dp), ) Spacer(Modifier.height(20.dp)) } - // 작품 추가 섹션 - item { - CollectionAddContentSection( - selectedContents = uiState.selectedContents, - contentDetailsMap = uiState.contentDetailsMap, - onDeleteRequest = { content -> - contentToDelete = content - isModalVisible = true - }, - onSpoilerChanged = onSpoilerChanged, - onReasonChanged = onReasonChanged, - onAddContentClick = onAddContentClick, - onSelectContentImage = onSelectContentImage, - onRemoveExistingContentImage = onRemoveExistingContentImage, - onRemoveContentImage = onRemoveContentImage, - modifier = Modifier.padding(horizontal = (16).dp), - ) - - Spacer(Modifier.height(20.dp)) - } + // 작품 추가 섹션 - 작품별로 개별 아이템이어야 특정 작품으로 정확히 스크롤할 수 있다. + collectionAddContentSection( + selectedContents = uiState.selectedContents, + contentDetailsMap = uiState.contentDetailsMap, + showValidationErrors = showValidationErrors, + onDeleteRequest = { content -> + contentToDelete = content + isModalVisible = true + }, + onSpoilerChanged = onSpoilerChanged, + onReasonChanged = onReasonChanged, + onAddContentClick = onAddContentClick, + onSelectContentImage = onSelectContentImage, + onRemoveExistingContentImage = onRemoveExistingContentImage, + onRemoveContentImage = onRemoveContentImage, + ) item { Text( @@ -266,23 +283,57 @@ fun CollectionCreateScreen( Spacer(Modifier.height(12.dp)) } + + // 완료 버튼 - 고정되지 않고 스크롤해야 노출됨 + item { + FlintLargeButton( + text = "완료", + state = FlintButtonState.Able, + onClick = { + when { + uiState.isFinishButtonEnabled -> { + showValidationErrors = false + onFinishClick() + } + !uiState.isRequiredFieldsFilled -> { + showValidationErrors = true + showRequiredFieldsToast = true + + val firstInvalidContentIndex = uiState.selectedContents.indexOfFirst { + uiState.contentDetailsMap[it.id]?.reason.isNullOrBlank() + } + val targetIndex = when { + uiState.title.isBlank() -> titleItemIndex + uiState.isPublic == null -> publicItemIndex + firstInvalidContentIndex >= 0 -> firstContentItemIndex + firstInvalidContentIndex + else -> addContentHeaderIndex + } + coroutineScope.launch { + lazyListState.animateScrollToItem(targetIndex) + } + } + } + }, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } } + } - FlintLargeButton( - text = "완료", - state = if (uiState.isFinishButtonEnabled) FlintButtonState.Able else FlintButtonState.Disable, - onClick = { - onFinishClick() - }, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - enabled = uiState.isFinishButtonEnabled, + if (showRequiredFieldsToast) { + ShowToast( + text = "필수 항목을 모두 입력해주세요", + imageVector = ImageVector.vectorResource(R.drawable.ic_toast_error), + paddingValues = PaddingValues.Zero, + yOffset = 120.dp, + imeYOffset = 16.dp, + hide = { showRequiredFieldsToast = false }, ) } - if (isModalVisible) { CollectionCreateContentDeleteModal( onConfirm = { @@ -326,6 +377,7 @@ private fun CollectionTitle( title: String, onTitleChanged: (String) -> Unit, modifier: Modifier = Modifier, + isError: Boolean = false, ){ Column(modifier = modifier) { Text( @@ -345,6 +397,7 @@ private fun CollectionTitle( singleLine = true, maxLines = 1, isShowLengthTitle = true, + isError = isError, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next ) @@ -395,6 +448,7 @@ private fun CollectionPublicSection( isPublic: Boolean?, onPublicChanged: (Boolean?) -> Unit, modifier: Modifier = Modifier, + isError: Boolean = false, ) { Column(modifier = modifier) { Text( @@ -412,7 +466,7 @@ private fun CollectionPublicSection( state = when (isPublic) { true -> FlintButtonState.ColorOutline false -> FlintButtonState.Disable - else -> FlintButtonState.Outline + else -> if (isError) FlintButtonState.Error else FlintButtonState.Outline }, onClick = { onPublicChanged(true) }, modifier = Modifier.weight(1f), @@ -426,7 +480,7 @@ private fun CollectionPublicSection( state = when (isPublic) { true -> FlintButtonState.Disable false -> FlintButtonState.ColorOutline - else -> FlintButtonState.Outline + else -> if (isError) FlintButtonState.Error else FlintButtonState.Outline }, onClick = { onPublicChanged(false) }, modifier = Modifier.weight(1f), @@ -436,8 +490,7 @@ private fun CollectionPublicSection( } } -@Composable -private fun CollectionAddContentSection( +private fun LazyListScope.collectionAddContentSection( selectedContents: ImmutableList, contentDetailsMap: Map, onDeleteRequest: (SearchContentItemModel) -> Unit, @@ -447,36 +500,47 @@ private fun CollectionAddContentSection( onSelectContentImage: (contentId: String) -> Unit, onRemoveExistingContentImage: (contentId: String, index: Int) -> Unit, onRemoveContentImage: (contentId: String, index: Int) -> Unit, - modifier: Modifier = Modifier, + showValidationErrors: Boolean, ) { - Column(modifier = modifier) { - Text( - text = "작품 추가", - color = FlintTheme.colors.white, - style = FlintTheme.typography.head3M18, - ) + // 작품별로 개별 LazyColumn 아이템이어야 특정 작품 위치로 정확히 스크롤할 수 있다. + item { + val isContentCountError = showValidationErrors && selectedContents.size < 2 - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "작품을 2개 이상 추가해주세요.", - color = FlintTheme.colors.gray200, - style = FlintTheme.typography.body2R14, - ) + Column(modifier = Modifier.padding(horizontal = 16.dp)) { Text( - text = "${selectedContents.size}/10", + text = "작품 추가", color = FlintTheme.colors.white, - style = FlintTheme.typography.body2R14, + style = FlintTheme.typography.head3M18, ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "작품을 2개 이상 추가해주세요.", + color = if (isContentCountError) FlintTheme.colors.error500 else 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() + items( + items = selectedContents, + key = { content -> content.id }, + ) { content -> + val detail = contentDetailsMap[content.id] ?: ContentDetail() - Spacer(Modifier.height(28.dp)) + // LazyColumn 의 verticalArrangement(16dp)와 합쳐 기존과 동일한 28dp 간격을 만든다. + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Spacer(Modifier.height(12.dp)) Icon( painter = painterResource(R.drawable.ic_deselect_large_pri), @@ -515,21 +579,28 @@ private fun CollectionAddContentSection( onSelectImageClick = { onSelectContentImage(content.id) }, isSpoiler = detail.isSpoiler, onSpoilerChanged = { isSpoiler -> onSpoilerChanged(content.id, isSpoiler) }, + isError = showValidationErrors && detail.reason.isBlank(), ) } + } - Spacer(Modifier.height(28.dp)) + item { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Spacer(Modifier.height(12.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) - ) + 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(4.dp)) + } } } 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 index 5fc3a4d7..d8fedbdf 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentReason.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentReason.kt @@ -41,6 +41,7 @@ fun CollectionCreateContentReason( isSpoiler: Boolean, onSpoilerChanged: (Boolean) -> Unit, modifier: Modifier = Modifier, + isError: Boolean = false, ) { Column(modifier = modifier) { Text( @@ -61,7 +62,8 @@ fun CollectionCreateContentReason( singleLine = false, maxLength = Int.MAX_VALUE, maxLines = Int.MAX_VALUE, - isShowLengthTitle = false + isShowLengthTitle = false, + isError = isError, ) Row( 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 873d98fb..306ea2e0 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 @@ -45,14 +45,17 @@ data class CollectionCreateUiState( } ) - val isFinishButtonEnabled: Boolean get() = - !isLoading && + val isRequiredFieldsFilled: Boolean get() = title.isNotBlank() && isPublic != null && selectedContents.size >= 2 && - (!isEditMode || hasChanges) && selectedContents.all { contentDetailsMap[it.id]?.reason?.isNotBlank() == true } + val isFinishButtonEnabled: Boolean get() = + !isLoading && + isRequiredFieldsFilled && + (!isEditMode || hasChanges) + val isCancelModalVisible: Boolean = contentDetailsMap.values.any { it.reason.isNotBlank() } } diff --git a/app/src/main/res/drawable/ic_toast_error.xml b/app/src/main/res/drawable/ic_toast_error.xml new file mode 100644 index 00000000..14b2c9cb --- /dev/null +++ b/app/src/main/res/drawable/ic_toast_error.xml @@ -0,0 +1,15 @@ + + + + From 12b46c72436095eed5b857266f7c8e1ade71c259 Mon Sep 17 00:00:00 2001 From: chanmi1125 Date: Tue, 18 Aug 2026 11:04:33 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[#31]=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=81=EC=84=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/CollectionDetailContent.kt | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt index 2e5562c3..92d16ef2 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt @@ -44,24 +44,25 @@ fun CollectionDetailContent( onSpoilClick: (contentId: String) -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .background(FlintTheme.colors.subBackground), - ) { - Spacer(Modifier.height(48.dp)) + Column(modifier = modifier.fillMaxWidth()) { + // 카드 사이 36dp 여백은 배경색 밖에 둬서 화면 기본 배경(검정)이 그대로 보이게 한다. + Spacer(Modifier.height(36.dp)) - if (content.customImageUrls.isNotEmpty()) { - CollectionDetailContentCarousel(content = content) + Column( + modifier = Modifier + .fillMaxWidth() + .background(FlintTheme.colors.subBackground), + ) { + if (content.customImageUrls.isNotEmpty()) { + CollectionDetailContentCarousel(content = content) + } - Spacer(Modifier.height(32.dp)) + CollectionDetailContentInfo( + content = content, + onBookmarkIconClick = onBookmarkIconClick, + onSpoilClick = onSpoilClick, + ) } - - CollectionDetailContentInfo( - content = content, - onBookmarkIconClick = onBookmarkIconClick, - onSpoilClick = onSpoilClick, - ) } } @@ -123,7 +124,7 @@ private fun CollectionDetailContentInfo( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(top = 16.dp, start = 16.dp, end = 16.dp), ) { Row( modifier = Modifier.fillMaxWidth(), From 0abc811940d8ce407a3c9e4dfc207efb72ad808a Mon Sep 17 00:00:00 2001 From: chanmi1125 Date: Tue, 18 Aug 2026 11:06:26 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[#31]=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=91=B8=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/collectioncreate/CollectionCreateScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fdbace19..98823471 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -275,7 +275,7 @@ fun CollectionCreateScreen( item { Text( - text = "Flint에서 제공하는 영화 · 드라마를 포함한 모든 콘텐츠의 저작권은 각 권리자에게 있으며, 관련 법령에 따라 보호됩니다. \n컬렉션 이용 시 저작권을 준수해 주세요.", + text = "Flint에서 제공하는 영화 · 드라마를 포함한 모든 콘텐츠의 저작권은 각 권리자에게 있으며, 관련 법령에 따라 보호됩니다. 컬렉션 이용 시 저작권을 준수해 주세요.", color = FlintTheme.colors.gray300, style = FlintTheme.typography.caption1R12, modifier = Modifier.padding(horizontal = 16.dp) From 21b9fe1c725f9d551ed183383388dc87fc712ccd Mon Sep 17 00:00:00 2001 From: chanmi1125 Date: Tue, 18 Aug 2026 11:17:25 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[#31]=EC=BD=94=EB=93=9C=EB=9E=98=EB=B9=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionCreateScreen.kt | 227 +++++++++--------- 1 file changed, 116 insertions(+), 111 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 98823471..28e80f9e 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -8,6 +8,7 @@ import androidx.activity.result.contract.ActivityResultContracts 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.PaddingValues import androidx.compose.foundation.layout.Row @@ -202,136 +203,140 @@ fun CollectionCreateScreen( val addContentHeaderIndex = 4 val firstContentItemIndex = addContentHeaderIndex + 1 - Column( - modifier = - modifier - .fillMaxSize() - .background(color = FlintTheme.colors.background) + Box( + modifier = modifier.fillMaxSize(), ) { - FlintBackTopAppbar(onClick = onBackClick) - - LazyColumn( - state = lazyListState, - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(16.dp), + Column( + modifier = + Modifier + .fillMaxSize() + .background(color = FlintTheme.colors.background) ) { - // 썸네일 - item { - CollectionCreateThumbnail( - imageUrl = uiState.thumbnailImageUri ?: uiState.existingThumbnailUrl, - onClick = { isThumbnailBottomSheetVisible = true }, - ) + FlintBackTopAppbar(onClick = onBackClick) - Spacer(Modifier.height(20.dp)) - } + LazyColumn( + state = lazyListState, + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + // 썸네일 + item { + CollectionCreateThumbnail( + imageUrl = uiState.thumbnailImageUri ?: uiState.existingThumbnailUrl, + onClick = { isThumbnailBottomSheetVisible = true }, + ) - // 컬렉션 제목 - item { - CollectionTitle( - title = uiState.title, - onTitleChanged = onTitleChanged, - isError = showValidationErrors && uiState.title.isBlank(), - modifier = Modifier.padding(horizontal = (16).dp), - ) - } + Spacer(Modifier.height(20.dp)) + } - // 컬렉션 소개 - item { - CollectionDescription( - description = uiState.description, - onDescriptionChanged = onDescriptionChanged, - modifier = Modifier.padding(horizontal = (16).dp), - ) - } + // 컬렉션 제목 + item { + CollectionTitle( + title = uiState.title, + onTitleChanged = onTitleChanged, + isError = showValidationErrors && uiState.title.isBlank(), + modifier = Modifier.padding(horizontal = (16).dp), + ) + } - // 컬렉션 공개 여부 - item { - CollectionPublicSection( - isPublic = uiState.isPublic, - onPublicChanged = onPublicChanged, - isError = showValidationErrors && uiState.isPublic == null, - modifier = Modifier.padding(horizontal = (16).dp), - ) + // 컬렉션 소개 + item { + CollectionDescription( + description = uiState.description, + onDescriptionChanged = onDescriptionChanged, + modifier = Modifier.padding(horizontal = (16).dp), + ) + } - Spacer(Modifier.height(20.dp)) - } + // 컬렉션 공개 여부 + item { + CollectionPublicSection( + isPublic = uiState.isPublic, + onPublicChanged = onPublicChanged, + isError = showValidationErrors && uiState.isPublic == null, + modifier = Modifier.padding(horizontal = (16).dp), + ) - // 작품 추가 섹션 - 작품별로 개별 아이템이어야 특정 작품으로 정확히 스크롤할 수 있다. - collectionAddContentSection( - selectedContents = uiState.selectedContents, - contentDetailsMap = uiState.contentDetailsMap, - showValidationErrors = showValidationErrors, - onDeleteRequest = { content -> - contentToDelete = content - isModalVisible = true - }, - onSpoilerChanged = onSpoilerChanged, - onReasonChanged = onReasonChanged, - onAddContentClick = onAddContentClick, - onSelectContentImage = onSelectContentImage, - onRemoveExistingContentImage = onRemoveExistingContentImage, - onRemoveContentImage = onRemoveContentImage, - ) + Spacer(Modifier.height(20.dp)) + } - item { - Text( - text = "Flint에서 제공하는 영화 · 드라마를 포함한 모든 콘텐츠의 저작권은 각 권리자에게 있으며, 관련 법령에 따라 보호됩니다. 컬렉션 이용 시 저작권을 준수해 주세요.", - color = FlintTheme.colors.gray300, - style = FlintTheme.typography.caption1R12, - modifier = Modifier.padding(horizontal = 16.dp) + // 작품 추가 섹션 - 작품별로 개별 아이템이어야 특정 작품으로 정확히 스크롤할 수 있다. + collectionAddContentSection( + selectedContents = uiState.selectedContents, + contentDetailsMap = uiState.contentDetailsMap, + showValidationErrors = showValidationErrors, + onDeleteRequest = { content -> + contentToDelete = content + isModalVisible = true + }, + onSpoilerChanged = onSpoilerChanged, + onReasonChanged = onReasonChanged, + onAddContentClick = onAddContentClick, + onSelectContentImage = onSelectContentImage, + onRemoveExistingContentImage = onRemoveExistingContentImage, + onRemoveContentImage = onRemoveContentImage, ) - Spacer(Modifier.height(12.dp)) - } + item { + Text( + text = "Flint에서 제공하는 영화 · 드라마를 포함한 모든 콘텐츠의 저작권은 각 권리자에게 있으며, 관련 법령에 따라 보호됩니다. 컬렉션 이용 시 저작권을 준수해 주세요.", + color = FlintTheme.colors.gray300, + style = FlintTheme.typography.caption1R12, + modifier = Modifier.padding(horizontal = 16.dp) + ) - // 완료 버튼 - 고정되지 않고 스크롤해야 노출됨 - item { - FlintLargeButton( - text = "완료", - state = FlintButtonState.Able, - onClick = { - when { - uiState.isFinishButtonEnabled -> { - showValidationErrors = false - onFinishClick() - } - !uiState.isRequiredFieldsFilled -> { - showValidationErrors = true - showRequiredFieldsToast = true + Spacer(Modifier.height(12.dp)) + } - val firstInvalidContentIndex = uiState.selectedContents.indexOfFirst { - uiState.contentDetailsMap[it.id]?.reason.isNullOrBlank() + // 완료 버튼 - 고정되지 않고 스크롤해야 노출됨 + item { + FlintLargeButton( + text = "완료", + state = FlintButtonState.Able, + onClick = { + when { + uiState.isFinishButtonEnabled -> { + showValidationErrors = false + onFinishClick() } - val targetIndex = when { - uiState.title.isBlank() -> titleItemIndex - uiState.isPublic == null -> publicItemIndex - firstInvalidContentIndex >= 0 -> firstContentItemIndex + firstInvalidContentIndex - else -> addContentHeaderIndex - } - coroutineScope.launch { - lazyListState.animateScrollToItem(targetIndex) + !uiState.isRequiredFieldsFilled -> { + showValidationErrors = true + showRequiredFieldsToast = true + + val firstInvalidContentIndex = uiState.selectedContents.indexOfFirst { + uiState.contentDetailsMap[it.id]?.reason.isNullOrBlank() + } + val targetIndex = when { + uiState.title.isBlank() -> titleItemIndex + uiState.isPublic == null -> publicItemIndex + firstInvalidContentIndex >= 0 -> firstContentItemIndex + firstInvalidContentIndex + else -> addContentHeaderIndex + } + coroutineScope.launch { + lazyListState.animateScrollToItem(targetIndex) + } } } - } - }, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) + }, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } } } - } - if (showRequiredFieldsToast) { - ShowToast( - text = "필수 항목을 모두 입력해주세요", - imageVector = ImageVector.vectorResource(R.drawable.ic_toast_error), - paddingValues = PaddingValues.Zero, - yOffset = 120.dp, - imeYOffset = 16.dp, - hide = { showRequiredFieldsToast = false }, - ) + if (showRequiredFieldsToast) { + ShowToast( + text = "필수 항목을 모두 입력해주세요", + imageVector = ImageVector.vectorResource(R.drawable.ic_toast_error), + paddingValues = PaddingValues.Zero, + yOffset = 120.dp, + imeYOffset = 16.dp, + hide = { showRequiredFieldsToast = false }, + ) + } } if (isModalVisible) { From b0cf59ec55167d8985029522a26bda00be1de4e6 Mon Sep 17 00:00:00 2001 From: chanmi1125 Date: Tue, 18 Aug 2026 20:00:08 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[#31]=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../collectioncreate/CollectionCreateScreen.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 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 28e80f9e..258ca5b8 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -193,7 +193,7 @@ fun CollectionCreateScreen( var contentToDelete by remember { mutableStateOf(null) } var isThumbnailBottomSheetVisible by remember { mutableStateOf(false) } var showValidationErrors by rememberSaveable { mutableStateOf(false) } - var showRequiredFieldsToast by remember { mutableStateOf(false) } + var toastMessage by remember { mutableStateOf(null) } val lazyListState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() @@ -292,7 +292,7 @@ fun CollectionCreateScreen( item { FlintLargeButton( text = "완료", - state = FlintButtonState.Able, + state = if (uiState.isLoading) FlintButtonState.Disable else FlintButtonState.Able, onClick = { when { uiState.isFinishButtonEnabled -> { @@ -301,7 +301,7 @@ fun CollectionCreateScreen( } !uiState.isRequiredFieldsFilled -> { showValidationErrors = true - showRequiredFieldsToast = true + toastMessage = "필수 항목을 모두 입력해주세요" val firstInvalidContentIndex = uiState.selectedContents.indexOfFirst { uiState.contentDetailsMap[it.id]?.reason.isNullOrBlank() @@ -316,25 +316,28 @@ fun CollectionCreateScreen( lazyListState.animateScrollToItem(targetIndex) } } + uiState.isLoading -> Unit + else -> toastMessage = "변경된 내용이 없어요" } }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp), + enabled = !uiState.isLoading, ) } } } - if (showRequiredFieldsToast) { + toastMessage?.let { message -> ShowToast( - text = "필수 항목을 모두 입력해주세요", + text = message, imageVector = ImageVector.vectorResource(R.drawable.ic_toast_error), paddingValues = PaddingValues.Zero, yOffset = 120.dp, imeYOffset = 16.dp, - hide = { showRequiredFieldsToast = false }, + hide = { toastMessage = null }, ) } } From 3720fffa700d62bfe633868dd5a2b8f9990a8bca Mon Sep 17 00:00:00 2001 From: chanmi1125 Date: Tue, 18 Aug 2026 20:23:58 +0900 Subject: [PATCH 8/9] =?UTF-8?q?[#31]=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionCreateScreen.kt | 62 +++++++++++++++---- 1 file changed, 49 insertions(+), 13 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 258ca5b8..bd113b36 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -25,6 +25,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -197,12 +199,22 @@ fun CollectionCreateScreen( val lazyListState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() - // LazyColumn 아이템 순서가 고정이므로, 인덱스로 필수 항목 섹션을 가리켜 에러 발생 시 스크롤한다. + // 필드 자체에 매달아 두면 리스트 구성이 바뀌어도(아이템 추가/삭제/순서 변경) 정확한 필드로 스크롤된다. + // 다만 완료 버튼은 리스트 맨 아래에 있어서, 에러 필드가 화면 밖 -> 아직 컴포즈되지 않았을 수 있다. + // 이 경우 bringIntoView() 가 실패하므로, 대략적인 인덱스로 먼저 스크롤해 컴포지션을 강제한다. + // (인덱스가 다소 어긋나도 근처로만 가면 되므로, 이 하드코딩은 point 2 만큼 취약하지 않다.) val titleItemIndex = 1 val publicItemIndex = 3 val addContentHeaderIndex = 4 val firstContentItemIndex = addContentHeaderIndex + 1 + val titleBringIntoViewRequester = remember { BringIntoViewRequester() } + val publicBringIntoViewRequester = remember { BringIntoViewRequester() } + val addContentBringIntoViewRequester = remember { BringIntoViewRequester() } + val contentReasonBringIntoViewRequesters = remember { mutableMapOf() } + fun reasonBringIntoViewRequester(contentId: String): BringIntoViewRequester = + contentReasonBringIntoViewRequesters.getOrPut(contentId) { BringIntoViewRequester() } + Box( modifier = modifier.fillMaxSize(), ) { @@ -235,6 +247,7 @@ fun CollectionCreateScreen( title = uiState.title, onTitleChanged = onTitleChanged, isError = showValidationErrors && uiState.title.isBlank(), + bringIntoViewRequester = titleBringIntoViewRequester, modifier = Modifier.padding(horizontal = (16).dp), ) } @@ -254,13 +267,14 @@ fun CollectionCreateScreen( isPublic = uiState.isPublic, onPublicChanged = onPublicChanged, isError = showValidationErrors && uiState.isPublic == null, + bringIntoViewRequester = publicBringIntoViewRequester, modifier = Modifier.padding(horizontal = (16).dp), ) Spacer(Modifier.height(20.dp)) } - // 작품 추가 섹션 - 작품별로 개별 아이템이어야 특정 작품으로 정확히 스크롤할 수 있다. + // 작품 추가 섹션 collectionAddContentSection( selectedContents = uiState.selectedContents, contentDetailsMap = uiState.contentDetailsMap, @@ -275,6 +289,8 @@ fun CollectionCreateScreen( onSelectContentImage = onSelectContentImage, onRemoveExistingContentImage = onRemoveExistingContentImage, onRemoveContentImage = onRemoveContentImage, + headerBringIntoViewRequester = addContentBringIntoViewRequester, + reasonBringIntoViewRequesterFor = ::reasonBringIntoViewRequester, ) item { @@ -306,14 +322,24 @@ fun CollectionCreateScreen( val firstInvalidContentIndex = uiState.selectedContents.indexOfFirst { uiState.contentDetailsMap[it.id]?.reason.isNullOrBlank() } - val targetIndex = when { - uiState.title.isBlank() -> titleItemIndex - uiState.isPublic == null -> publicItemIndex - firstInvalidContentIndex >= 0 -> firstContentItemIndex + firstInvalidContentIndex - else -> addContentHeaderIndex - } coroutineScope.launch { - lazyListState.animateScrollToItem(targetIndex) + val (coarseIndex, requester) = when { + uiState.title.isBlank() -> + titleItemIndex to titleBringIntoViewRequester + uiState.isPublic == null -> + publicItemIndex to publicBringIntoViewRequester + firstInvalidContentIndex >= 0 -> { + val contentId = uiState.selectedContents[firstInvalidContentIndex].id + (firstContentItemIndex + firstInvalidContentIndex) to + reasonBringIntoViewRequester(contentId) + } + else -> + addContentHeaderIndex to addContentBringIntoViewRequester + } + // 에러 필드가 화면 밖이라 아직 컴포즈되지 않았을 수 있으므로 먼저 근처로 스크롤해 + // 컴포지션을 강제한 뒤, 정확한 위치는 bringIntoView() 가 보정한다. + lazyListState.scrollToItem(coarseIndex) + runCatching { requester.bringIntoView() } } } uiState.isLoading -> Unit @@ -384,6 +410,7 @@ fun CollectionCreateScreen( private fun CollectionTitle( title: String, onTitleChanged: (String) -> Unit, + bringIntoViewRequester: BringIntoViewRequester, modifier: Modifier = Modifier, isError: Boolean = false, ){ @@ -397,7 +424,9 @@ private fun CollectionTitle( Spacer(Modifier.height(16.dp)) CollectionInputTextField( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .bringIntoViewRequester(bringIntoViewRequester), value = title, placeholder = "컬렉션의 제목을 입력해주세요.", onValueChanged = onTitleChanged, @@ -455,6 +484,7 @@ private fun CollectionDescription( private fun CollectionPublicSection( isPublic: Boolean?, onPublicChanged: (Boolean?) -> Unit, + bringIntoViewRequester: BringIntoViewRequester, modifier: Modifier = Modifier, isError: Boolean = false, ) { @@ -467,7 +497,7 @@ private fun CollectionPublicSection( Spacer(Modifier.height(16.dp)) - Row { + Row(modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester)) { FlintIconButton( text = "공개", iconRes = R.drawable.ic_share, @@ -509,12 +539,17 @@ private fun LazyListScope.collectionAddContentSection( onRemoveExistingContentImage: (contentId: String, index: Int) -> Unit, onRemoveContentImage: (contentId: String, index: Int) -> Unit, showValidationErrors: Boolean, + headerBringIntoViewRequester: BringIntoViewRequester, + reasonBringIntoViewRequesterFor: (contentId: String) -> BringIntoViewRequester, ) { - // 작품별로 개별 LazyColumn 아이템이어야 특정 작품 위치로 정확히 스크롤할 수 있다. item { val isContentCountError = showValidationErrors && selectedContents.size < 2 - Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .bringIntoViewRequester(headerBringIntoViewRequester), + ) { Text( text = "작품 추가", color = FlintTheme.colors.white, @@ -588,6 +623,7 @@ private fun LazyListScope.collectionAddContentSection( isSpoiler = detail.isSpoiler, onSpoilerChanged = { isSpoiler -> onSpoilerChanged(content.id, isSpoiler) }, isError = showValidationErrors && detail.reason.isBlank(), + modifier = Modifier.bringIntoViewRequester(reasonBringIntoViewRequesterFor(content.id)), ) } } From 6ff81d67547dc41aa2dcbd952b7b5026dbb4e962 Mon Sep 17 00:00:00 2001 From: chanmi1125 Date: Tue, 18 Aug 2026 20:32:29 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[#31]=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/toast/ShowToast.kt | 3 ++- .../collectioncreate/CollectionCreateScreen.kt | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/flint/core/designsystem/component/toast/ShowToast.kt b/app/src/main/java/com/flint/core/designsystem/component/toast/ShowToast.kt index 760b5796..17e3c3e4 100644 --- a/app/src/main/java/com/flint/core/designsystem/component/toast/ShowToast.kt +++ b/app/src/main/java/com/flint/core/designsystem/component/toast/ShowToast.kt @@ -35,8 +35,9 @@ fun ShowToast( yOffset: Dp, hide: () -> Unit, imeYOffset: Dp = yOffset, + key: Any = text, ) { - LaunchedEffect(Unit) { + LaunchedEffect(key) { delay(2.seconds) hide() } 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 bd113b36..100f36e9 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -196,6 +196,13 @@ fun CollectionCreateScreen( var isThumbnailBottomSheetVisible by remember { mutableStateOf(false) } var showValidationErrors by rememberSaveable { mutableStateOf(false) } var toastMessage by remember { mutableStateOf(null) } + var toastRequestId by remember { mutableStateOf(0) } + // 같은 문자열을 다시 대입하면 State 값이 안 바뀌어(구조적 동등성) 리컴포즈가 안 될 수 있으므로, + // 매 요청마다 카운터를 증가시켜 ShowToast 의 LaunchedEffect 타이머가 항상 재시작되도록 한다. + fun showToast(message: String) { + toastMessage = message + toastRequestId++ + } val lazyListState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() @@ -317,7 +324,7 @@ fun CollectionCreateScreen( } !uiState.isRequiredFieldsFilled -> { showValidationErrors = true - toastMessage = "필수 항목을 모두 입력해주세요" + showToast("필수 항목을 모두 입력해주세요") val firstInvalidContentIndex = uiState.selectedContents.indexOfFirst { uiState.contentDetailsMap[it.id]?.reason.isNullOrBlank() @@ -343,7 +350,7 @@ fun CollectionCreateScreen( } } uiState.isLoading -> Unit - else -> toastMessage = "변경된 내용이 없어요" + else -> showToast("변경된 내용이 없어요") } }, modifier = @@ -359,6 +366,7 @@ fun CollectionCreateScreen( toastMessage?.let { message -> ShowToast( text = message, + key = toastRequestId, imageVector = ImageVector.vectorResource(R.drawable.ic_toast_error), paddingValues = PaddingValues.Zero, yOffset = 120.dp,