diff --git a/app/src/main/java/team/ppac/navigation/FarmemeNavHost.kt b/app/src/main/java/team/ppac/navigation/FarmemeNavHost.kt index 2d7f3ab5..b0ef8f6e 100644 --- a/app/src/main/java/team/ppac/navigation/FarmemeNavHost.kt +++ b/app/src/main/java/team/ppac/navigation/FarmemeNavHost.kt @@ -18,6 +18,8 @@ import team.ppac.search.detail.navigation.navigateToSearchDetail import team.ppac.search.detail.navigation.searchDetailScreen import team.ppac.search.main.navigation.navigateToSearch import team.ppac.search.main.navigation.searchScreen +import team.ppac.search.result.navigation.navigateToSearchResult +import team.ppac.search.result.navigation.searchResultScreen @Composable fun FarmemeNavHost( @@ -41,13 +43,17 @@ fun FarmemeNavHost( ) searchScreen( analyticsHelper = analyticsHelper, - navigateToSearchDetail = { navController.navigateToSearchDetail(it) } + navigateToSearchDetail = { navController.navigateToSearchDetail(it) }, + navigateToSearchResult = { navController.navigateToSearchResult() } ) searchDetailScreen( analyticsHelper = analyticsHelper, navigateBack = { navController.popBackStack() }, navigateToMemeDetail = navigateToDetail ) + searchResultScreen( + navigateBack = { navController.popBackStack() }, + ) myPageScreen( analyticsHelper = analyticsHelper, navigateToDetail = navigateToDetail, diff --git a/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/toolbar/Toolbar.kt b/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/toolbar/Toolbar.kt index 282af9cd..0380800d 100644 --- a/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/toolbar/Toolbar.kt +++ b/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/toolbar/Toolbar.kt @@ -28,13 +28,12 @@ fun FarmemeSearchToolbar( modifier: Modifier = Modifier, text: String, onTextChanged: (String) -> Unit, - navigationIcon: (@Composable () -> Unit)? = null, ) { Row( modifier = modifier .padding( horizontal = 20.dp, - vertical = 10.dp + vertical = 16.dp ) .statusBarsPadding() .fillMaxWidth(), @@ -43,7 +42,7 @@ fun FarmemeSearchToolbar( Box( modifier = Modifier.size(20.dp) ) { - navigationIcon?.invoke() + FarmemeIcon.Back(Modifier.size(20.dp)) } Spacer(modifier = Modifier.size(12.dp)) FarmemeSearchTextField( @@ -127,9 +126,6 @@ private fun PreviewSearchToolbar() { FarmemeSearchToolbar( text = text, onTextChanged = { text = it }, - navigationIcon = { - FarmemeIcon.Back() - } ) } diff --git a/feature/search/src/main/java/team/ppac/search/detail/SearchDetailScreen.kt b/feature/search/src/main/java/team/ppac/search/detail/SearchDetailScreen.kt index 2a6d674a..965ff4c8 100644 --- a/feature/search/src/main/java/team/ppac/search/detail/SearchDetailScreen.kt +++ b/feature/search/src/main/java/team/ppac/search/detail/SearchDetailScreen.kt @@ -74,7 +74,7 @@ internal fun SearchDetailScreen( uiState.isLoading -> { SearchDetailLoadingContent( modifier = Modifier.fillMaxSize(), - uiState = uiState + isLoading = uiState.isLoading ) } diff --git a/feature/search/src/main/java/team/ppac/search/detail/component/SearchDetailLoadingContent.kt b/feature/search/src/main/java/team/ppac/search/detail/component/SearchDetailLoadingContent.kt index 4487bbc1..1273eb68 100644 --- a/feature/search/src/main/java/team/ppac/search/detail/component/SearchDetailLoadingContent.kt +++ b/feature/search/src/main/java/team/ppac/search/detail/component/SearchDetailLoadingContent.kt @@ -21,12 +21,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import team.ppac.common.android.util.showSkeleton import team.ppac.designsystem.foundation.FarmemeRadius -import team.ppac.search.detail.mvi.SearchDetailUiState @Composable internal fun SearchDetailLoadingContent( modifier: Modifier = Modifier, - uiState: SearchDetailUiState, + isLoading: Boolean, ) { Column( modifier = modifier @@ -38,7 +37,7 @@ internal fun SearchDetailLoadingContent( .height(18.dp) .padding(start = 20.dp) .clip(FarmemeRadius.Radius4.shape) - .showSkeleton(isLoading = uiState.isLoading) + .showSkeleton(isLoading = isLoading) ) Spacer(modifier = Modifier.size(20.dp)) Row( @@ -56,7 +55,7 @@ internal fun SearchDetailLoadingContent( .fillMaxWidth() .height(210.dp) .clip(RoundedCornerShape(16.dp)) - .showSkeleton(isLoading = uiState.isLoading) + .showSkeleton(isLoading = isLoading) ) Spacer(modifier = Modifier.size(10.dp)) Box( @@ -64,7 +63,7 @@ internal fun SearchDetailLoadingContent( .fillMaxWidth() .height(18.dp) .clip(FarmemeRadius.Radius4.shape) - .showSkeleton(isLoading = uiState.isLoading) + .showSkeleton(isLoading = isLoading) ) Spacer(modifier = Modifier.size(10.dp)) Box( @@ -72,7 +71,7 @@ internal fun SearchDetailLoadingContent( .width(80.dp) .height(18.dp) .clip(FarmemeRadius.Radius4.shape) - .showSkeleton(isLoading = uiState.isLoading) + .showSkeleton(isLoading = isLoading) ) } } @@ -86,7 +85,7 @@ private fun SearchDetailLoadingContentPreview() { Box(modifier = Modifier.background(Color.White)) { SearchDetailLoadingContent( modifier = Modifier.fillMaxSize(), - uiState = SearchDetailUiState.INITIAL_STATE + isLoading = true ) } } \ No newline at end of file diff --git a/feature/search/src/main/java/team/ppac/search/main/SearchRoute.kt b/feature/search/src/main/java/team/ppac/search/main/SearchRoute.kt index cadf3faa..2735e6a8 100644 --- a/feature/search/src/main/java/team/ppac/search/main/SearchRoute.kt +++ b/feature/search/src/main/java/team/ppac/search/main/SearchRoute.kt @@ -22,6 +22,7 @@ internal fun SearchRoute( analyticsHelper: AnalyticsHelper, viewModel: SearchViewModel = hiltViewModel(), navigateToSearchDetail: (String) -> Unit, + navigateToSearchResult: () -> Unit, ) { ComposableLifecycle { _, event -> when (event) { @@ -38,6 +39,7 @@ internal fun SearchRoute( viewModel.sideEffect.collect { sideEffect -> when (sideEffect) { is SearchSideEffect.NavigateToSearchDetail -> navigateToSearchDetail(sideEffect.argument) + is SearchSideEffect.NavigateToSearchResult -> navigateToSearchResult() } } } diff --git a/feature/search/src/main/java/team/ppac/search/main/SearchViewModel.kt b/feature/search/src/main/java/team/ppac/search/main/SearchViewModel.kt index c62540cd..9669580a 100644 --- a/feature/search/src/main/java/team/ppac/search/main/SearchViewModel.kt +++ b/feature/search/src/main/java/team/ppac/search/main/SearchViewModel.kt @@ -50,7 +50,8 @@ class SearchViewModel @Inject constructor( } is SearchIntent.ClickSearch -> { - showServiceOpenDialog(true) +// showServiceOpenDialog(true) + postSideEffect(SearchSideEffect.NavigateToSearchResult) } is SearchIntent.ClickKeywordCard -> { diff --git a/feature/search/src/main/java/team/ppac/search/main/component/SearchBar.kt b/feature/search/src/main/java/team/ppac/search/main/component/SearchBar.kt index 853b1c41..9b176a58 100644 --- a/feature/search/src/main/java/team/ppac/search/main/component/SearchBar.kt +++ b/feature/search/src/main/java/team/ppac/search/main/component/SearchBar.kt @@ -49,7 +49,7 @@ internal fun FarmemeSearchBar( Spacer(modifier = Modifier.size(12.dp)) Text( modifier = Modifier.fillMaxWidth(), - text = "\uD83D\uDEA7 검색은 오픈 준비 중!", + text = "찾고 싶은 밈 있어?", style = FarmemeTheme.typography.body.large.medium.copy( color = FarmemeTheme.textColor.tertiary ) diff --git a/feature/search/src/main/java/team/ppac/search/main/mvi/SearchSideEffect.kt b/feature/search/src/main/java/team/ppac/search/main/mvi/SearchSideEffect.kt index d710a6f7..0fd78aaa 100644 --- a/feature/search/src/main/java/team/ppac/search/main/mvi/SearchSideEffect.kt +++ b/feature/search/src/main/java/team/ppac/search/main/mvi/SearchSideEffect.kt @@ -3,7 +3,6 @@ package team.ppac.search.main.mvi import team.ppac.common.android.base.UiSideEffect sealed class SearchSideEffect : UiSideEffect { - data class NavigateToSearchDetail( - val argument: String, - ) : SearchSideEffect() + data class NavigateToSearchDetail(val argument: String) : SearchSideEffect() + data object NavigateToSearchResult : SearchSideEffect() } \ No newline at end of file diff --git a/feature/search/src/main/java/team/ppac/search/main/navigation/SearchNavigation.kt b/feature/search/src/main/java/team/ppac/search/main/navigation/SearchNavigation.kt index ad91e7f6..7d9cf872 100644 --- a/feature/search/src/main/java/team/ppac/search/main/navigation/SearchNavigation.kt +++ b/feature/search/src/main/java/team/ppac/search/main/navigation/SearchNavigation.kt @@ -14,13 +14,15 @@ fun NavController.navigateToSearch(navOptions: NavOptions) = navigate(SEARCH_ROU fun NavGraphBuilder.searchScreen( analyticsHelper: AnalyticsHelper, navigateToSearchDetail: (String) -> Unit, + navigateToSearchResult: () -> Unit, ) { composable( route = SEARCH_ROUTE ) { SearchRoute( analyticsHelper = analyticsHelper, - navigateToSearchDetail = navigateToSearchDetail + navigateToSearchDetail = navigateToSearchDetail, + navigateToSearchResult = navigateToSearchResult, ) } } \ No newline at end of file diff --git a/feature/search/src/main/java/team/ppac/search/result/SearchResultRoute.kt b/feature/search/src/main/java/team/ppac/search/result/SearchResultRoute.kt new file mode 100644 index 00000000..889c1ad6 --- /dev/null +++ b/feature/search/src/main/java/team/ppac/search/result/SearchResultRoute.kt @@ -0,0 +1,24 @@ +package team.ppac.search.result + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import team.ppac.common.android.base.BaseComposable + +@Composable +internal fun SearchResultRoute( + modifier: Modifier = Modifier, + viewModel: SearchResultViewModel = hiltViewModel(), +) { + BaseComposable(viewModel = viewModel) { uiState -> + SearchResultScreen( + modifier = modifier, + uiState = uiState, + onQueryChange = viewModel::updateSearchQuery, + handleLoadStates = viewModel::handleLoadErrorStates, + onRetryClick = {}, + onMemeClick = {}, + onCopyClick = { _, _ ->}, + ) + } +} \ No newline at end of file diff --git a/feature/search/src/main/java/team/ppac/search/result/SearchResultScreen.kt b/feature/search/src/main/java/team/ppac/search/result/SearchResultScreen.kt new file mode 100644 index 00000000..83d484ea --- /dev/null +++ b/feature/search/src/main/java/team/ppac/search/result/SearchResultScreen.kt @@ -0,0 +1,91 @@ +package team.ppac.search.result + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.LayoutDirection +import androidx.paging.LoadStates +import team.ppac.common.android.component.error.FarmemeErrorScreen +import team.ppac.common.android.extension.collectPagingItemsWithHandleState +import team.ppac.designsystem.component.scaffold.FarmemeScaffold +import team.ppac.designsystem.component.tabbar.TabBarHeight +import team.ppac.designsystem.component.toolbar.FarmemeSearchToolbar +import team.ppac.search.detail.component.EmptyResultContent +import team.ppac.search.detail.component.SearchDetailLoadingContent +import team.ppac.search.detail.component.SearchDetailResultContent +import team.ppac.search.result.mvi.SearchResultUiState + +@Composable +internal fun SearchResultScreen( + modifier: Modifier = Modifier, + uiState: SearchResultUiState, + handleLoadStates: (LoadStates) -> Unit, + onQueryChange: (String) -> Unit, + onRetryClick: () -> Unit, + onMemeClick: (String) -> Unit, + onCopyClick: (String, String) -> Unit, +) { + val searchResults = uiState.searchResults.collectPagingItemsWithHandleState(handleLoadStates) + + when { + uiState.isError -> { + FarmemeErrorScreen( + modifier = Modifier + .fillMaxSize() + .padding(bottom = TabBarHeight), + title = "정보를 불러오지 못 했어요.\n새로고침 해주세요.", + onRetryClick = onRetryClick + ) + } + + else -> { + FarmemeScaffold( + modifier = modifier.fillMaxSize(), + topBar = { + FarmemeSearchToolbar( + text = uiState.query, + onTextChanged = onQueryChange + ) + } + ) { paddingValues -> + val innerPadding = PaddingValues( + top = paddingValues.calculateTopPadding(), + start = paddingValues.calculateStartPadding(LayoutDirection.Ltr), + end = paddingValues.calculateEndPadding(LayoutDirection.Ltr), + bottom = paddingValues.calculateBottomPadding() + TabBarHeight + ) + + when { + uiState.isLoading -> { + SearchDetailLoadingContent( + modifier = Modifier.fillMaxSize(), + isLoading = uiState.isLoading + ) + } + + else -> { + Column( + modifier = Modifier.padding(innerPadding) + ) { + if (uiState.totalMemeCount == 0) { + EmptyResultContent() + } else { + SearchDetailResultContent( + totalItemCount = uiState.totalMemeCount, + searchResults = searchResults, + onMemeClick = onMemeClick, + onCopyClick = onCopyClick, + ) + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/feature/search/src/main/java/team/ppac/search/result/SearchResultViewModel.kt b/feature/search/src/main/java/team/ppac/search/result/SearchResultViewModel.kt new file mode 100644 index 00000000..8df0261c --- /dev/null +++ b/feature/search/src/main/java/team/ppac/search/result/SearchResultViewModel.kt @@ -0,0 +1,55 @@ +package team.ppac.search.result + +import androidx.lifecycle.SavedStateHandle +import androidx.paging.LoadState +import androidx.paging.LoadStates +import dagger.hilt.android.lifecycle.HiltViewModel +import team.ppac.common.android.base.BaseViewModel +import team.ppac.errorhandling.FarmemeNetworkException +import team.ppac.search.result.mvi.SearchResultIntent +import team.ppac.search.result.mvi.SearchResultSideEffect +import team.ppac.search.result.mvi.SearchResultUiState +import javax.inject.Inject + +@HiltViewModel +class SearchResultViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, +) : BaseViewModel(savedStateHandle) { + + override fun createInitialState(savedStateHandle: SavedStateHandle): SearchResultUiState { + return SearchResultUiState.INITIAL_STATE + } + + override suspend fun handleIntent(intent: SearchResultIntent) { + TODO("Not yet implemented") + } + + override fun handleClientException(throwable: Throwable) { + TODO("Not yet implemented") + } + + fun updateSearchQuery(query: String) { + reduce { copy(query = query) } + } + + fun handleLoadErrorStates(loadStates: LoadStates) { + val errorLoadState = arrayOf( + loadStates.prepend, + loadStates.append, + loadStates.refresh + ).filterIsInstance(LoadState.Error::class.java).firstOrNull() + + val exception = errorLoadState?.error + if (exception != null) { + when (exception) { + is FarmemeNetworkException -> { + updateErrorState(isError = true) + } + } + } + } + + private fun updateErrorState(isError: Boolean) { + reduce { copy(isError = isError) } + } +} \ No newline at end of file diff --git a/feature/search/src/main/java/team/ppac/search/result/mvi/SearchResultIntent.kt b/feature/search/src/main/java/team/ppac/search/result/mvi/SearchResultIntent.kt new file mode 100644 index 00000000..054709d9 --- /dev/null +++ b/feature/search/src/main/java/team/ppac/search/result/mvi/SearchResultIntent.kt @@ -0,0 +1,5 @@ +package team.ppac.search.result.mvi + +import team.ppac.common.android.base.UiIntent + +sealed interface SearchResultIntent : UiIntent \ No newline at end of file diff --git a/feature/search/src/main/java/team/ppac/search/result/mvi/SearchResultSideEffect.kt b/feature/search/src/main/java/team/ppac/search/result/mvi/SearchResultSideEffect.kt new file mode 100644 index 00000000..f91db2a7 --- /dev/null +++ b/feature/search/src/main/java/team/ppac/search/result/mvi/SearchResultSideEffect.kt @@ -0,0 +1,5 @@ +package team.ppac.search.result.mvi + +import team.ppac.common.android.base.UiSideEffect + +sealed interface SearchResultSideEffect : UiSideEffect diff --git a/feature/search/src/main/java/team/ppac/search/result/mvi/SearchResultUiState.kt b/feature/search/src/main/java/team/ppac/search/result/mvi/SearchResultUiState.kt new file mode 100644 index 00000000..321dab35 --- /dev/null +++ b/feature/search/src/main/java/team/ppac/search/result/mvi/SearchResultUiState.kt @@ -0,0 +1,26 @@ +package team.ppac.search.result.mvi + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import team.ppac.common.android.base.UiState +import team.ppac.search.detail.model.SearchResultUiModel + +data class SearchResultUiState( + val isLoading: Boolean, + val isError: Boolean, + val query: String, + val totalMemeCount: Int, + val searchResults: Flow>, +) : UiState { + + companion object { + val INITIAL_STATE = SearchResultUiState( + isLoading = true, + isError = false, + query = "", + totalMemeCount = 0, + searchResults = flowOf(PagingData.empty()), + ) + } +} \ No newline at end of file diff --git a/feature/search/src/main/java/team/ppac/search/result/navigation/SearchResultNavigation.kt b/feature/search/src/main/java/team/ppac/search/result/navigation/SearchResultNavigation.kt new file mode 100644 index 00000000..974c0f68 --- /dev/null +++ b/feature/search/src/main/java/team/ppac/search/result/navigation/SearchResultNavigation.kt @@ -0,0 +1,20 @@ +package team.ppac.search.result.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import team.ppac.search.result.SearchResultRoute + +const val SEARCH_RESULT_ROUTE = "search_result" + +fun NavController.navigateToSearchResult() = navigate(SEARCH_RESULT_ROUTE) + +fun NavGraphBuilder.searchResultScreen( + navigateBack: () -> Unit, +) { + composable( + route = SEARCH_RESULT_ROUTE + ) { + SearchResultRoute() + } +} \ No newline at end of file