From 9294c0651cc96e9296c9263cb00aa75ad66f7889 Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Wed, 31 Jan 2024 17:51:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B3=B4=EB=82=B8=20=EB=B4=89=ED=88=AC?= =?UTF-8?q?=20=EC=B5=9C=EA=B7=BC=20=EA=B2=80=EC=83=89=EC=96=B4=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../susu/feature/navigator/MainNavigator.kt | 6 ++ .../com/susu/feature/navigator/MainScreen.kt | 1 + .../SentEnvelopeSearchContract.kt | 19 ++++ .../SentEnvelopeSearchScreen.kt | 95 ++++++++++++++----- .../SentEnvelopeSearchViewModel.kt | 57 +++++++++++ .../java/com/susu/feature/sent/SentScreen.kt | 2 + .../feature/sent/navigation/SentNavigation.kt | 14 +++ 7 files changed, 169 insertions(+), 25 deletions(-) create mode 100644 feature/sent/src/main/java/com/susu/feature/envelopesearch/SentEnvelopeSearchContract.kt create mode 100644 feature/sent/src/main/java/com/susu/feature/envelopesearch/SentEnvelopeSearchViewModel.kt diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt index b2cb8055..28b334d0 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt @@ -38,6 +38,7 @@ import com.susu.feature.sent.navigation.navigateSentEnvelope import com.susu.feature.sent.navigation.navigateSentEnvelopeAdd import com.susu.feature.sent.navigation.navigateSentEnvelopeDetail import com.susu.feature.sent.navigation.navigateSentEnvelopeEdit +import com.susu.feature.sent.navigation.navigateSentEnvelopeSearch import com.susu.feature.statistics.navigation.navigateStatistics internal class MainNavigator( @@ -64,6 +65,7 @@ internal class MainNavigator( SentRoute.sentEnvelopeRoute, SentRoute.sentEnvelopeDetailRoute, SentRoute.sentEnvelopeEditRoute, + SentRoute.sentEnvelopeSearchRoute, CommunityRoute.route, CommunityRoute.voteAddRoute, CommunityRoute.voteSearchRoute, @@ -116,6 +118,10 @@ internal class MainNavigator( navController.navigateSentEnvelopeAdd() } + fun navigateSentEnvelopeSearch() { + navController.navigateSentEnvelopeSearch() + } + fun navigateLogin() { navController.navigate(LoginSignupRoute.Parent.Login.route) { popUpTo(id = navController.graph.id) { diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt index 29b30a9b..1abdae26 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt @@ -84,6 +84,7 @@ internal fun MainScreen( navigateSentEnvelopeDetail = navigator::navigateSentEnvelopeDetail, navigateSentEnvelopeEdit = navigator::navigateSentEnvelopeEdit, navigateSentEnvelopeAdd = navigator::navigateSentEnvelopeAdd, + navigateSentEnvelopeSearch = navigator::navigateSentEnvelopeSearch, handleException = viewModel::handleException, ) diff --git a/feature/sent/src/main/java/com/susu/feature/envelopesearch/SentEnvelopeSearchContract.kt b/feature/sent/src/main/java/com/susu/feature/envelopesearch/SentEnvelopeSearchContract.kt new file mode 100644 index 00000000..9427f88b --- /dev/null +++ b/feature/sent/src/main/java/com/susu/feature/envelopesearch/SentEnvelopeSearchContract.kt @@ -0,0 +1,19 @@ +package com.susu.feature.envelopesearch + +import com.susu.core.model.Envelope +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +data class EnvelopeSearchState( + val searchKeyword: String = "", + val recentSearchKeywordList: PersistentList = persistentListOf(), + val envelopeList: PersistentList = persistentListOf(), +) : UiState + +sealed interface EnvelopeSearchEffect : SideEffect { + data object PopBackStack : EnvelopeSearchEffect + data class NavigateEnvelopDetail(val envelope: Envelope) : EnvelopeSearchEffect + data object FocusClear : EnvelopeSearchEffect +} diff --git a/feature/sent/src/main/java/com/susu/feature/envelopesearch/SentEnvelopeSearchScreen.kt b/feature/sent/src/main/java/com/susu/feature/envelopesearch/SentEnvelopeSearchScreen.kt index c4863ffe..3bf0f72a 100644 --- a/feature/sent/src/main/java/com/susu/feature/envelopesearch/SentEnvelopeSearchScreen.kt +++ b/feature/sent/src/main/java/com/susu/feature/envelopesearch/SentEnvelopeSearchScreen.kt @@ -9,12 +9,16 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign 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 com.susu.core.designsystem.component.appbar.SusuDefaultAppBar import com.susu.core.designsystem.component.appbar.icon.BackIcon import com.susu.core.designsystem.component.container.SusuRecentSearchContainer @@ -23,30 +27,68 @@ import com.susu.core.designsystem.theme.Gray60 import com.susu.core.designsystem.theme.Gray80 import com.susu.core.designsystem.theme.SusuTheme import com.susu.core.model.Envelope -import com.susu.core.model.Friend +import com.susu.core.ui.extension.collectWithLifecycle import com.susu.feature.sent.R import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +@OptIn(FlowPreview::class) @Composable -fun SentEnvelopeSearchRoute() { +fun SentEnvelopeSearchRoute( + viewModel: EnvelopeSearchViewModel = hiltViewModel(), + popBackStack: () -> Unit, +) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + EnvelopeSearchEffect.FocusClear -> {} + is EnvelopeSearchEffect.NavigateEnvelopDetail -> {} + EnvelopeSearchEffect.PopBackStack -> popBackStack() + } + } + + LaunchedEffect(key1 = Unit) { + viewModel.getEnvelopeRecentSearchList() + } + + LaunchedEffect(key1 = uiState.searchKeyword) { + snapshotFlow { uiState.searchKeyword } + .debounce(100L) + .collect(viewModel::getEnvelopeList) + } + + SentEnvelopeSearchScreen( + uiState = uiState, + onSearchKeywordUpdated = viewModel::updateSearchKeyword, + onClickClearIcon = { viewModel.updateSearchKeyword("") }, + onSelectRecentSearch = { + viewModel.upsertEnvelopeRecentSearch(it) + viewModel.updateSearchKeyword(it) + }, + onDeleteRecentSearch = viewModel::deleteEnvelopeRecentSearch, + popBackStack = popBackStack, + ) } @Composable fun SentEnvelopeSearchScreen( - searchText: String = "", - recentSearch: PersistentList = persistentListOf(), - searchResult: PersistentList = persistentListOf(), - onSelectRecentSearch: (Int) -> Unit = {}, - onDeleteRecentSearch: (Int) -> Unit = {}, + uiState: EnvelopeSearchState = EnvelopeSearchState(), + onSearchKeywordUpdated: (String) -> Unit = {}, + onClickClearIcon: () -> Unit = {}, + onSelectRecentSearch: (String) -> Unit = {}, + onDeleteRecentSearch: (String) -> Unit = {}, onClickEnvelope: (Envelope) -> Unit = {}, + popBackStack: () -> Unit = {}, ) { Column( modifier = Modifier.fillMaxSize(), ) { SusuDefaultAppBar( leftIcon = { - BackIcon() + BackIcon(onClick = popBackStack) }, ) Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxs)) @@ -55,11 +97,13 @@ fun SentEnvelopeSearchScreen( horizontal = SusuTheme.spacing.spacing_m, vertical = SusuTheme.spacing.spacing_xxs, ), - value = searchText, + value = uiState.searchKeyword, placeholder = stringResource(R.string.sent_envelope_search_search_title), + onValueChange = onSearchKeywordUpdated, + onClickClearIcon = onClickClearIcon, ) - if (searchText.isEmpty()) { - if (recentSearch.isEmpty()) { + if (uiState.searchKeyword.isEmpty()) { + if (uiState.recentSearchKeywordList.isEmpty()) { EmptyRecentSearch(modifier = Modifier.fillMaxWidth()) } else { Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxl)) @@ -71,8 +115,10 @@ fun SentEnvelopeSearchScreen( ) Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_m)) RecentSearchColumn( - modifier = Modifier.fillMaxWidth().padding(horizontal = SusuTheme.spacing.spacing_m), - recentSearchList = recentSearch, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = SusuTheme.spacing.spacing_m), + recentSearchList = uiState.recentSearchKeywordList, onClickItem = onSelectRecentSearch, onClickClearIcon = onDeleteRecentSearch, ) @@ -85,13 +131,15 @@ fun SentEnvelopeSearchScreen( style = SusuTheme.typography.title_xxs, color = Gray60, ) - if (searchResult.isEmpty()) { + if (uiState.envelopeList.isEmpty()) { EmptySearchEnvelope(modifier = Modifier.fillMaxWidth()) } else { Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_m)) SearchEnvelopeColumn( - modifier = Modifier.fillMaxWidth().padding(horizontal = SusuTheme.spacing.spacing_m), - searchResult = searchResult, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = SusuTheme.spacing.spacing_m), + searchResult = uiState.envelopeList, onClickItem = onClickEnvelope, ) } @@ -128,18 +176,18 @@ fun EmptyRecentSearch( fun RecentSearchColumn( modifier: Modifier = Modifier, recentSearchList: PersistentList = persistentListOf(), - onClickItem: (Int) -> Unit = {}, - onClickClearIcon: (Int) -> Unit = {}, + onClickItem: (String) -> Unit = {}, + onClickClearIcon: (String) -> Unit = {}, ) { Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_m), ) { - recentSearchList.forEachIndexed { index, keyword -> + recentSearchList.forEach { keyword -> SusuRecentSearchContainer( text = keyword, - onClick = { onClickItem(index) }, - onClickCloseIcon = { onClickClearIcon(index) }, + onClick = { onClickItem(keyword) }, + onClickCloseIcon = { onClickClearIcon(keyword) }, ) } } @@ -194,9 +242,6 @@ fun SearchEnvelopeColumn( @Composable fun SentEnvelopeSearchScreenPreview() { SusuTheme { - SentEnvelopeSearchScreen( - searchText = "d", - searchResult = persistentListOf(Envelope(friend = Friend(name = "김수수"))), - ) + SentEnvelopeSearchScreen() } } diff --git a/feature/sent/src/main/java/com/susu/feature/envelopesearch/SentEnvelopeSearchViewModel.kt b/feature/sent/src/main/java/com/susu/feature/envelopesearch/SentEnvelopeSearchViewModel.kt new file mode 100644 index 00000000..9a429899 --- /dev/null +++ b/feature/sent/src/main/java/com/susu/feature/envelopesearch/SentEnvelopeSearchViewModel.kt @@ -0,0 +1,57 @@ +package com.susu.feature.envelopesearch + +import androidx.lifecycle.viewModelScope +import com.susu.core.ui.base.BaseViewModel +import com.susu.domain.usecase.enveloperecentsearch.DeleteEnvelopeRecentSearchUseCase +import com.susu.domain.usecase.enveloperecentsearch.GetEnvelopeRecentSearchListUseCase +import com.susu.domain.usecase.enveloperecentsearch.UpsertEnvelopeRecentSearchUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class EnvelopeSearchViewModel @Inject constructor( + private val getEnvelopeRecentSearchUserCase: GetEnvelopeRecentSearchListUseCase, + private val deleteEnvelopeRecentSearchUseCase: DeleteEnvelopeRecentSearchUseCase, + private val upsertEnvelopeRecentSearchUseCase: UpsertEnvelopeRecentSearchUseCase, +) : BaseViewModel(EnvelopeSearchState()) { + + fun getEnvelopeRecentSearchList() { + viewModelScope.launch { + getEnvelopeRecentSearchUserCase().onSuccess(::updateRecentSearchList) + } + } + + fun deleteEnvelopeRecentSearch(search: String) { + viewModelScope.launch { + deleteEnvelopeRecentSearchUseCase(search).onSuccess(::updateRecentSearchList) + } + } + + fun upsertEnvelopeRecentSearch(search: String) { + viewModelScope.launch { + upsertEnvelopeRecentSearchUseCase(search).onSuccess(::updateRecentSearchList) + } + } + + fun updateSearchKeyword(search: String) = intent { + copy( + searchKeyword = search, + envelopeList = if (search.isBlank()) persistentListOf() else envelopeList, + ) + } + + fun getEnvelopeList(search: String) = viewModelScope.launch { + // TODO: 친구 검색 -> 결과가 있으면 봉투 검색 + // TODO: 카테고리 검색 -> 결과가 있으면 봉투 검색 + } + + private fun updateRecentSearchList(searchList: List) { + intent { + copy(recentSearchKeywordList = searchList.toPersistentList()) + } + } + +} diff --git a/feature/sent/src/main/java/com/susu/feature/sent/SentScreen.kt b/feature/sent/src/main/java/com/susu/feature/sent/SentScreen.kt index e824e0c2..7066d057 100644 --- a/feature/sent/src/main/java/com/susu/feature/sent/SentScreen.kt +++ b/feature/sent/src/main/java/com/susu/feature/sent/SentScreen.kt @@ -43,11 +43,13 @@ fun SentRoute( padding: PaddingValues, navigateSentEnvelope: () -> Unit, navigateSentEnvelopeAdd: () -> Unit, + navigateSentEnvelopeSearch: () -> Unit, ) { SentScreen( padding = padding, onClickHistoryShowAll = navigateSentEnvelope, onClickAddEnvelope = navigateSentEnvelopeAdd, + onClickSearchIcon = navigateSentEnvelopeSearch, ) } diff --git a/feature/sent/src/main/java/com/susu/feature/sent/navigation/SentNavigation.kt b/feature/sent/src/main/java/com/susu/feature/sent/navigation/SentNavigation.kt index 874997fa..aa63e741 100644 --- a/feature/sent/src/main/java/com/susu/feature/sent/navigation/SentNavigation.kt +++ b/feature/sent/src/main/java/com/susu/feature/sent/navigation/SentNavigation.kt @@ -9,6 +9,7 @@ import com.susu.feature.envelope.SentEnvelopeRoute import com.susu.feature.envelopeadd.SentEnvelopeAddRoute import com.susu.feature.envelopedetail.SentEnvelopeDetailRoute import com.susu.feature.envelopeedit.SentEnvelopeEditRoute +import com.susu.feature.envelopesearch.SentEnvelopeSearchRoute import com.susu.feature.sent.SentRoute fun NavController.navigateSent(navOptions: NavOptions) { @@ -31,6 +32,10 @@ fun NavController.navigateSentEnvelopeAdd() { navigate(SentRoute.sentEnvelopeAddRoute) } +fun NavController.navigateSentEnvelopeSearch() { + navigate(SentRoute.sentEnvelopeSearchRoute) +} + fun NavGraphBuilder.sentNavGraph( padding: PaddingValues, popBackStack: () -> Unit, @@ -38,6 +43,7 @@ fun NavGraphBuilder.sentNavGraph( navigateSentEnvelopeDetail: () -> Unit, navigateSentEnvelopeEdit: () -> Unit, navigateSentEnvelopeAdd: () -> Unit, + navigateSentEnvelopeSearch: () -> Unit, handleException: (Throwable, () -> Unit) -> Unit, ) { composable(route = SentRoute.route) { @@ -45,6 +51,7 @@ fun NavGraphBuilder.sentNavGraph( padding = padding, navigateSentEnvelope = navigateSentEnvelope, navigateSentEnvelopeAdd = navigateSentEnvelopeAdd, + navigateSentEnvelopeSearch = navigateSentEnvelopeSearch, ) } @@ -75,6 +82,12 @@ fun NavGraphBuilder.sentNavGraph( handleException = handleException, ) } + + composable(route = SentRoute.sentEnvelopeSearchRoute) { + SentEnvelopeSearchRoute( + popBackStack = popBackStack, + ) + } } object SentRoute { @@ -83,4 +96,5 @@ object SentRoute { const val sentEnvelopeDetailRoute = "sent-envelope-detail" const val sentEnvelopeEditRoute = "sent-envelope-edit" const val sentEnvelopeAddRoute = "sent-envelope-add" + const val sentEnvelopeSearchRoute = "sent-envelope-search" }