From 654e0ab819a0b9d662de95a2a01b736e073a08f7 Mon Sep 17 00:00:00 2001 From: TTRR1007 Date: Tue, 10 Mar 2026 20:34:25 +0900 Subject: [PATCH 01/11] feat: Callvan list filter and search --- .../koin/feature/callvan/Constant.kt | 3 + .../callvan/enums/CallvanFilterType.kt | 115 ++++++ .../ui/list/component/CallvanFilterChip.kt | 61 ++++ .../ui/list/component/FilterBottomSheet.kt | 332 ++++++++++++++++++ .../list/component/FilterBottomSheetItem.kt | 43 +++ .../ui/list/component/ItemSearchTextField.kt | 82 +++++ .../src/main/res/drawable/ic_list_filter.xml | 9 + .../src/main/res/drawable/ic_process.xml | 15 + .../main/res/drawable/ic_search_vector.xml | 13 + .../callvan/src/main/res/values/strings.xml | 26 ++ 10 files changed, 699 insertions(+) create mode 100644 feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/Constant.kt create mode 100644 feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/enums/CallvanFilterType.kt create mode 100644 feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/CallvanFilterChip.kt create mode 100644 feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt create mode 100644 feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheetItem.kt create mode 100644 feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt create mode 100644 feature/callvan/src/main/res/drawable/ic_list_filter.xml create mode 100644 feature/callvan/src/main/res/drawable/ic_process.xml create mode 100644 feature/callvan/src/main/res/drawable/ic_search_vector.xml diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/Constant.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/Constant.kt new file mode 100644 index 0000000000..9c2a16314a --- /dev/null +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/Constant.kt @@ -0,0 +1,3 @@ +package `in`.koreatech.koin.feature.callvan + +const val TEXT_FIELD_MAX_LENGTH = 255 diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/enums/CallvanFilterType.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/enums/CallvanFilterType.kt new file mode 100644 index 0000000000..9a5ef11384 --- /dev/null +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/enums/CallvanFilterType.kt @@ -0,0 +1,115 @@ +package `in`.koreatech.koin.feature.callvan.enums + +import android.os.Parcelable +import androidx.annotation.StringRes +import `in`.koreatech.koin.feature.callvan.R +import kotlinx.parcelize.Parcelize + +@Parcelize +sealed class CallvanFilterType( + @StringRes open val stringRes: Int, + open val value: String +) : Parcelable { + + @Parcelize + sealed class DeparturesFilterType( + @StringRes override val stringRes: Int, + override val value: String + ) : CallvanFilterType(stringRes, value) { + @Parcelize + object All : DeparturesFilterType(R.string.filter_list_all, "ALL") + + @Parcelize + object FrontGate : DeparturesFilterType(R.string.filter_list_front_gate, "FRONT_GATE") + + @Parcelize + object BackGate : DeparturesFilterType(R.string.filter_list_back_gate, "BACK_GATE") + + @Parcelize + object TennisCourt : DeparturesFilterType(R.string.filter_list_tennis_court, "TENNIS_COURT") + + @Parcelize + object DormitoryMain : DeparturesFilterType(R.string.filter_list_dormitory_main, "DORMITORY_MAIN") + + @Parcelize + object DormitorySub : DeparturesFilterType(R.string.filter_list_dormitory_sub, "DORMITORY_SUB") + + @Parcelize + object Terminal : DeparturesFilterType(R.string.filter_list_terminal, "TERMINAL") + + @Parcelize + object Station : DeparturesFilterType(R.string.filter_list_station, "STATION") + + @Parcelize + object AsanStation : DeparturesFilterType(R.string.filter_list_asan_station, "ASAN_STATION") + } + + @Parcelize + sealed class ArrivalsFilterType( + @StringRes override val stringRes: Int, + override val value: String + ) : CallvanFilterType(stringRes, value) { + @Parcelize + object All : ArrivalsFilterType(R.string.filter_list_all, "ALL") + + @Parcelize + object FrontGate : ArrivalsFilterType(R.string.filter_list_front_gate, "FRONT_GATE") + + @Parcelize + object BackGate : ArrivalsFilterType(R.string.filter_list_back_gate, "BACK_GATE") + + @Parcelize + object TennisCourt : ArrivalsFilterType(R.string.filter_list_tennis_court, "TENNIS_COURT") + + @Parcelize + object DormitoryMain : ArrivalsFilterType(R.string.filter_list_dormitory_main, "DORMITORY_MAIN") + + @Parcelize + object DormitorySub : ArrivalsFilterType(R.string.filter_list_dormitory_sub, "DORMITORY_SUB") + + @Parcelize + object Terminal : ArrivalsFilterType(R.string.filter_list_terminal, "TERMINAL") + + @Parcelize + object Station : ArrivalsFilterType(R.string.filter_list_station, "STATION") + + @Parcelize + object AsanStation : ArrivalsFilterType(R.string.filter_list_asan_station, "ASAN_STATION") + } + + @Parcelize + sealed class StatusesType( + @StringRes override val stringRes: Int, + override val value: String + ) : CallvanFilterType(stringRes, value) { + @Parcelize + object All : StatusesType(R.string.filter_list_all, "ALL") + + @Parcelize + object Recruiting : StatusesType(R.string.filter_list_recruiting, "RECRUITING") + + @Parcelize + object Closed : StatusesType(R.string.filter_list_closed, "CLOSED") + + @Parcelize + object Completed : StatusesType(R.string.filter_list_completed, "COMPLETED") + } + + @Parcelize + sealed class SortType( + @StringRes override val stringRes: Int, + override val value: String + ) : CallvanFilterType(stringRes, value) { + @Parcelize + object LatestDesc : SortType(R.string.filter_list_latest_desc, "LATEST_DESC") + + @Parcelize + object LatestAsc : SortType(R.string.filter_list_latest_asc, "LATEST_ASC") + + @Parcelize + object DepartureDesc : SortType(R.string.filter_list_departure_desc, "DEPARTURE_DESC") + + @Parcelize + object DepartureAsc : SortType(R.string.filter_list_departure_asc, "DEPARTURE_ASC") + } +} diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/CallvanFilterChip.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/CallvanFilterChip.kt new file mode 100644 index 0000000000..19fbb52015 --- /dev/null +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/CallvanFilterChip.kt @@ -0,0 +1,61 @@ +package `in`.koreatech.koin.feature.callvan.ui.list.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import `in`.koreatech.koin.core.designsystem.theme.KoinTheme +import `in`.koreatech.koin.core.designsystem.theme.RebrandKoinTheme +import `in`.koreatech.koin.feature.callvan.R + +@Composable +fun CallvanFilterChip(onClick: () -> Unit) { + Surface( + onClick = { + onClick() + }, + modifier = Modifier + .height(34.dp), + shape = RoundedCornerShape(24.dp), + color = RebrandKoinTheme.colors.primary100, + contentColor = RebrandKoinTheme.colors.primary900 + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp) + + ) { + Text( + text = stringResource(R.string.filter_container), + style = KoinTheme.typography.bold14 + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_list_filter), + contentDescription = "", + modifier = Modifier.size(16.dp) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun CallvanFilterChipPreview() { + KoinTheme { + CallvanFilterChip(onClick = {}) + } +} diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt new file mode 100644 index 0000000000..1671dbf081 --- /dev/null +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt @@ -0,0 +1,332 @@ +package `in`.koreatech.koin.feature.callvan.ui.list.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +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.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import `in`.koreatech.koin.core.designsystem.theme.KoinTheme +import `in`.koreatech.koin.core.designsystem.theme.RebrandKoinTheme +import `in`.koreatech.koin.feature.callvan.R +import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType +import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.ArrivalsFilterType +import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.DeparturesFilterType +import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.SortType +import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.StatusesType +import `in`.koreatech.koin.feature.callvan.ui.component.CallvanBottomSheet +import kotlin.collections.map +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FilterBottomSheet( + onDismissRequest: () -> Unit, + selectedSortType: SortType, + selectedStatusesType: StatusesType, + selectedArrivalsType: ImmutableList, + selectedDeparturesType: ImmutableList, + onApply: (SortType, StatusesType, ImmutableList, ImmutableList) -> Unit +) { + var selectedSortType by remember { mutableStateOf(selectedSortType) } + var selectedStatusesType by remember { mutableStateOf(selectedStatusesType) } + var selectedArrivalsType by remember { mutableStateOf(selectedArrivalsType) } + var selectedDeparturesType by remember { mutableStateOf(selectedDeparturesType) } + + CallvanBottomSheet( + title = stringResource(R.string.filter_container), + onDismiss = onDismissRequest, + showCloseButton = true + ) { + FilterBottomSheetContent( + selectedSortType = selectedSortType, + selectedStatusesType = selectedStatusesType, + selectedDeparturesType = selectedDeparturesType, + selectedArrivalsType = selectedArrivalsType, + onSortTypeChange = { selectedSortType = it as SortType }, + onStatusesTypeChange = { selectedStatusesType = it as StatusesType }, + onArrivalsTypeChange = { + val newSelected = it.map { type -> type as ArrivalsFilterType } + selectedArrivalsType = if ( + selectedArrivalsType.size == 1 && + selectedArrivalsType.first() == ArrivalsFilterType.All + ) { + (newSelected - ArrivalsFilterType.All).toPersistentList() + } else if (ArrivalsFilterType.All in newSelected) { + persistentListOf(ArrivalsFilterType.All) + } else { + newSelected.toPersistentList() + } + }, + onDeparturesTypeChange = { + val newSelected = it.map { type -> type as DeparturesFilterType } + selectedDeparturesType = if ( + selectedDeparturesType.size == 1 && + selectedDeparturesType.first() == DeparturesFilterType.All + ) { + (newSelected - DeparturesFilterType.All).toPersistentList() + } else if (DeparturesFilterType.All in newSelected) { + persistentListOf(DeparturesFilterType.All) + } else { + newSelected.toPersistentList() + } + }, + onReset = { + selectedSortType = SortType.LatestDesc + selectedStatusesType = StatusesType.All + selectedDeparturesType = persistentListOf(DeparturesFilterType.All) + selectedArrivalsType = persistentListOf(ArrivalsFilterType.All) + }, + onApplyClick = { + onApply( + selectedSortType, + selectedStatusesType, + selectedDeparturesType, + selectedArrivalsType + ) + onDismissRequest() + } + ) + } +} + +@Composable +fun FilterBottomSheetContent( + selectedSortType: SortType, + selectedStatusesType: StatusesType, + selectedDeparturesType: ImmutableList, + selectedArrivalsType: ImmutableList, + onSortTypeChange: (CallvanFilterType) -> Unit, + onStatusesTypeChange: (CallvanFilterType) -> Unit, + onDeparturesTypeChange: (ImmutableList) -> Unit, + onArrivalsTypeChange: (ImmutableList) -> Unit, + onReset: () -> Unit, + onApplyClick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 20.dp) + ) { + Column( + modifier = Modifier + .padding(top = 12.dp, start = 32.dp, bottom = 12.dp, end = 12.dp) + ) { + FilterSection( + title = stringResource(R.string.filter_list_sort_order), + items = persistentListOf( + SortType.LatestDesc, + SortType.LatestAsc, + SortType.DepartureDesc, + SortType.DepartureAsc + ), + selectedItem = selectedSortType, + onItemSelected = onSortTypeChange + ) + HorizontalDivider(color = KoinTheme.colors.neutral300) + FilterSection( + title = stringResource(R.string.filter_list_recruitment_status), + items = persistentListOf( + StatusesType.All, + StatusesType.Recruiting, + StatusesType.Closed, + StatusesType.Completed + ), + selectedItem = selectedStatusesType, + onItemSelected = onStatusesTypeChange + ) + HorizontalDivider(color = KoinTheme.colors.neutral300) + FilterDuplicateSection( + title = stringResource(R.string.filter_list_origin), + items = persistentListOf( + DeparturesFilterType.All, DeparturesFilterType.FrontGate, + DeparturesFilterType.BackGate, DeparturesFilterType.TennisCourt, + DeparturesFilterType.DormitoryMain, DeparturesFilterType.DormitorySub, + DeparturesFilterType.Terminal, DeparturesFilterType.Station, + DeparturesFilterType.AsanStation + ), + selectedItems = selectedDeparturesType, + onItemSelected = onDeparturesTypeChange + ) + HorizontalDivider(color = KoinTheme.colors.neutral300) + FilterDuplicateSection( + title = stringResource(R.string.filter_list_destination), + items = persistentListOf( + ArrivalsFilterType.All, ArrivalsFilterType.FrontGate, + ArrivalsFilterType.BackGate, ArrivalsFilterType.TennisCourt, + ArrivalsFilterType.DormitoryMain, ArrivalsFilterType.DormitorySub, + ArrivalsFilterType.Terminal, ArrivalsFilterType.Station, + ArrivalsFilterType.AsanStation + ), + selectedItems = selectedArrivalsType, + onItemSelected = onArrivalsTypeChange + ) + HorizontalDivider(color = KoinTheme.colors.neutral300) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, start = 32.dp, bottom = 12.dp, end = 12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + OutlinedButton( + onClick = onReset, + shape = KoinTheme.shapes.medium, + border = BorderStroke(1.dp, KoinTheme.colors.neutral300), + modifier = Modifier.weight(1f).height(48.dp), + colors = ButtonDefaults.outlinedButtonColors(containerColor = KoinTheme.colors.neutral0) + ) { + Text( + text = stringResource(R.string.filter_list_reset), + color = KoinTheme.colors.neutral600, + style = KoinTheme.typography.bold16 + ) + Spacer(modifier = Modifier.width(4.dp)) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_process), + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = KoinTheme.colors.neutral500 + ) + } + Button( + onClick = onApplyClick, + shape = KoinTheme.shapes.medium, + colors = ButtonDefaults.buttonColors(containerColor = RebrandKoinTheme.colors.primary500), + modifier = Modifier.weight(2f).height(48.dp) + ) { + Text( + text = stringResource(R.string.filter_list_adapt), + color = KoinTheme.colors.neutral0, + style = KoinTheme.typography.bold16 + ) + } + } + } +} + +@Composable +fun FilterSection( + title: String, + items: ImmutableList, + selectedItem: CallvanFilterType, + onItemSelected: (CallvanFilterType) -> Unit +) { + Column(modifier = Modifier.padding(vertical = 12.dp)) { + Text( + text = title, + style = KoinTheme.typography.bold16, + color = KoinTheme.colors.neutral800, + modifier = Modifier.padding(bottom = 12.dp) + ) + FlowRow( + modifier = Modifier.fillMaxWidth(), + maxItemsInEachRow = 4 + ) { + items.forEach { item -> + FilterBottomSheetItem( + text = stringResource(item.stringRes), + isSelected = item == selectedItem, + onClick = { onItemSelected(item) } + ) + } + } + } +} +private const val AT_LEAST_COUNT = 1 + +@Composable +fun FilterDuplicateSection( + title: String, + items: ImmutableList, + selectedItems: ImmutableList, + onItemSelected: (ImmutableList) -> Unit +) { + Column(modifier = Modifier.padding(vertical = 12.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = title, + style = KoinTheme.typography.bold16, + color = KoinTheme.colors.neutral800 + ) + Text( + text = stringResource(R.string.filter_list_other_place_hint), + style = KoinTheme.typography.regular12, + color = KoinTheme.colors.neutral500 + ) + } + FlowRow( + modifier = Modifier.fillMaxWidth(), + maxItemsInEachRow = 5 + ) { + items.forEach { item -> + FilterBottomSheetItem( + text = stringResource(item.stringRes), + isSelected = item in selectedItems, + onClick = { + onItemSelected( + if (item in selectedItems) { + if (selectedItems.size > AT_LEAST_COUNT) { + (selectedItems - item).toPersistentList() + } else { + return@FilterBottomSheetItem + } + } else { + (selectedItems + item).toPersistentList() + } + ) + } + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun FilterBottomSheetContentPreview() { + RebrandKoinTheme { + FilterBottomSheetContent( + selectedSortType = SortType.LatestDesc, + selectedStatusesType = StatusesType.All, + selectedDeparturesType = persistentListOf(DeparturesFilterType.All), + selectedArrivalsType = persistentListOf(ArrivalsFilterType.All), + onSortTypeChange = {}, + onStatusesTypeChange = {}, + onDeparturesTypeChange = {}, + onArrivalsTypeChange = {}, + onReset = {}, + onApplyClick = {} + ) + } +} diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheetItem.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheetItem.kt new file mode 100644 index 0000000000..8485e82be1 --- /dev/null +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheetItem.kt @@ -0,0 +1,43 @@ +package `in`.koreatech.koin.feature.callvan.ui.list.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import `in`.koreatech.koin.core.designsystem.theme.KoinTheme +import `in`.koreatech.koin.core.designsystem.theme.RebrandKoinTheme + +@Composable +fun FilterBottomSheetItem( + text: String, + isSelected: Boolean, + onClick: () -> Unit +) { + Surface( + onClick = onClick, + shape = RoundedCornerShape(24.dp), + border = BorderStroke( + width = 1.dp, + color = if (isSelected) RebrandKoinTheme.colors.primary500 else KoinTheme.colors.neutral300 + ), + color = KoinTheme.colors.neutral0, + modifier = Modifier.padding(end = 8.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.padding(vertical = 8.dp, horizontal = 12.dp) + ) { + Text( + text = text, + color = if (isSelected) RebrandKoinTheme.colors.primary500 else KoinTheme.colors.neutral500, + style = KoinTheme.typography.bold14 + ) + } + } +} diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt new file mode 100644 index 0000000000..f560ad32a3 --- /dev/null +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt @@ -0,0 +1,82 @@ +package `in`.koreatech.koin.feature.callvan.ui.list.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import `in`.koreatech.koin.core.designsystem.theme.KoinTheme +import `in`.koreatech.koin.feature.callvan.R +import `in`.koreatech.koin.feature.callvan.TEXT_FIELD_MAX_LENGTH + +@Composable +fun ItemSearchTextField( + value: String, + modifier: Modifier = Modifier, + maxLength: Int = TEXT_FIELD_MAX_LENGTH, + hint: String = stringResource(R.string.callvan_search_hint), + textStyle: TextStyle = KoinTheme.typography.regular14.copy(color = KoinTheme.colors.neutral600), + onValueChange: (String) -> Unit = {} +) { + Row( + modifier = modifier + .fillMaxWidth() + .background( + color = KoinTheme.colors.neutral100, + shape = KoinTheme.shapes.extraSmall + ) + .padding(vertical = 8.dp, horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + BasicTextField( + modifier = Modifier.weight(1f), + value = value, + textStyle = textStyle, + singleLine = true, + onValueChange = { + if (it.length < maxLength) { + onValueChange(it) + } else { + onValueChange(it.take(maxLength)) + } + }, + decorationBox = { innerTextField -> + Box { + if (value.isEmpty()) { + Text( + text = hint, + color = KoinTheme.colors.neutral600 + ) + } + innerTextField() + } + } + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_search_vector), + contentDescription = "" + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun ItemSearchTextFieldPreview() { + ItemSearchTextField( + value = "", + hint = "검색어를 입력해주세요.", + onValueChange = {} + ) +} diff --git a/feature/callvan/src/main/res/drawable/ic_list_filter.xml b/feature/callvan/src/main/res/drawable/ic_list_filter.xml new file mode 100644 index 0000000000..91115f4a85 --- /dev/null +++ b/feature/callvan/src/main/res/drawable/ic_list_filter.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/feature/callvan/src/main/res/drawable/ic_process.xml b/feature/callvan/src/main/res/drawable/ic_process.xml new file mode 100644 index 0000000000..304d0fe774 --- /dev/null +++ b/feature/callvan/src/main/res/drawable/ic_process.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/feature/callvan/src/main/res/drawable/ic_search_vector.xml b/feature/callvan/src/main/res/drawable/ic_search_vector.xml new file mode 100644 index 0000000000..eaa922c07f --- /dev/null +++ b/feature/callvan/src/main/res/drawable/ic_search_vector.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/feature/callvan/src/main/res/values/strings.xml b/feature/callvan/src/main/res/values/strings.xml index 83aa5b1a95..ba7e81ab9b 100644 --- a/feature/callvan/src/main/res/values/strings.xml +++ b/feature/callvan/src/main/res/values/strings.xml @@ -53,4 +53,30 @@ 욕설 욕설, 성적인 언어, 비방하는 언어를 사용했습니다. 기타 + + 필터 + 초기화 + 적용하기 + 정렬 + 모집 상태 + 출발지 + 도착지 + 기타 장소는 검색창을 이용해주세요. + 전체 + 정문 + 후문 + 테니스장 + 본관동 + 별관동 + 천안터미널 + 천안역 + 천안아산역 + 모집중 + 모집마감 + 모집완료 + 게시글 등록순 (내림차순) + 게시글 등록순 (오름차순) + 출발일 (내림차순) + 출발일 (오름차순) + 검색어를 입력해주세요. \ No newline at end of file From 0fb1fa9c7de53b203a419cb2716e7fcb11c0e545 Mon Sep 17 00:00:00 2001 From: TTRR1007 Date: Sat, 14 Mar 2026 18:47:44 +0900 Subject: [PATCH 02/11] refactor: apply suggestions from code review --- .../feature/callvan/enums/CallvanFilterType.kt | 16 ++++++++-------- .../ui/list/component/FilterBottomSheet.kt | 5 +++++ .../ui/list/component/FilterBottomSheetItem.kt | 6 +++++- .../ui/list/component/ItemSearchTextField.kt | 2 +- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/enums/CallvanFilterType.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/enums/CallvanFilterType.kt index 9a5ef11384..380c972027 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/enums/CallvanFilterType.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/enums/CallvanFilterType.kt @@ -8,16 +8,16 @@ import kotlinx.parcelize.Parcelize @Parcelize sealed class CallvanFilterType( @StringRes open val stringRes: Int, - open val value: String + open val value: String? ) : Parcelable { @Parcelize sealed class DeparturesFilterType( @StringRes override val stringRes: Int, - override val value: String + override val value: String? ) : CallvanFilterType(stringRes, value) { @Parcelize - object All : DeparturesFilterType(R.string.filter_list_all, "ALL") + object All : DeparturesFilterType(R.string.filter_list_all, null) @Parcelize object FrontGate : DeparturesFilterType(R.string.filter_list_front_gate, "FRONT_GATE") @@ -47,10 +47,10 @@ sealed class CallvanFilterType( @Parcelize sealed class ArrivalsFilterType( @StringRes override val stringRes: Int, - override val value: String + override val value: String? ) : CallvanFilterType(stringRes, value) { @Parcelize - object All : ArrivalsFilterType(R.string.filter_list_all, "ALL") + object All : ArrivalsFilterType(R.string.filter_list_all, null) @Parcelize object FrontGate : ArrivalsFilterType(R.string.filter_list_front_gate, "FRONT_GATE") @@ -80,10 +80,10 @@ sealed class CallvanFilterType( @Parcelize sealed class StatusesType( @StringRes override val stringRes: Int, - override val value: String + override val value: String? ) : CallvanFilterType(stringRes, value) { @Parcelize - object All : StatusesType(R.string.filter_list_all, "ALL") + object All : StatusesType(R.string.filter_list_all, null) @Parcelize object Recruiting : StatusesType(R.string.filter_list_recruiting, "RECRUITING") @@ -98,7 +98,7 @@ sealed class CallvanFilterType( @Parcelize sealed class SortType( @StringRes override val stringRes: Int, - override val value: String + override val value: String? ) : CallvanFilterType(stringRes, value) { @Parcelize object LatestDesc : SortType(R.string.filter_list_latest_desc, "LATEST_DESC") diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt index 1671dbf081..7ac7e15a79 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -134,8 +136,11 @@ fun FilterBottomSheetContent( .fillMaxWidth() .padding(bottom = 20.dp) ) { + val scrollState = rememberScrollState() Column( modifier = Modifier + .weight(1f, fill = false) + .verticalScroll(scrollState) .padding(top = 12.dp, start = 32.dp, bottom = 12.dp, end = 12.dp) ) { FilterSection( diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheetItem.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheetItem.kt index 8485e82be1..2592a34b3b 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheetItem.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheetItem.kt @@ -9,6 +9,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import `in`.koreatech.koin.core.designsystem.theme.KoinTheme import `in`.koreatech.koin.core.designsystem.theme.RebrandKoinTheme @@ -27,7 +29,9 @@ fun FilterBottomSheetItem( color = if (isSelected) RebrandKoinTheme.colors.primary500 else KoinTheme.colors.neutral300 ), color = KoinTheme.colors.neutral0, - modifier = Modifier.padding(end = 8.dp) + modifier = Modifier + .padding(end = 8.dp) + .semantics { selected = isSelected } ) { Box( contentAlignment = Alignment.Center, diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt index f560ad32a3..b78a151040 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt @@ -66,7 +66,7 @@ fun ItemSearchTextField( ) Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_search_vector), - contentDescription = "" + contentDescription = null ) } } From 5ee23ede995c31ae1577375da7dfe8c7f3ac14ad Mon Sep 17 00:00:00 2001 From: TTRR1007 Date: Sat, 14 Mar 2026 21:49:47 +0900 Subject: [PATCH 03/11] refactor: apply suggestions from Nitpick comments --- .../ui/list/FilterBottomSheetActions.kt | 13 ++ .../callvan/ui/list/FilterBottomSheetState.kt | 14 ++ .../ui/list/component/FilterBottomSheet.kt | 154 +++++++++--------- 3 files changed, 105 insertions(+), 76 deletions(-) create mode 100644 feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt create mode 100644 feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt new file mode 100644 index 0000000000..187800e26d --- /dev/null +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt @@ -0,0 +1,13 @@ +package `in`.koreatech.koin.feature.callvan.ui.list + +import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType +import kotlinx.collections.immutable.ImmutableList + +data class FilterBottomSheetActions( + val onSortTypeChange: (CallvanFilterType) -> Unit, + val onStatusesTypeChange: (CallvanFilterType) -> Unit, + val onDeparturesTypeChange: (ImmutableList) -> Unit, + val onArrivalsTypeChange: (ImmutableList) -> Unit, + val onReset: () -> Unit, + val onApplyClick: () -> Unit +) \ No newline at end of file diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt new file mode 100644 index 0000000000..8baeeda2cd --- /dev/null +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt @@ -0,0 +1,14 @@ +package `in`.koreatech.koin.feature.callvan.ui.list + +import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.ArrivalsFilterType +import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.DeparturesFilterType +import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.SortType +import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.StatusesType +import kotlinx.collections.immutable.ImmutableList + +data class FilterBottomSheetState( + val selectedSortType: SortType, + val selectedStatusesType: StatusesType, + val selectedDeparturesType: ImmutableList, + val selectedArrivalsType: ImmutableList +) \ No newline at end of file diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt index 7ac7e15a79..bbb1364498 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt @@ -41,6 +41,8 @@ import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.DeparturesFil import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.SortType import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.StatusesType import `in`.koreatech.koin.feature.callvan.ui.component.CallvanBottomSheet +import `in`.koreatech.koin.feature.callvan.ui.list.FilterBottomSheetActions +import `in`.koreatech.koin.feature.callvan.ui.list.FilterBottomSheetState import kotlin.collections.map import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -67,69 +69,65 @@ fun FilterBottomSheet( showCloseButton = true ) { FilterBottomSheetContent( - selectedSortType = selectedSortType, - selectedStatusesType = selectedStatusesType, - selectedDeparturesType = selectedDeparturesType, - selectedArrivalsType = selectedArrivalsType, - onSortTypeChange = { selectedSortType = it as SortType }, - onStatusesTypeChange = { selectedStatusesType = it as StatusesType }, - onArrivalsTypeChange = { - val newSelected = it.map { type -> type as ArrivalsFilterType } - selectedArrivalsType = if ( - selectedArrivalsType.size == 1 && - selectedArrivalsType.first() == ArrivalsFilterType.All - ) { - (newSelected - ArrivalsFilterType.All).toPersistentList() - } else if (ArrivalsFilterType.All in newSelected) { - persistentListOf(ArrivalsFilterType.All) - } else { - newSelected.toPersistentList() - } - }, - onDeparturesTypeChange = { - val newSelected = it.map { type -> type as DeparturesFilterType } - selectedDeparturesType = if ( - selectedDeparturesType.size == 1 && - selectedDeparturesType.first() == DeparturesFilterType.All - ) { - (newSelected - DeparturesFilterType.All).toPersistentList() - } else if (DeparturesFilterType.All in newSelected) { - persistentListOf(DeparturesFilterType.All) - } else { - newSelected.toPersistentList() + state = FilterBottomSheetState( + selectedSortType = selectedSortType, + selectedStatusesType = selectedStatusesType, + selectedDeparturesType = selectedDeparturesType, + selectedArrivalsType = selectedArrivalsType + ), + actions = FilterBottomSheetActions( + onSortTypeChange = { selectedSortType = it as SortType }, + onStatusesTypeChange = { selectedStatusesType = it as StatusesType }, + onArrivalsTypeChange = { + val newSelected = it.map { type -> type as ArrivalsFilterType } + selectedArrivalsType = if ( + selectedArrivalsType.size == 1 && + selectedArrivalsType.first() == ArrivalsFilterType.All + ) { + (newSelected - ArrivalsFilterType.All).toPersistentList() + } else if (ArrivalsFilterType.All in newSelected) { + persistentListOf(ArrivalsFilterType.All) + } else { + newSelected.toPersistentList() + } + }, + onDeparturesTypeChange = { + val newSelected = it.map { type -> type as DeparturesFilterType } + selectedDeparturesType = if ( + selectedDeparturesType.size == 1 && + selectedDeparturesType.first() == DeparturesFilterType.All + ) { + (newSelected - DeparturesFilterType.All).toPersistentList() + } else if (DeparturesFilterType.All in newSelected) { + persistentListOf(DeparturesFilterType.All) + } else { + newSelected.toPersistentList() + } + }, + onReset = { + selectedSortType = SortType.LatestDesc + selectedStatusesType = StatusesType.All + selectedDeparturesType = persistentListOf(DeparturesFilterType.All) + selectedArrivalsType = persistentListOf(ArrivalsFilterType.All) + }, + onApplyClick = { + onApply( + selectedSortType, + selectedStatusesType, + selectedDeparturesType, + selectedArrivalsType + ) + onDismissRequest() } - }, - onReset = { - selectedSortType = SortType.LatestDesc - selectedStatusesType = StatusesType.All - selectedDeparturesType = persistentListOf(DeparturesFilterType.All) - selectedArrivalsType = persistentListOf(ArrivalsFilterType.All) - }, - onApplyClick = { - onApply( - selectedSortType, - selectedStatusesType, - selectedDeparturesType, - selectedArrivalsType - ) - onDismissRequest() - } + ) ) } } @Composable fun FilterBottomSheetContent( - selectedSortType: SortType, - selectedStatusesType: StatusesType, - selectedDeparturesType: ImmutableList, - selectedArrivalsType: ImmutableList, - onSortTypeChange: (CallvanFilterType) -> Unit, - onStatusesTypeChange: (CallvanFilterType) -> Unit, - onDeparturesTypeChange: (ImmutableList) -> Unit, - onArrivalsTypeChange: (ImmutableList) -> Unit, - onReset: () -> Unit, - onApplyClick: () -> Unit + state: FilterBottomSheetState, + actions: FilterBottomSheetActions ) { Column( modifier = Modifier @@ -151,8 +149,8 @@ fun FilterBottomSheetContent( SortType.DepartureDesc, SortType.DepartureAsc ), - selectedItem = selectedSortType, - onItemSelected = onSortTypeChange + selectedItem = state.selectedSortType, + onItemSelected = actions.onSortTypeChange ) HorizontalDivider(color = KoinTheme.colors.neutral300) FilterSection( @@ -163,8 +161,8 @@ fun FilterBottomSheetContent( StatusesType.Closed, StatusesType.Completed ), - selectedItem = selectedStatusesType, - onItemSelected = onStatusesTypeChange + selectedItem = state.selectedStatusesType, + onItemSelected = actions.onStatusesTypeChange ) HorizontalDivider(color = KoinTheme.colors.neutral300) FilterDuplicateSection( @@ -176,8 +174,8 @@ fun FilterBottomSheetContent( DeparturesFilterType.Terminal, DeparturesFilterType.Station, DeparturesFilterType.AsanStation ), - selectedItems = selectedDeparturesType, - onItemSelected = onDeparturesTypeChange + selectedItems = state.selectedDeparturesType, + onItemSelected = actions.onDeparturesTypeChange ) HorizontalDivider(color = KoinTheme.colors.neutral300) FilterDuplicateSection( @@ -189,8 +187,8 @@ fun FilterBottomSheetContent( ArrivalsFilterType.Terminal, ArrivalsFilterType.Station, ArrivalsFilterType.AsanStation ), - selectedItems = selectedArrivalsType, - onItemSelected = onArrivalsTypeChange + selectedItems = state.selectedArrivalsType, + onItemSelected = actions.onArrivalsTypeChange ) HorizontalDivider(color = KoinTheme.colors.neutral300) } @@ -202,7 +200,7 @@ fun FilterBottomSheetContent( horizontalArrangement = Arrangement.spacedBy(10.dp) ) { OutlinedButton( - onClick = onReset, + onClick = actions.onReset, shape = KoinTheme.shapes.medium, border = BorderStroke(1.dp, KoinTheme.colors.neutral300), modifier = Modifier.weight(1f).height(48.dp), @@ -222,7 +220,7 @@ fun FilterBottomSheetContent( ) } Button( - onClick = onApplyClick, + onClick = actions.onApplyClick, shape = KoinTheme.shapes.medium, colors = ButtonDefaults.buttonColors(containerColor = RebrandKoinTheme.colors.primary500), modifier = Modifier.weight(2f).height(48.dp) @@ -322,16 +320,20 @@ fun FilterDuplicateSection( private fun FilterBottomSheetContentPreview() { RebrandKoinTheme { FilterBottomSheetContent( - selectedSortType = SortType.LatestDesc, - selectedStatusesType = StatusesType.All, - selectedDeparturesType = persistentListOf(DeparturesFilterType.All), - selectedArrivalsType = persistentListOf(ArrivalsFilterType.All), - onSortTypeChange = {}, - onStatusesTypeChange = {}, - onDeparturesTypeChange = {}, - onArrivalsTypeChange = {}, - onReset = {}, - onApplyClick = {} + state = FilterBottomSheetState( + selectedSortType = SortType.LatestDesc, + selectedStatusesType = StatusesType.All, + selectedDeparturesType = persistentListOf(DeparturesFilterType.All), + selectedArrivalsType = persistentListOf(ArrivalsFilterType.All) + ), + actions = FilterBottomSheetActions( + onSortTypeChange = {}, + onStatusesTypeChange = {}, + onDeparturesTypeChange = {}, + onArrivalsTypeChange = {}, + onReset = {}, + onApplyClick = {} + ) ) } } From d1f0335958be1670b7e9f296f6752cc533cf09b5 Mon Sep 17 00:00:00 2001 From: TTRR1007 Date: Sat, 14 Mar 2026 22:16:37 +0900 Subject: [PATCH 04/11] chore: kt lint --- .../koin/feature/callvan/ui/list/FilterBottomSheetActions.kt | 2 +- .../koin/feature/callvan/ui/list/FilterBottomSheetState.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt index 187800e26d..ce0eda6569 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt @@ -10,4 +10,4 @@ data class FilterBottomSheetActions( val onArrivalsTypeChange: (ImmutableList) -> Unit, val onReset: () -> Unit, val onApplyClick: () -> Unit -) \ No newline at end of file +) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt index 8baeeda2cd..8a5b91d807 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt @@ -11,4 +11,4 @@ data class FilterBottomSheetState( val selectedStatusesType: StatusesType, val selectedDeparturesType: ImmutableList, val selectedArrivalsType: ImmutableList -) \ No newline at end of file +) From 683a5bfae173c796d587b28193b94f73fc2524ab Mon Sep 17 00:00:00 2001 From: TTRR1007 Date: Sat, 14 Mar 2026 23:49:17 +0900 Subject: [PATCH 05/11] refactor: add @Immutable and default values to FilterBottomSheetState --- .../feature/callvan/ui/list/FilterBottomSheetState.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt index 8a5b91d807..1ad3ddc865 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt @@ -1,14 +1,17 @@ package `in`.koreatech.koin.feature.callvan.ui.list +import androidx.compose.runtime.Immutable import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.ArrivalsFilterType import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.DeparturesFilterType import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.SortType import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.StatusesType import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +@Immutable data class FilterBottomSheetState( - val selectedSortType: SortType, - val selectedStatusesType: StatusesType, - val selectedDeparturesType: ImmutableList, - val selectedArrivalsType: ImmutableList + val selectedSortType: SortType = SortType.LatestDesc, + val selectedStatusesType: StatusesType = StatusesType.All, + val selectedDeparturesType: ImmutableList = persistentListOf(DeparturesFilterType.All), + val selectedArrivalsType: ImmutableList = persistentListOf(ArrivalsFilterType.All) ) From 416c11843f3488404ddbda1fb04f56ea4f98fdcc Mon Sep 17 00:00:00 2001 From: TTRR1007 Date: Sun, 15 Mar 2026 21:41:47 +0900 Subject: [PATCH 06/11] fix: fix filter state and compile errors in FilterBottomSheet --- .../ui/list/FilterBottomSheetActions.kt | 8 +- .../ui/list/component/FilterBottomSheet.kt | 102 +++++++----------- 2 files changed, 42 insertions(+), 68 deletions(-) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt index ce0eda6569..02922bf4b3 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt @@ -4,10 +4,10 @@ import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType import kotlinx.collections.immutable.ImmutableList data class FilterBottomSheetActions( - val onSortTypeChange: (CallvanFilterType) -> Unit, - val onStatusesTypeChange: (CallvanFilterType) -> Unit, - val onDeparturesTypeChange: (ImmutableList) -> Unit, - val onArrivalsTypeChange: (ImmutableList) -> Unit, + val onSortTypeChange: (CallvanFilterType.SortType) -> Unit, + val onStatusesTypeChange: (CallvanFilterType.StatusesType) -> Unit, + val onDeparturesTypeChange: (ImmutableList) -> Unit, + val onArrivalsTypeChange: (ImmutableList) -> Unit, val onReset: () -> Unit, val onApplyClick: () -> Unit ) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt index bbb1364498..bae35f1a77 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt @@ -43,7 +43,6 @@ import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.StatusesType import `in`.koreatech.koin.feature.callvan.ui.component.CallvanBottomSheet import `in`.koreatech.koin.feature.callvan.ui.list.FilterBottomSheetActions import `in`.koreatech.koin.feature.callvan.ui.list.FilterBottomSheetState -import kotlin.collections.map import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -58,10 +57,10 @@ fun FilterBottomSheet( selectedDeparturesType: ImmutableList, onApply: (SortType, StatusesType, ImmutableList, ImmutableList) -> Unit ) { - var selectedSortType by remember { mutableStateOf(selectedSortType) } - var selectedStatusesType by remember { mutableStateOf(selectedStatusesType) } - var selectedArrivalsType by remember { mutableStateOf(selectedArrivalsType) } - var selectedDeparturesType by remember { mutableStateOf(selectedDeparturesType) } + var currentSortType by remember { mutableStateOf(selectedSortType) } + var currentStatusesType by remember { mutableStateOf(selectedStatusesType) } + var currentArrivalsType by remember { mutableStateOf(selectedArrivalsType) } + var currentDeparturesType by remember { mutableStateOf(selectedDeparturesType) } CallvanBottomSheet( title = stringResource(R.string.filter_container), @@ -70,52 +69,28 @@ fun FilterBottomSheet( ) { FilterBottomSheetContent( state = FilterBottomSheetState( - selectedSortType = selectedSortType, - selectedStatusesType = selectedStatusesType, - selectedDeparturesType = selectedDeparturesType, - selectedArrivalsType = selectedArrivalsType + selectedSortType = currentSortType, + selectedStatusesType = currentStatusesType, + selectedDeparturesType = currentDeparturesType, + selectedArrivalsType = currentArrivalsType ), actions = FilterBottomSheetActions( - onSortTypeChange = { selectedSortType = it as SortType }, - onStatusesTypeChange = { selectedStatusesType = it as StatusesType }, - onArrivalsTypeChange = { - val newSelected = it.map { type -> type as ArrivalsFilterType } - selectedArrivalsType = if ( - selectedArrivalsType.size == 1 && - selectedArrivalsType.first() == ArrivalsFilterType.All - ) { - (newSelected - ArrivalsFilterType.All).toPersistentList() - } else if (ArrivalsFilterType.All in newSelected) { - persistentListOf(ArrivalsFilterType.All) - } else { - newSelected.toPersistentList() - } - }, - onDeparturesTypeChange = { - val newSelected = it.map { type -> type as DeparturesFilterType } - selectedDeparturesType = if ( - selectedDeparturesType.size == 1 && - selectedDeparturesType.first() == DeparturesFilterType.All - ) { - (newSelected - DeparturesFilterType.All).toPersistentList() - } else if (DeparturesFilterType.All in newSelected) { - persistentListOf(DeparturesFilterType.All) - } else { - newSelected.toPersistentList() - } - }, + onSortTypeChange = { currentSortType = it }, + onStatusesTypeChange = { currentStatusesType = it }, + onArrivalsTypeChange = { currentArrivalsType = it }, + onDeparturesTypeChange = { currentDeparturesType = it }, onReset = { - selectedSortType = SortType.LatestDesc - selectedStatusesType = StatusesType.All - selectedDeparturesType = persistentListOf(DeparturesFilterType.All) - selectedArrivalsType = persistentListOf(ArrivalsFilterType.All) + currentSortType = SortType.LatestDesc + currentStatusesType = StatusesType.All + currentDeparturesType = persistentListOf(DeparturesFilterType.All) + currentArrivalsType = persistentListOf(ArrivalsFilterType.All) }, onApplyClick = { onApply( - selectedSortType, - selectedStatusesType, - selectedDeparturesType, - selectedArrivalsType + currentSortType, + currentStatusesType, + currentDeparturesType, + currentArrivalsType ) onDismissRequest() } @@ -175,6 +150,7 @@ fun FilterBottomSheetContent( DeparturesFilterType.AsanStation ), selectedItems = state.selectedDeparturesType, + allItem = DeparturesFilterType.All, onItemSelected = actions.onDeparturesTypeChange ) HorizontalDivider(color = KoinTheme.colors.neutral300) @@ -188,6 +164,7 @@ fun FilterBottomSheetContent( ArrivalsFilterType.AsanStation ), selectedItems = state.selectedArrivalsType, + allItem = ArrivalsFilterType.All, onItemSelected = actions.onArrivalsTypeChange ) HorizontalDivider(color = KoinTheme.colors.neutral300) @@ -236,11 +213,11 @@ fun FilterBottomSheetContent( } @Composable -fun FilterSection( +fun FilterSection( title: String, - items: ImmutableList, - selectedItem: CallvanFilterType, - onItemSelected: (CallvanFilterType) -> Unit + items: ImmutableList, + selectedItem: T, + onItemSelected: (T) -> Unit ) { Column(modifier = Modifier.padding(vertical = 12.dp)) { Text( @@ -263,14 +240,14 @@ fun FilterSection( } } } -private const val AT_LEAST_COUNT = 1 @Composable -fun FilterDuplicateSection( +fun FilterDuplicateSection( title: String, - items: ImmutableList, - selectedItems: ImmutableList, - onItemSelected: (ImmutableList) -> Unit + items: ImmutableList, + selectedItems: ImmutableList, + allItem: T, + onItemSelected: (ImmutableList) -> Unit ) { Column(modifier = Modifier.padding(vertical = 12.dp)) { Row( @@ -297,17 +274,14 @@ fun FilterDuplicateSection( text = stringResource(item.stringRes), isSelected = item in selectedItems, onClick = { - onItemSelected( - if (item in selectedItems) { - if (selectedItems.size > AT_LEAST_COUNT) { - (selectedItems - item).toPersistentList() - } else { - return@FilterBottomSheetItem - } - } else { - (selectedItems + item).toPersistentList() + val newSelection = when (item) { + in selectedItems -> { + val removed = selectedItems.filter { it != item }.toPersistentList() + removed.ifEmpty { persistentListOf(allItem) } } - ) + else -> (selectedItems.filter { it != allItem } + item).toPersistentList() + } + onItemSelected(newSelection) } ) } From 9434dd7b1aba535522f3df62e6e235a45f8e1cb9 Mon Sep 17 00:00:00 2001 From: TTRR1007 Date: Sun, 15 Mar 2026 22:27:03 +0900 Subject: [PATCH 07/11] refactor: add modifier parameter to FilterSection and FilterDuplicateSection --- .../feature/callvan/ui/list/component/FilterBottomSheet.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt index bae35f1a77..8c16da4e0a 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/FilterBottomSheet.kt @@ -217,9 +217,10 @@ fun FilterSection( title: String, items: ImmutableList, selectedItem: T, + modifier: Modifier = Modifier, onItemSelected: (T) -> Unit ) { - Column(modifier = Modifier.padding(vertical = 12.dp)) { + Column(modifier = modifier.padding(vertical = 12.dp)) { Text( text = title, style = KoinTheme.typography.bold16, @@ -247,9 +248,10 @@ fun FilterDuplicateSection( items: ImmutableList, selectedItems: ImmutableList, allItem: T, + modifier: Modifier = Modifier, onItemSelected: (ImmutableList) -> Unit ) { - Column(modifier = Modifier.padding(vertical = 12.dp)) { + Column(modifier = modifier.padding(vertical = 12.dp)) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -275,6 +277,7 @@ fun FilterDuplicateSection( isSelected = item in selectedItems, onClick = { val newSelection = when (item) { + allItem -> persistentListOf(allItem) in selectedItems -> { val removed = selectedItems.filter { it != item }.toPersistentList() removed.ifEmpty { persistentListOf(allItem) } From 49420b73181d2cb2e89c574fd34a4a8bb2762269 Mon Sep 17 00:00:00 2001 From: TTRR1007 Date: Sun, 15 Mar 2026 23:16:57 +0900 Subject: [PATCH 08/11] refactor: add @Stable annotation --- .../koin/feature/callvan/ui/list/FilterBottomSheetActions.kt | 4 +++- .../koin/feature/callvan/ui/list/FilterBottomSheetState.kt | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt index 02922bf4b3..9fa972f48c 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt @@ -1,8 +1,10 @@ package `in`.koreatech.koin.feature.callvan.ui.list -import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType +import androidx.compose.runtime.Stable import kotlinx.collections.immutable.ImmutableList +import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType +@Stable data class FilterBottomSheetActions( val onSortTypeChange: (CallvanFilterType.SortType) -> Unit, val onStatusesTypeChange: (CallvanFilterType.StatusesType) -> Unit, diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt index 1ad3ddc865..bd03350eb2 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt @@ -1,12 +1,12 @@ package `in`.koreatech.koin.feature.callvan.ui.list import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.ArrivalsFilterType import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.DeparturesFilterType import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.SortType import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.StatusesType -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf @Immutable data class FilterBottomSheetState( From c9dbea3a246bf85545cf29ef7b653446c35f00aa Mon Sep 17 00:00:00 2001 From: TTRR1007 Date: Sun, 15 Mar 2026 23:45:08 +0900 Subject: [PATCH 09/11] chore: kt lint --- .../koin/feature/callvan/ui/list/FilterBottomSheetActions.kt | 2 +- .../koin/feature/callvan/ui/list/FilterBottomSheetState.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt index 9fa972f48c..0c718bd1d6 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetActions.kt @@ -1,8 +1,8 @@ package `in`.koreatech.koin.feature.callvan.ui.list import androidx.compose.runtime.Stable -import kotlinx.collections.immutable.ImmutableList import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType +import kotlinx.collections.immutable.ImmutableList @Stable data class FilterBottomSheetActions( diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt index bd03350eb2..1ad3ddc865 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/FilterBottomSheetState.kt @@ -1,12 +1,12 @@ package `in`.koreatech.koin.feature.callvan.ui.list import androidx.compose.runtime.Immutable -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.ArrivalsFilterType import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.DeparturesFilterType import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.SortType import `in`.koreatech.koin.feature.callvan.enums.CallvanFilterType.StatusesType +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Immutable data class FilterBottomSheetState( From f50fb8d8ba42a4807f569fd3e63b747783dad444 Mon Sep 17 00:00:00 2001 From: TTRR1007 Date: Tue, 17 Mar 2026 01:47:28 +0900 Subject: [PATCH 10/11] refactor: simplify maxLength clamping logic using take() --- .../callvan/ui/list/component/ItemSearchTextField.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt index b78a151040..1bcf753942 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt @@ -45,13 +45,7 @@ fun ItemSearchTextField( value = value, textStyle = textStyle, singleLine = true, - onValueChange = { - if (it.length < maxLength) { - onValueChange(it) - } else { - onValueChange(it.take(maxLength)) - } - }, + onValueChange = { onValueChange(it.take(maxLength)) }, decorationBox = { innerTextField -> Box { if (value.isEmpty()) { From 085b34cbf4feb2b42a5a4a5e1b93298b842cad20 Mon Sep 17 00:00:00 2001 From: TTRR1007 Date: Tue, 17 Mar 2026 10:04:09 +0900 Subject: [PATCH 11/11] fix: prevent IllegalArgumentException for negative maxLength --- .../feature/callvan/ui/list/component/ItemSearchTextField.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt index 1bcf753942..5aecf7c8ce 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/list/component/ItemSearchTextField.kt @@ -30,6 +30,7 @@ fun ItemSearchTextField( textStyle: TextStyle = KoinTheme.typography.regular14.copy(color = KoinTheme.colors.neutral600), onValueChange: (String) -> Unit = {} ) { + val safeMaxLength = maxLength.coerceAtLeast(0) Row( modifier = modifier .fillMaxWidth() @@ -45,7 +46,7 @@ fun ItemSearchTextField( value = value, textStyle = textStyle, singleLine = true, - onValueChange = { onValueChange(it.take(maxLength)) }, + onValueChange = { onValueChange(it.take(safeMaxLength)) }, decorationBox = { innerTextField -> Box { if (value.isEmpty()) {