From f6fa3decd12748579f5e528bdb7ebcf0ffb5dae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EA=B0=B1?= Date: Sun, 21 Sep 2025 18:56:04 +0900 Subject: [PATCH 1/6] feat#135 add search module --- domain/search/.gitignore | 1 + domain/search/build.gradle.kts | 8 +++++++ .../com/teamsolply/solply/search/MyClass.kt | 4 ++++ feature/serach/.gitignore | 1 + feature/serach/build.gradle.kts | 12 +++++++++++ feature/serach/consumer-rules.pro | 0 feature/serach/proguard-rules.pro | 21 +++++++++++++++++++ feature/serach/src/main/AndroidManifest.xml | 4 ++++ settings.gradle.kts | 2 ++ 9 files changed, 53 insertions(+) create mode 100644 domain/search/.gitignore create mode 100644 domain/search/build.gradle.kts create mode 100644 domain/search/src/main/java/com/teamsolply/solply/search/MyClass.kt create mode 100644 feature/serach/.gitignore create mode 100644 feature/serach/build.gradle.kts create mode 100644 feature/serach/consumer-rules.pro create mode 100644 feature/serach/proguard-rules.pro create mode 100644 feature/serach/src/main/AndroidManifest.xml diff --git a/domain/search/.gitignore b/domain/search/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/domain/search/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/search/build.gradle.kts b/domain/search/build.gradle.kts new file mode 100644 index 00000000..2fb9e4a5 --- /dev/null +++ b/domain/search/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + alias(libs.plugins.solply.java.library) +} + +dependencies { + implementation(projects.core.model) + implementation(libs.bundles.coroutine) +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/teamsolply/solply/search/MyClass.kt b/domain/search/src/main/java/com/teamsolply/solply/search/MyClass.kt new file mode 100644 index 00000000..61df4a82 --- /dev/null +++ b/domain/search/src/main/java/com/teamsolply/solply/search/MyClass.kt @@ -0,0 +1,4 @@ +package com.teamsolply.solply.search + +class MyClass { +} \ No newline at end of file diff --git a/feature/serach/.gitignore b/feature/serach/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/serach/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/serach/build.gradle.kts b/feature/serach/build.gradle.kts new file mode 100644 index 00000000..c7678c1a --- /dev/null +++ b/feature/serach/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(libs.plugins.solply.android.library) + +} + +android { + namespace = "com.teamsolply.solply.search" +} + +dependencies { + implementation(projects.domain.search) +} \ No newline at end of file diff --git a/feature/serach/consumer-rules.pro b/feature/serach/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/serach/proguard-rules.pro b/feature/serach/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/serach/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/serach/src/main/AndroidManifest.xml b/feature/serach/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/serach/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 1a540fc8..f0057a98 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -63,3 +63,5 @@ include(":feature:place") include(":feature:course") include(":feature:collection") include(":feature:maps") +include(":feature:serach") +include(":domain:search") From c5bbbab234f530ebc820a30a605e9a86b4c2ffa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EA=B0=B1?= Date: Sun, 21 Sep 2025 18:56:21 +0900 Subject: [PATCH 2/6] feat#135 add solply search bar component --- .../component/searchbar/SolplySearchBar.kt | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/searchbar/SolplySearchBar.kt diff --git a/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/searchbar/SolplySearchBar.kt b/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/searchbar/SolplySearchBar.kt new file mode 100644 index 00000000..008d4ad2 --- /dev/null +++ b/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/searchbar/SolplySearchBar.kt @@ -0,0 +1,103 @@ +package com.teamsolply.solply.designsystem.component.searchbar + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.teamsolply.solply.designsystem.R +import com.teamsolply.solply.designsystem.theme.SolplyTheme + + +@Composable +fun SearchBox( + modifier: Modifier, + query: String, + onQueryChange: (String) -> Unit, + onImageClick: () -> Unit, +) { + val keyboardController = LocalSoftwareKeyboardController.current + val focusRequester = remember { FocusRequester() } + + Row( + modifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = SolplyTheme.colors.gray300, + shape = RoundedCornerShape(20.dp) + ) + .background(color = SolplyTheme.colors.white) + .padding(vertical = 14.dp, horizontal = 20.dp) + .clickable { + focusRequester.requestFocus() + keyboardController?.show() + }, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + + BasicTextField( + value = query, + onValueChange = onQueryChange, + modifier = modifier + .weight(1f) + .fillMaxWidth() + .focusRequester(focusRequester), + textStyle = SolplyTheme.typography.body16R.copy(color = SolplyTheme.colors.black), + decorationBox = { innerTextField -> + if (query.isEmpty()) { + Text( + text = "찾는 공간을 입력하세요", + style = SolplyTheme.typography.body16R, + color = SolplyTheme.colors.gray500, + ) + } + innerTextField() + }, + maxLines = 1, + singleLine = true, + ) + Image( + painter = painterResource(id = R.drawable.ic_setting), // ic_search 134 브랜치 들어오고 바꿔야함 + contentDescription = "Search Icon", + modifier = Modifier + .clickable { + onImageClick() + keyboardController?.hide() + }, + ) + } +} + +@Preview +@Composable +fun SearchBoxPreview() { + SolplyTheme { + SearchBox( + modifier = Modifier.fillMaxWidth(), + query = "", + onQueryChange = {}, + onImageClick = {}, + ) + } +} \ No newline at end of file From 8d1748292da0bbdced09ee4820937b9fa2d6852c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EA=B0=B1?= Date: Mon, 22 Sep 2025 15:21:38 +0900 Subject: [PATCH 3/6] feat#135 add search item component --- feature/serach/build.gradle.kts | 3 +- .../solply/search/component/SearchItem.kt | 117 ++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 feature/serach/src/main/java/com/teamsolply/solply/search/component/SearchItem.kt diff --git a/feature/serach/build.gradle.kts b/feature/serach/build.gradle.kts index c7678c1a..c4dbd608 100644 --- a/feature/serach/build.gradle.kts +++ b/feature/serach/build.gradle.kts @@ -1,6 +1,5 @@ plugins { - alias(libs.plugins.solply.android.library) - + alias(libs.plugins.solply.feature) } android { diff --git a/feature/serach/src/main/java/com/teamsolply/solply/search/component/SearchItem.kt b/feature/serach/src/main/java/com/teamsolply/solply/search/component/SearchItem.kt new file mode 100644 index 00000000..10ae6a8f --- /dev/null +++ b/feature/serach/src/main/java/com/teamsolply/solply/search/component/SearchItem.kt @@ -0,0 +1,117 @@ +package com.teamsolply.solply.search.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.teamsolply.solply.designsystem.component.chip.PlaceTag +import com.teamsolply.solply.designsystem.theme.SolplyTheme +import com.teamsolply.solply.model.PlaceType +import com.teamsolply.solply.ui.extension.customClickable +import com.teamsolply.solply.ui.image.AdaptationImage +import formatTextToPlaceItemTitle + +@Composable +internal fun SearchItem( + placeName: String, + placeTag: PlaceType, + placeAddress: String, + placeImageUrl: String, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + Column( + modifier = modifier + .fillMaxWidth() + .customClickable(rippleEnabled = false) { onClick() } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 12.dp) + ) { + AdaptationImage( + imageUrl = placeImageUrl, + modifier = Modifier + .size(52.dp) + .clip(RoundedCornerShape(12.dp)) + .background(SolplyTheme.colors.gray200), + contentScale = ContentScale.Crop + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Column(modifier = Modifier.weight(1f)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + PlaceTag( + type = placeTag, + modifier = Modifier.padding(end = 4.dp) + ) + Text( + text = placeName.formatTextToPlaceItemTitle(), + color = SolplyTheme.colors.black, + style = SolplyTheme.typography.title15M, + modifier = Modifier.weight(1f) + ) + } + + Spacer(modifier = Modifier.height(6.dp)) + + Text( + text = placeAddress, + color = SolplyTheme.colors.gray700, + style = SolplyTheme.typography.caption12R, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() + ) + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF, widthDp = 360) +@Composable +private fun SearchItemPreview() { + SolplyTheme { + Column { + val shopping = PlaceType.entries.firstOrNull { it.name == "SHOPPING" } ?: PlaceType.ALL + val food = PlaceType.entries.firstOrNull { it.name == "FOOD" } ?: PlaceType.ALL + val walk = PlaceType.entries.firstOrNull { it.name == "WALK" } ?: PlaceType.ALL + + SearchItem( + placeName = "공간 이름", + placeTag = shopping, + placeAddress = "상세주소 한 줄만", + placeImageUrl = "https://picsum.photos/200/200", + onClick = {} + ) + SearchItem( + placeName = "공간 이름", + placeTag = food, + placeAddress = "상세주소 한 줄인데 넘어가면 어떻게 되는지 잘 잘리는지 ", + placeImageUrl = "https://picsum.photos/200/200", + onClick = {} + ) + } + } +} \ No newline at end of file From 6070ce266a657eb284a5e843a2241f5695216545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EA=B0=B1?= Date: Thu, 25 Sep 2025 21:16:41 +0900 Subject: [PATCH 4/6] feat#135 add module and navigation --- app/build.gradle.kts | 1 + .../component/searchbar/SolplySearchBar.kt | 4 +- feature/main/build.gradle.kts | 1 + .../teamsolply/solply/main/MainActivity.kt | 3 +- .../com/teamsolply/solply/main/MainScreen.kt | 55 +++++- feature/serach/build.gradle.kts | 6 +- .../solply/search/SearchContract.kt | 40 +++++ .../teamsolply/solply/search/SearchScreen.kt | 165 ++++++++++++++++++ .../solply/search/SearchViewModel.kt | 118 +++++++++++++ .../search/navigation/SearchNavigation.kt | 33 ++++ 10 files changed, 421 insertions(+), 5 deletions(-) create mode 100644 feature/serach/src/main/java/com/teamsolply/solply/search/SearchContract.kt create mode 100644 feature/serach/src/main/java/com/teamsolply/solply/search/SearchScreen.kt create mode 100644 feature/serach/src/main/java/com/teamsolply/solply/search/SearchViewModel.kt create mode 100644 feature/serach/src/main/java/com/teamsolply/solply/search/navigation/SearchNavigation.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c3d48890..4336c09f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,5 +67,6 @@ dependencies { implementation(projects.feature.course) implementation(projects.feature.collection) implementation(projects.feature.maps) + implementation(projects.feature.serach) implementation(libs.kakao.login) } diff --git a/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/searchbar/SolplySearchBar.kt b/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/searchbar/SolplySearchBar.kt index 008d4ad2..2a46f8fc 100644 --- a/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/searchbar/SolplySearchBar.kt +++ b/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/searchbar/SolplySearchBar.kt @@ -29,7 +29,7 @@ import com.teamsolply.solply.designsystem.theme.SolplyTheme @Composable -fun SearchBox( +fun SolplySearchbar( modifier: Modifier, query: String, onQueryChange: (String) -> Unit, @@ -93,7 +93,7 @@ fun SearchBox( @Composable fun SearchBoxPreview() { SolplyTheme { - SearchBox( + SolplySearchbar( modifier = Modifier.fillMaxWidth(), query = "", onQueryChange = {}, diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index 770530c7..db663db6 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(projects.feature.oauth) implementation(projects.feature.onboarding) implementation(projects.feature.place) + implementation(projects.feature.serach) implementation(projects.feature.course) implementation(projects.feature.maps) implementation(projects.feature.collection) diff --git a/feature/main/src/main/java/com/teamsolply/solply/main/MainActivity.kt b/feature/main/src/main/java/com/teamsolply/solply/main/MainActivity.kt index 88fc7ae2..14c31672 100644 --- a/feature/main/src/main/java/com/teamsolply/solply/main/MainActivity.kt +++ b/feature/main/src/main/java/com/teamsolply/solply/main/MainActivity.kt @@ -21,7 +21,8 @@ class MainActivity : ComponentActivity() { setContent { SolplyTheme { MainScreen( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), + isFreshLaunch = savedInstanceState == null ) } } diff --git a/feature/main/src/main/java/com/teamsolply/solply/main/MainScreen.kt b/feature/main/src/main/java/com/teamsolply/solply/main/MainScreen.kt index c6c8b092..d06ea6c0 100644 --- a/feature/main/src/main/java/com/teamsolply/solply/main/MainScreen.kt +++ b/feature/main/src/main/java/com/teamsolply/solply/main/MainScreen.kt @@ -15,15 +15,18 @@ import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.navOptions +import androidx.navigation.NavDestination.Companion.hasRoute import com.teamsolply.solply.collection.collection.course.courseCollectionNavGraph import com.teamsolply.solply.collection.collection.place.placeCollectionNavGraph import com.teamsolply.solply.collection.navigation.collectionNavGraph @@ -37,25 +40,52 @@ import com.teamsolply.solply.designsystem.theme.SolplyTheme import com.teamsolply.solply.main.component.MainBottomBar import com.teamsolply.solply.main.model.SolplySnackBarData import com.teamsolply.solply.main.splash.splashNavGraph +import com.teamsolply.solply.model.MapsType import com.teamsolply.solply.maps.navigation.mapsNavGraph import com.teamsolply.solply.model.SnackBarType import com.teamsolply.solply.oauth.navigation.oauthNavGraph import com.teamsolply.solply.onboarding.navigation.onBoardingNavGraph import com.teamsolply.solply.place.navigation.placeNavGraph +import com.teamsolply.solply.search.navigation.Search +import com.teamsolply.solply.search.navigation.searchNavGraph import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first @Composable internal fun MainScreen( modifier: Modifier = Modifier, - navigator: MainNavigator = rememberMainNavigator() + navigator: MainNavigator = rememberMainNavigator(), + isFreshLaunch: Boolean = false ) { val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } val currentSnackbarJob = remember { mutableStateOf(null) } val currentSnackbarState = remember { mutableStateOf(SolplySnackBarData()) } + val navController = navigator.navController + + if (isFreshLaunch) { + LaunchedEffect(navController) { + val initialDestination = snapshotFlow { navController.currentBackStackEntry } + .filterNotNull() + .first() + .destination + + if (!initialDestination.hasRoute(Search::class)) { + val initialNavOptions = navOptions { + popUpTo(0) { + inclusive = true + } + launchSingleTop = true + } + navigator.navigateToSearch(initialNavOptions) + } + } + } + suspend fun showTextSnackBar(message: String) { currentSnackbarJob.value?.join() currentSnackbarJob.value = coroutineScope.launch { @@ -128,6 +158,29 @@ internal fun MainScreen( .background(color = SolplyTheme.colors.gray100) .fillMaxSize() ) { + searchNavGraph( + paddingValues = innerPadding, + onBack = navigator::navigateToBack, + navigateToPlaceDetail = { townId, placeId -> + val navOptions = navOptions {} + navigator.navigateToMaps( + mapsType = MapsType.PLACE_DETAIL.name, + townId = townId, + placeId = placeId, + courseId = null, + navOptions = navOptions + ) + }, + onNoPlaceClick = { + val navOptions = navOptions { + popUpTo(0) { + inclusive = true + } + launchSingleTop = true + } + navigator.navigateToPlace(navOptions = navOptions) + } + ) splashNavGraph( navigateToOauth = { val navOptions = navOptions { diff --git a/feature/serach/build.gradle.kts b/feature/serach/build.gradle.kts index c4dbd608..4a622c52 100644 --- a/feature/serach/build.gradle.kts +++ b/feature/serach/build.gradle.kts @@ -7,5 +7,9 @@ android { } dependencies { + implementation(projects.core.designsystem) + implementation(projects.core.model) + implementation(projects.core.ui) + implementation(projects.domain.place) implementation(projects.domain.search) -} \ No newline at end of file +} diff --git a/feature/serach/src/main/java/com/teamsolply/solply/search/SearchContract.kt b/feature/serach/src/main/java/com/teamsolply/solply/search/SearchContract.kt new file mode 100644 index 00000000..601f77b6 --- /dev/null +++ b/feature/serach/src/main/java/com/teamsolply/solply/search/SearchContract.kt @@ -0,0 +1,40 @@ +package com.teamsolply.solply.search + +import com.teamsolply.solply.model.PlaceType +import com.teamsolply.solply.ui.base.SideEffect +import com.teamsolply.solply.ui.base.UiIntent +import com.teamsolply.solply.ui.base.UiState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +data class SearchState( + val query: String = "", + val isLoading: Boolean = false, + val results: PersistentList = persistentListOf(), + val selectedTownId: Long? = null +) : UiState { + val resultCount: Int get() = results.size + val hasQuery: Boolean get() = query.isNotBlank() +} + +data class SearchItemUi( + val id: Long, + val name: String, + val tag: PlaceType, + val address: String, + val imageUrl: String +) + +sealed interface SearchIntent : UiIntent { + data class QueryChanged(val value: String) : SearchIntent + data object ClearQuery : SearchIntent + data object Submit : SearchIntent + data class ClickItem(val id: Long) : SearchIntent + data object ClickNoResult : SearchIntent + data object Retry : SearchIntent +} + +sealed interface SearchSideEffect : SideEffect { + data class NavigateToPlaceDetail(val townId: Long, val placeId: Long) : SearchSideEffect + data object NavigateToNoResult : SearchSideEffect +} diff --git a/feature/serach/src/main/java/com/teamsolply/solply/search/SearchScreen.kt b/feature/serach/src/main/java/com/teamsolply/solply/search/SearchScreen.kt new file mode 100644 index 00000000..a8268f1e --- /dev/null +++ b/feature/serach/src/main/java/com/teamsolply/solply/search/SearchScreen.kt @@ -0,0 +1,165 @@ +package com.teamsolply.solply.search + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.teamsolply.solply.designsystem.component.searchbar.SolplySearchbar +import com.teamsolply.solply.designsystem.component.topbar.SolplyTopBar +import com.teamsolply.solply.designsystem.theme.SolplyTheme +import com.teamsolply.solply.search.component.SearchItem +import com.teamsolply.solply.ui.extension.customClickable +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun SearchRoute( + paddingValues: PaddingValues, + onBack: () -> Unit, + navigateToPlaceDetail: (Long, Long) -> Unit, + onNoPlaceClick: () -> Unit, + viewModel: SearchViewModel = hiltViewModel() +) { + val state by viewModel.uiState.collectAsState() + LaunchedEffect(Unit) { + viewModel.sideEffect.collectLatest { se -> + when (se) { + is SearchSideEffect.NavigateToPlaceDetail -> navigateToPlaceDetail(se.townId, se.placeId) + SearchSideEffect.NavigateToNoResult -> onNoPlaceClick() + } + } + } + LaunchedEffect(Unit) { viewModel.sendIntent(SearchIntent.Retry) } + + SearchScreen( + modifier = Modifier.padding(paddingValues), + state = state, + onBack = onBack, + onQueryChanged = { viewModel.sendIntent(SearchIntent.QueryChanged(it)) }, + onClearQuery = { viewModel.sendIntent(SearchIntent.ClearQuery) }, + onSubmit = { viewModel.sendIntent(SearchIntent.Submit) }, + onClickItem = { id -> viewModel.sendIntent(SearchIntent.ClickItem(id)) }, + onClickNoResult = { viewModel.sendIntent(SearchIntent.ClickNoResult) } + ) +} + +@Composable +fun SearchScreen( + modifier: Modifier = Modifier, + state: SearchState, + onBack: () -> Unit, + onQueryChanged: (String) -> Unit, + onClearQuery: () -> Unit, + onSubmit: () -> Unit, + onClickItem: (Long) -> Unit, + onClickNoResult: () -> Unit +) { + BackHandler(onBack = onBack) + + Column( + modifier = modifier + .fillMaxSize() + .padding(bottom = 12.dp) + ) { + SolplyTopBar( + barText = "검색하기", + onBackButtonClick = onBack + ) + + SolplySearchbar( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + query = state.query, + onQueryChange = { + onQueryChanged(it) + if (it.isBlank()) { + onClearQuery() + } + }, + onImageClick = onSubmit + ) + + if (state.hasQuery) { + Text( + text = "검색 결과 ${state.resultCount}개", + style = SolplyTheme.typography.button14M, + color = SolplyTheme.colors.gray800, + modifier = Modifier.padding(start = 20.dp, top = 32.dp, bottom = 16.dp) + ) + } + + if (state.hasQuery) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + ) { + item { + HorizontalDivider( + thickness = 1.dp, + color = SolplyTheme.colors.gray200 + ) + } + + val items = state.results.toImmutableList() + itemsIndexed(items) { _, item -> + SearchItem( + placeName = item.name, + placeTag = item.tag, + placeAddress = item.address, + placeImageUrl = item.imageUrl, + onClick = { onClickItem(item.id) } + ) + } + + item { + HorizontalDivider( + thickness = 1.dp, + color = SolplyTheme.colors.gray200 + ) + } + + item { + Row( + modifier = Modifier + .fillMaxWidth() + .customClickable(rippleEnabled = false) { onClickNoResult() } + .padding(horizontal = 20.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "찾는 장소가 없어요", + style = SolplyTheme.typography.button14M, + color = SolplyTheme.colors.gray700, + modifier = Modifier.weight(1f) + ) + Icon( + painter = painterResource(id = com.teamsolply.solply.designsystem.R.drawable.ic_arrow_right_icon), + contentDescription = "arrow-right-icon", + tint = SolplyTheme.colors.gray700 + ) + } + } + + item { + HorizontalDivider( + thickness = 1.dp, + color = SolplyTheme.colors.gray200 + ) + } + } + } + } +} diff --git a/feature/serach/src/main/java/com/teamsolply/solply/search/SearchViewModel.kt b/feature/serach/src/main/java/com/teamsolply/solply/search/SearchViewModel.kt new file mode 100644 index 00000000..d29c0592 --- /dev/null +++ b/feature/serach/src/main/java/com/teamsolply/solply/search/SearchViewModel.kt @@ -0,0 +1,118 @@ +package com.teamsolply.solply.search + +import androidx.lifecycle.viewModelScope +import com.teamsolply.solply.model.PlaceType +import com.teamsolply.solply.place.repository.PlaceRepository +import com.teamsolply.solply.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + private val repository: PlaceRepository +) : BaseViewModel(SearchState()) { + + private var typingJob: Job? = null + + init { + reduce { copy(query = "", results = emptyList().toPersistentList()) } + } + + override fun handleIntent(intent: SearchIntent) { + when (intent) { + is SearchIntent.QueryChanged -> { + reduce { copy(query = intent.value) } + typingJob?.cancel() + typingJob = viewModelScope.launch { + delay(250) + search(currentState.query) + } + } + SearchIntent.ClearQuery -> { + typingJob?.cancel() + reduce { + copy( + query = "", + isLoading = false, + results = emptyList().toPersistentList(), + selectedTownId = null + ) + } + } + SearchIntent.Submit -> { + viewModelScope.launch { search(currentState.query) } + } + is SearchIntent.ClickItem -> { + val townId = currentState.selectedTownId ?: return + postSideEffect( + SearchSideEffect.NavigateToPlaceDetail( + townId = townId, + placeId = intent.id + ) + ) + } + SearchIntent.ClickNoResult -> { + postSideEffect(SearchSideEffect.NavigateToNoResult) + } + SearchIntent.Retry -> { + // 필요 시 최근 검색어 복구/추천 키워드 로딩 등을 붙일 자리 + } + } + } + + private suspend fun search(query: String) { + if (query.isBlank()) { + reduce { copy(isLoading = false, results = emptyList().toPersistentList()) } + return + } + reduce { copy(isLoading = true) } + + val filtered = DUMMY_RESULTS.filter { item -> + val normalizedQuery = query.trim().lowercase() + item.name.lowercase().contains(normalizedQuery) || item.address.lowercase().contains(normalizedQuery) + } + + val townId = repository.getUserInfo() + .map { it.selectedTown.townId } + .getOrDefault(DEFAULT_TOWN_ID) + + reduce { + copy( + isLoading = false, + selectedTownId = townId, + results = filtered.toPersistentList() + ) + } + } + + private companion object { + const val DEFAULT_TOWN_ID = 0L + + val DUMMY_RESULTS = listOf( + SearchItemUi( + id = 1, + name = "솔플리솔플리", + tag = PlaceType.FOOD, + address = "주소주소주소주소", + imageUrl = "" + ), + SearchItemUi( + id = 2, + name = "솔플리솔플리", + tag = PlaceType.BOOKSTORE, + imageUrl = "" + ), + SearchItemUi( + id = 3, + name = "솔플리솔플리", + tag = PlaceType.UNIQUE_SPACE, + address = "주소주소주소주소주소주소주소주주소주소주소주소", + imageUrl = "" + ) + ) + } +} diff --git a/feature/serach/src/main/java/com/teamsolply/solply/search/navigation/SearchNavigation.kt b/feature/serach/src/main/java/com/teamsolply/solply/search/navigation/SearchNavigation.kt new file mode 100644 index 00000000..6e22b729 --- /dev/null +++ b/feature/serach/src/main/java/com/teamsolply/solply/search/navigation/SearchNavigation.kt @@ -0,0 +1,33 @@ +package com.teamsolply.solply.search.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.teamsolply.solply.navigation.Route +import com.teamsolply.solply.search.SearchRoute +import kotlinx.serialization.Serializable + +fun NavController.navigateSearch(navOptions: NavOptions) { + navigate(Search, navOptions) +} + +fun NavGraphBuilder.searchNavGraph( + paddingValues: PaddingValues, + onBack: () -> Unit, + navigateToPlaceDetail: (Long, Long) -> Unit, + onNoPlaceClick: () -> Unit +) { + composable { + SearchRoute( + paddingValues = paddingValues, + onBack = onBack, + navigateToPlaceDetail = navigateToPlaceDetail, + onNoPlaceClick = onNoPlaceClick + ) + } +} + +@Serializable +data object Search : Route From 9441b4a61710d5528a0f7a942314f3ea687b418d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EA=B0=B1?= Date: Thu, 25 Sep 2025 21:17:37 +0900 Subject: [PATCH 5/6] feat#135 add navigate function to main --- .../designsystem/component/card/SolplyPlaceCard.kt | 10 +++++----- .../java/com/teamsolply/solply/main/MainNavigator.kt | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/card/SolplyPlaceCard.kt b/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/card/SolplyPlaceCard.kt index fe0e9bee..d9685a5e 100644 --- a/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/card/SolplyPlaceCard.kt +++ b/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/card/SolplyPlaceCard.kt @@ -55,11 +55,11 @@ fun SolplyPlaceCard( else -> SolplyTheme.colors.gray400 } val iconBackGroundColor = when (placeType) { - PlaceType.CAFE -> SolplyTheme.colors.red200 - PlaceType.FOOD -> SolplyTheme.colors.yellow100 - PlaceType.WALKING, PlaceType.UNIQUE_SPACE -> SolplyTheme.colors.green200 - PlaceType.SHOPPING, PlaceType.BOOKSTORE -> SolplyTheme.colors.purple100 - else -> SolplyTheme.colors.gray200 + PlaceType.CAFE -> SolplyTheme.colors.red500 + PlaceType.FOOD -> SolplyTheme.colors.yellow400 + PlaceType.WALKING, PlaceType.UNIQUE_SPACE -> SolplyTheme.colors.green500 + PlaceType.SHOPPING, PlaceType.BOOKSTORE -> SolplyTheme.colors.purple200 + else -> SolplyTheme.colors.purple500 } Column( diff --git a/feature/main/src/main/java/com/teamsolply/solply/main/MainNavigator.kt b/feature/main/src/main/java/com/teamsolply/solply/main/MainNavigator.kt index cb47ab79..961b210f 100644 --- a/feature/main/src/main/java/com/teamsolply/solply/main/MainNavigator.kt +++ b/feature/main/src/main/java/com/teamsolply/solply/main/MainNavigator.kt @@ -19,6 +19,8 @@ import com.teamsolply.solply.maps.navigation.navigateMaps import com.teamsolply.solply.oauth.navigation.navigateOauth import com.teamsolply.solply.onboarding.navigation.navigateOnBoarding import com.teamsolply.solply.place.navigation.navigatePlace +import com.teamsolply.solply.search.navigation.Search +import com.teamsolply.solply.search.navigation.navigateSearch internal class MainNavigator( val navController: NavHostController @@ -85,6 +87,10 @@ internal class MainNavigator( navController.navigatePlace(navOptions) } + fun navigateToSearch(navOptions: NavOptions) { + navController.navigateSearch(navOptions) + } + fun navigateToCourse(navOptions: NavOptions) { navController.navigateCourse(navOptions) } From 3489f78807dbf6f9998ac17239bbde3729167066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EA=B0=B1?= Date: Thu, 25 Sep 2025 21:24:37 +0900 Subject: [PATCH 6/6] feat#135 ktlint formatting --- .../component/searchbar/SolplySearchBar.kt | 19 +++++++------------ domain/search/build.gradle.kts | 2 +- .../com/teamsolply/solply/search/MyClass.kt | 3 +-- .../teamsolply/solply/main/MainNavigator.kt | 1 - .../com/teamsolply/solply/main/MainScreen.kt | 6 +++--- .../bottomsheet/PlaceDetailBottomSheet.kt | 1 - .../teamsolply/solply/place/PlaceScreen.kt | 1 - .../teamsolply/solply/search/SearchScreen.kt | 7 ++++++- .../solply/search/component/SearchItem.kt | 4 ++-- 9 files changed, 20 insertions(+), 24 deletions(-) diff --git a/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/searchbar/SolplySearchBar.kt b/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/searchbar/SolplySearchBar.kt index 2a46f8fc..889b8231 100644 --- a/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/searchbar/SolplySearchBar.kt +++ b/core/designsystem/src/main/java/com/teamsolply/solply/designsystem/component/searchbar/SolplySearchBar.kt @@ -15,11 +15,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview @@ -27,13 +24,12 @@ import androidx.compose.ui.unit.dp import com.teamsolply.solply.designsystem.R import com.teamsolply.solply.designsystem.theme.SolplyTheme - @Composable fun SolplySearchbar( modifier: Modifier, query: String, onQueryChange: (String) -> Unit, - onImageClick: () -> Unit, + onImageClick: () -> Unit ) { val keyboardController = LocalSoftwareKeyboardController.current val focusRequester = remember { FocusRequester() } @@ -53,9 +49,8 @@ fun SolplySearchbar( keyboardController?.show() }, horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically ) { - BasicTextField( value = query, onValueChange = onQueryChange, @@ -69,13 +64,13 @@ fun SolplySearchbar( Text( text = "찾는 공간을 입력하세요", style = SolplyTheme.typography.body16R, - color = SolplyTheme.colors.gray500, + color = SolplyTheme.colors.gray500 ) } innerTextField() }, maxLines = 1, - singleLine = true, + singleLine = true ) Image( painter = painterResource(id = R.drawable.ic_setting), // ic_search 134 브랜치 들어오고 바꿔야함 @@ -84,7 +79,7 @@ fun SolplySearchbar( .clickable { onImageClick() keyboardController?.hide() - }, + } ) } } @@ -97,7 +92,7 @@ fun SearchBoxPreview() { modifier = Modifier.fillMaxWidth(), query = "", onQueryChange = {}, - onImageClick = {}, + onImageClick = {} ) } -} \ No newline at end of file +} diff --git a/domain/search/build.gradle.kts b/domain/search/build.gradle.kts index 2fb9e4a5..7f643ef7 100644 --- a/domain/search/build.gradle.kts +++ b/domain/search/build.gradle.kts @@ -5,4 +5,4 @@ plugins { dependencies { implementation(projects.core.model) implementation(libs.bundles.coroutine) -} \ No newline at end of file +} diff --git a/domain/search/src/main/java/com/teamsolply/solply/search/MyClass.kt b/domain/search/src/main/java/com/teamsolply/solply/search/MyClass.kt index 61df4a82..16ef3f8d 100644 --- a/domain/search/src/main/java/com/teamsolply/solply/search/MyClass.kt +++ b/domain/search/src/main/java/com/teamsolply/solply/search/MyClass.kt @@ -1,4 +1,3 @@ package com.teamsolply.solply.search -class MyClass { -} \ No newline at end of file +class MyClass diff --git a/feature/main/src/main/java/com/teamsolply/solply/main/MainNavigator.kt b/feature/main/src/main/java/com/teamsolply/solply/main/MainNavigator.kt index 961b210f..1cfaf853 100644 --- a/feature/main/src/main/java/com/teamsolply/solply/main/MainNavigator.kt +++ b/feature/main/src/main/java/com/teamsolply/solply/main/MainNavigator.kt @@ -19,7 +19,6 @@ import com.teamsolply.solply.maps.navigation.navigateMaps import com.teamsolply.solply.oauth.navigation.navigateOauth import com.teamsolply.solply.onboarding.navigation.navigateOnBoarding import com.teamsolply.solply.place.navigation.navigatePlace -import com.teamsolply.solply.search.navigation.Search import com.teamsolply.solply.search.navigation.navigateSearch internal class MainNavigator( diff --git a/feature/main/src/main/java/com/teamsolply/solply/main/MainScreen.kt b/feature/main/src/main/java/com/teamsolply/solply/main/MainScreen.kt index d06ea6c0..1611c1b8 100644 --- a/feature/main/src/main/java/com/teamsolply/solply/main/MainScreen.kt +++ b/feature/main/src/main/java/com/teamsolply/solply/main/MainScreen.kt @@ -24,9 +24,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp +import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.compose.NavHost import androidx.navigation.navOptions -import androidx.navigation.NavDestination.Companion.hasRoute import com.teamsolply.solply.collection.collection.course.courseCollectionNavGraph import com.teamsolply.solply.collection.collection.place.placeCollectionNavGraph import com.teamsolply.solply.collection.navigation.collectionNavGraph @@ -40,8 +40,8 @@ import com.teamsolply.solply.designsystem.theme.SolplyTheme import com.teamsolply.solply.main.component.MainBottomBar import com.teamsolply.solply.main.model.SolplySnackBarData import com.teamsolply.solply.main.splash.splashNavGraph -import com.teamsolply.solply.model.MapsType import com.teamsolply.solply.maps.navigation.mapsNavGraph +import com.teamsolply.solply.model.MapsType import com.teamsolply.solply.model.SnackBarType import com.teamsolply.solply.oauth.navigation.oauthNavGraph import com.teamsolply.solply.onboarding.navigation.onBoardingNavGraph @@ -50,9 +50,9 @@ import com.teamsolply.solply.search.navigation.Search import com.teamsolply.solply.search.navigation.searchNavGraph import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Job -import kotlinx.coroutines.launch import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch @Composable internal fun MainScreen( diff --git a/feature/maps/src/main/java/com/teamsolply/solply/maps/component/bottomsheet/PlaceDetailBottomSheet.kt b/feature/maps/src/main/java/com/teamsolply/solply/maps/component/bottomsheet/PlaceDetailBottomSheet.kt index d88cf977..b5e4c191 100644 --- a/feature/maps/src/main/java/com/teamsolply/solply/maps/component/bottomsheet/PlaceDetailBottomSheet.kt +++ b/feature/maps/src/main/java/com/teamsolply/solply/maps/component/bottomsheet/PlaceDetailBottomSheet.kt @@ -3,7 +3,6 @@ package com.teamsolply.solply.maps.component.bottomsheet import ClickableAnnotatedText import android.content.Context import android.content.Intent -import android.util.Log import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/feature/place/src/main/java/com/teamsolply/solply/place/PlaceScreen.kt b/feature/place/src/main/java/com/teamsolply/solply/place/PlaceScreen.kt index 9fb4ed13..a16946e7 100644 --- a/feature/place/src/main/java/com/teamsolply/solply/place/PlaceScreen.kt +++ b/feature/place/src/main/java/com/teamsolply/solply/place/PlaceScreen.kt @@ -1,6 +1,5 @@ package com.teamsolply.solply.place -import android.util.Log import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/feature/serach/src/main/java/com/teamsolply/solply/search/SearchScreen.kt b/feature/serach/src/main/java/com/teamsolply/solply/search/SearchScreen.kt index a8268f1e..bf6fe519 100644 --- a/feature/serach/src/main/java/com/teamsolply/solply/search/SearchScreen.kt +++ b/feature/serach/src/main/java/com/teamsolply/solply/search/SearchScreen.kt @@ -1,7 +1,12 @@ package com.teamsolply.solply.search import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.HorizontalDivider diff --git a/feature/serach/src/main/java/com/teamsolply/solply/search/component/SearchItem.kt b/feature/serach/src/main/java/com/teamsolply/solply/search/component/SearchItem.kt index 10ae6a8f..ca00247c 100644 --- a/feature/serach/src/main/java/com/teamsolply/solply/search/component/SearchItem.kt +++ b/feature/serach/src/main/java/com/teamsolply/solply/search/component/SearchItem.kt @@ -33,7 +33,7 @@ internal fun SearchItem( placeAddress: String, placeImageUrl: String, modifier: Modifier = Modifier, - onClick: () -> Unit = {}, + onClick: () -> Unit = {} ) { Column( modifier = modifier @@ -114,4 +114,4 @@ private fun SearchItemPreview() { ) } } -} \ No newline at end of file +}