diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/dialog/ChoiceDialog.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/dialog/ChoiceDialog.kt index 4d2f97fac..136d7cd68 100644 --- a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/dialog/ChoiceDialog.kt +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/dialog/ChoiceDialog.kt @@ -1,4 +1,4 @@ -package `in`.koreatech.koin.core.designsystem.component +package `in`.koreatech.koin.core.designsystem.component.dialog import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/domain/src/main/java/in/koreatech/koin/domain/error/busv2/ErrorType.kt b/domain/src/main/java/in/koreatech/koin/domain/error/busv2/ErrorType.kt new file mode 100644 index 000000000..6d260a619 --- /dev/null +++ b/domain/src/main/java/in/koreatech/koin/domain/error/busv2/ErrorType.kt @@ -0,0 +1,6 @@ +package `in`.koreatech.koin.domain.error.busv2 + +sealed class SearchBusError: Throwable() { + class EmptyDeparture: SearchBusError() + class EmptyArrival: SearchBusError() +} \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/usecase/busv2/SearchBusV2UseCase.kt b/domain/src/main/java/in/koreatech/koin/domain/usecase/busv2/SearchBusV2UseCase.kt new file mode 100644 index 000000000..af90e655e --- /dev/null +++ b/domain/src/main/java/in/koreatech/koin/domain/usecase/busv2/SearchBusV2UseCase.kt @@ -0,0 +1,21 @@ +package `in`.koreatech.koin.domain.usecase.busv2 + +import `in`.koreatech.koin.domain.error.busv2.SearchBusError +import javax.inject.Inject + +class SearchBusV2UseCase @Inject constructor( + // private val busRepository +) { + + suspend operator fun invoke(departure: String, arrival: String): Result { + return when { + departure.isEmpty() -> Result.failure(SearchBusError.EmptyDeparture()) + arrival.isEmpty() -> Result.failure(SearchBusError.EmptyArrival()) + else -> { + // TODO repository Search bus + // busRepository.searchBus(departure, arrival) + Result.success(Unit) + } + } + } +} \ No newline at end of file diff --git a/feature/bus/build.gradle.kts b/feature/bus/build.gradle.kts index 1ad02fcdf..cc5e5ec37 100644 --- a/feature/bus/build.gradle.kts +++ b/feature/bus/build.gradle.kts @@ -37,4 +37,6 @@ dependencies { implementation("androidx.navigation:navigation-compose:2.8.3") implementation(libs.kotlinx.serialization.json) + + implementation(libs.androidx.constraintlayout.compose) } \ No newline at end of file diff --git a/feature/bus/src/main/java/in/koreatech/bus/navigation/BusNavigation.kt b/feature/bus/src/main/java/in/koreatech/bus/navigation/BusNavigation.kt index 15f7eeea2..6e59f2fe9 100644 --- a/feature/bus/src/main/java/in/koreatech/bus/navigation/BusNavigation.kt +++ b/feature/bus/src/main/java/in/koreatech/bus/navigation/BusNavigation.kt @@ -9,6 +9,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import `in`.koreatech.bus.screen.search.composable.BusSearchScreen import `in`.koreatech.bus.screen.timetable.composable.BusTimetableScreen @Composable @@ -34,5 +35,15 @@ fun BusNavigation( onNavigationIconClick = { navController.popBackStack() } ) } + + composable { + BusSearchScreen( + modifier = Modifier.fillMaxSize(), + onNavigationIconClick = { navController.popBackStack() }, + onSearchSuccess = { + // navController.navigate(Routes.SearchedTimetable(it)) + } + ) + } } } \ No newline at end of file diff --git a/feature/bus/src/main/java/in/koreatech/bus/navigation/Routes.kt b/feature/bus/src/main/java/in/koreatech/bus/navigation/Routes.kt index 9b4fc63f5..f53194a1b 100644 --- a/feature/bus/src/main/java/in/koreatech/bus/navigation/Routes.kt +++ b/feature/bus/src/main/java/in/koreatech/bus/navigation/Routes.kt @@ -5,4 +5,5 @@ import kotlinx.serialization.Serializable internal object Routes { @Serializable data object BusTimetable + @Serializable data object BusSearch } \ No newline at end of file diff --git a/feature/bus/src/main/java/in/koreatech/bus/screen/search/composable/BusSearchContentView.kt b/feature/bus/src/main/java/in/koreatech/bus/screen/search/composable/BusSearchContentView.kt new file mode 100644 index 000000000..05aeafd3c --- /dev/null +++ b/feature/bus/src/main/java/in/koreatech/bus/screen/search/composable/BusSearchContentView.kt @@ -0,0 +1,199 @@ +package `in`.koreatech.bus.screen.search.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +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.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import `in`.koreatech.koin.core.designsystem.component.button.FilledButton +import `in`.koreatech.koin.core.designsystem.component.tab.KoinSurface +import `in`.koreatech.koin.core.designsystem.noRippleClickable +import `in`.koreatech.koin.core.designsystem.theme.KoinTheme +import `in`.koreatech.koin.feature.bus.R + +@Composable +internal fun BusSearchContentView( + departure: String, + arrival: String, + modifier: Modifier = Modifier, + searchButtonEnabled: Boolean = false, + onSwapIconClicked: () -> Unit = {}, + onSearchClicked: () -> Unit = {}, + onDepartureFieldClicked: () -> Unit = {}, + onArrivalFieldClicked: () -> Unit = {} +) { + + KoinSurface { + Column(modifier = modifier) { + Text( + modifier = Modifier, + text = stringResource(R.string.introduce_bus_search), + style = KoinTheme.typography.medium16, + color = KoinTheme.colors.neutral800 + ) + + Text( + modifier = Modifier.padding(top = 2.dp), + text = stringResource(R.string.caution_possibly_inaccurate), + style = KoinTheme.typography.regular12, + color = KoinTheme.colors.neutral600 + ) + + ConstraintLayout( + modifier = Modifier + .fillMaxWidth() + .padding(top = 46.dp) + .height(IntrinsicSize.Min) + ) { + val (departureText, arrivalText, departureField, arrivalField, iconSwap) = createRefs() + + Text( + modifier = Modifier.constrainAs(departureText) { + start.linkTo(departureField.start) + top.linkTo(parent.top) + end.linkTo(departureField.end) + }, + text = stringResource(R.string.departure), + style = KoinTheme.typography.medium16, + color = KoinTheme.colors.primary500 + ) + Text( + modifier = Modifier.constrainAs(arrivalText) { + top.linkTo(parent.top) + start.linkTo(arrivalField.start) + end.linkTo(arrivalField.end) + }, + text = stringResource(R.string.arrival), + style = KoinTheme.typography.medium16, + color = KoinTheme.colors.primary500 + ) + + BusSearchInput( + place = departure, + placeholder = stringResource(R.string.select_departure), + modifier = Modifier + .padding(top = 10.dp) + .noRippleClickable { + onDepartureFieldClicked() + } + .constrainAs(departureField) { + top.linkTo(iconSwap.top) + bottom.linkTo(iconSwap.bottom) + start.linkTo(parent.start) + end.linkTo(iconSwap.start) + + width = Dimension.fillToConstraints + height = Dimension.preferredWrapContent + } + ) + + IconButton( + onClick = onSwapIconClicked, + modifier = Modifier + .padding(top = 10.dp) + .padding(horizontal = 16.dp, vertical = 12.dp) + .size(32.dp) + .constrainAs(iconSwap) { + top.linkTo(departureText.bottom) + start.linkTo(departureField.end) + end.linkTo(arrivalField.start) + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_swap), // TODO : 아이콘 깨짐 + contentDescription = stringResource(R.string.swap_content_description), + ) + } + + BusSearchInput( + place = arrival, + placeholder = stringResource(R.string.select_arrival), + modifier = Modifier + .padding(top = 10.dp) + .noRippleClickable { + onArrivalFieldClicked() + } + .constrainAs(arrivalField) { + top.linkTo(iconSwap.top) + bottom.linkTo(iconSwap.bottom) + start.linkTo(iconSwap.end) + end.linkTo(parent.end) + + width = Dimension.fillToConstraints + height = Dimension.preferredWrapContent + } + ) + } + + Spacer(modifier = Modifier.weight(1f)) + FilledButton( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 30.dp), + enabled = searchButtonEnabled, + text = stringResource(R.string.search), + contentPadding = PaddingValues(vertical = 12.dp), + onClick = onSearchClicked + ) + } + } +} + +@Composable +private fun BusSearchInput( + place: String, + placeholder: String, + modifier: Modifier = Modifier, +) { + + val isPlaceDetermined = place.isNotEmpty() + + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .background(if (isPlaceDetermined.not()) KoinTheme.colors.neutral100 else Color.Transparent) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (isPlaceDetermined.not()) + Text( + text = placeholder, + maxLines = 1, + style = KoinTheme.typography.regular14, + color = KoinTheme.colors.neutral400 // TODO neutral450 ? + ) + else { + Text( + text = place, + maxLines = 1, + style = KoinTheme.typography.bold18, + color = KoinTheme.colors.neutral800 + ) + } + } + } +} \ No newline at end of file diff --git a/feature/bus/src/main/java/in/koreatech/bus/screen/search/composable/BusSearchScreen.kt b/feature/bus/src/main/java/in/koreatech/bus/screen/search/composable/BusSearchScreen.kt new file mode 100644 index 000000000..55af22c5f --- /dev/null +++ b/feature/bus/src/main/java/in/koreatech/bus/screen/search/composable/BusSearchScreen.kt @@ -0,0 +1,175 @@ +package `in`.koreatech.bus.screen.search.composable + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import `in`.koreatech.bus.screen.search.type.PlaceSelectMode +import `in`.koreatech.bus.screen.search.viewmodel.BusSearchViewModel +import `in`.koreatech.bus.screen.search.viewmodel.SearchBusUiState +import `in`.koreatech.koin.core.designsystem.component.topbar.KoinTopAppBar +import `in`.koreatech.koin.feature.bus.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BusSearchScreen( + modifier: Modifier = Modifier, + onNavigationIconClick: () -> Unit = {}, + onSearchSuccess: (Unit) -> Unit = {}, + viewModel: BusSearchViewModel = hiltViewModel() +) { + + val context = LocalContext.current + + val departure by viewModel.departure.collectAsStateWithLifecycle() + val arrival by viewModel.arrival.collectAsStateWithLifecycle() + + val searchButtonEnabled by remember { derivedStateOf { departure.isNotEmpty() && arrival.isNotEmpty() } } + + var placeSelectMode by remember { mutableStateOf(PlaceSelectMode.NONE) } + + Column( + modifier = modifier + ) { + KoinTopAppBar( + title = stringResource(R.string.title_bus_search), + onNavigationIconClick = onNavigationIconClick + ) + + BusSearchContentView( + modifier = Modifier + .fillMaxSize() + .padding(top = 16.dp) + .padding(horizontal = 24.dp), + departure = departure, + arrival = arrival, + searchButtonEnabled = searchButtonEnabled, + onSwapIconClicked = viewModel::swapDepartureAndArrival, + onSearchClicked = viewModel::search, + onDepartureFieldClicked = { placeSelectMode = PlaceSelectMode.DEPARTURE }, + onArrivalFieldClicked = { placeSelectMode = PlaceSelectMode.ARRIVAL } + ) + } + + if (placeSelectMode != PlaceSelectMode.NONE) { + SelectPlaceBottomSheet( + onDismissRequest = { placeSelectMode = PlaceSelectMode.NONE }, + selectMode = placeSelectMode, + onConfirmSelection = { + when (placeSelectMode) { + PlaceSelectMode.DEPARTURE -> { + placeSelectMode = PlaceSelectMode.ARRIVAL + viewModel.setDeparture(context.getString(it.titleRes)) + } + + PlaceSelectMode.ARRIVAL -> { + placeSelectMode = PlaceSelectMode.NONE + viewModel.setArrival(context.getString(it.titleRes)) + } + + PlaceSelectMode.NONE -> Unit + } + }, + modifier = Modifier, + ) + } + + + LaunchedEffect(Unit) { + viewModel.searchBusUiState.collect { + when(it) { + is SearchBusUiState.Loading -> Unit + is SearchBusUiState.EmptyDeparture -> Unit + is SearchBusUiState.EmptyArrival -> Unit + is SearchBusUiState.Success -> onSearchSuccess(it.data) // TODO : data 타입 + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +@Composable +private fun BusSearchScreenPreview() { + Column( + modifier = Modifier.fillMaxWidth() + ) { + KoinTopAppBar( + title = stringResource(R.string.title_bus_search), + onNavigationIconClick = { } + ) + + BusSearchContentView( + departure = "", + arrival = "", + modifier = Modifier + .fillMaxSize() + .padding(top = 16.dp) + .padding(horizontal = 24.dp), + onSwapIconClicked = { } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +@Composable +private fun BusSearchScreen2Preview() { + Column( + modifier = Modifier.fillMaxWidth() + ) { + KoinTopAppBar( + title = stringResource(R.string.title_bus_search), + onNavigationIconClick = { } + ) + + BusSearchContentView( + departure = "코리아텍", + arrival = "", + modifier = Modifier + .fillMaxSize() + .padding(top = 16.dp) + .padding(horizontal = 24.dp), + onSwapIconClicked = { } + ) + } +} +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +@Composable +private fun BusSearchScreen3Preview() { + Column( + modifier = Modifier.fillMaxWidth() + ) { + KoinTopAppBar( + title = stringResource(R.string.title_bus_search), + onNavigationIconClick = { } + ) + + BusSearchContentView( + departure = "코리아텍", + arrival = "천안역", + modifier = Modifier + .fillMaxSize() + .padding(top = 16.dp) + .padding(horizontal = 24.dp), + onSwapIconClicked = { }, + searchButtonEnabled = true + ) + } +} \ No newline at end of file diff --git a/feature/bus/src/main/java/in/koreatech/bus/screen/search/composable/SelectPlaceBottomSheet.kt b/feature/bus/src/main/java/in/koreatech/bus/screen/search/composable/SelectPlaceBottomSheet.kt new file mode 100644 index 000000000..cdef4448c --- /dev/null +++ b/feature/bus/src/main/java/in/koreatech/bus/screen/search/composable/SelectPlaceBottomSheet.kt @@ -0,0 +1,108 @@ +package `in`.koreatech.bus.screen.search.composable + +import androidx.compose.foundation.layout.PaddingValues +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.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ModalBottomSheet +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.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import `in`.koreatech.bus.screen.search.type.PlaceSelectMode +import `in`.koreatech.bus.screen.search.type.PlaceType +import `in`.koreatech.koin.core.designsystem.component.button.FilledButton +import `in`.koreatech.koin.core.designsystem.component.chip.TextChipGroup +import `in`.koreatech.koin.core.designsystem.theme.KoinTheme +import `in`.koreatech.koin.feature.bus.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SelectPlaceBottomSheet( + onDismissRequest: () -> Unit, + selectMode: PlaceSelectMode, + onConfirmSelection: (selectedPlace: PlaceType) -> Unit, + modifier: Modifier = Modifier, +) { + require(selectMode != PlaceSelectMode.NONE) { + "SelectPlaceBottomSheet should not be used with PlaceSelectMode.NONE" + } + + val context = LocalContext.current + + var selectedPlace by remember { mutableStateOf(PlaceType.KOREATECH) } + val sheetTitle = when (selectMode) { + PlaceSelectMode.DEPARTURE -> stringResource(R.string.question_departure) + PlaceSelectMode.ARRIVAL -> stringResource(R.string.question_arrival) + PlaceSelectMode.NONE -> "" + } + val buttonText = when (selectMode) { + PlaceSelectMode.DEPARTURE -> stringResource(R.string.action_select_arrival) + PlaceSelectMode.ARRIVAL -> stringResource(R.string.confirm_selection) + PlaceSelectMode.NONE -> "" + } + + ModalBottomSheet( + onDismissRequest = onDismissRequest, + modifier = modifier, + containerColor = Color.White, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + ) { + + Text( + text = sheetTitle, + style = KoinTheme.typography.medium18, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(horizontal = 32.dp).padding(bottom = 12.dp) + ) + + HorizontalDivider( + color = KoinTheme.colors.neutral200 + ) + + TextChipGroup( + modifier = Modifier.padding(horizontal = 32.dp, vertical = 16.dp), + titles = PlaceType.entries.map { context.getString(it.titleRes) }, + onChipSelected = { + selectedPlace = PlaceType.entries.find { type -> + context.getString(type.titleRes) == it + } ?: PlaceType.KOREATECH + }, + selectedChipIndexes = intArrayOf(selectedPlace.ordinal), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + showClickRipple = false + ) + + Spacer(modifier = Modifier.height(140.dp)) + FilledButton( + modifier = Modifier.fillMaxWidth().padding(horizontal = 32.dp).padding(bottom = 36.dp), + text = buttonText, + onClick = { onConfirmSelection(selectedPlace) }, + contentPadding = PaddingValues(vertical = 12.dp) + ) + } +} + +@Preview +@Composable +private fun SelectPlaceBottomSheetPreview() { + SelectPlaceBottomSheet( + onDismissRequest = {}, + selectMode = PlaceSelectMode.DEPARTURE, + onConfirmSelection = {} + ) +} \ No newline at end of file diff --git a/feature/bus/src/main/java/in/koreatech/bus/screen/search/type/PlaceSelectMode.kt b/feature/bus/src/main/java/in/koreatech/bus/screen/search/type/PlaceSelectMode.kt new file mode 100644 index 000000000..84865ecdc --- /dev/null +++ b/feature/bus/src/main/java/in/koreatech/bus/screen/search/type/PlaceSelectMode.kt @@ -0,0 +1,7 @@ +package `in`.koreatech.bus.screen.search.type + +enum class PlaceSelectMode { + DEPARTURE, + ARRIVAL, + NONE +} diff --git a/feature/bus/src/main/java/in/koreatech/bus/screen/search/type/PlaceType.kt b/feature/bus/src/main/java/in/koreatech/bus/screen/search/type/PlaceType.kt new file mode 100644 index 000000000..6a3c2335e --- /dev/null +++ b/feature/bus/src/main/java/in/koreatech/bus/screen/search/type/PlaceType.kt @@ -0,0 +1,12 @@ +package `in`.koreatech.bus.screen.search.type + +import androidx.annotation.StringRes +import `in`.koreatech.koin.feature.bus.R + +internal enum class PlaceType( + @StringRes val titleRes: Int +) { + KOREATECH(R.string.koreatech), + CHEONAN_STATION(R.string.cheonan_station), + CHEONAN_TERMINAL(R.string.cheonan_terminal), +} \ No newline at end of file diff --git a/feature/bus/src/main/java/in/koreatech/bus/screen/search/viewmodel/BusSearchViewModel.kt b/feature/bus/src/main/java/in/koreatech/bus/screen/search/viewmodel/BusSearchViewModel.kt new file mode 100644 index 000000000..bd25ffb26 --- /dev/null +++ b/feature/bus/src/main/java/in/koreatech/bus/screen/search/viewmodel/BusSearchViewModel.kt @@ -0,0 +1,65 @@ +package `in`.koreatech.bus.screen.search.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import `in`.koreatech.koin.domain.error.busv2.SearchBusError +import `in`.koreatech.koin.domain.usecase.busv2.SearchBusV2UseCase +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class BusSearchViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + private val searchBusV2UseCase: SearchBusV2UseCase +) : ViewModel() { + + val departure = savedStateHandle.getStateFlow(KEY_DEPARTURE, "") + val arrival = savedStateHandle.getStateFlow(KEY_ARRIVAL, "") + + private val _searchBusUiState = MutableSharedFlow() + val searchBusUiState = _searchBusUiState.asSharedFlow() + + fun setDeparture(departure: String) { + savedStateHandle[KEY_DEPARTURE] = departure + } + + fun setArrival(arrival: String) { + savedStateHandle[KEY_ARRIVAL] = arrival + } + + fun swapDepartureAndArrival() { + val currentDeparture = departure.value + val currentArrival = arrival.value + setDeparture(currentArrival) + setArrival(currentDeparture) + } + + fun search() { + viewModelScope.launch { + searchBusV2UseCase(departure.value, arrival.value).onSuccess { + _searchBusUiState.emit(SearchBusUiState.Success(Unit)) // TODO: 리턴 타입 + }.onFailure { + when (it) { + is SearchBusError.EmptyDeparture -> _searchBusUiState.emit(SearchBusUiState.EmptyDeparture) + is SearchBusError.EmptyArrival -> _searchBusUiState.emit(SearchBusUiState.EmptyArrival) + } + } + } + } + + companion object { + private const val KEY_DEPARTURE = "departure" + private const val KEY_ARRIVAL = "arrival" + } +} + +sealed interface SearchBusUiState { + data object Loading : SearchBusUiState + data class Success(val data: Unit) : SearchBusUiState // TODO: 리턴 타입 + data object EmptyDeparture : SearchBusUiState + data object EmptyArrival : SearchBusUiState +} \ No newline at end of file diff --git a/feature/bus/src/main/res/drawable/ic_swap.xml b/feature/bus/src/main/res/drawable/ic_swap.xml new file mode 100644 index 000000000..d6d2d6a78 --- /dev/null +++ b/feature/bus/src/main/res/drawable/ic_swap.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/feature/bus/src/main/res/values/strings.xml b/feature/bus/src/main/res/values/strings.xml index 3e3d52010..8609bc8c4 100644 --- a/feature/bus/src/main/res/values/strings.xml +++ b/feature/bus/src/main/res/values/strings.xml @@ -27,6 +27,23 @@ 노선 운행 + + 교통편 조회하기 + 목적지까지 가장 빠른 교통편을 알려드릴게요. + 학기 중 시간표와 다를 수 있습니다. + 출발 + 도착 + 출발지 선택 + 도착지 선택 + 출발지와 도착지를 바꾸기 + 조회하기 + 어디서 출발하시나요? + 목적지가 어디인가요? + 도착지 선택하기 + 확인하기 + 코리아텍 + 천안역 + 천안터미널 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f94ec6283..f41e5ec9e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ activityComposeVersion = "1.8.2" activityKtxVersion = "1.5.0" cardviewVersion = "1.0.0" +constraintlayoutComposeVersion = "1.1.0" inAppUpdateVersion = "2.1.0" featureDeliveryKtxVersion = "2.1.0" firebaseBomVersion = "32.5.0" @@ -76,6 +77,7 @@ kotlinxCollectionsImmutableVersion = "0.3.8" androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtxVersion" } androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "cardviewVersion" } androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayoutVersion" } +androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutComposeVersion" } androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtxVersion" } androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtxVersion" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtxVersion" }