From ff758d34e9066aa1caf8c1e0466e636b85ae2358 Mon Sep 17 00:00:00 2001 From: soyeonLee126 Date: Sun, 8 Mar 2026 18:41:55 +0900 Subject: [PATCH 1/3] =?UTF-8?q?-=20common=20circle=20button=20core=20?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20-=20keywordchip=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/ui/component/CommonCircleButton.kt | 41 ++++ .../core/ui/component/KeywordChip.kt | 216 ++++++++++++++++++ .../presentation/modify/ModifyScreen.kt | 104 ++------- .../presentation/modify/TagInputDialog.kt | 2 +- .../fooddiary/presentation/modify/UiCompat.kt | 83 ------- 5 files changed, 275 insertions(+), 171 deletions(-) create mode 100644 core/ui/src/main/java/com/nexters/fooddiary/core/ui/component/CommonCircleButton.kt create mode 100644 core/ui/src/main/java/com/nexters/fooddiary/core/ui/component/KeywordChip.kt delete mode 100644 presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/UiCompat.kt diff --git a/core/ui/src/main/java/com/nexters/fooddiary/core/ui/component/CommonCircleButton.kt b/core/ui/src/main/java/com/nexters/fooddiary/core/ui/component/CommonCircleButton.kt new file mode 100644 index 0000000..08c166e --- /dev/null +++ b/core/ui/src/main/java/com/nexters/fooddiary/core/ui/component/CommonCircleButton.kt @@ -0,0 +1,41 @@ +package com.nexters.fooddiary.core.ui.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.nexters.fooddiary.core.ui.theme.AppTypography +import com.nexters.fooddiary.core.ui.theme.White + +@Composable +fun CommonCircleButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + buttonText: String, + contentColor: Color = White, + border: BorderStroke? = null, + buttonColors: ButtonColors = ButtonDefaults.buttonColors(), +) { + Button( + onClick = onClick, + modifier = modifier, + shape = RoundedCornerShape(999.dp), + border = border, + colors = buttonColors, + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = buttonText, + style = AppTypography.p14.copy(fontWeight = FontWeight.Medium), + color = contentColor, + ) + } +} diff --git a/core/ui/src/main/java/com/nexters/fooddiary/core/ui/component/KeywordChip.kt b/core/ui/src/main/java/com/nexters/fooddiary/core/ui/component/KeywordChip.kt new file mode 100644 index 0000000..a2e67f0 --- /dev/null +++ b/core/ui/src/main/java/com/nexters/fooddiary/core/ui/component/KeywordChip.kt @@ -0,0 +1,216 @@ +package com.nexters.fooddiary.core.ui.component + +import androidx.compose.foundation.Image +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.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +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.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.nexters.fooddiary.core.ui.R.drawable +import com.nexters.fooddiary.core.ui.theme.AppTypography +import com.nexters.fooddiary.core.ui.theme.Gray050 +import com.nexters.fooddiary.core.ui.theme.Gray300 +import com.nexters.fooddiary.core.ui.theme.Gray400 +import com.nexters.fooddiary.core.ui.theme.Sd800 +import com.nexters.fooddiary.core.ui.theme.SdBase + +private val KeywordChipShape = RoundedCornerShape(999.dp) + +@Composable +fun KeywordChip( + text: String, + modifier: Modifier = Modifier, + selected: Boolean = false, + onClick: (() -> Unit)? = null, + selectedContainerColor: Color = Gray050, + selectedContentColor: Color = Color.Black, + unselectedContainerColor: Color = Sd800, + unselectedContentColor: Color = Gray400, + trailingContent: @Composable (() -> Unit)? = null, +) { + val containerColor = if (selected) selectedContainerColor else unselectedContainerColor + val contentColor = if (selected) selectedContentColor else unselectedContentColor + + Row( + modifier = modifier + .background(color = containerColor, shape = KeywordChipShape) + .let { base -> + if (onClick == null) base else base.clickable(onClick = onClick) + } + .padding(start = 14.dp, end = 14.dp, top = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = text, + style = AppTypography.p14, + color = contentColor, + ) + trailingContent?.invoke() + } +} + +@Composable +fun KeywordChipGroup( + keywords: Collection, + modifier: Modifier = Modifier, + selectedKeywords: Set = emptySet(), + onKeywordClick: ((String) -> Unit)? = null, + horizontalSpacing: Int = 8, + verticalSpacing: Int = 8, + selectedContainerColor: Color = Gray050, + selectedContentColor: Color = Color.Black, + unselectedContainerColor: Color = Sd800, + unselectedContentColor: Color = Gray400, +) { + FlowRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(horizontalSpacing.dp), + verticalArrangement = Arrangement.spacedBy(verticalSpacing.dp), + ) { + keywords.forEach { keyword -> + key(keyword) { + KeywordChip( + text = keyword, + selected = selectedKeywords.contains(keyword), + onClick = onKeywordClick?.let { click -> { click(keyword) } }, + selectedContainerColor = selectedContainerColor, + selectedContentColor = selectedContentColor, + unselectedContainerColor = unselectedContainerColor, + unselectedContentColor = unselectedContentColor, + ) + } + } + } +} + +@Composable +fun EditableKeywordChipGroup( + keywords: List, + onAddClick: () -> Unit, + onKeywordRemove: (String) -> Unit, + removeContentDescription: String, + addContentDescription: String, + modifier: Modifier = Modifier, + horizontalSpacing: Int = 8, + verticalSpacing: Int = 4, +) { + FlowRow( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(horizontalSpacing.dp), + verticalArrangement = Arrangement.spacedBy(verticalSpacing.dp), + ) { + keywords.forEach { keyword -> + key(keyword) { + KeywordChip( + text = keyword, + trailingContent = { + Image( + painter = painterResource(drawable.ic_circle_close), + contentDescription = removeContentDescription, + modifier = Modifier + .size(18.dp) + .clickable { onKeywordRemove(keyword) }, + ) + }, + ) + } + } + Box( + modifier = Modifier + .size(34.dp) + .clip(CircleShape) + .background(Sd800) + .clickable(onClick = onAddClick), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = addContentDescription, + tint = Gray400, + ) + } + } +} + +@Composable +fun TasteKeywordSection( + title: String, + keywords: List, + modifier: Modifier = Modifier, + selectedKeywords: Set = emptySet(), + onKeywordClick: ((String) -> Unit)? = null, +) { + Row( + modifier = modifier + .background( + color = Color.White.copy(alpha = 0.02f), + shape = RoundedCornerShape(16.dp), + ) + .padding(horizontal = 16.dp, vertical = 24.dp), + ) { + androidx.compose.foundation.layout.Column( + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + Text( + text = title, + style = AppTypography.p15.copy(fontWeight = FontWeight.SemiBold), + color = Gray050, + ) + KeywordChipGroup( + keywords = keywords, + selectedKeywords = selectedKeywords, + onKeywordClick = onKeywordClick, + horizontalSpacing = 8, + verticalSpacing = 8, + unselectedContainerColor = Sd800, + unselectedContentColor = Gray400, + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF191821) +@Composable +private fun TasteKeywordSectionPreview() { + Row( + modifier = Modifier + .background(SdBase) + .padding(16.dp), + ) { + TasteKeywordSection( + title = "나의 입맛과\n가장 잘 어울리는 키워드", + keywords = listOf("#집밥", "#중식", "#튀김", "#소면"), + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF191821) +@Composable +private fun KeywordChipGroupSelectedPreview() { + KeywordChipGroup( + keywords = listOf("한식", "일식", "중식", "양식"), + selectedKeywords = setOf("중식"), + unselectedContentColor = Gray300, + ) +} diff --git a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyScreen.kt b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyScreen.kt index e561cd8..9e1639a 100644 --- a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyScreen.kt +++ b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyScreen.kt @@ -7,20 +7,16 @@ 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.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.clickable import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon @@ -42,18 +38,20 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import coil3.compose.AsyncImage import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel import com.nexters.fooddiary.core.ui.R.drawable import com.nexters.fooddiary.core.ui.alert.DialogData import com.nexters.fooddiary.core.ui.alert.SnackBarData +import com.nexters.fooddiary.core.ui.component.CommonCircleButton import com.nexters.fooddiary.core.ui.component.DetailScreenHeader +import com.nexters.fooddiary.core.ui.component.EditableKeywordChipGroup +import com.nexters.fooddiary.core.ui.component.KeywordChipGroup import com.nexters.fooddiary.core.ui.theme.AppTypography import com.nexters.fooddiary.core.ui.theme.Gray050 import com.nexters.fooddiary.core.ui.theme.Gray200 -import com.nexters.fooddiary.core.ui.theme.Gray400 +import com.nexters.fooddiary.core.ui.theme.Gray300 import com.nexters.fooddiary.core.ui.theme.Gray600 import com.nexters.fooddiary.core.ui.theme.Sd800 import com.nexters.fooddiary.core.ui.theme.Sd900 @@ -64,9 +62,7 @@ import com.nexters.fooddiary.presentation.modify.navigation.ModifySearchResult private const val PLACEHOLDER_IMAGE_URL = "https://picsum.photos/200/300" private val SectionTitleColor = Gray050 -private val ChipInactiveBg = Sd800 private val InputBg = Sd900 -private val ChipShape = RoundedCornerShape(999.dp) private val InputShape = RoundedCornerShape(10.dp) @Composable @@ -156,6 +152,8 @@ private fun ModifyScreenContent( val sectionCategory = stringResource(R.string.modify_section_category) val sectionAddress = stringResource(R.string.modify_section_address) val sectionTag = stringResource(R.string.modify_section_tag) + val deleteContentDesc = stringResource(R.string.modify_delete) + val addTagContentDesc = stringResource(R.string.modify_tag_add) Scaffold( modifier = modifier, @@ -204,10 +202,11 @@ private fun ModifyScreenContent( Section( sectionTitle = sectionCategory, ) { - CommonChips( - categories = state.categories, - selectedCategory = state.selectedCategory, - onSelect = onSelect, + KeywordChipGroup( + keywords = state.categories, + selectedKeywords = setOf(state.selectedCategory), + onKeywordClick = onSelect, + unselectedContentColor = Gray300, ) } } @@ -226,10 +225,12 @@ private fun ModifyScreenContent( Section( sectionTitle = sectionTag, ) { - TagChips( - tags = state.tags, - onRemove = onRemoveTag, - onAddChip = onAddChip + EditableKeywordChipGroup( + keywords = state.tags, + onKeywordRemove = onRemoveTag, + onAddClick = onAddChip, + removeContentDescription = deleteContentDesc, + addContentDescription = addTagContentDesc, ) } } @@ -339,77 +340,6 @@ private fun AddressLineItem(line: String) { } } -@Composable -private fun TagChips( - tags: List, - onRemove: (String) -> Unit, - onAddChip: () -> Unit = {}, -) { - val deleteContentDesc = stringResource(R.string.modify_delete) - val addTagContentDesc = stringResource(R.string.modify_tag_add) - FlowRow( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - tags.forEach { tag -> - key(tag) { - TagChipItem( - tag = tag, - onRemove = onRemove, - deleteContentDesc = deleteContentDesc, - ) - } - } - Box( - modifier = Modifier - .clip(ChipShape) - .background(ChipInactiveBg) - .clickable { - onAddChip() - } - .size(34.dp) - .padding(10.dp), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = addTagContentDesc, - tint = Gray400, - ) - } - } -} - -@Composable -private fun TagChipItem( - tag: String, - onRemove: (String) -> Unit, - deleteContentDesc: String, -) { - Row( - modifier = Modifier - .clip(ChipShape) - .background(ChipInactiveBg) - .padding(start = 14.dp, end = 8.dp, top = 8.dp, bottom = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = tag, - style = AppTypography.p14, - color = Gray400, - ) - Image( - painter = painterResource(drawable.ic_circle_close), - contentDescription = deleteContentDesc, - modifier = Modifier - .size(18.dp) - .clickable { onRemove(tag) }, - ) - } -} - @Composable private fun ModifyBottomButtons( onDelete: () -> Unit, diff --git a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/TagInputDialog.kt b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/TagInputDialog.kt index 178f7d8..91037df 100644 --- a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/TagInputDialog.kt +++ b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/TagInputDialog.kt @@ -26,10 +26,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import com.nexters.fooddiary.core.ui.component.CommonCircleButton import com.nexters.fooddiary.core.ui.theme.AppTypography import com.nexters.fooddiary.core.ui.theme.Gray050 import com.nexters.fooddiary.core.ui.theme.Gray200 -import com.nexters.fooddiary.core.ui.theme.Gray600 import com.nexters.fooddiary.core.ui.theme.Sd800 import com.nexters.fooddiary.core.ui.theme.SdBase diff --git a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/UiCompat.kt b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/UiCompat.kt deleted file mode 100644 index 2b890d1..0000000 --- a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/UiCompat.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.nexters.fooddiary.presentation.modify - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonColors -import androidx.compose.material3.ButtonDefaults -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.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.nexters.fooddiary.core.ui.theme.AppTypography -import com.nexters.fooddiary.core.ui.theme.Gray050 -import com.nexters.fooddiary.core.ui.theme.Gray300 -import com.nexters.fooddiary.core.ui.theme.Gray400 -import com.nexters.fooddiary.core.ui.theme.Sd800 -import com.nexters.fooddiary.core.ui.theme.White - -@Composable -internal fun CommonCircleButton( - modifier: Modifier = Modifier, - onClick: () -> Unit, - buttonText: String, - contentColor: Color = White, - border: BorderStroke? = null, - buttonColors: ButtonColors = ButtonDefaults.buttonColors(), -) { - Button( - onClick = onClick, - modifier = modifier, - shape = RoundedCornerShape(999.dp), - border = border, - colors = buttonColors, - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), - ) { - Text( - text = buttonText, - style = AppTypography.p14.copy(fontWeight = FontWeight.Medium), - color = contentColor, - ) - } -} - -@Composable -internal fun CommonChips( - categories: Set, - selectedCategory: String, - onSelect: (String) -> Unit, -) { - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - categories.forEach { category -> - val isSelected = category == selectedCategory - val bgColor = if (isSelected) Gray050 else Sd800 - val textColor = if (isSelected) Color.Black else Gray300 - Row( - modifier = Modifier - .background(bgColor, RoundedCornerShape(999.dp)) - .clickable { onSelect(category) } - .padding(horizontal = 14.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = category, - style = AppTypography.p14, - color = textColor, - ) - } - } - } -} From 8a876b8681ebdd260c02860f8c978bdec6cd61a6 Mon Sep 17 00:00:00 2001 From: soyeonLee126 Date: Sun, 8 Mar 2026 18:49:40 +0900 Subject: [PATCH 2/3] =?UTF-8?q?-=20painter=EB=A5=BC=20=EC=A7=80=EC=97=AD?= =?UTF-8?q?=20=EB=B3=80=EC=88=98=EB=A1=9C=20=EB=B6=84=EB=A6=AC=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EC=9E=AC=EC=82=AC=EC=9A=A9=20-=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EB=90=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A5=BC=20remember=EB=A1=9C=20?= =?UTF-8?q?=EB=A9=94=EB=AA=A8=EC=9D=B4=EC=A0=9C=EC=9D=B4=EC=85=98=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20recompositio?= =?UTF-8?q?n=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nexters/fooddiary/core/ui/component/KeywordChip.kt | 4 +++- .../nexters/fooddiary/presentation/modify/ModifyScreen.kt | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/ui/src/main/java/com/nexters/fooddiary/core/ui/component/KeywordChip.kt b/core/ui/src/main/java/com/nexters/fooddiary/core/ui/component/KeywordChip.kt index a2e67f0..8578f9a 100644 --- a/core/ui/src/main/java/com/nexters/fooddiary/core/ui/component/KeywordChip.kt +++ b/core/ui/src/main/java/com/nexters/fooddiary/core/ui/component/KeywordChip.kt @@ -115,6 +115,8 @@ fun EditableKeywordChipGroup( horizontalSpacing: Int = 8, verticalSpacing: Int = 4, ) { + val removePainter = painterResource(drawable.ic_circle_close) + FlowRow( modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(horizontalSpacing.dp), @@ -126,7 +128,7 @@ fun EditableKeywordChipGroup( text = keyword, trailingContent = { Image( - painter = painterResource(drawable.ic_circle_close), + painter = removePainter, contentDescription = removeContentDescription, modifier = Modifier .size(18.dp) diff --git a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyScreen.kt b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyScreen.kt index 9e1639a..f445bfc 100644 --- a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyScreen.kt +++ b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyScreen.kt @@ -154,6 +154,12 @@ private fun ModifyScreenContent( val sectionTag = stringResource(R.string.modify_section_tag) val deleteContentDesc = stringResource(R.string.modify_delete) val addTagContentDesc = stringResource(R.string.modify_tag_add) + val selectedCategories = remember(state.selectedCategory) { + state.selectedCategory + .takeIf { it.isNotBlank() } + ?.let { setOf(it) } + ?: emptySet() + } Scaffold( modifier = modifier, @@ -204,7 +210,7 @@ private fun ModifyScreenContent( ) { KeywordChipGroup( keywords = state.categories, - selectedKeywords = setOf(state.selectedCategory), + selectedKeywords = selectedCategories, onKeywordClick = onSelect, unselectedContentColor = Gray300, ) From 77ab0a8800e1da38954f1ea1edbf2265b3d005d9 Mon Sep 17 00:00:00 2001 From: soyeonLee126 Date: Sun, 8 Mar 2026 18:53:16 +0900 Subject: [PATCH 3/3] =?UTF-8?q?-=20modify=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EC=B9=A9=20=EA=B3=B5=EC=9A=A9=ED=99=94=20=EB=B0=8F=20=EC=83=81?= =?UTF-8?q?=ED=83=9C/=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 2 + presentation/modify/build.gradle.kts | 1 + .../presentation/modify/ModifyScreen.kt | 12 +++--- .../presentation/modify/ModifyState.kt | 14 ++++--- .../presentation/modify/ModifyViewModel.kt | 38 +++++++++++-------- 5 files changed, 42 insertions(+), 25 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 09bd376..454ca7c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ hiltNavigationCompose = "1.3.0" retrofit = "3.0.0" okhttp = "5.3.2" kotlinxSerialization = "1.9.0" +kotlinxImmutable = "0.3.8" coroutines = "1.10.2" mavericks = "3.0.12" ksp = "2.2.21-2.0.5" @@ -88,6 +89,7 @@ okhttp-mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver", # Kotlinx Serialization kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } +kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxImmutable" } # Coroutines kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } diff --git a/presentation/modify/build.gradle.kts b/presentation/modify/build.gradle.kts index 33ba8d4..49a7dc5 100644 --- a/presentation/modify/build.gradle.kts +++ b/presentation/modify/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.navigation.compose) implementation(libs.kotlinx.serialization.core) + implementation(libs.kotlinx.collections.immutable) implementation(libs.coil.compose) implementation(libs.coil.network.okhttp) diff --git a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyScreen.kt b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyScreen.kt index f445bfc..697eaf1 100644 --- a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyScreen.kt +++ b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyScreen.kt @@ -30,6 +30,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import kotlinx.coroutines.flow.collectLatest +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -386,17 +388,17 @@ private fun ModifyScreenPreview() { state = ModifyState( diaryId = "preview", selectedCategory = "한식", - categories = setOf("한식", "일식", "중식", "양식", "카페·디저트"), + categories = persistentSetOf("한식", "일식", "중식", "양식", "카페·디저트"), addressSearchQuery = "서울 강남구", - addressLines = listOf("서울특별시 강남구 테헤란로 123", "역삼동 456-7"), + addressLines = persistentListOf("서울특별시 강남구 테헤란로 123", "역삼동 456-7"), roadAddress = "서울특별시 강남구 테헤란로 123", restaurantName = "맛있는 밥집", restaurantUrl = "https://example.com/restaurant", note = "점심에 친구들이랑 같이 왔어요. 김치찌개가 특히 맛있었습니다!", - photoIds = listOf(1, 2), - photoUrls = listOf(PLACEHOLDER_IMAGE_URL, PLACEHOLDER_IMAGE_URL), + photoIds = persistentListOf(1, 2), + photoUrls = persistentListOf(PLACEHOLDER_IMAGE_URL, PLACEHOLDER_IMAGE_URL), coverPhotoId = 1, - tags = listOf("맛집", "친구모임", "점심"), + tags = persistentListOf("맛집", "친구모임", "점심"), ), onRemovePhotoAt = {}, ) diff --git a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyState.kt b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyState.kt index 4f3d2cc..a18e365 100644 --- a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyState.kt +++ b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyState.kt @@ -1,6 +1,10 @@ package com.nexters.fooddiary.presentation.modify import com.airbnb.mvrx.MavericksState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf sealed interface ModifyError { data object Save : ModifyError @@ -10,17 +14,17 @@ data class ModifyState( val diaryId: String = "", val isInitialSynced: Boolean = false, val selectedCategory: String = "", - val categories: Set = setOf(), + val categories: ImmutableSet = persistentSetOf(), val isAddressManuallyUpdated: Boolean = false, val addressSearchQuery: String = "", - val addressLines: List = emptyList(), + val addressLines: ImmutableList = persistentListOf(), val roadAddress: String = "", val restaurantName: String = "", val restaurantUrl: String = "", val note: String = "", - val photoIds: List = emptyList(), - val photoUrls: List = emptyList(), + val photoIds: ImmutableList = persistentListOf(), + val photoUrls: ImmutableList = persistentListOf(), val coverPhotoId: Int? = null, - val tags: List = emptyList(), + val tags: ImmutableList = persistentListOf(), val error: ModifyError? = null, ) : MavericksState diff --git a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyViewModel.kt b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyViewModel.kt index da106c0..9826c4c 100644 --- a/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyViewModel.kt +++ b/presentation/modify/src/main/java/com/nexters/fooddiary/presentation/modify/ModifyViewModel.kt @@ -16,6 +16,9 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentSet sealed interface ModifyEvent { data object Saved : ModifyEvent @@ -55,7 +58,8 @@ class ModifyViewModel @AssistedInject constructor( val normalizedAddressLines = normalizedName .takeIf { it.isNotBlank() } ?.let(::listOf) - ?: emptyList() + ?.toPersistentList() + ?: persistentEmptyStringList setState { copy( addressSearchQuery = normalizedRoadAddress.ifBlank { normalizedName }, @@ -69,7 +73,7 @@ class ModifyViewModel @AssistedInject constructor( } fun removeTag(tag: String) { - setState { copy(tags = tags.filter { it != tag }) } + setState { copy(tags = tags.filter { it != tag }.toPersistentList()) } } fun addTag(tag: String) { @@ -107,16 +111,18 @@ class ModifyViewModel @AssistedInject constructor( val entryCategory = entry.category val mergedCategories = entryCategory ?.takeIf { it.isNotBlank() } - ?.let { categories + it } + ?.let { categories.toPersistentSet().add(it) } ?: categories val normalizedAddressLines = entry.restaurantName ?.takeIf { it.isNotBlank() } ?.let(::listOf) - ?: emptyList() + ?.toPersistentList() + ?: persistentEmptyStringList + val entryTags = entry.tags.toPersistentList() val shouldKeepAddress = isAddressManuallyUpdated copy( - photoIds = entry.photos.map { it.photoId.toInt() }, - photoUrls = entry.photos.map { it.imageUrl }, + photoIds = entry.photos.map { it.photoId.toInt() }.toPersistentList(), + photoUrls = entry.photos.map { it.imageUrl }.toPersistentList(), coverPhotoId = entry.coverPhotoId.toInt(), selectedCategory = entryCategory?.takeIf { it.isNotBlank() } ?: selectedCategory, categories = mergedCategories, @@ -126,7 +132,7 @@ class ModifyViewModel @AssistedInject constructor( restaurantName = if (shouldKeepAddress) restaurantName else (entry.restaurantName ?: ""), restaurantUrl = if (shouldKeepAddress) restaurantUrl else (entry.mapLink ?: ""), note = entry.note ?: "", - tags = entry.tags.ifEmpty { tags }, + tags = if (entryTags.isEmpty()) tags else entryTags, isInitialSynced = true, ) } @@ -202,23 +208,23 @@ class ModifyViewModel @AssistedInject constructor( internal fun normalizeTag(tag: String): String? = tag.trim().takeIf { it.isNotBlank() } -internal fun appendTagIfMissing(tags: List, newTag: String): List? = - if (newTag in tags) null else tags + newTag +internal fun appendTagIfMissing(tags: ImmutableList, newTag: String): ImmutableList? = + if (newTag in tags) null else tags.toPersistentList().add(newTag) internal data class RemovePhotoStateResult( - val photoIds: List, - val photoUrls: List, + val photoIds: ImmutableList, + val photoUrls: ImmutableList, val coverPhotoId: Int?, ) internal fun removePhotoAtState( - photoIds: List, - photoUrls: List, + photoIds: ImmutableList, + photoUrls: ImmutableList, coverPhotoId: Int?, index: Int, ): RemovePhotoStateResult { - val newPhotoIds = photoIds.filterIndexed { i, _ -> i != index } - val newPhotoUrls = photoUrls.filterIndexed { i, _ -> i != index } + val newPhotoIds = photoIds.filterIndexed { i, _ -> i != index }.toPersistentList() + val newPhotoUrls = photoUrls.filterIndexed { i, _ -> i != index }.toPersistentList() val removedPhotoId = photoIds.getOrNull(index) val newCoverPhotoId = when { removedPhotoId == null -> coverPhotoId @@ -232,6 +238,8 @@ internal fun removePhotoAtState( ) } +private val persistentEmptyStringList = emptyList().toPersistentList() + internal fun ModifyState.toUpdateDiaryParam(): UpdateDiaryParam = UpdateDiaryParam( category = selectedCategory.takeIf { it.isNotBlank() },