From 19d249818bf92e83a5f4e702b5018b42d0c77a0e Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Sun, 8 Sep 2024 20:23:18 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=ED=94=8C=EB=A1=9C=ED=8C=85=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/network/build.gradle.kts | 5 +--- .../network/di/common}/AuthInterceptor.kt | 2 +- .../core/network/di/di}/InterceptorModule.kt | 3 ++- .../core/network/di/di/NetworkModule.kt | 2 -- .../feature/postlist/PostListScreen.kt | 24 ++++++++++++++++++- .../src/main/res/drawable/ic_post_plus.xml | 18 ++++++++++++++ 6 files changed, 45 insertions(+), 9 deletions(-) rename core/{interceptor/src/main/java/com/withpeace/withpeace/core/interceptor => network/src/main/java/com/withpeace/withpeace/core/network/di/common}/AuthInterceptor.kt (98%) rename core/{interceptor/src/main/java/com/withpeace/withpeace/core/interceptor => network/src/main/java/com/withpeace/withpeace/core/network/di/di}/InterceptorModule.kt (84%) create mode 100644 feature/postlist/src/main/res/drawable/ic_post_plus.xml diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 09dc670f..f865698b 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -31,8 +31,5 @@ dependencies { implementation(libs.retrofit.core) implementation(libs.okhttp.logging) implementation(libs.skydoves.sandwich) - kapt(libs.tikxml.processor) - implementation(libs.tikxml.core) - implementation(libs.retrofit.tikxml.converter) - implementation(libs.tikxml.annotation) + implementation(project(":core:datastore")) } diff --git a/core/interceptor/src/main/java/com/withpeace/withpeace/core/interceptor/AuthInterceptor.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/common/AuthInterceptor.kt similarity index 98% rename from core/interceptor/src/main/java/com/withpeace/withpeace/core/interceptor/AuthInterceptor.kt rename to core/network/src/main/java/com/withpeace/withpeace/core/network/di/common/AuthInterceptor.kt index 0b2fdba1..325157e8 100644 --- a/core/interceptor/src/main/java/com/withpeace/withpeace/core/interceptor/AuthInterceptor.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/common/AuthInterceptor.kt @@ -1,4 +1,4 @@ -package com.withpeace.withpeace.core.interceptor +package com.withpeace.withpeace.core.network.di.common import com.skydoves.sandwich.suspendMapSuccess import com.skydoves.sandwich.suspendOnError diff --git a/core/interceptor/src/main/java/com/withpeace/withpeace/core/interceptor/InterceptorModule.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/InterceptorModule.kt similarity index 84% rename from core/interceptor/src/main/java/com/withpeace/withpeace/core/interceptor/InterceptorModule.kt rename to core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/InterceptorModule.kt index b75a642a..d661b6bd 100644 --- a/core/interceptor/src/main/java/com/withpeace/withpeace/core/interceptor/InterceptorModule.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/InterceptorModule.kt @@ -1,6 +1,7 @@ -package com.withpeace.withpeace.core.interceptor +package com.withpeace.withpeace.core.network.di.di import com.withpeace.withpeace.core.datastore.dataStore.token.TokenPreferenceDataSource +import com.withpeace.withpeace.core.network.di.common.AuthInterceptor import com.withpeace.withpeace.core.network.di.service.AuthService import dagger.Module import dagger.Provides diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/NetworkModule.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/NetworkModule.kt index 7b7f1f39..d6e62489 100644 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/NetworkModule.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/NetworkModule.kt @@ -2,8 +2,6 @@ package com.withpeace.withpeace.core.network.di.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.skydoves.sandwich.adapters.ApiResponseCallAdapterFactory -import com.tickaroo.tikxml.TikXml -import com.tickaroo.tikxml.retrofit.TikXmlConverterFactory import com.withpeace.withpeace.core.network.BuildConfig import dagger.Module import dagger.Provides diff --git a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListScreen.kt b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListScreen.kt index f012cb99..8844a2cf 100644 --- a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListScreen.kt +++ b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListScreen.kt @@ -18,8 +18,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Text import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable @@ -94,7 +96,11 @@ fun PostListScreen( onTopicChanged: (PostTopicUiModel) -> Unit = {}, navigateToDetail: (postId: Long) -> Unit = {}, ) { - Column(modifier = Modifier.fillMaxSize().background(WithpeaceTheme.colors.SystemWhite)) { + Column( + modifier = Modifier + .fillMaxSize() + .background(WithpeaceTheme.colors.SystemWhite), + ) { Spacer(modifier = Modifier.height(8.dp)) TopicTabs( currentTopic = currentTopic, @@ -128,6 +134,22 @@ fun PostListScreen( } } } + Box(modifier = Modifier.fillMaxSize()) { + FloatingActionButton( + shape = CircleShape, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(bottom = 16.dp, end = 16.dp), + onClick = { }, + containerColor = WithpeaceTheme.colors.MainPurple, + ) { + Image( + painter = painterResource(id = com.withpeace.withpeace.feature.postlist.R.drawable.ic_post_plus), + contentDescription = "글 작성" + ) + } + } + TrackScreenViewEvent(screenName = "post_list") } diff --git a/feature/postlist/src/main/res/drawable/ic_post_plus.xml b/feature/postlist/src/main/res/drawable/ic_post_plus.xml new file mode 100644 index 00000000..eadaeeec --- /dev/null +++ b/feature/postlist/src/main/res/drawable/ic_post_plus.xml @@ -0,0 +1,18 @@ + + + + From 4640773d1b480a7d19d6c59767742c86e0ef8278 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Sun, 8 Sep 2024 20:26:51 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=EA=B8=80=20=EC=93=B0=EA=B8=B0=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=9D=B4=EB=8F=99=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/withpeace/withpeace/navigation/NavHost.kt | 3 +++ .../withpeace/withpeace/feature/postlist/PostListScreen.kt | 7 ++++++- .../feature/postlist/navigation/PostListNavigation.kt | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt index 90d280e1..0fe14393 100644 --- a/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt +++ b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt @@ -319,6 +319,9 @@ fun WithpeaceNavHost( onAuthExpired = { onAuthExpired(onShowSnackBar, navController) }, + onClickRegisterPost = { + navController.navigateToRegisterPost() + } ) } } diff --git a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListScreen.kt b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListScreen.kt index 8844a2cf..2818edc8 100644 --- a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListScreen.kt +++ b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListScreen.kt @@ -69,6 +69,7 @@ fun PostListRoute( navigateToDetail: (postId: Long) -> Unit, onShowSnackBar: (String) -> Unit, onAuthExpired: () -> Unit, + onClickRegisterPost: () -> Unit = {}, ) { val postListPagingData = viewModel.postListPagingFlow.collectAsLazyPagingItems() val currentTopic by viewModel.currentTopic.collectAsStateWithLifecycle() @@ -77,6 +78,7 @@ fun PostListRoute( postListPagingData = postListPagingData, onTopicChanged = viewModel::onTopicChanged, navigateToDetail = navigateToDetail, + onClickRegisterPost = onClickRegisterPost, ) LaunchedEffect(null) { @@ -95,6 +97,7 @@ fun PostListScreen( postListPagingData: LazyPagingItems, onTopicChanged: (PostTopicUiModel) -> Unit = {}, navigateToDetail: (postId: Long) -> Unit = {}, + onClickRegisterPost: () -> Unit = {}, ) { Column( modifier = Modifier @@ -140,7 +143,9 @@ fun PostListScreen( modifier = Modifier .align(Alignment.BottomEnd) .padding(bottom = 16.dp, end = 16.dp), - onClick = { }, + onClick = { + onClickRegisterPost() + }, containerColor = WithpeaceTheme.colors.MainPurple, ) { Image( diff --git a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/navigation/PostListNavigation.kt b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/navigation/PostListNavigation.kt index 2502843c..edb3ff96 100644 --- a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/navigation/PostListNavigation.kt +++ b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/navigation/PostListNavigation.kt @@ -20,6 +20,7 @@ fun NavGraphBuilder.postListGraph( onShowSnackBar: (String) -> Unit, navigateToPostDetail: (postId: Long) -> Unit, onAuthExpired: () -> Unit, + onClickRegisterPost: () -> Unit = {}, ) { composable( route = POST_LIST_ROUTE, @@ -36,6 +37,7 @@ fun NavGraphBuilder.postListGraph( onShowSnackBar = onShowSnackBar, navigateToDetail = navigateToPostDetail, onAuthExpired = onAuthExpired, + onClickRegisterPost = onClickRegisterPost, ) } } From 744263ec0a9bfa7fa7200095f951bfa01295ff9c Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Wed, 25 Sep 2024 17:27:25 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=EB=B0=94=ED=85=80=EB=B0=94=20?= =?UTF-8?q?=EB=A6=AC=EB=89=B4=EC=96=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + .../withpeace/MainBottomNavigation.kt | 30 +- .../com/withpeace/withpeace/WithpeaceApp.kt | 10 +- .../withpeace/withpeace/navigation/NavHost.kt | 20 + .../main/res/drawable/ic_bottom_community.xml | 14 + .../drawable/ic_bottom_community_select.xml | 16 + ...c_bottom_post.xml => ic_bottom_policy.xml} | 0 ...select.xml => ic_bottom_policy_select.xml} | 0 app/src/main/res/values/strings.xml | 4 +- .../withpeace/feature/home/HomeScreen.kt | 58 -- .../withpeace/feature/home/HomeViewModel.kt | 2 +- feature/policylist/.gitignore | 1 + feature/policylist/build.gradle.kts | 12 + feature/policylist/consumer-rules.pro | 0 feature/policylist/proguard-rules.pro | 21 + .../policylist/ExampleInstrumentedTest.kt | 24 + .../policylist/src/main/AndroidManifest.xml | 4 + .../feature/policylist/PolicyListScreen.kt | 512 ++++++++++++++++++ .../feature/policylist/PolicyListViewModel.kt | 167 ++++++ .../filtersetting/FilterBottomSheet.kt | 342 ++++++++++++ .../uistate/FilterListUiState.kt | 36 ++ .../navigation/PolicyListNavigation.kt | 32 ++ .../uistate/PolicyFiltersUiModel.kt | 28 + .../policylist/uistate/PolicyListUiEvent.kt | 7 + .../policylist/uistate/YouthPolicyUiModel.kt | 43 ++ .../src/main/res/drawable/ic_filter.xml | 12 + .../src/main/res/drawable/ic_filter_close.xml | 9 + .../main/res/drawable/ic_filter_expanded.xml | 12 + .../src/main/res/drawable/ic_filter_fold.xml | 12 + .../src/main/res/drawable/ic_home_logo.xml | 15 + .../drawable/ic_home_thumbnail_example.png | Bin 0 -> 3553 bytes .../src/main/res/drawable/ic_no_result.xml | 21 + .../src/main/res/values/strings.xml | 14 + .../feature/policylist/ExampleUnitTest.kt | 17 + settings.gradle.kts | 1 + 35 files changed, 1410 insertions(+), 87 deletions(-) create mode 100644 app/src/main/res/drawable/ic_bottom_community.xml create mode 100644 app/src/main/res/drawable/ic_bottom_community_select.xml rename app/src/main/res/drawable/{ic_bottom_post.xml => ic_bottom_policy.xml} (100%) rename app/src/main/res/drawable/{ic_bottom_post_select.xml => ic_bottom_policy_select.xml} (100%) create mode 100644 feature/policylist/.gitignore create mode 100644 feature/policylist/build.gradle.kts create mode 100644 feature/policylist/consumer-rules.pro create mode 100644 feature/policylist/proguard-rules.pro create mode 100644 feature/policylist/src/androidTest/java/com/withpeace/withpeace/feature/policylist/ExampleInstrumentedTest.kt create mode 100644 feature/policylist/src/main/AndroidManifest.xml create mode 100644 feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListScreen.kt create mode 100644 feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListViewModel.kt create mode 100644 feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt create mode 100644 feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/uistate/FilterListUiState.kt create mode 100644 feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/navigation/PolicyListNavigation.kt create mode 100644 feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/PolicyFiltersUiModel.kt create mode 100644 feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/PolicyListUiEvent.kt create mode 100644 feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/YouthPolicyUiModel.kt create mode 100644 feature/policylist/src/main/res/drawable/ic_filter.xml create mode 100644 feature/policylist/src/main/res/drawable/ic_filter_close.xml create mode 100644 feature/policylist/src/main/res/drawable/ic_filter_expanded.xml create mode 100644 feature/policylist/src/main/res/drawable/ic_filter_fold.xml create mode 100644 feature/policylist/src/main/res/drawable/ic_home_logo.xml create mode 100644 feature/policylist/src/main/res/drawable/ic_home_thumbnail_example.png create mode 100644 feature/policylist/src/main/res/drawable/ic_no_result.xml create mode 100644 feature/policylist/src/main/res/values/strings.xml create mode 100644 feature/policylist/src/test/java/com/withpeace/withpeace/feature/policylist/ExampleUnitTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b2784c6a..1fb0406c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { implementation(project(":feature:policydetail")) implementation(project(":feature:policybookmarks")) implementation(project(":feature:disablepolicy")) + implementation(project(":feature:policylist")) implementation(project(":core:ui")) implementation(project(":core:interceptor")) implementation(project(":core:data")) diff --git a/app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt b/app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt index d5199f6e..8bb2228e 100644 --- a/app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt +++ b/app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt @@ -1,6 +1,5 @@ package com.withpeace.withpeace -import android.util.Log import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.material3.NavigationBar @@ -19,10 +18,10 @@ import androidx.navigation.navOptions import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme import com.withpeace.withpeace.feature.home.navigation.HOME_ROUTE import com.withpeace.withpeace.feature.home.navigation.navigateHome +import com.withpeace.withpeace.feature.policylist.navigation.POLICY_LIST_ROUTE +import com.withpeace.withpeace.feature.policylist.navigation.navigateToPolicyList import com.withpeace.withpeace.feature.postlist.navigation.POST_LIST_ROUTE import com.withpeace.withpeace.feature.postlist.navigation.navigateToPostList -import com.withpeace.withpeace.feature.registerpost.navigation.REGISTER_POST_ROUTE -import com.withpeace.withpeace.feature.registerpost.navigation.navigateToRegisterPost import com.withpeace.withpeace.navigation.MY_PAGE_NESTED_ROUTE @Composable @@ -79,8 +78,8 @@ private fun NavController.navigateToTabScreen(bottomTab: BottomTab) { when (bottomTab) { BottomTab.HOME -> navigateHome(tabNavOptions) + BottomTab.POLICY -> navigateToPolicyList(tabNavOptions) BottomTab.POST -> navigateToPostList(tabNavOptions) - BottomTab.REGISTER_POST -> navigateToRegisterPost() BottomTab.MY_PAGE -> navigate(MY_PAGE_NESTED_ROUTE, tabNavOptions) } } @@ -97,18 +96,18 @@ enum class BottomTab( contentDescription = R.string.home, HOME_ROUTE, ), + POLICY( + iconUnSelectedResId = R.drawable.ic_bottom_policy, + iconSelectedResId = R.drawable.ic_bottom_policy_select, + contentDescription = R.string.youth_policy, + POLICY_LIST_ROUTE, + ), POST( - iconUnSelectedResId = R.drawable.ic_bottom_post, - iconSelectedResId = R.drawable.ic_bottom_post_select, + iconUnSelectedResId = R.drawable.ic_bottom_community, + iconSelectedResId = R.drawable.ic_bottom_community_select, contentDescription = R.string.post, POST_LIST_ROUTE, ), - REGISTER_POST( - iconUnSelectedResId = R.drawable.ic_upload, - iconSelectedResId = R.drawable.ic_upload, - contentDescription = R.string.register, - REGISTER_POST_ROUTE, - ), MY_PAGE( iconUnSelectedResId = R.drawable.ic_bottom_my_page, iconSelectedResId = R.drawable.ic_bottom_my_page_select, @@ -116,12 +115,5 @@ enum class BottomTab( MY_PAGE_NESTED_ROUTE, ), ; - - companion object { - operator fun contains(route: String): Boolean { - if (route == REGISTER_POST_ROUTE) return false - return entries.map { it.route }.contains(route) - } - } } // https://stackoverflow.com/questions/76721423/compose-navigation-go-to-top-level-destination-when-clicking-on-navigation-bar \ No newline at end of file diff --git a/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt b/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt index a2f190dc..cef33eed 100644 --- a/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt +++ b/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt @@ -57,16 +57,12 @@ fun WithpeaceApp( Scaffold( bottomBar = { - if ( - BottomTab.contains(parentDestination?.route ?: currentDestination?.route ?: "") - ) { - MainBottomBar( + MainBottomBar( currentDestination = if (parentDestination?.route == null) { currentDestination ?: return@Scaffold } else parentDestination, - navController = navController, - ) - } + navController = navController, + ) }, modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt index 0fe14393..5d292e93 100644 --- a/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt +++ b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt @@ -32,6 +32,7 @@ import com.withpeace.withpeace.feature.policyconsent.navigation.navigateToPolicy import com.withpeace.withpeace.feature.policyconsent.navigation.policyConsentGraph import com.withpeace.withpeace.feature.policydetail.navigation.navigateToPolicyDetail import com.withpeace.withpeace.feature.policydetail.navigation.policyDetailNavGraph +import com.withpeace.withpeace.feature.policylist.navigation.policyListGraph import com.withpeace.withpeace.feature.postdetail.navigation.POST_DETAIL_ROUTE_WITH_ARGUMENT import com.withpeace.withpeace.feature.postdetail.navigation.navigateToPostDetail import com.withpeace.withpeace.feature.postdetail.navigation.postDetailGraph @@ -323,6 +324,25 @@ fun WithpeaceNavHost( navController.navigateToRegisterPost() } ) + policyListGraph( + onShowSnackBar = { onShowSnackBar(SnackbarState(it)) }, + onNavigationSnackBar = { + onShowSnackBar( + SnackbarState( + it, + SnackbarType.Navigator( + actionName = "목록 보러가기", + action = { + navController.navigatePolicyBookmarks() + }, + ), + ), + ) + }, + onPolicyClick = { + navController.navigateToPolicyDetail(policyId = it) + }, + ) } } diff --git a/app/src/main/res/drawable/ic_bottom_community.xml b/app/src/main/res/drawable/ic_bottom_community.xml new file mode 100644 index 00000000..ba5734e9 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_community.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_bottom_community_select.xml b/app/src/main/res/drawable/ic_bottom_community_select.xml new file mode 100644 index 00000000..259e6cd6 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_community_select.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_bottom_post.xml b/app/src/main/res/drawable/ic_bottom_policy.xml similarity index 100% rename from app/src/main/res/drawable/ic_bottom_post.xml rename to app/src/main/res/drawable/ic_bottom_policy.xml diff --git a/app/src/main/res/drawable/ic_bottom_post_select.xml b/app/src/main/res/drawable/ic_bottom_policy_select.xml similarity index 100% rename from app/src/main/res/drawable/ic_bottom_post_select.xml rename to app/src/main/res/drawable/ic_bottom_policy_select.xml diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85131293..dca505e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ 청하 - 게시판 + 청년정책 + 커뮤니티 마이페이지 - 등록 diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt index 894f4943..3df1d1d9 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import androidx.hilt.navigation.compose.hiltViewModel @@ -135,63 +134,6 @@ fun HomeScreen( modifier = modifier.height(1.dp), color = WithpeaceTheme.colors.SystemGray3, ) - when(youthPolicies.loadState.refresh) { - is LoadState.Loading -> { - Box(Modifier.fillMaxSize()) { - CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - color = WithpeaceTheme.colors.MainPurple, - ) - } - } - - is LoadState.Error -> { - Box(Modifier.fillMaxSize()) { - Text( - text = "네트워크 상태를 확인해주세요.", - modifier = Modifier.align(Alignment.Center), - ) - } - } - - is LoadState.NotLoading -> { - if (youthPolicies.itemCount == 0) { - Column( - modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(modifier = modifier.height(213.dp)) - Image( - painter = painterResource(id = R.drawable.ic_no_result), - contentDescription = stringResource( - R.string.no_result, - ), - ) - Spacer(modifier = modifier.height(24.dp)) - Text( - text = "조건에 맞는 정책이 없어요.", - style = WithpeaceTheme.typography.semiBold16Sp, - color = WithpeaceTheme.colors.SystemBlack, - letterSpacing = 0.16.sp, - lineHeight = 21.sp, - ) - Spacer(modifier = modifier.height(8.dp)) - Text( - text = "필터 조건을 변경한 후 다시 시도해 보세요.", - style = WithpeaceTheme.typography.caption, - color = WithpeaceTheme.colors.SystemBlack, - ) - } - } else { - PolicyItems( - modifier, - youthPolicies, - onPolicyClick, - onBookmarkClick = onBookmarkClick, - ) - } - } - } } TrackScreenViewEvent(screenName = "home") } diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt index a38bb3d3..eafdfb24 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt @@ -43,8 +43,8 @@ import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @HiltViewModel class HomeViewModel @Inject constructor( - private val youthPoliciesUseCase: GetYouthPoliciesUseCase, private val bookmarkPolicyUseCase: BookmarkPolicyUseCase, + private val youthPoliciesUseCase: GetYouthPoliciesUseCase, ) : ViewModel() { private val bookmarkStateFlow = MutableStateFlow(mapOf()) // paging 처리를 위한 북마크 여부 상태 홀더 diff --git a/feature/policylist/.gitignore b/feature/policylist/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/policylist/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/policylist/build.gradle.kts b/feature/policylist/build.gradle.kts new file mode 100644 index 00000000..0bb33277 --- /dev/null +++ b/feature/policylist/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("convention.feature") +} + +android { + namespace = "com.withpeace.withpeace.feature.policylist" +} + +dependencies { + implementation(libs.androidx.paging.common) + implementation(libs.androidx.pagingCompose) +} \ No newline at end of file diff --git a/feature/policylist/consumer-rules.pro b/feature/policylist/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/policylist/proguard-rules.pro b/feature/policylist/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/policylist/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/policylist/src/androidTest/java/com/withpeace/withpeace/feature/policylist/ExampleInstrumentedTest.kt b/feature/policylist/src/androidTest/java/com/withpeace/withpeace/feature/policylist/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..867cd9cf --- /dev/null +++ b/feature/policylist/src/androidTest/java/com/withpeace/withpeace/feature/policylist/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.withpeace.withpeace.feature.policylist + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.withpeace.withpeace.feature.policylist.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/policylist/src/main/AndroidManifest.xml b/feature/policylist/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/policylist/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListScreen.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListScreen.kt new file mode 100644 index 00000000..7e29fddb --- /dev/null +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListScreen.kt @@ -0,0 +1,512 @@ +package com.withpeace.withpeace.feature.policylist + +import androidx.compose.foundation.Image +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.stopScroll +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +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.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.LoadState +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme +import com.withpeace.withpeace.core.designsystem.util.dropShadow +import com.withpeace.withpeace.core.ui.analytics.TrackScreenViewEvent +import com.withpeace.withpeace.core.ui.bookmark.BookmarkButton +import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel +import com.withpeace.withpeace.core.ui.policy.RegionUiModel +import com.withpeace.withpeace.feature.policylist.filtersetting.FilterBottomSheet +import com.withpeace.withpeace.feature.policylist.uistate.PolicyListUiEvent +import com.withpeace.withpeace.feature.policylist.uistate.PolicyFiltersUiModel +import com.withpeace.withpeace.feature.policylist.uistate.YouthPolicyUiModel +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch + +@Composable +fun PolicyListRoute( + onShowSnackBar: (message: String) -> Unit = {}, + onNavigationSnackBar: (message: String) -> Unit = {}, + viewModel: PolicyListViewModel = hiltViewModel(), + onPolicyClick: (String) -> Unit, +) { + val youthPolicyPagingData = viewModel.youthPolicyPagingFlow.collectAsLazyPagingItems() + val selectedFilterUiState = viewModel.selectingFilters.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = viewModel.uiEvent) { + viewModel.uiEvent.collect { + when (it) { + PolicyListUiEvent.BookmarkSuccess -> { + onNavigationSnackBar("관심 목록에 추가되었습니다.") + } + + PolicyListUiEvent.BookmarkFailure -> { + onShowSnackBar("찜하기에 실패했습니다. 다시 시도해주세요.") + } + + PolicyListUiEvent.UnBookmarkSuccess -> { + onShowSnackBar("관심목록에서 삭제되었습니다.") + } + } + } + } + PolicyListScreen( + youthPolicies = youthPolicyPagingData, + selectedFilterUiState = selectedFilterUiState.value, + onDismissRequest = viewModel::onCancelFilter, + onClassificationCheckChanged = viewModel::onCheckClassification, + onRegionCheckChanged = viewModel::onCheckRegion, + onFilterAllOff = viewModel::onFilterAllOff, + onSearchWithFilter = viewModel::onCompleteFilter, + onCloseFilter = viewModel::onCancelFilter, + onPolicyClick = onPolicyClick, + onBookmarkClick = viewModel::bookmark, + ) +} + +@Composable +fun PolicyListScreen( + modifier: Modifier = Modifier, + youthPolicies: LazyPagingItems, + selectedFilterUiState: PolicyFiltersUiModel, + onDismissRequest: () -> Unit, + onClassificationCheckChanged: (ClassificationUiModel) -> Unit, + onRegionCheckChanged: (RegionUiModel) -> Unit, + onFilterAllOff: () -> Unit, + onSearchWithFilter: () -> Unit, + onCloseFilter: () -> Unit, + onPolicyClick: (String) -> Unit, + onBookmarkClick: (id: String, isChecked: Boolean) -> Unit, + + ) { + Column(modifier = modifier.fillMaxSize()) { + HomeHeader( + modifier = modifier, + onDismissRequest = onDismissRequest, + selectedFilterUiState = selectedFilterUiState, + onClassificationCheckChanged = onClassificationCheckChanged, + onRegionCheckChanged = onRegionCheckChanged, + onFilterAllOff = onFilterAllOff, + onSearchWithFilter = onSearchWithFilter, + onCloseFilter = onCloseFilter, + ) + HorizontalDivider( + modifier = modifier.height(1.dp), + color = WithpeaceTheme.colors.SystemGray3, + ) + when (youthPolicies.loadState.refresh) { + is LoadState.Loading -> { + Box(Modifier.fillMaxSize()) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center), + color = WithpeaceTheme.colors.MainPurple, + ) + } + } + + is LoadState.Error -> { + Box(Modifier.fillMaxSize()) { + Text( + text = "네트워크 상태를 확인해주세요.", + modifier = Modifier.align(Alignment.Center), + ) + } + } + + is LoadState.NotLoading -> { + if (youthPolicies.itemCount == 0) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = modifier.height(213.dp)) + Image( + painter = painterResource(id = R.drawable.ic_no_result), + contentDescription = stringResource( + R.string.no_result, + ), + ) + Spacer(modifier = modifier.height(24.dp)) + Text( + text = "조건에 맞는 정책이 없어요.", + style = WithpeaceTheme.typography.semiBold16Sp, + color = WithpeaceTheme.colors.SystemBlack, + letterSpacing = 0.16.sp, + lineHeight = 21.sp, + ) + Spacer(modifier = modifier.height(8.dp)) + Text( + text = "필터 조건을 변경한 후 다시 시도해 보세요.", + style = WithpeaceTheme.typography.caption, + color = WithpeaceTheme.colors.SystemBlack, + ) + } + } else { + PolicyItems( + modifier, + youthPolicies, + onPolicyClick, + onBookmarkClick = onBookmarkClick, + ) + } + } + } + } + TrackScreenViewEvent(screenName = "policyList") +} + +@Composable +private fun PolicyItems( + modifier: Modifier, + youthPolicies: LazyPagingItems, + onPolicyClick: (String) -> Unit, + onBookmarkClick: (id: String, isChecked: Boolean) -> Unit, +) { + Column( + modifier = modifier + .fillMaxSize() + .background(Color(0xFFF8F9FB)) + .padding(horizontal = 24.dp), + ) { + Spacer(modifier = modifier.height(8.dp)) + LazyColumn( + modifier = modifier + .fillMaxSize() + .testTag("home:policies"), + contentPadding = PaddingValues(bottom = 16.dp), + ) { + items( + count = youthPolicies.itemCount, + key = youthPolicies.itemKey { it.id }, + ) { + val youthPolicy = youthPolicies[it] ?: throw IllegalStateException() + Spacer(modifier = modifier.height(8.dp)) + YouthPolicyCard( + modifier = modifier, + youthPolicy = youthPolicy, + onPolicyClick = onPolicyClick, + onBookmarkClick = onBookmarkClick, + ) + } + item { + if (youthPolicies.loadState.append is LoadState.Loading) { + Column( + modifier = modifier + .padding(top = 8.dp) + .fillMaxWidth() + .background( + Color.Transparent, + ), + ) { + CircularProgressIndicator( + modifier.align(Alignment.CenterHorizontally), + color = WithpeaceTheme.colors.MainPurple, + ) + } + } + } + } + + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun HomeHeader( + modifier: Modifier, + selectedFilterUiState: PolicyFiltersUiModel, + onDismissRequest: () -> Unit, + onClassificationCheckChanged: (ClassificationUiModel) -> Unit, + onRegionCheckChanged: (RegionUiModel) -> Unit, + onFilterAllOff: () -> Unit, + onSearchWithFilter: () -> Unit, + onCloseFilter: () -> Unit, +) { + var showBottomSheet by remember { mutableStateOf(false) } + val bottomSheetChildScrollState = rememberScrollState() + + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ) + val scope = rememberCoroutineScope() + + LaunchedEffect(bottomSheetChildScrollState.canScrollBackward) { + if (bottomSheetChildScrollState.value == 0) { + bottomSheetChildScrollState.stopScroll(MutatePriority.PreventUserInput) + } + } + + Box( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 16.dp), + ) { + Image( + modifier = modifier + .align(Alignment.BottomCenter) + .size(47.dp), + painter = painterResource(id = R.drawable.ic_home_logo), + contentDescription = stringResource(R.string.cheongha_logo), + ) + Image( + modifier = modifier + .size(24.dp) + .align(Alignment.CenterEnd) + .clickable { + showBottomSheet = true + }, + painter = painterResource(id = R.drawable.ic_filter), + contentDescription = stringResource(R.string.filter), + ) + } + if (showBottomSheet) { + ModalBottomSheet( + modifier = modifier, + dragHandle = null, + onDismissRequest = { + onDismissRequest() + showBottomSheet = false + }, + sheetState = sheetState, + ) { + FilterBottomSheet( + modifier = modifier, + scrollState = bottomSheetChildScrollState, + selectedFilterUiState = selectedFilterUiState, + onClassificationCheckChanged = onClassificationCheckChanged, + onRegionCheckChanged = onRegionCheckChanged, + onFilterAllOff = onFilterAllOff, + onSearchWithFilter = { + scope.launch { sheetState.hide() }.invokeOnCompletion { + showBottomSheet = false + onSearchWithFilter() + } + }, + onCloseFilter = { + scope.launch { sheetState.hide() }.invokeOnCompletion { + showBottomSheet = false + onCloseFilter() + } + }, + ) + } + } +} + +@Composable +private fun YouthPolicyCard( + modifier: Modifier, + youthPolicy: YouthPolicyUiModel, + onPolicyClick: (String) -> Unit, + onBookmarkClick: (id: String, isChecked: Boolean) -> Unit, +) { + Card( + modifier = modifier + .fillMaxWidth() + .background( + color = WithpeaceTheme.colors.SystemWhite, + shape = RoundedCornerShape(size = 10.dp), + ) + .dropShadow( + color = Color(0x1A000000), + blurRadius = 4.dp, + spreadRadius = 2.dp, + borderRadius = 10.dp, + ) + .clickable { + onPolicyClick(youthPolicy.id) + }, + ) { + ConstraintLayout( + modifier = modifier + .fillMaxWidth() + .background(WithpeaceTheme.colors.SystemWhite) + .padding(16.dp), + ) { + val ( + title, content, + region, ageRange, thumbnail, heart, + ) = createRefs() + + Text( + text = youthPolicy.title, + modifier.constrainAs( + title, + constrainBlock = { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(thumbnail.start, margin = 8.dp) + width = Dimension.fillToConstraints + }, + ), + color = WithpeaceTheme.colors.SystemBlack, + style = WithpeaceTheme.typography.homePolicyTitle, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + Text( + text = youthPolicy.content, + modifier = modifier + .constrainAs( + content, + constrainBlock = { + top.linkTo(title.bottom, margin = 8.dp) + start.linkTo(parent.start) + end.linkTo(thumbnail.start, margin = 8.dp) + width = Dimension.fillToConstraints + }, + ), + color = WithpeaceTheme.colors.SystemBlack, + style = WithpeaceTheme.typography.homePolicyContent, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + ) + BookmarkButton( + isClicked = youthPolicy.isBookmarked, + modifier = modifier.constrainAs( + heart, + ) { + top.linkTo(content.bottom, margin = 8.dp) + start.linkTo(parent.start) + bottom.linkTo(parent.bottom) + }, + onClick = { isClicked -> + onBookmarkClick(youthPolicy.id, isClicked) + }, + ) + + Text( + text = youthPolicy.region.name, + color = WithpeaceTheme.colors.MainPurple, + modifier = modifier + .constrainAs( + region, + constrainBlock = { + top.linkTo(heart.top) + start.linkTo(heart.end, margin = 8.dp) + bottom.linkTo(heart.bottom) + }, + ) + .background( + color = WithpeaceTheme.colors.SubPurple, + shape = RoundedCornerShape(size = 5.dp), + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + style = WithpeaceTheme.typography.Tag, + ) + + Text( + text = youthPolicy.ageInfo, + color = WithpeaceTheme.colors.SystemGray1, + modifier = modifier + .constrainAs( + ageRange, + constrainBlock = { + top.linkTo(region.top) + start.linkTo(region.end, margin = 8.dp) + bottom.linkTo(region.bottom) + }, + ) + .background( + color = WithpeaceTheme.colors.SystemGray3, + shape = RoundedCornerShape(size = 5.dp), + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + style = WithpeaceTheme.typography.Tag, + ) + + + Image( + modifier = modifier + .size(57.dp) + .clip(RoundedCornerShape(10.dp)) + .constrainAs( + ref = thumbnail, + constrainBlock = { + start.linkTo(title.end) + end.linkTo(parent.end) + top.linkTo(parent.top) + }, + ), + painter = painterResource(id = youthPolicy.classification.drawableResId), + contentDescription = stringResource(R.string.policy_classification_image), + ) + } + } +} + +@Composable +@Preview(showBackground = true) +fun PolicyListPreview() { + WithpeaceTheme { + PolicyListScreen( + youthPolicies = + flowOf( + PagingData.from( + listOf( + YouthPolicyUiModel( + id = "deterruisset", + title = "epicurei", + content = "interdum", + region = RegionUiModel.대구, + ageInfo = "quo", + classification = ClassificationUiModel.JOB, + isBookmarked = false, + ), + ), + ), + ).collectAsLazyPagingItems(), + selectedFilterUiState = PolicyFiltersUiModel(), + onDismissRequest = { }, + onClassificationCheckChanged = {}, + onRegionCheckChanged = {}, + onFilterAllOff = {}, + onSearchWithFilter = {}, + onCloseFilter = {}, + onPolicyClick = {}, + onBookmarkClick = { id, isChecked -> }, + ) + } +} diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListViewModel.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListViewModel.kt new file mode 100644 index 00000000..5e4275ff --- /dev/null +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListViewModel.kt @@ -0,0 +1,167 @@ +package com.withpeace.withpeace.feature.policylist + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.map +import com.withpeace.withpeace.core.domain.extension.groupBy +import com.withpeace.withpeace.core.domain.model.error.ResponseError +import com.withpeace.withpeace.core.domain.model.policy.BookmarkInfo +import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters +import com.withpeace.withpeace.core.domain.usecase.BookmarkPolicyUseCase +import com.withpeace.withpeace.core.domain.usecase.GetYouthPoliciesUseCase +import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel +import com.withpeace.withpeace.core.ui.policy.RegionUiModel +import com.withpeace.withpeace.core.ui.policy.toDomain +import com.withpeace.withpeace.feature.policylist.uistate.PolicyListUiEvent +import com.withpeace.withpeace.feature.policylist.uistate.PolicyFiltersUiModel +import com.withpeace.withpeace.feature.policylist.uistate.YouthPolicyUiModel +import com.withpeace.withpeace.feature.policylist.uistate.toDomain +import com.withpeace.withpeace.feature.policylist.uistate.toUiModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) +@HiltViewModel +class PolicyListViewModel @Inject constructor( + private val bookmarkPolicyUseCase: BookmarkPolicyUseCase, + private val youthPoliciesUseCase: GetYouthPoliciesUseCase, +) : ViewModel() { + private val bookmarkStateFlow = + MutableStateFlow(mapOf()) // paging 처리를 위한 북마크 여부 상태 홀더 + + private val _youthPolicyPagingFlow = MutableStateFlow(PagingData.empty()) + val youthPolicyPagingFlow = + combine( + _youthPolicyPagingFlow.asStateFlow(), + bookmarkStateFlow, + ) { youthPolicyPagingFlow, bookmarkFlow -> + youthPolicyPagingFlow.map { + lastByWhetherSuccessOfBookmarks[it.id] = it.isBookmarked + val bookmarkState = bookmarkFlow[it.id] + it.copy(isBookmarked = bookmarkState ?: it.isBookmarked) + } + }.cachedIn(viewModelScope) + + private val _selectingFilters = MutableStateFlow(PolicyFilters()) + val selectingFilters: StateFlow = + _selectingFilters.map { it.toUiModel() }.stateIn( + scope = viewModelScope, + SharingStarted.WhileSubscribed(), + PolicyFiltersUiModel(), + ) + + private val _uiEvent = Channel() + val uiEvent = _uiEvent.receiveAsFlow() + + private val debounceFlow = MutableSharedFlow(replay = 1) + + private val lastByWhetherSuccessOfBookmarks = + mutableMapOf() // optimistic UI에서 실패시에 사용할 캐시 데이터 + + private var completedFilters = PolicyFilters() + + init { + viewModelScope.launch { + debounceFlow.groupBy { it.id }.flatMapMerge { + it.second.debounce(300L) + }.collectLatest { bookmarkInfo -> // policyBookmarkViewModel과 다른 이유를 찾아보기 + bookmarkPolicyUseCase( + bookmarkInfo.id, bookmarkInfo.isBookmarked, + onError = { + bookmarkStateFlow.update { + it + mapOf( + bookmarkInfo.id to (lastByWhetherSuccessOfBookmarks[bookmarkInfo.id] + ?: !bookmarkInfo.isBookmarked), + ) + } + _uiEvent.send(PolicyListUiEvent.BookmarkFailure) + }, + ).collect { result -> + lastByWhetherSuccessOfBookmarks[result.id] = result.isBookmarked + if (result.isBookmarked) { + _uiEvent.send(PolicyListUiEvent.BookmarkSuccess) + } else { + _uiEvent.send(PolicyListUiEvent.UnBookmarkSuccess) + } + } + + } + } + fetchData() + } + + + private fun fetchData() { + viewModelScope.launch { + _youthPolicyPagingFlow.update { + youthPoliciesUseCase( + filterInfo = completedFilters, + onError = { + when (it) { + ResponseError.EXPIRED_TOKEN_ERROR -> {} + else -> {} + } + }, + ).map { + it.map { youthPolicy -> + youthPolicy.toUiModel() + } + }.cachedIn(viewModelScope).firstOrNull() ?: PagingData.empty() + } + } + } + + fun onCheckClassification(classification: ClassificationUiModel) { + _selectingFilters.update { + it.updateClassification(classification.toDomain()) + } + } + + fun onCheckRegion(region: RegionUiModel) { + _selectingFilters.update { + it.updateRegion(region.toDomain()) + } + } + + fun onCompleteFilter() { + completedFilters = selectingFilters.value.toDomain() + fetchData() + } + + fun onCancelFilter() { + _selectingFilters.update { completedFilters } + } + + fun onFilterAllOff() { + _selectingFilters.update { + it.removeAll() + } + } + + fun bookmark(id: String, isChecked: Boolean) { + bookmarkStateFlow.update { it + mapOf(id to isChecked) } + viewModelScope.launch { + debounceFlow.emit(BookmarkInfo(id, isChecked)) + } + } +} \ No newline at end of file diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt new file mode 100644 index 00000000..e2379282 --- /dev/null +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt @@ -0,0 +1,342 @@ +package com.withpeace.withpeace.feature.policylist.filtersetting + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme +import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel +import com.withpeace.withpeace.core.ui.policy.RegionUiModel +import com.withpeace.withpeace.feature.policylist.filtersetting.uistate.FilterListUiState +import com.withpeace.withpeace.feature.policylist.uistate.PolicyFiltersUiModel +import com.withpeace.withpeace.feature.policylist.R + +@Composable +fun FilterBottomSheet( + scrollState: ScrollState, + modifier: Modifier, + selectedFilterUiState: PolicyFiltersUiModel, + onClassificationCheckChanged: (ClassificationUiModel) -> Unit, + onRegionCheckChanged: (RegionUiModel) -> Unit, + onFilterAllOff: () -> Unit, + onSearchWithFilter: () -> Unit, + onCloseFilter: () -> Unit, +) { + val filterListUiState = + remember { mutableStateOf(FilterListUiState().getStateByFilterState(selectedFilterUiState)) } + val configuration = LocalConfiguration.current + val screenHeight = configuration.screenHeightDp.dp + val footerHeight = remember { mutableStateOf(0.dp) } + val localDensity = LocalDensity.current + Box( + modifier = modifier + .heightIn(0.dp, screenHeight) + .background(WithpeaceTheme.colors.SystemWhite), + ) { + FilterFooter( + modifier = modifier + .align(Alignment.BottomCenter) + .onSizeChanged { + footerHeight.value = with(localDensity) { it.height.toDp() } + }, + onFilterAllOff = onFilterAllOff, + onSearchWithFilter = onSearchWithFilter, + ) + Column( + modifier = modifier + .align(Alignment.TopCenter) + .padding(bottom = footerHeight.value), + ) { + FilterHeader( + modifier = modifier, + onCloseFilter = onCloseFilter, + ) + ScrollableFilterSection( + modifier = modifier, + filterListUiState = filterListUiState.value, + selectedFilterUiState = selectedFilterUiState, + onClassificationCheckChanged = onClassificationCheckChanged, + onRegionCheckChanged = onRegionCheckChanged, + onClassificationMoreViewClick = { + filterListUiState.value = + filterListUiState.value.copy(isClassificationExpanded = !filterListUiState.value.isClassificationExpanded) + }, + onRegionMoreViewClick = { + filterListUiState.value = + filterListUiState.value.copy(isRegionExpanded = !filterListUiState.value.isRegionExpanded) + }, + scrollState = scrollState, + ) + } + } +} + +@Composable +private fun FilterHeader(modifier: Modifier, onCloseFilter: () -> Unit) { + Spacer(modifier = modifier.height(24.dp)) + Row(modifier = modifier.padding(horizontal = 16.dp)) { + Image( + painter = painterResource(id = R.drawable.ic_filter_close), + modifier = modifier.clickable { + onCloseFilter() + }, + contentDescription = stringResource( + R.string.filter_close, + ), + ) + Text( + text = stringResource(id = R.string.filter), + modifier = modifier.padding(start = 8.dp), + style = WithpeaceTheme.typography.title1, + color = WithpeaceTheme.colors.SystemBlack, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider( + modifier = modifier + .fillMaxWidth() + .background(WithpeaceTheme.colors.SystemGray3) + .height(1.dp), + ) +} + +@Composable +private fun ScrollableFilterSection( + modifier: Modifier, + filterListUiState: FilterListUiState, + selectedFilterUiState: PolicyFiltersUiModel, + onClassificationCheckChanged: (ClassificationUiModel) -> Unit, + onRegionCheckChanged: (RegionUiModel) -> Unit, + onClassificationMoreViewClick: () -> Unit, + onRegionMoreViewClick: () -> Unit, + scrollState: ScrollState, +) { + val scrollSectionHeight = remember { mutableStateOf(0.dp) } + val localDensity = LocalDensity.current + val columnModifier = modifier.padding(horizontal = 24.dp) + + Column( + modifier = + if (scrollSectionHeight.value == 0.dp) columnModifier + .onSizeChanged { + if (!filterListUiState.isRegionExpanded && !filterListUiState.isClassificationExpanded) { + scrollSectionHeight.value = with(localDensity) { it.height.toDp() } + } + } + .verticalScroll(scrollState) + else columnModifier + .height(scrollSectionHeight.value) + .verticalScroll(scrollState) + ) { + Spacer(modifier = modifier.height(16.dp)) + Text( + text = stringResource(R.string.policy_classfication), + style = WithpeaceTheme.typography.title2, + color = WithpeaceTheme.colors.SystemBlack, + ) + Spacer(modifier = modifier.height(16.dp)) + Column( + modifier = modifier.animateContentSize( + animationSpec = spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessMedium, + ), + ), + ) { + filterListUiState.getClassifications().forEach { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = + Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = it.stringResId), + style = WithpeaceTheme.typography.body, + color = WithpeaceTheme.colors.SystemBlack, + ) + Checkbox( + colors = CheckboxDefaults.colors( + checkedColor = WithpeaceTheme.colors.MainPurple, + uncheckedColor = WithpeaceTheme.colors.SystemGray2, + checkmarkColor = WithpeaceTheme.colors.SystemWhite, + ), + checked = selectedFilterUiState.classifications.contains(it), + onCheckedChange = { _ -> onClassificationCheckChanged(it) }, + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = modifier + .fillMaxWidth() + .clickable { + onClassificationMoreViewClick() + }, + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(id = if (filterListUiState.isClassificationExpanded) R.string.filter_fold else R.string.filter_expanded), + color = WithpeaceTheme.colors.SystemGray1, + style = WithpeaceTheme.typography.caption, + modifier = modifier.padding(end = 4.dp), + ) + Image( + painterResource(id = if (filterListUiState.isClassificationExpanded) R.drawable.ic_filter_fold else R.drawable.ic_filter_expanded), + contentDescription = stringResource(id = R.string.filter_expanded), + ) + } + Spacer(modifier = modifier.height(16.dp)) + HorizontalDivider( + modifier = modifier.height(1.dp), + color = WithpeaceTheme.colors.SystemGray3, + ) + Spacer(modifier = modifier.height(16.dp)) + Text( + text = stringResource(id = R.string.region), + style = WithpeaceTheme.typography.title2, + color = WithpeaceTheme.colors.SystemBlack, + ) + + Spacer(modifier = modifier.height(16.dp)) + Column( + modifier = modifier.animateContentSize( + animationSpec = spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessMedium, + ), + ), + ) { + filterListUiState.getRegions().forEach { filterItem -> + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = + Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = filterItem.name, + style = WithpeaceTheme.typography.body, + color = WithpeaceTheme.colors.SystemBlack, + ) + Checkbox( + colors = CheckboxDefaults.colors( + checkedColor = WithpeaceTheme.colors.MainPurple, + uncheckedColor = WithpeaceTheme.colors.SystemGray2, + checkmarkColor = WithpeaceTheme.colors.SystemWhite, + ), + checked = selectedFilterUiState.regions.contains(filterItem), + onCheckedChange = { onRegionCheckChanged(filterItem) }, + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = modifier + .fillMaxWidth() + .clickable { + onRegionMoreViewClick() + }, + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource( + if (filterListUiState.isRegionExpanded) R.string.filter_fold else R.string.filter_expanded, + ), + color = WithpeaceTheme.colors.SystemGray1, + style = WithpeaceTheme.typography.caption, + modifier = modifier.padding(end = 4.dp), + ) + Image( + painterResource(id = if (filterListUiState.isRegionExpanded) R.drawable.ic_filter_fold else R.drawable.ic_filter_expanded), + contentDescription = stringResource(id = R.string.filter_expanded), + ) + } + Spacer(modifier = Modifier.height(24.dp)) + } +} + +@Composable +private fun FilterFooter( + modifier: Modifier, + onFilterAllOff: () -> Unit, + onSearchWithFilter: () -> Unit, +) { + Column(modifier = modifier.wrapContentHeight()) { + Spacer( + modifier = modifier + .fillMaxWidth() + .height(4.dp) + .background(WithpeaceTheme.colors.SystemGray3), + ) + Row( + modifier = modifier + .padding(horizontal = 24.dp, vertical = 16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton( + modifier = modifier, + onClick = { onFilterAllOff() }, + ) { + Text( + text = stringResource(R.string.filter_all_off), + color = WithpeaceTheme.colors.SystemGray1, + style = WithpeaceTheme.typography.body, + ) + } + TextButton( + contentPadding = PaddingValues(vertical = 12.dp, horizontal = 32.dp), + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.MainPurple, + contentColor = WithpeaceTheme.colors.SystemWhite, + disabledContainerColor = WithpeaceTheme.colors.MainPurple, + disabledContentColor = WithpeaceTheme.colors.SystemWhite, + ), + shape = RoundedCornerShape(5.dp), + onClick = { onSearchWithFilter() }, + ) { + Text(text = stringResource(R.string.search)) + } + } + } +} + diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/uistate/FilterListUiState.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/uistate/FilterListUiState.kt new file mode 100644 index 00000000..4598fa13 --- /dev/null +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/uistate/FilterListUiState.kt @@ -0,0 +1,36 @@ +package com.withpeace.withpeace.feature.policylist.filtersetting.uistate + +import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel +import com.withpeace.withpeace.core.ui.policy.RegionUiModel +import com.withpeace.withpeace.feature.policylist.uistate.PolicyFiltersUiModel + +data class FilterListUiState( + val isClassificationExpanded: Boolean = false, + val isRegionExpanded: Boolean = false, +) { + private val allClassifications: List = ClassificationUiModel.entries + private val allRegions: List = RegionUiModel.entries + + fun getStateByFilterState(filtersUiModel: PolicyFiltersUiModel): FilterListUiState { + return this.copy( + isClassificationExpanded = allClassifications.indexOf(filtersUiModel.classifications.lastOrNull()) >= FOLDED_CLASSIFICATION_ITEM_COUNT, + isRegionExpanded = allRegions.indexOf(filtersUiModel.regions.lastOrNull()) >= FOLDED_REGION_ITEM_COUNT, + ) + } + + fun getClassifications(): List { + return if (isClassificationExpanded) allClassifications.dropLast(ETC_COUNT) + else allClassifications.subList(0, FOLDED_CLASSIFICATION_ITEM_COUNT) + } + + fun getRegions(): List { + return if (isRegionExpanded) allRegions.dropLast(ETC_COUNT) + else allRegions.subList(0, FOLDED_REGION_ITEM_COUNT) + } + + companion object { + private const val FOLDED_CLASSIFICATION_ITEM_COUNT = 3 + private const val FOLDED_REGION_ITEM_COUNT = 7 + private const val ETC_COUNT = 1 + } +} \ No newline at end of file diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/navigation/PolicyListNavigation.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/navigation/PolicyListNavigation.kt new file mode 100644 index 00000000..c11a2121 --- /dev/null +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/navigation/PolicyListNavigation.kt @@ -0,0 +1,32 @@ +package com.withpeace.withpeace.feature.policylist.navigation + +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.withpeace.withpeace.feature.policylist.PolicyListRoute + +const val POLICY_LIST_ROUTE = "policy_list_route" + +fun NavController.navigateToPolicyList(navOptions: NavOptions? = null) = + navigate(POLICY_LIST_ROUTE, navOptions) + +fun NavGraphBuilder.policyListGraph( + onShowSnackBar: (message: String) -> Unit, + onNavigationSnackBar: (message: String) -> Unit = {}, + onPolicyClick: (String) -> Unit, +) { + composable( + route = POLICY_LIST_ROUTE, + enterTransition = { EnterTransition.None }, + exitTransition = { ExitTransition.None }, + ) { + PolicyListRoute( + onNavigationSnackBar = onNavigationSnackBar, + onShowSnackBar = onShowSnackBar, + onPolicyClick = onPolicyClick, + ) + } +} diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/PolicyFiltersUiModel.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/PolicyFiltersUiModel.kt new file mode 100644 index 00000000..957a5139 --- /dev/null +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/PolicyFiltersUiModel.kt @@ -0,0 +1,28 @@ +package com.withpeace.withpeace.feature.policylist.uistate + +import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters +import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel +import com.withpeace.withpeace.core.ui.policy.RegionUiModel +import com.withpeace.withpeace.core.ui.policy.toDomain +import com.withpeace.withpeace.core.ui.policy.toUiModel + +data class PolicyFiltersUiModel( + val classifications: List = listOf(), + val regions: List = listOf(), +) + +fun PolicyFilters.toUiModel(): PolicyFiltersUiModel { + return PolicyFiltersUiModel( + regions = regions.map { it.toUiModel() }, + classifications = classifications.map { + it.toUiModel() + }, + ) +} + +fun PolicyFiltersUiModel.toDomain(): PolicyFilters { + return PolicyFilters( + regions = regions.map { it.toDomain() }, + classifications = classifications.map { it.toDomain() }, + ) +} \ No newline at end of file diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/PolicyListUiEvent.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/PolicyListUiEvent.kt new file mode 100644 index 00000000..7f4282bf --- /dev/null +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/PolicyListUiEvent.kt @@ -0,0 +1,7 @@ +package com.withpeace.withpeace.feature.policylist.uistate + +sealed interface PolicyListUiEvent { + data object BookmarkSuccess : PolicyListUiEvent + data object BookmarkFailure : PolicyListUiEvent + data object UnBookmarkSuccess : PolicyListUiEvent +} \ No newline at end of file diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/YouthPolicyUiModel.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/YouthPolicyUiModel.kt new file mode 100644 index 00000000..05de0328 --- /dev/null +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/YouthPolicyUiModel.kt @@ -0,0 +1,43 @@ +package com.withpeace.withpeace.feature.policylist.uistate + +import com.withpeace.withpeace.core.domain.model.policy.YouthPolicy +import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel +import com.withpeace.withpeace.core.ui.policy.RegionUiModel +import com.withpeace.withpeace.core.ui.policy.toDomain +import com.withpeace.withpeace.core.ui.policy.toUiModel + +data class YouthPolicyUiModel( + val id: String, + val title: String, + val content: String, + val region: RegionUiModel, + val ageInfo: String, + val classification: ClassificationUiModel, + val isBookmarked: Boolean, +) + +fun YouthPolicy.toUiModel(): YouthPolicyUiModel { + return YouthPolicyUiModel( + id = id, + title = title, + content = introduce, + region = region.toUiModel(), + ageInfo = ageInfo, + classification = policyClassification.toUiModel(), + isBookmarked = isBookmarked, + ) +} + +fun YouthPolicyUiModel.toDomain(): YouthPolicy { + return YouthPolicy( + id = id, + title = title, + introduce = content, + region = region.toDomain(), + policyClassification = classification.toDomain(), + ageInfo = ageInfo, + isBookmarked = isBookmarked, + ) +} + +// 오류처리(Response 값 확인)+ 에러 상황 처리 logout(home 포함) \ No newline at end of file diff --git a/feature/policylist/src/main/res/drawable/ic_filter.xml b/feature/policylist/src/main/res/drawable/ic_filter.xml new file mode 100644 index 00000000..e8ad2a36 --- /dev/null +++ b/feature/policylist/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,12 @@ + + + diff --git a/feature/policylist/src/main/res/drawable/ic_filter_close.xml b/feature/policylist/src/main/res/drawable/ic_filter_close.xml new file mode 100644 index 00000000..3b9176a8 --- /dev/null +++ b/feature/policylist/src/main/res/drawable/ic_filter_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/policylist/src/main/res/drawable/ic_filter_expanded.xml b/feature/policylist/src/main/res/drawable/ic_filter_expanded.xml new file mode 100644 index 00000000..00b563cf --- /dev/null +++ b/feature/policylist/src/main/res/drawable/ic_filter_expanded.xml @@ -0,0 +1,12 @@ + + + diff --git a/feature/policylist/src/main/res/drawable/ic_filter_fold.xml b/feature/policylist/src/main/res/drawable/ic_filter_fold.xml new file mode 100644 index 00000000..fd1a600d --- /dev/null +++ b/feature/policylist/src/main/res/drawable/ic_filter_fold.xml @@ -0,0 +1,12 @@ + + + diff --git a/feature/policylist/src/main/res/drawable/ic_home_logo.xml b/feature/policylist/src/main/res/drawable/ic_home_logo.xml new file mode 100644 index 00000000..cc68f430 --- /dev/null +++ b/feature/policylist/src/main/res/drawable/ic_home_logo.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/feature/policylist/src/main/res/drawable/ic_home_thumbnail_example.png b/feature/policylist/src/main/res/drawable/ic_home_thumbnail_example.png new file mode 100644 index 0000000000000000000000000000000000000000..bac4677ce6899811c2b602a2fae2c4e62730d6bd GIT binary patch literal 3553 zcmV<74Ic7|P)*l?&|95DtA@anKs|r=Ty6K0#sGo3K&-P>8dl-sr~IW zeQWJ1r755+SAa9K8XMPvW{7`jnd}GdnvhMIeI+2z%xRpN!yPC0Ps^rYU0k^{0g4DN zu~34<9WCO5mLR>8rNkIx;SLu+{5Jq8@=pX(<95kw2vS3Fvm{Mxm15Ml1?y`9Qot#l zyov;-j5JkP`NQQ+!^MxtifZjGVs8>9eq!&%&Y<-^FTI_I1ZrOfc+f?r-gUo3>(;K4-V@{PCu`WDg8e|jk8Fgoi=e5o`m>*5=jgqt4jrMQ%~ZBM z##>;nG{fpk(0+u_rf;dl6t(5R>767%LQ0%71;c%1q2-|}HAy)a1@y*d1qH`JZ`Ofr z+pz2awKGq``_9LB{~~_>*cZ@$>=8tk-S!ywZS=OJfbt8dG%Pfl;LImJ6(a*=M|VNA zMD4LXdA)Z(W_XQ)06K^ehm5-sts#yPoIej0TgdAM7BBo14~}0zC(@NBC0>S=3%Fi- z8F|-3)j5b}d>XaIOBl-fxaUv|mB~WoCXCi3TB@5IOTCJti5UvG$cpH*)D!`-7KA@L zV}{yvbMW#$@?K}jv@4KKvxKWpe*@W^gW;)z=sWoiME+Z0*Vgg;(~skn{sjufF(@yW zm}Cm*X3gCUR5TgK$tD`4y~2${Y`k<1MIB+$>cg?I5SuuJ+YVBBdA#Al2eJ6_MJ&x- z#^%B-ibEqDlY{=j40E21*c(oQg9IWm=~jSIt&l;VR7o6dffaC3L{p{7OT$98oyBF? zo164;joIQ9{(HniwV6dH(iKQw<#Ek@5UYL({Tn~QWb_=uYdKuJmdF3!&0I+Hj0&T$ z`4E66x-w8uKfI=av7(KELJk=!E3#wk4IxH@24J#Zzj6t#<#RF|Yz&U$=)^ewRPQX5 zbOlmA-^q_*`c04F_=}HYq1K z2Veid5gaUdyxEOhsf{MNNv_leD|Rl;n0%>8{YrL(%-CJ1EbER)P*>44NdSr(YoIr} zgsf*HL$!Ezk|c$Z+C7KOHAqSL_s?(PThG-XqnqE3@zvj#I025~tt}CNVHBEx6AMBj ztzSOJNpc8M;nDdgaeXc)cSk4ET?#SDm~@T-DC;j7-f0<=Y;jZvac02ypC#$U^6p)6p%8mfBEkOZpg3NV`xul7Sajc6Q${sbVwWv!(wkk4oSP!_a^gkm33oqK%DR z%d}}Fm65w-s?dqF4^qSMO37PFN5(JhO_G3ZjqL`tC5c-nw9U%)a2qkCK)f*snCl|7 zXOhTwQ?2iF}D3h%prkk{D8B3fcJ88%sg_)r1O zd~wm-XUoZA^yDPU`Ds(DMx6!M4$PI47xD74n#RQcd<@aTcQN+thY(aeZ1_cNZ#g*i zrUo3+FpD`d-hCUZWhZ9!pT|!=cocJ+zL?w@aSn$KVdlOhywx)Lu!g7eQR-+L5Jt)(=!D5 zgDVfPPTMu#@uFuY`IeIbz1u##(G#_?1#f$WMJ)-O z&@xj`$)YCNOpzv-=|hvK{y0~s-=%>?TM6S4_!3chzS2;5;=(#wT>{ah<^(8KB0CH7 zyiQ>`VliHvKqg_<_k9rtTU>e%DMp zQ)L@IsZHTpz5HFo+v|ul3*p8Z%B2$+Km{4KiIJkh=_v*Sv0Hc4u$*l%I|4Tit#nWc zuZ5w3NlVtE#<5#g`OT)OM79?fOsWyyTOOFiXCA3w`N$wvtLRGF3OrIOMfEbO)eRgP zAH(5$-bh;Z0M=F(arM$U{Q8AI;@J2V6rF9jgGZ&=fx`m?K4IhBN?THW5>x~XW3!n<$Xs@g3o4h_{RH>VS!A1<$8#jRUa=bl(AH2)Gf1w zYJ?=*8dE)PvNXWY@tZy*J~!s(n40J&u8V7-kF&q{GN$h9$9(ZU=tkNAsSL}w9y5&h z-@_Sx9kNe-lmOwKf$O;Vr{Es4xYh>%~o3pdnv%l4JRueaavldy`4H`OjR`g`iVk%EKa%ulpt=R z$$m~;*?`M6h1J{`Ce>B+6dY`4rqQi*lqZEp_ST-o0HbLy7dMI-3k6$zM?wqdE87pJ zF#1=KwAGOMyZ24cOOBMLmGp95-g7BGgmJZkfnG*y>ka5u+9&975*vG)=gDwb9NAJr z5u7cW3Jv{IQeu(B>sJ90$MGwU-8=!(rK>M3K(b+mroK+%I+6|8FB-w7KGyZ(R@<~L zVJuWuv|s{8s}GPY@TAui4w&b^wN4>G_x504ah@cgxw?ctQp5~8plEuTH3JtjtgZlapcuBW$uyg?>#QBP1lGZooa>t zZK3S;V313BasL}p$sCnJ<`smZU1?jBMr#Z_YsH80+hf1sYSl7^w$EU&@eGC=e?qo# zh3&{bl7-ML%W0J~mStdCCrfcimkag+Z0##*ud!KT=~jkE`c^7T;>5UxQao-fCA6$= zt+lf%#F#7}KCAOU9mY=YVJ!5%+h{l+mr;mzFuvZ4f&317nK7iQHyW~%MIhtVTJM@_ zOVhG?WnNb{;V~L#33y>(3J*->akZ+_?qK)QRdd;5deCO&3d!I(h#mS&!&fM+imiGa zY=pji%uacP;X=TO95YabhUX-05i9j&ZK$01W3;x892H(Q`Il8I&&;{_JR z84ojkC-J%yHkRZ6GJJQxqQ3a6xVw zC}O_WLsRzl{f(E~uE~~Zs~bD7IA>lHX_H+*3?4E^uC&Uk{VVc6->ti2#LC=ugMIDA z8Y<$B7WvY>b^p;@;MxDgBEKlG1gU)E{!SFZB|n`|t>1;n7w+Yk1^?>bNtYNSE+`f% b3KaPZI#L*i0xOH=00000NkvXXu0mjf< + + + + diff --git a/feature/policylist/src/main/res/values/strings.xml b/feature/policylist/src/main/res/values/strings.xml new file mode 100644 index 00000000..7ef98b14 --- /dev/null +++ b/feature/policylist/src/main/res/values/strings.xml @@ -0,0 +1,14 @@ + + + 청하 로고 + 필터 + 필터 닫기 + 더보기 + 정책분야 + 지역 + 전체 해제 + 검색하기 + 접기 + 정책 분류 이미지 + 검색 결과 없음 + \ No newline at end of file diff --git a/feature/policylist/src/test/java/com/withpeace/withpeace/feature/policylist/ExampleUnitTest.kt b/feature/policylist/src/test/java/com/withpeace/withpeace/feature/policylist/ExampleUnitTest.kt new file mode 100644 index 00000000..046cecf1 --- /dev/null +++ b/feature/policylist/src/test/java/com/withpeace/withpeace/feature/policylist/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.withpeace.withpeace.feature.policylist + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 75a93bdc..417d22b7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -44,3 +44,4 @@ include(":feature:termsofservice") include(":benchmark") include(":feature:policybookmarks") include(":feature:disablepolicy") +include(":feature:policylist") From 34e7fa18b3ebbf51bccb7937714a059ceb57ce29 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Fri, 27 Sep 2024 15:50:09 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=ED=99=88=20UI=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/theme/Color.kt | 4 + feature/home/build.gradle.kts | 1 + .../withpeace/feature/home/HomeScreen.kt | 554 ++++++------------ .../withpeace/feature/home/HomeViewModel.kt | 157 ----- .../src/main/res/drawable/ic_circle_info.xml | 11 + .../filtersetting/FilterBottomSheet.kt | 4 +- gradle/libs.versions.toml | 2 + 7 files changed, 211 insertions(+), 522 deletions(-) create mode 100644 feature/home/src/main/res/drawable/ic_circle_info.xml diff --git a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Color.kt index ed063d26..847e1ef5 100644 --- a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Color.kt @@ -26,6 +26,8 @@ val systemGray4 = Color(0xFF696969) val systemError = Color(0xFFF0474B) val systemSuccess = Color(0xFF3BD569) val systemHyperLink = Color(0xFF20BCBB) +val gray3_70 = Color(0xB2ECECEF) +val toolTipBackground = Color(0xFFF9FBFB) val snackbarBlack = Color(0xFF000000) @@ -46,6 +48,8 @@ data class WithPeaceColor( val SystemSuccess: Color = systemSuccess, val SystemHyperLink: Color = systemHyperLink, val SnackbarBlack: Color = snackbarBlack, + val Gray3_70: Color = gray3_70, + val ToolTipBackground: Color = toolTipBackground, ) val lightColor = WithPeaceColor() diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index 939e9427..b23fff45 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -7,6 +7,7 @@ android { } dependencies { + implementation(libs.skydoves.balloon) implementation(libs.androidx.paging.common) implementation(libs.androidx.pagingCompose) } \ No newline at end of file diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt index 3df1d1d9..1e26ed04 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt @@ -1,67 +1,47 @@ package com.withpeace.withpeace.feature.home import androidx.compose.foundation.Image -import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.stopScroll +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row 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.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue 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.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.paging.LoadState -import androidx.paging.PagingData -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemKey +import com.skydoves.balloon.BalloonAnimation +import com.skydoves.balloon.BalloonSizeSpec +import com.skydoves.balloon.compose.Balloon +import com.skydoves.balloon.compose.rememberBalloonBuilder +import com.skydoves.balloon.compose.setBackgroundColor import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme -import com.withpeace.withpeace.core.designsystem.util.dropShadow import com.withpeace.withpeace.core.ui.analytics.TrackScreenViewEvent -import com.withpeace.withpeace.core.ui.bookmark.BookmarkButton -import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel -import com.withpeace.withpeace.core.ui.policy.RegionUiModel -import com.withpeace.withpeace.feature.home.filtersetting.FilterBottomSheet -import com.withpeace.withpeace.feature.home.uistate.HomeUiEvent -import com.withpeace.withpeace.feature.home.uistate.PolicyFiltersUiModel -import com.withpeace.withpeace.feature.home.uistate.YouthPolicyUiModel -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.launch @Composable fun HomeRoute( @@ -70,155 +50,211 @@ fun HomeRoute( viewModel: HomeViewModel = hiltViewModel(), onPolicyClick: (String) -> Unit, ) { - val youthPolicyPagingData = viewModel.youthPolicyPagingFlow.collectAsLazyPagingItems() - val selectedFilterUiState = viewModel.selectingFilters.collectAsStateWithLifecycle() - - LaunchedEffect(key1 = viewModel.uiEvent) { - viewModel.uiEvent.collect { - when (it) { - HomeUiEvent.BookmarkSuccess -> { - onNavigationSnackBar("관심 목록에 추가되었습니다.") - } - - HomeUiEvent.BookmarkFailure -> { - onShowSnackBar("찜하기에 실패했습니다. 다시 시도해주세요.") - } - - HomeUiEvent.UnBookmarkSuccess -> { - onShowSnackBar("관심목록에서 삭제되었습니다.") - } - } - } - } - HomeScreen( - youthPolicies = youthPolicyPagingData, - selectedFilterUiState = selectedFilterUiState.value, - onDismissRequest = viewModel::onCancelFilter, - onClassificationCheckChanged = viewModel::onCheckClassification, - onRegionCheckChanged = viewModel::onCheckRegion, - onFilterAllOff = viewModel::onFilterAllOff, - onSearchWithFilter = viewModel::onCompleteFilter, - onCloseFilter = viewModel::onCancelFilter, - onPolicyClick = onPolicyClick, - onBookmarkClick = viewModel::bookmark - ) + HomeScreen() } @Composable fun HomeScreen( modifier: Modifier = Modifier, - youthPolicies: LazyPagingItems, - selectedFilterUiState: PolicyFiltersUiModel, - onDismissRequest: () -> Unit, - onClassificationCheckChanged: (ClassificationUiModel) -> Unit, - onRegionCheckChanged: (RegionUiModel) -> Unit, - onFilterAllOff: () -> Unit, - onSearchWithFilter: () -> Unit, - onCloseFilter: () -> Unit, - onPolicyClick: (String) -> Unit, - onBookmarkClick: (id: String, isChecked: Boolean) -> Unit, - ) { Column(modifier = modifier.fillMaxSize()) { HomeHeader( modifier = modifier, - onDismissRequest = onDismissRequest, - selectedFilterUiState = selectedFilterUiState, - onClassificationCheckChanged = onClassificationCheckChanged, - onRegionCheckChanged = onRegionCheckChanged, - onFilterAllOff = onFilterAllOff, - onSearchWithFilter = onSearchWithFilter, - onCloseFilter = onCloseFilter, ) HorizontalDivider( modifier = modifier.height(1.dp), color = WithpeaceTheme.colors.SystemGray3, ) + + ScrollSection(modifier) + } TrackScreenViewEvent(screenName = "home") } +@OptIn(ExperimentalLayoutApi::class) @Composable -private fun PolicyItems( - modifier: Modifier, - youthPolicies: LazyPagingItems, - onPolicyClick: (String) -> Unit, - onBookmarkClick: (id: String, isChecked: Boolean) -> Unit, -) { - Column( - modifier = modifier - .fillMaxSize() - .background(Color(0xFFF8F9FB)) - .padding(horizontal = 24.dp), +private fun ScrollSection(modifier: Modifier) { + val builder = rememberBalloonBuilder { + setIsVisibleArrow(false) + setWidth(BalloonSizeSpec.WRAP) + setHeight(BalloonSizeSpec.WRAP) + setPadding(12) + setCornerRadius(12f) + setBackgroundColor(Color(0xFFF9FBFB)) + setBalloonAnimation(BalloonAnimation.FADE) + setArrowSize(0) + } + + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(bottom = 24.dp), ) { - Spacer(modifier = modifier.height(8.dp)) - LazyColumn( - modifier = modifier - .fillMaxSize() - .testTag("home:policies"), - contentPadding = PaddingValues(bottom = 16.dp), - ) { - items( - count = youthPolicies.itemCount, - key = youthPolicies.itemKey { it.id }, + item { + Column( + modifier = modifier + .fillMaxWidth() + .background(color = WithpeaceTheme.colors.Gray3_70) + .padding(horizontal = 24.dp, vertical = 12.dp), ) { - val youthPolicy = youthPolicies[it] ?: throw IllegalStateException() + Row{ + Text( + text = "관심 키워드", + style = WithpeaceTheme.typography.caption, + color = WithpeaceTheme.colors.SystemGray1, + ) + Spacer(modifier = modifier.width(4.dp)) + Balloon( + builder = builder, + balloonContent = { + Text( + text = "선택하신 관심 정책 분야 및 지역으로,\n" + + "핫한 정책 및 맞춤정책 추천 시 해당 키워드 기반으\n로 추천이 진행됩니다.", + color = Color(0x9C101014), + style = WithpeaceTheme.typography.homePolicyContent, + ) + }, + ) { balloonWindow -> + Image( + modifier = modifier.clickable { + balloonWindow.showAsDropDown() + }, + painter = painterResource(id = R.drawable.ic_circle_info), + contentDescription = "", + ) + } + } + Spacer(modifier = modifier.height(8.dp)) - YouthPolicyCard( - modifier = modifier, - youthPolicy = youthPolicy, - onPolicyClick = onPolicyClick, - onBookmarkClick = onBookmarkClick, - ) - } - item { - if (youthPolicies.loadState.append is LoadState.Loading) { - Column( + FlowRow { + Image( + painter = painterResource(id = R.drawable.ic_filter), modifier = modifier - .padding(top = 8.dp) - .fillMaxWidth() .background( - Color.Transparent, - ), - ) { - CircularProgressIndicator( - modifier.align(Alignment.CenterHorizontally), + color = WithpeaceTheme.colors.SubPurple, + shape = CircleShape, + ) + .padding(4.dp) + .size(16.dp), + contentDescription = "", + ) + List(5) { //TODO("데이터 변경") + Spacer(modifier = modifier.width(8.dp)) + Text( + text = "#부산", + style = WithpeaceTheme.typography.Tag, color = WithpeaceTheme.colors.MainPurple, + modifier = modifier + .background( + color = WithpeaceTheme.colors.SubPurple, + shape = RoundedCornerShape(7.dp), + ) + .padding(6.dp), ) } } } + Spacer(modifier = modifier.height(16.dp)) + Text( + modifier = modifier.padding(horizontal = 24.dp), + text = "지금 핫한 정책", + color = WithpeaceTheme.colors.SnackbarBlack, + style = WithpeaceTheme.typography.title2, + ) + Spacer(modifier = modifier.height(16.dp)) + LazyRow( + contentPadding = PaddingValues(horizontal = 24.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(6) { + Column { + Image( + modifier = modifier + .size(140.dp) + .clip(RoundedCornerShape(10.dp)), + painter = painterResource(id = com.withpeace.withpeace.core.ui.R.drawable.ic_policy_participation_right), + contentDescription = "", + ) + Spacer(modifier = modifier.height(8.dp)) + Text( + modifier = modifier.width(140.dp), + text = "울산대학교 대학일자리플러스센터(거점형)", + style = WithpeaceTheme.typography.caption, + color = WithpeaceTheme.colors.SnackbarBlack, + ) + } + } + } + Spacer(modifier = modifier.height(24.dp)) + Text( + modifier = modifier.padding(horizontal = 24.dp), + text = "맞춤 정책을 추천해드릴게요!", + color = WithpeaceTheme.colors.SnackbarBlack, + style = WithpeaceTheme.typography.title2, + ) + Spacer(modifier = modifier.height(8.dp)) + Text( + modifier = modifier.padding(horizontal = 24.dp), + text = "관심 키워드와 추천 알고리즘을 기반으로 정책을 추천해드려요.", + style = WithpeaceTheme.typography.homePolicyContent, + color = WithpeaceTheme.colors.SystemGray1, + ) + Spacer(modifier = modifier.height(16.dp)) + LazyRow( + contentPadding = PaddingValues(horizontal = 24.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(6) { + Column { + Image( + modifier = modifier + .size(140.dp) + .clip(RoundedCornerShape(10.dp)), + painter = painterResource(id = com.withpeace.withpeace.core.ui.R.drawable.ic_policy_participation_right), + contentDescription = "", + ) + Spacer(modifier = modifier.height(8.dp)) + Text( + modifier = modifier.width(140.dp), + text = "울산대학교 대학일자리플러스센터(거점형)", + style = WithpeaceTheme.typography.caption, + color = WithpeaceTheme.colors.SnackbarBlack, + ) + } + } + } + Spacer(modifier = modifier.height(24.dp)) + Text( + modifier = modifier.padding(horizontal = 24.dp), + text = "커뮤니티", + color = WithpeaceTheme.colors.SnackbarBlack, + style = WithpeaceTheme.typography.title2, + ) + Spacer(modifier = modifier.height(16.dp)) + } + items(6) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.padding(start = 24.dp, end = 17.dp), + ) { + Text(text = "자유 게시판") + Spacer(modifier = modifier.width(16.dp)) + Text( + style = WithpeaceTheme.typography.caption, + text = "토트넘 vs K리그 누가 이길 것 같나요? 토트넘 vs K리그 누가 이길 것 같나요?", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } - } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun HomeHeader( modifier: Modifier, - selectedFilterUiState: PolicyFiltersUiModel, - onDismissRequest: () -> Unit, - onClassificationCheckChanged: (ClassificationUiModel) -> Unit, - onRegionCheckChanged: (RegionUiModel) -> Unit, - onFilterAllOff: () -> Unit, - onSearchWithFilter: () -> Unit, - onCloseFilter: () -> Unit, ) { - var showBottomSheet by remember { mutableStateOf(false) } - val bottomSheetChildScrollState = rememberScrollState() - - val sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true, - ) - val scope = rememberCoroutineScope() - - LaunchedEffect(bottomSheetChildScrollState.canScrollBackward) { - if (bottomSheetChildScrollState.value == 0) { - bottomSheetChildScrollState.stopScroll(MutatePriority.PreventUserInput) - } - } - Box( modifier = modifier .fillMaxWidth() @@ -232,223 +268,15 @@ private fun HomeHeader( painter = painterResource(id = R.drawable.ic_home_logo), contentDescription = stringResource(R.string.cheongha_logo), ) - Image( - modifier = modifier - .size(24.dp) - .align(Alignment.CenterEnd) - .clickable { - showBottomSheet = true - }, - painter = painterResource(id = R.drawable.ic_filter), - contentDescription = stringResource(R.string.filter), - ) - } - if (showBottomSheet) { - ModalBottomSheet( - modifier = modifier, - dragHandle = null, - onDismissRequest = { - onDismissRequest() - showBottomSheet = false - }, - sheetState = sheetState, - ) { - FilterBottomSheet( - modifier = modifier, - scrollState = bottomSheetChildScrollState, - selectedFilterUiState = selectedFilterUiState, - onClassificationCheckChanged = onClassificationCheckChanged, - onRegionCheckChanged = onRegionCheckChanged, - onFilterAllOff = onFilterAllOff, - onSearchWithFilter = { - scope.launch { sheetState.hide() }.invokeOnCompletion { - showBottomSheet = false - onSearchWithFilter() - } - }, - onCloseFilter = { - scope.launch { sheetState.hide() }.invokeOnCompletion { - showBottomSheet = false - onCloseFilter() - } - }, - ) - } - } -} - -@Composable -private fun YouthPolicyCard( - modifier: Modifier, - youthPolicy: YouthPolicyUiModel, - onPolicyClick: (String) -> Unit, - onBookmarkClick: (id: String, isChecked: Boolean) -> Unit, -) { - Card( - modifier = modifier - .fillMaxWidth() - .background( - color = WithpeaceTheme.colors.SystemWhite, - shape = RoundedCornerShape(size = 10.dp), - ) - .dropShadow( - color = Color(0x1A000000), - blurRadius = 4.dp, - spreadRadius = 2.dp, - borderRadius = 10.dp, - ) - .clickable { - onPolicyClick(youthPolicy.id) - }, - ) { - ConstraintLayout( - modifier = modifier - .fillMaxWidth() - .background(WithpeaceTheme.colors.SystemWhite) - .padding(16.dp), - ) { - val ( - title, content, - region, ageRange, thumbnail, heart, - ) = createRefs() - - Text( - text = youthPolicy.title, - modifier.constrainAs( - title, - constrainBlock = { - top.linkTo(parent.top) - start.linkTo(parent.start) - end.linkTo(thumbnail.start, margin = 8.dp) - width = Dimension.fillToConstraints - }, - ), - color = WithpeaceTheme.colors.SystemBlack, - style = WithpeaceTheme.typography.homePolicyTitle, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - Text( - text = youthPolicy.content, - modifier = modifier - .constrainAs( - content, - constrainBlock = { - top.linkTo(title.bottom, margin = 8.dp) - start.linkTo(parent.start) - end.linkTo(thumbnail.start, margin = 8.dp) - width = Dimension.fillToConstraints - }, - ), - color = WithpeaceTheme.colors.SystemBlack, - style = WithpeaceTheme.typography.homePolicyContent, - overflow = TextOverflow.Ellipsis, - maxLines = 2, - ) - BookmarkButton( - isClicked = youthPolicy.isBookmarked, - modifier = modifier.constrainAs( - heart, - ) { - top.linkTo(content.bottom, margin = 8.dp) - start.linkTo(parent.start) - bottom.linkTo(parent.bottom) - }, - onClick = { isClicked -> - onBookmarkClick(youthPolicy.id, isClicked) - }, - ) - - Text( - text = youthPolicy.region.name, - color = WithpeaceTheme.colors.MainPurple, - modifier = modifier - .constrainAs( - region, - constrainBlock = { - top.linkTo(heart.top) - start.linkTo(heart.end, margin = 8.dp) - bottom.linkTo(heart.bottom) - }, - ) - .background( - color = WithpeaceTheme.colors.SubPurple, - shape = RoundedCornerShape(size = 5.dp), - ) - .padding(horizontal = 8.dp, vertical = 4.dp), - style = WithpeaceTheme.typography.Tag, - ) - - Text( - text = youthPolicy.ageInfo, - color = WithpeaceTheme.colors.SystemGray1, - modifier = modifier - .constrainAs( - ageRange, - constrainBlock = { - top.linkTo(region.top) - start.linkTo(region.end, margin = 8.dp) - bottom.linkTo(region.bottom) - }, - ) - .background( - color = WithpeaceTheme.colors.SystemGray3, - shape = RoundedCornerShape(size = 5.dp), - ) - .padding(horizontal = 8.dp, vertical = 4.dp), - style = WithpeaceTheme.typography.Tag, - ) - - - Image( - modifier = modifier - .size(57.dp) - .clip(RoundedCornerShape(10.dp)) - .constrainAs( - ref = thumbnail, - constrainBlock = { - start.linkTo(title.end) - end.linkTo(parent.end) - top.linkTo(parent.top) - }, - ), - painter = painterResource(id = youthPolicy.classification.drawableResId), - contentDescription = stringResource(R.string.policy_classification_image), - ) - } - } -} - -@Composable -@Preview(showBackground = true) -fun HomePreview() { - WithpeaceTheme { - HomeScreen( - youthPolicies = - flowOf( - PagingData.from( - listOf( - YouthPolicyUiModel( - id = "deterruisset", - title = "epicurei", - content = "interdum", - region = RegionUiModel.대구, - ageInfo = "quo", - classification = ClassificationUiModel.JOB, - isBookmarked = false, - ), - ), - ), - ).collectAsLazyPagingItems(), - selectedFilterUiState = PolicyFiltersUiModel(), - onDismissRequest = { }, - onClassificationCheckChanged = {}, - onRegionCheckChanged = {}, - onFilterAllOff = {}, - onSearchWithFilter = {}, - onCloseFilter = {}, - onPolicyClick = {}, - onBookmarkClick = { id, isChecked->} - ) + // Image( + // modifier = modifier + // .size(24.dp) + // .align(Alignment.CenterEnd) + // .clickable { + // showBottomSheet = true + // }, + // painter = painterResource(id = R.drawable.ic_filter), + // contentDescription = stringResource(R.string.filter), + // ) } -} +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt index eafdfb24..39209a15 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt @@ -1,167 +1,10 @@ package com.withpeace.withpeace.feature.home import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.PagingData -import androidx.paging.cachedIn -import androidx.paging.map -import com.withpeace.withpeace.core.domain.extension.groupBy -import com.withpeace.withpeace.core.domain.model.error.ResponseError -import com.withpeace.withpeace.core.domain.model.policy.BookmarkInfo -import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters -import com.withpeace.withpeace.core.domain.usecase.BookmarkPolicyUseCase -import com.withpeace.withpeace.core.domain.usecase.GetYouthPoliciesUseCase -import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel -import com.withpeace.withpeace.core.ui.policy.RegionUiModel -import com.withpeace.withpeace.core.ui.policy.toDomain -import com.withpeace.withpeace.feature.home.uistate.HomeUiEvent -import com.withpeace.withpeace.feature.home.uistate.PolicyFiltersUiModel -import com.withpeace.withpeace.feature.home.uistate.YouthPolicyUiModel -import com.withpeace.withpeace.feature.home.uistate.toDomain -import com.withpeace.withpeace.feature.home.uistate.toUiModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flatMapMerge -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import javax.inject.Inject -@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @HiltViewModel class HomeViewModel @Inject constructor( - private val bookmarkPolicyUseCase: BookmarkPolicyUseCase, - private val youthPoliciesUseCase: GetYouthPoliciesUseCase, ) : ViewModel() { - private val bookmarkStateFlow = - MutableStateFlow(mapOf()) // paging 처리를 위한 북마크 여부 상태 홀더 - - private val _youthPolicyPagingFlow = MutableStateFlow(PagingData.empty()) - val youthPolicyPagingFlow = - combine( - _youthPolicyPagingFlow.asStateFlow(), - bookmarkStateFlow, - ) { youthPolicyPagingFlow, bookmarkFlow -> - youthPolicyPagingFlow.map { - lastByWhetherSuccessOfBookmarks[it.id] = it.isBookmarked - val bookmarkState = bookmarkFlow[it.id] - it.copy(isBookmarked = bookmarkState ?: it.isBookmarked) - } - }.cachedIn(viewModelScope) - - private val _selectingFilters = MutableStateFlow(PolicyFilters()) - val selectingFilters: StateFlow = - _selectingFilters.map { it.toUiModel() }.stateIn( - scope = viewModelScope, - SharingStarted.WhileSubscribed(), - PolicyFiltersUiModel(), - ) - - private val _uiEvent = Channel() - val uiEvent = _uiEvent.receiveAsFlow() - - private val debounceFlow = MutableSharedFlow(replay = 1) - - private val lastByWhetherSuccessOfBookmarks = - mutableMapOf() // optimistic UI에서 실패시에 사용할 캐시 데이터 - - private var completedFilters = PolicyFilters() - - init { - viewModelScope.launch { - debounceFlow.groupBy { it.id }.flatMapMerge { - it.second.debounce(300L) - }.collectLatest { bookmarkInfo -> // policyBookmarkViewModel과 다른 이유를 찾아보기 - bookmarkPolicyUseCase( - bookmarkInfo.id, bookmarkInfo.isBookmarked, - onError = { - bookmarkStateFlow.update { - it + mapOf( - bookmarkInfo.id to (lastByWhetherSuccessOfBookmarks[bookmarkInfo.id] - ?: !bookmarkInfo.isBookmarked), - ) - } - _uiEvent.send(HomeUiEvent.BookmarkFailure) - }, - ).collect { result -> - lastByWhetherSuccessOfBookmarks[result.id] = result.isBookmarked - if (result.isBookmarked) { - _uiEvent.send(HomeUiEvent.BookmarkSuccess) - } else { - _uiEvent.send(HomeUiEvent.UnBookmarkSuccess) - } - } - - } - } - fetchData() - } - - - private fun fetchData() { - viewModelScope.launch { - _youthPolicyPagingFlow.update { - youthPoliciesUseCase( - filterInfo = completedFilters, - onError = { - when (it) { - ResponseError.EXPIRED_TOKEN_ERROR -> {} - else -> {} - } - }, - ).map { - it.map { youthPolicy -> - youthPolicy.toUiModel() - } - }.cachedIn(viewModelScope).firstOrNull() ?: PagingData.empty() - } - } - } - - fun onCheckClassification(classification: ClassificationUiModel) { - _selectingFilters.update { - it.updateClassification(classification.toDomain()) - } - } - - fun onCheckRegion(region: RegionUiModel) { - _selectingFilters.update { - it.updateRegion(region.toDomain()) - } - } - - fun onCompleteFilter() { - completedFilters = selectingFilters.value.toDomain() - fetchData() - } - - fun onCancelFilter() { - _selectingFilters.update { completedFilters } - } - - fun onFilterAllOff() { - _selectingFilters.update { - it.removeAll() - } - } - - fun bookmark(id: String, isChecked: Boolean) { - bookmarkStateFlow.update { it + mapOf(id to isChecked) } - viewModelScope.launch { - debounceFlow.emit(BookmarkInfo(id, isChecked)) - } - } } diff --git a/feature/home/src/main/res/drawable/ic_circle_info.xml b/feature/home/src/main/res/drawable/ic_circle_info.xml new file mode 100644 index 00000000..20635334 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_circle_info.xml @@ -0,0 +1,11 @@ + + + diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt index e2379282..e5d25e69 100644 --- a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt @@ -165,7 +165,7 @@ private fun ScrollableFilterSection( Text( text = stringResource(R.string.policy_classfication), style = WithpeaceTheme.typography.title2, - color = WithpeaceTheme.colors.SystemBlack, + color = WithpeaceTheme.colors.SnackbarBlack, ) Spacer(modifier = modifier.height(16.dp)) Column( @@ -230,7 +230,7 @@ private fun ScrollableFilterSection( Text( text = stringResource(id = R.string.region), style = WithpeaceTheme.typography.title2, - color = WithpeaceTheme.colors.SystemBlack, + color = WithpeaceTheme.colors.SnackbarBlack, ) Spacer(modifier = modifier.height(16.dp)) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 86282654..2d63f4bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,7 @@ tikxml = "0.8.13" landscapist = "2.2.13" sandwich = "1.3.7" composeShimmer = "1.0.5" +balloon = "1.6.7" junit4 = "4.13.2" junitVintageEngine = "5.10.0" @@ -131,6 +132,7 @@ skydoves-landscapist-bom = { group = "com.github.skydoves", name = "landscapist- skydoves-landscapist-coil = { group = "com.github.skydoves", name = "landscapist-coil", version.ref = "landscapist" } skydoves-landscapist-glide = { group = "com.github.skydoves", name = "landscapist-glide", version.ref = "landscapist" } skydoves-landscapist-placeholder = { group = "com.github.skydoves", name = "landscapist-placeholder", version.ref = "landscapist" } +skydoves-balloon = { group = "com.github.skydoves", name = "balloon-compose", version.ref = "balloon" } landscapist-animation = { group = "com.github.skydoves", name = "landscapist-animation" } compose-shimmer = { group = "com.valentinilk.shimmer", name = "compose-shimmer", version.ref = "composeShimmer" } skydoves-sandwich = { group = "com.github.skydoves", name = "sandwich", version.ref = "sandwich" } From 88d1487aa61328fe86e86b7a0c18dc086414d2f1 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Sun, 29 Sep 2024 19:23:20 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=ED=95=84=ED=84=B0=20UI=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filtersetting/FilterBottomSheet.kt | 282 +++++++++--------- .../filtersetting}/PolicyFiltersUiModel.kt | 2 +- core/ui/src/main/res/drawable/ic_filter.xml | 12 + .../src/main/res/drawable/ic_filter_close.xml | 9 + core/ui/src/main/res/values/strings.xml | 9 + .../withpeace/feature/home/HomeScreen.kt | 110 ++++++- .../withpeace/feature/home/HomeViewModel.kt | 53 ++++ .../uistate/FilterListUiState.kt | 36 --- .../feature/policylist/PolicyListScreen.kt | 5 +- .../feature/policylist/PolicyListViewModel.kt | 5 +- .../filtersetting/FilterBottomSheet.kt | 273 +++++++++-------- .../filtersetting}/PolicyFiltersUiModel.kt | 3 +- .../uistate/FilterListUiState.kt | 36 --- 13 files changed, 480 insertions(+), 355 deletions(-) rename {feature/home/src/main/java/com/withpeace/withpeace/feature/home => core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy}/filtersetting/FilterBottomSheet.kt (50%) rename {feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate => core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting}/PolicyFiltersUiModel.kt (93%) create mode 100644 core/ui/src/main/res/drawable/ic_filter.xml create mode 100644 core/ui/src/main/res/drawable/ic_filter_close.xml delete mode 100644 feature/home/src/main/java/com/withpeace/withpeace/feature/home/filtersetting/uistate/FilterListUiState.kt rename feature/{home/src/main/java/com/withpeace/withpeace/feature/home/uistate => policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting}/PolicyFiltersUiModel.kt (85%) delete mode 100644 feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/uistate/FilterListUiState.kt diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/filtersetting/FilterBottomSheet.kt b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/FilterBottomSheet.kt similarity index 50% rename from feature/home/src/main/java/com/withpeace/withpeace/feature/home/filtersetting/FilterBottomSheet.kt rename to core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/FilterBottomSheet.kt index 3f02e149..d6173e88 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/filtersetting/FilterBottomSheet.kt +++ b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/FilterBottomSheet.kt @@ -1,8 +1,6 @@ -package com.withpeace.withpeace.feature.home.filtersetting +package com.withpeace.withpeace.core.ui.policy.filtersetting -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background @@ -10,6 +8,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -18,11 +18,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ButtonColors -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -31,6 +30,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity @@ -38,11 +38,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme +import com.withpeace.withpeace.core.ui.R import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel import com.withpeace.withpeace.core.ui.policy.RegionUiModel -import com.withpeace.withpeace.feature.home.R -import com.withpeace.withpeace.feature.home.filtersetting.uistate.FilterListUiState -import com.withpeace.withpeace.feature.home.uistate.PolicyFiltersUiModel @Composable fun FilterBottomSheet( @@ -56,7 +54,14 @@ fun FilterBottomSheet( onCloseFilter: () -> Unit, ) { val filterListUiState = - remember { mutableStateOf(FilterListUiState().getStateByFilterState(selectedFilterUiState)) } + remember { + mutableStateOf( + PolicyFiltersUiModel( + classifications = ClassificationUiModel.entries, + regions = RegionUiModel.entries, + ), + ) + } val configuration = LocalConfiguration.current val screenHeight = configuration.screenHeightDp.dp val footerHeight = remember { mutableStateOf(0.dp) } @@ -90,14 +95,6 @@ fun FilterBottomSheet( selectedFilterUiState = selectedFilterUiState, onClassificationCheckChanged = onClassificationCheckChanged, onRegionCheckChanged = onRegionCheckChanged, - onClassificationMoreViewClick = { - filterListUiState.value = - filterListUiState.value.copy(isClassificationExpanded = !filterListUiState.value.isClassificationExpanded) - }, - onRegionMoreViewClick = { - filterListUiState.value = - filterListUiState.value.copy(isRegionExpanded = !filterListUiState.value.isRegionExpanded) - }, scrollState = scrollState, ) } @@ -133,93 +130,116 @@ private fun FilterHeader(modifier: Modifier, onCloseFilter: () -> Unit) { ) } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun ScrollableFilterSection( modifier: Modifier, - filterListUiState: FilterListUiState, + filterListUiState: PolicyFiltersUiModel, selectedFilterUiState: PolicyFiltersUiModel, onClassificationCheckChanged: (ClassificationUiModel) -> Unit, onRegionCheckChanged: (RegionUiModel) -> Unit, - onClassificationMoreViewClick: () -> Unit, - onRegionMoreViewClick: () -> Unit, scrollState: ScrollState, ) { - val scrollSectionHeight = remember { mutableStateOf(0.dp) } - val localDensity = LocalDensity.current - val columnModifier = modifier.padding(horizontal = 24.dp) - Column( - modifier = - if (scrollSectionHeight.value == 0.dp) columnModifier - .onSizeChanged { - if (!filterListUiState.isRegionExpanded && !filterListUiState.isClassificationExpanded) { - scrollSectionHeight.value = with(localDensity) { it.height.toDp() } - } - } - .verticalScroll(scrollState) - else columnModifier - .height(scrollSectionHeight.value) + modifier = modifier .verticalScroll(scrollState) + .padding(horizontal = 24.dp), ) { Spacer(modifier = modifier.height(16.dp)) Text( - text = stringResource(R.string.policy_classfication), + text = stringResource(R.string.policy_classification_image), style = WithpeaceTheme.typography.title2, - color = WithpeaceTheme.colors.SystemBlack, + color = WithpeaceTheme.colors.SnackbarBlack, ) Spacer(modifier = modifier.height(16.dp)) - Column( - modifier = modifier.animateContentSize( - animationSpec = spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - ), - ) { - filterListUiState.getClassifications().forEach { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = - Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(id = it.stringResId), - style = WithpeaceTheme.typography.body, - color = WithpeaceTheme.colors.SystemBlack, - ) - Checkbox( - colors = CheckboxDefaults.colors( - checkedColor = WithpeaceTheme.colors.MainPurple, - uncheckedColor = WithpeaceTheme.colors.SystemGray2, - checkmarkColor = WithpeaceTheme.colors.SystemWhite, + LazyRow { + items(3) { + val filterItem = filterListUiState.classifications[it] + if (selectedFilterUiState.classifications.contains(filterItem)) { + TextButton( + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.MainPurple, + contentColor = WithpeaceTheme.colors.SystemWhite, + disabledContainerColor = Color.Transparent, + disabledContentColor = WithpeaceTheme.colors.SystemWhite, ), - checked = selectedFilterUiState.classifications.contains(it), - onCheckedChange = { _ -> onClassificationCheckChanged(it) }, - ) + onClick = { onClassificationCheckChanged(filterItem) }, + modifier = modifier + .padding(end = 8.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = stringResource(id = filterItem.stringResId), + ) + } + } else { + TextButton( + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.SystemWhite, + contentColor = WithpeaceTheme.colors.SystemBlack, + disabledContainerColor = Color.Transparent, + disabledContentColor = WithpeaceTheme.colors.SystemBlack, + ), + onClick = { onClassificationCheckChanged(filterItem) }, + border = BorderStroke( + width = 1.dp, + color = WithpeaceTheme.colors.SystemGray2, + ), + modifier = modifier.padding(end = 8.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = stringResource(id = filterItem.stringResId), + color = WithpeaceTheme.colors.SystemBlack, + ) + } } } } - - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = modifier - .fillMaxWidth() - .clickable { - onClassificationMoreViewClick() - }, - horizontalArrangement = Arrangement.Center, - ) { - Text( - text = stringResource(id = if (filterListUiState.isClassificationExpanded) R.string.filter_fold else R.string.filter_expanded), - color = WithpeaceTheme.colors.SystemGray1, - style = WithpeaceTheme.typography.caption, - modifier = modifier.padding(end = 4.dp), - ) - Image( - painterResource(id = if (filterListUiState.isClassificationExpanded) R.drawable.ic_filter_fold else R.drawable.ic_filter_expanded), - contentDescription = stringResource(id = R.string.filter_expanded), - ) + Spacer(modifier = modifier.height(8.dp)) + LazyRow { + items(2) { + val filterItem = filterListUiState.classifications[it + 3] + if (selectedFilterUiState.classifications.contains(filterItem)) { + TextButton( + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.MainPurple, + contentColor = WithpeaceTheme.colors.SystemWhite, + disabledContainerColor = Color.Transparent, + disabledContentColor = WithpeaceTheme.colors.SystemWhite, + ), + onClick = { onClassificationCheckChanged(filterItem) }, + modifier = modifier + .padding(end = 8.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = stringResource(id = filterItem.stringResId), + ) + } + } else { + TextButton( + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.SystemWhite, + contentColor = WithpeaceTheme.colors.SystemBlack, + disabledContainerColor = Color.Transparent, + disabledContentColor = WithpeaceTheme.colors.SystemBlack, + ), + onClick = { onClassificationCheckChanged(filterItem) }, + border = BorderStroke( + width = 1.dp, + color = WithpeaceTheme.colors.SystemGray2, + ), + modifier = modifier.padding(end = 8.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = stringResource(id = filterItem.stringResId), + color = WithpeaceTheme.colors.SystemBlack, + ) + } + } + } } Spacer(modifier = modifier.height(16.dp)) HorizontalDivider( @@ -230,66 +250,54 @@ private fun ScrollableFilterSection( Text( text = stringResource(id = R.string.region), style = WithpeaceTheme.typography.title2, - color = WithpeaceTheme.colors.SystemBlack, + color = WithpeaceTheme.colors.SnackbarBlack, ) Spacer(modifier = modifier.height(16.dp)) - Column( - modifier = modifier.animateContentSize( - animationSpec = spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - ), - ) { - filterListUiState.getRegions().forEach { filterItem -> - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = - Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = filterItem.name, - style = WithpeaceTheme.typography.body, - color = WithpeaceTheme.colors.SystemBlack, - ) - Checkbox( - colors = CheckboxDefaults.colors( - checkedColor = WithpeaceTheme.colors.MainPurple, - uncheckedColor = WithpeaceTheme.colors.SystemGray2, - checkmarkColor = WithpeaceTheme.colors.SystemWhite, + FlowRow { + filterListUiState.regions.dropLast(1).forEach { filterItem -> + if (selectedFilterUiState.regions.contains(filterItem)) { + TextButton( + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.MainPurple, + contentColor = WithpeaceTheme.colors.SystemWhite, + disabledContainerColor = Color.Transparent, + disabledContentColor = WithpeaceTheme.colors.SystemWhite, ), - checked = selectedFilterUiState.regions.contains(filterItem), - onCheckedChange = { onRegionCheckChanged(filterItem) }, - ) + onClick = { onRegionCheckChanged(filterItem) }, + modifier = modifier + .padding(end = 8.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = filterItem.name, + ) + } + } else { + TextButton( + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.SystemWhite, + contentColor = WithpeaceTheme.colors.SystemBlack, + disabledContainerColor = Color.Transparent, + disabledContentColor = WithpeaceTheme.colors.SystemBlack, + ), + onClick = { onRegionCheckChanged(filterItem) }, + border = BorderStroke( + width = 1.dp, + color = WithpeaceTheme.colors.SystemGray2, + ), + modifier = modifier.padding(end = 8.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = filterItem.name, + color = WithpeaceTheme.colors.SystemBlack, + ) + } } } } - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = modifier - .fillMaxWidth() - .clickable { - onRegionMoreViewClick() - }, - horizontalArrangement = Arrangement.Center, - ) { - Text( - text = stringResource( - if (filterListUiState.isRegionExpanded) R.string.filter_fold else R.string.filter_expanded, - ), - color = WithpeaceTheme.colors.SystemGray1, - style = WithpeaceTheme.typography.caption, - modifier = modifier.padding(end = 4.dp), - ) - Image( - painterResource(id = if (filterListUiState.isRegionExpanded) R.drawable.ic_filter_fold else R.drawable.ic_filter_expanded), - contentDescription = stringResource(id = R.string.filter_expanded), - ) - } - Spacer(modifier = Modifier.height(24.dp)) } } diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/PolicyFiltersUiModel.kt b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/PolicyFiltersUiModel.kt similarity index 93% rename from feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/PolicyFiltersUiModel.kt rename to core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/PolicyFiltersUiModel.kt index 957a5139..e2ad44f5 100644 --- a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/uistate/PolicyFiltersUiModel.kt +++ b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/PolicyFiltersUiModel.kt @@ -1,4 +1,4 @@ -package com.withpeace.withpeace.feature.policylist.uistate +package com.withpeace.withpeace.core.ui.policy.filtersetting import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel diff --git a/core/ui/src/main/res/drawable/ic_filter.xml b/core/ui/src/main/res/drawable/ic_filter.xml new file mode 100644 index 00000000..e8ad2a36 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_filter_close.xml b/core/ui/src/main/res/drawable/ic_filter_close.xml new file mode 100644 index 00000000..3b9176a8 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_filter_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 9c31af9b..f0ff3d48 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -30,4 +30,13 @@ 복지,문화 참여,권리 기타 + + + 필터 + 필터 닫기 + 더보기 + 정책 분류 이미지 + 지역 + 전체 해제 + 검색하기 diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt index 1e26ed04..e6cb6ab2 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt @@ -1,8 +1,10 @@ package com.withpeace.withpeace.feature.home import androidx.compose.foundation.Image +import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.stopScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -19,13 +21,21 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape 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.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -35,6 +45,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.skydoves.balloon.BalloonAnimation import com.skydoves.balloon.BalloonSizeSpec import com.skydoves.balloon.compose.Balloon @@ -42,20 +53,41 @@ import com.skydoves.balloon.compose.rememberBalloonBuilder import com.skydoves.balloon.compose.setBackgroundColor import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme import com.withpeace.withpeace.core.ui.analytics.TrackScreenViewEvent +import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel +import com.withpeace.withpeace.core.ui.policy.RegionUiModel +import com.withpeace.withpeace.core.ui.policy.filtersetting.FilterBottomSheet +import com.withpeace.withpeace.core.ui.policy.filtersetting.PolicyFiltersUiModel +import kotlinx.coroutines.launch @Composable fun HomeRoute( onShowSnackBar: (message: String) -> Unit = {}, - onNavigationSnackBar: (message: String) -> Unit = {}, viewModel: HomeViewModel = hiltViewModel(), + onNavigationSnackBar: (message: String) -> Unit = {}, onPolicyClick: (String) -> Unit, ) { - HomeScreen() + val selectedFilterUiState = viewModel.selectingFilters.collectAsStateWithLifecycle() + HomeScreen( + selectedFilterUiState = selectedFilterUiState.value, + onClassificationCheckChanged = viewModel::onCheckClassification, + onRegionCheckChanged = viewModel::onCheckRegion, + onFilterAllOff = viewModel::onFilterAllOff, + onSearchWithFilter = viewModel::onCompleteFilter, + onCloseFilter = viewModel::onCancelFilter, + onDismissRequest = viewModel::onCancelFilter, + ) } @Composable fun HomeScreen( modifier: Modifier = Modifier, + selectedFilterUiState: PolicyFiltersUiModel, + onDismissRequest: () -> Unit, + onClassificationCheckChanged: (ClassificationUiModel) -> Unit, + onRegionCheckChanged: (RegionUiModel) -> Unit, + onFilterAllOff: () -> Unit, + onSearchWithFilter: () -> Unit, + onCloseFilter: () -> Unit, ) { Column(modifier = modifier.fillMaxSize()) { HomeHeader( @@ -65,16 +97,32 @@ fun HomeScreen( modifier = modifier.height(1.dp), color = WithpeaceTheme.colors.SystemGray3, ) - - ScrollSection(modifier) + ScrollSection( + selectedFilterUiState = selectedFilterUiState, + onDismissRequest = onDismissRequest, + onClassificationCheckChanged = onClassificationCheckChanged, + onRegionCheckChanged = onRegionCheckChanged, + onFilterAllOff = onFilterAllOff, + onSearchWithFilter = onSearchWithFilter, + onCloseFilter = onCloseFilter, + ) } TrackScreenViewEvent(screenName = "home") } -@OptIn(ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable -private fun ScrollSection(modifier: Modifier) { +private fun ScrollSection( + modifier: Modifier = Modifier, + selectedFilterUiState: PolicyFiltersUiModel, + onDismissRequest: () -> Unit, + onClassificationCheckChanged: (ClassificationUiModel) -> Unit, + onRegionCheckChanged: (RegionUiModel) -> Unit, + onFilterAllOff: () -> Unit, + onSearchWithFilter: () -> Unit, + onCloseFilter: () -> Unit, +) { val builder = rememberBalloonBuilder { setIsVisibleArrow(false) setWidth(BalloonSizeSpec.WRAP) @@ -85,7 +133,52 @@ private fun ScrollSection(modifier: Modifier) { setBalloonAnimation(BalloonAnimation.FADE) setArrowSize(0) } + var showBottomSheet by remember { mutableStateOf(false) } + val bottomSheetChildScrollState = rememberScrollState() + + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ) + val scope = rememberCoroutineScope() + LaunchedEffect(bottomSheetChildScrollState.canScrollBackward) { + if (bottomSheetChildScrollState.value == 0) { + bottomSheetChildScrollState.stopScroll(MutatePriority.PreventUserInput) + } + } + + if (showBottomSheet) { + ModalBottomSheet( + modifier = modifier, + dragHandle = null, + onDismissRequest = { + onDismissRequest() + showBottomSheet = false + }, + sheetState = sheetState, + ) { + FilterBottomSheet( + modifier = modifier, + scrollState = bottomSheetChildScrollState, + selectedFilterUiState = selectedFilterUiState, + onClassificationCheckChanged = onClassificationCheckChanged, + onRegionCheckChanged = onRegionCheckChanged, + onFilterAllOff = onFilterAllOff, + onSearchWithFilter = { + scope.launch { sheetState.hide() }.invokeOnCompletion { + showBottomSheet = false + onSearchWithFilter() + } + }, + onCloseFilter = { + scope.launch { sheetState.hide() }.invokeOnCompletion { + showBottomSheet = false + onCloseFilter() + } + }, + ) + } + } LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), @@ -136,7 +229,10 @@ private fun ScrollSection(modifier: Modifier) { shape = CircleShape, ) .padding(4.dp) - .size(16.dp), + .size(16.dp) + .clickable { + showBottomSheet = true + }, contentDescription = "", ) List(5) { //TODO("데이터 변경") diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt index 39209a15..82ae027d 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt @@ -1,10 +1,63 @@ package com.withpeace.withpeace.feature.home import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters +import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel +import com.withpeace.withpeace.core.ui.policy.RegionUiModel +import com.withpeace.withpeace.core.ui.policy.filtersetting.PolicyFiltersUiModel +import com.withpeace.withpeace.core.ui.policy.filtersetting.toDomain +import com.withpeace.withpeace.core.ui.policy.filtersetting.toUiModel +import com.withpeace.withpeace.core.ui.policy.toDomain import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( ) : ViewModel() { + private val _selectingFilters = MutableStateFlow(PolicyFilters()) + val selectingFilters: StateFlow = + _selectingFilters.map { it.toUiModel() }.stateIn( + scope = viewModelScope, + SharingStarted.WhileSubscribed(), + PolicyFiltersUiModel(), + ) + + private var completedFilters = PolicyFilters() + + init { + } + + fun onCheckClassification(classification: ClassificationUiModel) { + _selectingFilters.update { + it.updateClassification(classification.toDomain()) + } + } + + fun onCheckRegion(region: RegionUiModel) { + _selectingFilters.update { + it.updateRegion(region.toDomain()) + } + } + + fun onCompleteFilter() { + completedFilters = selectingFilters.value.toDomain() + // api + } + + fun onCancelFilter() { + _selectingFilters.update { completedFilters } + } + + fun onFilterAllOff() { + _selectingFilters.update { + it.removeAll() + } + } } diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/filtersetting/uistate/FilterListUiState.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/filtersetting/uistate/FilterListUiState.kt deleted file mode 100644 index 50e7e366..00000000 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/filtersetting/uistate/FilterListUiState.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.withpeace.withpeace.feature.home.filtersetting.uistate - -import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel -import com.withpeace.withpeace.core.ui.policy.RegionUiModel -import com.withpeace.withpeace.feature.home.uistate.PolicyFiltersUiModel - -data class FilterListUiState( - val isClassificationExpanded: Boolean = false, - val isRegionExpanded: Boolean = false, -) { - private val allClassifications: List = ClassificationUiModel.entries - private val allRegions: List = RegionUiModel.entries - - fun getStateByFilterState(filtersUiModel: PolicyFiltersUiModel): FilterListUiState { - return this.copy( - isClassificationExpanded = allClassifications.indexOf(filtersUiModel.classifications.lastOrNull()) >= FOLDED_CLASSIFICATION_ITEM_COUNT, - isRegionExpanded = allRegions.indexOf(filtersUiModel.regions.lastOrNull()) >= FOLDED_REGION_ITEM_COUNT, - ) - } - - fun getClassifications(): List { - return if (isClassificationExpanded) allClassifications.dropLast(ETC_COUNT) - else allClassifications.subList(0, FOLDED_CLASSIFICATION_ITEM_COUNT) - } - - fun getRegions(): List { - return if (isRegionExpanded) allRegions.dropLast(ETC_COUNT) - else allRegions.subList(0, FOLDED_REGION_ITEM_COUNT) - } - - companion object { - private const val FOLDED_CLASSIFICATION_ITEM_COUNT = 3 - private const val FOLDED_REGION_ITEM_COUNT = 7 - private const val ETC_COUNT = 1 - } -} \ No newline at end of file diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListScreen.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListScreen.kt index 7e29fddb..c7eeba42 100644 --- a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListScreen.kt +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListScreen.kt @@ -57,9 +57,9 @@ import com.withpeace.withpeace.core.ui.analytics.TrackScreenViewEvent import com.withpeace.withpeace.core.ui.bookmark.BookmarkButton import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel import com.withpeace.withpeace.core.ui.policy.RegionUiModel -import com.withpeace.withpeace.feature.policylist.filtersetting.FilterBottomSheet +import com.withpeace.withpeace.core.ui.policy.filtersetting.FilterBottomSheet import com.withpeace.withpeace.feature.policylist.uistate.PolicyListUiEvent -import com.withpeace.withpeace.feature.policylist.uistate.PolicyFiltersUiModel +import com.withpeace.withpeace.core.ui.policy.filtersetting.PolicyFiltersUiModel import com.withpeace.withpeace.feature.policylist.uistate.YouthPolicyUiModel import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch @@ -457,7 +457,6 @@ private fun YouthPolicyCard( style = WithpeaceTheme.typography.Tag, ) - Image( modifier = modifier .size(57.dp) diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListViewModel.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListViewModel.kt index 5e4275ff..12bec2da 100644 --- a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListViewModel.kt +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListViewModel.kt @@ -14,10 +14,11 @@ import com.withpeace.withpeace.core.domain.usecase.GetYouthPoliciesUseCase import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel import com.withpeace.withpeace.core.ui.policy.RegionUiModel import com.withpeace.withpeace.core.ui.policy.toDomain +import com.withpeace.withpeace.core.ui.policy.filtersetting.PolicyFiltersUiModel +import com.withpeace.withpeace.core.ui.policy.filtersetting.toDomain +import com.withpeace.withpeace.core.ui.policy.filtersetting.toUiModel import com.withpeace.withpeace.feature.policylist.uistate.PolicyListUiEvent -import com.withpeace.withpeace.feature.policylist.uistate.PolicyFiltersUiModel import com.withpeace.withpeace.feature.policylist.uistate.YouthPolicyUiModel -import com.withpeace.withpeace.feature.policylist.uistate.toDomain import com.withpeace.withpeace.feature.policylist.uistate.toUiModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt index e5d25e69..7b45d93f 100644 --- a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt @@ -1,8 +1,6 @@ package com.withpeace.withpeace.feature.policylist.filtersetting -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background @@ -10,6 +8,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -18,11 +18,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ButtonColors -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -31,6 +30,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity @@ -40,8 +40,7 @@ import androidx.compose.ui.unit.dp import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel import com.withpeace.withpeace.core.ui.policy.RegionUiModel -import com.withpeace.withpeace.feature.policylist.filtersetting.uistate.FilterListUiState -import com.withpeace.withpeace.feature.policylist.uistate.PolicyFiltersUiModel +import com.withpeace.withpeace.core.ui.policy.filtersetting.PolicyFiltersUiModel import com.withpeace.withpeace.feature.policylist.R @Composable @@ -56,7 +55,14 @@ fun FilterBottomSheet( onCloseFilter: () -> Unit, ) { val filterListUiState = - remember { mutableStateOf(FilterListUiState().getStateByFilterState(selectedFilterUiState)) } + remember { + mutableStateOf( + PolicyFiltersUiModel( + classifications = ClassificationUiModel.entries, + regions = RegionUiModel.entries, + ), + ) + } val configuration = LocalConfiguration.current val screenHeight = configuration.screenHeightDp.dp val footerHeight = remember { mutableStateOf(0.dp) } @@ -90,14 +96,6 @@ fun FilterBottomSheet( selectedFilterUiState = selectedFilterUiState, onClassificationCheckChanged = onClassificationCheckChanged, onRegionCheckChanged = onRegionCheckChanged, - onClassificationMoreViewClick = { - filterListUiState.value = - filterListUiState.value.copy(isClassificationExpanded = !filterListUiState.value.isClassificationExpanded) - }, - onRegionMoreViewClick = { - filterListUiState.value = - filterListUiState.value.copy(isRegionExpanded = !filterListUiState.value.isRegionExpanded) - }, scrollState = scrollState, ) } @@ -133,33 +131,20 @@ private fun FilterHeader(modifier: Modifier, onCloseFilter: () -> Unit) { ) } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun ScrollableFilterSection( modifier: Modifier, - filterListUiState: FilterListUiState, + filterListUiState: PolicyFiltersUiModel, selectedFilterUiState: PolicyFiltersUiModel, onClassificationCheckChanged: (ClassificationUiModel) -> Unit, onRegionCheckChanged: (RegionUiModel) -> Unit, - onClassificationMoreViewClick: () -> Unit, - onRegionMoreViewClick: () -> Unit, scrollState: ScrollState, ) { - val scrollSectionHeight = remember { mutableStateOf(0.dp) } - val localDensity = LocalDensity.current - val columnModifier = modifier.padding(horizontal = 24.dp) - Column( - modifier = - if (scrollSectionHeight.value == 0.dp) columnModifier - .onSizeChanged { - if (!filterListUiState.isRegionExpanded && !filterListUiState.isClassificationExpanded) { - scrollSectionHeight.value = with(localDensity) { it.height.toDp() } - } - } - .verticalScroll(scrollState) - else columnModifier - .height(scrollSectionHeight.value) + modifier = modifier .verticalScroll(scrollState) + .padding(horizontal = 24.dp), ) { Spacer(modifier = modifier.height(16.dp)) Text( @@ -168,58 +153,94 @@ private fun ScrollableFilterSection( color = WithpeaceTheme.colors.SnackbarBlack, ) Spacer(modifier = modifier.height(16.dp)) - Column( - modifier = modifier.animateContentSize( - animationSpec = spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - ), - ) { - filterListUiState.getClassifications().forEach { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = - Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(id = it.stringResId), - style = WithpeaceTheme.typography.body, - color = WithpeaceTheme.colors.SystemBlack, - ) - Checkbox( - colors = CheckboxDefaults.colors( - checkedColor = WithpeaceTheme.colors.MainPurple, - uncheckedColor = WithpeaceTheme.colors.SystemGray2, - checkmarkColor = WithpeaceTheme.colors.SystemWhite, + LazyRow { + items(3) { + val filterItem = filterListUiState.classifications[it] + if (selectedFilterUiState.classifications.contains(filterItem)) { + TextButton( + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.MainPurple, + contentColor = WithpeaceTheme.colors.SystemWhite, + disabledContainerColor = Color.Transparent, + disabledContentColor = WithpeaceTheme.colors.SystemWhite, ), - checked = selectedFilterUiState.classifications.contains(it), - onCheckedChange = { _ -> onClassificationCheckChanged(it) }, - ) + onClick = { onClassificationCheckChanged(filterItem) }, + modifier = modifier + .padding(end = 8.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = stringResource(id = filterItem.stringResId), + ) + } + } else { + TextButton( + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.SystemWhite, + contentColor = WithpeaceTheme.colors.SystemBlack, + disabledContainerColor = Color.Transparent, + disabledContentColor = WithpeaceTheme.colors.SystemBlack, + ), + onClick = { onClassificationCheckChanged(filterItem) }, + border = BorderStroke( + width = 1.dp, + color = WithpeaceTheme.colors.SystemGray2, + ), + modifier = modifier.padding(end = 8.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = stringResource(id = filterItem.stringResId), + color = WithpeaceTheme.colors.SystemBlack, + ) + } } } } - - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = modifier - .fillMaxWidth() - .clickable { - onClassificationMoreViewClick() - }, - horizontalArrangement = Arrangement.Center, - ) { - Text( - text = stringResource(id = if (filterListUiState.isClassificationExpanded) R.string.filter_fold else R.string.filter_expanded), - color = WithpeaceTheme.colors.SystemGray1, - style = WithpeaceTheme.typography.caption, - modifier = modifier.padding(end = 4.dp), - ) - Image( - painterResource(id = if (filterListUiState.isClassificationExpanded) R.drawable.ic_filter_fold else R.drawable.ic_filter_expanded), - contentDescription = stringResource(id = R.string.filter_expanded), - ) + Spacer(modifier = modifier.height(8.dp)) + LazyRow { + items(2) { + val filterItem = filterListUiState.classifications[it + 3] + if (selectedFilterUiState.classifications.contains(filterItem)) { + TextButton( + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.MainPurple, + contentColor = WithpeaceTheme.colors.SystemWhite, + disabledContainerColor = Color.Transparent, + disabledContentColor = WithpeaceTheme.colors.SystemWhite, + ), + onClick = { onClassificationCheckChanged(filterItem) }, + modifier = modifier + .padding(end = 8.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = stringResource(id = filterItem.stringResId), + ) + } + } else { + TextButton( + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.SystemWhite, + contentColor = WithpeaceTheme.colors.SystemBlack, + disabledContainerColor = Color.Transparent, + disabledContentColor = WithpeaceTheme.colors.SystemBlack, + ), + onClick = { onClassificationCheckChanged(filterItem) }, + border = BorderStroke( + width = 1.dp, + color = WithpeaceTheme.colors.SystemGray2, + ), + modifier = modifier.padding(end = 8.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = stringResource(id = filterItem.stringResId), + color = WithpeaceTheme.colors.SystemBlack, + ) + } + } + } } Spacer(modifier = modifier.height(16.dp)) HorizontalDivider( @@ -234,62 +255,50 @@ private fun ScrollableFilterSection( ) Spacer(modifier = modifier.height(16.dp)) - Column( - modifier = modifier.animateContentSize( - animationSpec = spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium, - ), - ), - ) { - filterListUiState.getRegions().forEach { filterItem -> - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = - Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = filterItem.name, - style = WithpeaceTheme.typography.body, - color = WithpeaceTheme.colors.SystemBlack, - ) - Checkbox( - colors = CheckboxDefaults.colors( - checkedColor = WithpeaceTheme.colors.MainPurple, - uncheckedColor = WithpeaceTheme.colors.SystemGray2, - checkmarkColor = WithpeaceTheme.colors.SystemWhite, + FlowRow { + filterListUiState.regions.dropLast(1).forEach { filterItem -> + if (selectedFilterUiState.regions.contains(filterItem)) { + TextButton( + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.MainPurple, + contentColor = WithpeaceTheme.colors.SystemWhite, + disabledContainerColor = Color.Transparent, + disabledContentColor = WithpeaceTheme.colors.SystemWhite, + ), + onClick = { onRegionCheckChanged(filterItem) }, + modifier = modifier + .padding(end = 8.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = filterItem.name, + ) + } + } else { + TextButton( + colors = ButtonColors( + containerColor = WithpeaceTheme.colors.SystemWhite, + contentColor = WithpeaceTheme.colors.SystemBlack, + disabledContainerColor = Color.Transparent, + disabledContentColor = WithpeaceTheme.colors.SystemBlack, ), - checked = selectedFilterUiState.regions.contains(filterItem), - onCheckedChange = { onRegionCheckChanged(filterItem) }, - ) + onClick = { onRegionCheckChanged(filterItem) }, + border = BorderStroke( + width = 1.dp, + color = WithpeaceTheme.colors.SystemGray2, + ), + modifier = modifier.padding(end = 8.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = filterItem.name, + color = WithpeaceTheme.colors.SystemBlack, + ) + } } } } - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = modifier - .fillMaxWidth() - .clickable { - onRegionMoreViewClick() - }, - horizontalArrangement = Arrangement.Center, - ) { - Text( - text = stringResource( - if (filterListUiState.isRegionExpanded) R.string.filter_fold else R.string.filter_expanded, - ), - color = WithpeaceTheme.colors.SystemGray1, - style = WithpeaceTheme.typography.caption, - modifier = modifier.padding(end = 4.dp), - ) - Image( - painterResource(id = if (filterListUiState.isRegionExpanded) R.drawable.ic_filter_fold else R.drawable.ic_filter_expanded), - contentDescription = stringResource(id = R.string.filter_expanded), - ) - } - Spacer(modifier = Modifier.height(24.dp)) } } diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/PolicyFiltersUiModel.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/PolicyFiltersUiModel.kt similarity index 85% rename from feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/PolicyFiltersUiModel.kt rename to feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/PolicyFiltersUiModel.kt index 88c35818..d79b7c29 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/PolicyFiltersUiModel.kt +++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/PolicyFiltersUiModel.kt @@ -1,8 +1,9 @@ -package com.withpeace.withpeace.feature.home.uistate +package com.withpeace.withpeace.feature.policylist.filtersetting import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel import com.withpeace.withpeace.core.ui.policy.RegionUiModel +import com.withpeace.withpeace.core.ui.policy.filtersetting.PolicyFiltersUiModel import com.withpeace.withpeace.core.ui.policy.toDomain import com.withpeace.withpeace.core.ui.policy.toUiModel diff --git a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/uistate/FilterListUiState.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/uistate/FilterListUiState.kt deleted file mode 100644 index 4598fa13..00000000 --- a/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/uistate/FilterListUiState.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.withpeace.withpeace.feature.policylist.filtersetting.uistate - -import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel -import com.withpeace.withpeace.core.ui.policy.RegionUiModel -import com.withpeace.withpeace.feature.policylist.uistate.PolicyFiltersUiModel - -data class FilterListUiState( - val isClassificationExpanded: Boolean = false, - val isRegionExpanded: Boolean = false, -) { - private val allClassifications: List = ClassificationUiModel.entries - private val allRegions: List = RegionUiModel.entries - - fun getStateByFilterState(filtersUiModel: PolicyFiltersUiModel): FilterListUiState { - return this.copy( - isClassificationExpanded = allClassifications.indexOf(filtersUiModel.classifications.lastOrNull()) >= FOLDED_CLASSIFICATION_ITEM_COUNT, - isRegionExpanded = allRegions.indexOf(filtersUiModel.regions.lastOrNull()) >= FOLDED_REGION_ITEM_COUNT, - ) - } - - fun getClassifications(): List { - return if (isClassificationExpanded) allClassifications.dropLast(ETC_COUNT) - else allClassifications.subList(0, FOLDED_CLASSIFICATION_ITEM_COUNT) - } - - fun getRegions(): List { - return if (isRegionExpanded) allRegions.dropLast(ETC_COUNT) - else allRegions.subList(0, FOLDED_REGION_ITEM_COUNT) - } - - companion object { - private const val FOLDED_CLASSIFICATION_ITEM_COUNT = 3 - private const val FOLDED_REGION_ITEM_COUNT = 7 - private const val ETC_COUNT = 1 - } -} \ No newline at end of file From 6bb469da2914756ac9b902cebb882e4367c3e9aa Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Mon, 30 Sep 2024 00:33:20 +0900 Subject: [PATCH 6/9] chore: 2.1.2 --- app/build.gradle.kts | 4 +- .../withpeace/MainBottomNavigation.kt | 8 +- .../com/withpeace/withpeace/WithpeaceApp.kt | 10 +- .../withpeace/withpeace/navigation/NavHost.kt | 4 + .../core/data/mapper/RecentPostMapper.kt | 10 + .../mapper/youthpolicy/PolicyFilterMapper.kt | 17 ++ .../data/repository/DefaultPostRepository.kt | 8 + .../data/repository/DefaultUserRepository.kt | 22 ++ .../DefaultYouthPolicyRepository.kt | 18 ++ .../core/domain/model/post/RecentPost.kt | 7 + .../core/domain/repository/PostRepository.kt | 5 + .../core/domain/repository/UserRepository.kt | 9 +- .../repository/YouthPolicyRepository.kt | 8 + .../domain/usecase/GetHotPoliciesUseCase.kt | 12 + .../domain/usecase/GetRecentPostUseCase.kt | 12 + .../usecase/GetRecommendPoliciesUseCase.kt | 12 + .../policy/UserPolicyFilterResponse.kt | 9 + .../di/response/post/RecentPostResponse.kt | 10 + .../core/network/di/service/PolicyService.kt | 34 --- .../core/network/di/service/PostService.kt | 4 + .../core/network/di/service/UserService.kt | 10 + .../network/di/service/YouthPolicyService.kt | 6 + .../policy/filtersetting/FilterBottomSheet.kt | 2 +- core/ui/src/main/res/values/strings.xml | 1 + .../withpeace/feature/home/HomeScreen.kt | 224 +++++++++++++----- .../withpeace/feature/home/HomeViewModel.kt | 65 +++++ .../feature/home/navigation/HomeNavigation.kt | 3 + .../feature/home/uistate/HotPolicyUiState.kt | 7 + .../home/uistate/RecentPostsUiMapper.kt | 10 + .../home/uistate/RecentPostsUiState.kt | 16 ++ .../home/uistate/RecommendPolicyUiState.kt | 7 + .../feature/postlist/PostListViewModel.kt | 10 +- .../postlist/navigation/PostListNavigation.kt | 18 +- 33 files changed, 493 insertions(+), 109 deletions(-) create mode 100644 core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/RecentPostMapper.kt create mode 100644 core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/youthpolicy/PolicyFilterMapper.kt create mode 100644 core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/post/RecentPost.kt create mode 100644 core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetHotPoliciesUseCase.kt create mode 100644 core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetRecentPostUseCase.kt create mode 100644 core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetRecommendPoliciesUseCase.kt create mode 100644 core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/policy/UserPolicyFilterResponse.kt create mode 100644 core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/post/RecentPostResponse.kt delete mode 100644 core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/PolicyService.kt create mode 100644 feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/HotPolicyUiState.kt create mode 100644 feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/RecentPostsUiMapper.kt create mode 100644 feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/RecentPostsUiState.kt create mode 100644 feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/RecommendPolicyUiState.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1fb0406c..26b82061 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,8 +10,8 @@ android { defaultConfig { applicationId = "com.withpeace.withpeace" targetSdk = 34 - versionCode = 11 - versionName = "2.1.1" + versionCode = 12 + versionName = "2.2.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt b/app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt index 8bb2228e..16be7979 100644 --- a/app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt +++ b/app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt @@ -79,7 +79,7 @@ private fun NavController.navigateToTabScreen(bottomTab: BottomTab) { when (bottomTab) { BottomTab.HOME -> navigateHome(tabNavOptions) BottomTab.POLICY -> navigateToPolicyList(tabNavOptions) - BottomTab.POST -> navigateToPostList(tabNavOptions) + BottomTab.POST -> navigateToPostList(navOptions = tabNavOptions) BottomTab.MY_PAGE -> navigate(MY_PAGE_NESTED_ROUTE, tabNavOptions) } } @@ -115,5 +115,11 @@ enum class BottomTab( MY_PAGE_NESTED_ROUTE, ), ; + + companion object { + operator fun contains(route: String): Boolean { + return entries.map { it.route }.contains(route) + } + } } // https://stackoverflow.com/questions/76721423/compose-navigation-go-to-top-level-destination-when-clicking-on-navigation-bar \ No newline at end of file diff --git a/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt b/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt index cef33eed..a2f190dc 100644 --- a/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt +++ b/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt @@ -57,12 +57,16 @@ fun WithpeaceApp( Scaffold( bottomBar = { - MainBottomBar( + if ( + BottomTab.contains(parentDestination?.route ?: currentDestination?.route ?: "") + ) { + MainBottomBar( currentDestination = if (parentDestination?.route == null) { currentDestination ?: return@Scaffold } else parentDestination, - navController = navController, - ) + navController = navController, + ) + } }, modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt index 5d292e93..d2a20f3c 100644 --- a/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt +++ b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt @@ -38,6 +38,7 @@ import com.withpeace.withpeace.feature.postdetail.navigation.navigateToPostDetai import com.withpeace.withpeace.feature.postdetail.navigation.postDetailGraph import com.withpeace.withpeace.feature.postlist.navigation.POST_LIST_DELETED_POST_ID_ARGUMENT import com.withpeace.withpeace.feature.postlist.navigation.POST_LIST_ROUTE +import com.withpeace.withpeace.feature.postlist.navigation.navigateToPostList import com.withpeace.withpeace.feature.postlist.navigation.postListGraph import com.withpeace.withpeace.feature.privacypolicy.navigation.navigateToPrivacyPolicy import com.withpeace.withpeace.feature.privacypolicy.navigation.privacyPolicyGraph @@ -194,6 +195,9 @@ fun WithpeaceNavHost( onPolicyClick = { navController.navigateToPolicyDetail(policyId = it) }, + onPostClick = { + navController.navigateToPostList(it.name) + } ) policyDetailNavGraph( onShowSnackBar = { onShowSnackBar(SnackbarState(it)) }, diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/RecentPostMapper.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/RecentPostMapper.kt new file mode 100644 index 00000000..1919e8bb --- /dev/null +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/RecentPostMapper.kt @@ -0,0 +1,10 @@ +package com.withpeace.withpeace.core.data.mapper + +import com.withpeace.withpeace.core.domain.model.post.RecentPost +import com.withpeace.withpeace.core.network.di.response.post.RecentPostResponse + +fun RecentPostResponse.toDomain(): RecentPost { + return RecentPost( + id = postId, title = title, type = type.toDomain(), + ) +} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/youthpolicy/PolicyFilterMapper.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/youthpolicy/PolicyFilterMapper.kt new file mode 100644 index 00000000..235582dd --- /dev/null +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/youthpolicy/PolicyFilterMapper.kt @@ -0,0 +1,17 @@ +package com.withpeace.withpeace.core.data.mapper.youthpolicy + +import com.withpeace.withpeace.core.domain.model.policy.PolicyClassification +import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters +import com.withpeace.withpeace.core.domain.model.policy.PolicyRegion +import com.withpeace.withpeace.core.network.di.response.policy.UserPolicyFilterResponse + +fun UserPolicyFilterResponse.toDomain(): PolicyFilters { + return PolicyFilters( + regions = region.map { name -> + PolicyRegion.entries.find { it.name == name } ?: PolicyRegion.기타 + }, + classifications = classification.map { name -> + PolicyClassification.entries.find { it.name == name } ?: PolicyClassification.ETC + }, + ) +} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultPostRepository.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultPostRepository.kt index 0575eae0..eec8f336 100644 --- a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultPostRepository.kt +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultPostRepository.kt @@ -18,6 +18,7 @@ import com.withpeace.withpeace.core.domain.model.error.ResponseError import com.withpeace.withpeace.core.domain.model.post.Post import com.withpeace.withpeace.core.domain.model.post.PostDetail import com.withpeace.withpeace.core.domain.model.post.PostTopic +import com.withpeace.withpeace.core.domain.model.post.RecentPost import com.withpeace.withpeace.core.domain.model.post.RegisterPost import com.withpeace.withpeace.core.domain.model.post.ReportType import com.withpeace.withpeace.core.domain.repository.PostRepository @@ -147,6 +148,13 @@ class DefaultPostRepository @Inject constructor( } } + override fun getRecentPost(onError: suspend (CheonghaError) -> Unit): Flow> = + flow { + postService.getRecentPost().suspendMapSuccess { + emit(this.data.map { it.toDomain() }) + }.handleApiFailure(onError) + } + private fun getImageRequestBodies( imageUris: List, ): List { diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultUserRepository.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultUserRepository.kt index 0d946c56..4a8b6664 100644 --- a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultUserRepository.kt +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultUserRepository.kt @@ -7,6 +7,8 @@ import com.withpeace.withpeace.core.analytics.AnalyticsEvent import com.withpeace.withpeace.core.analytics.AnalyticsHelper import com.withpeace.withpeace.core.data.analytics.event import com.withpeace.withpeace.core.data.mapper.toDomain +import com.withpeace.withpeace.core.data.mapper.youthpolicy.toCode +import com.withpeace.withpeace.core.data.mapper.youthpolicy.toDomain import com.withpeace.withpeace.core.data.util.convertToFile import com.withpeace.withpeace.core.data.util.handleApiFailure import com.withpeace.withpeace.core.datastore.dataStore.token.TokenPreferenceDataSource @@ -15,6 +17,7 @@ import com.withpeace.withpeace.core.domain.model.SignUpInfo import com.withpeace.withpeace.core.domain.model.error.CheonghaError import com.withpeace.withpeace.core.domain.model.error.ClientError import com.withpeace.withpeace.core.domain.model.error.ResponseError +import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters import com.withpeace.withpeace.core.domain.model.profile.ChangedProfile import com.withpeace.withpeace.core.domain.model.profile.Nickname import com.withpeace.withpeace.core.domain.model.profile.ProfileInfo @@ -133,6 +136,25 @@ class DefaultUserRepository @Inject constructor( } } + override fun updatePolicyFilter( + policyFilters: PolicyFilters, + onError: suspend (CheonghaError) -> Unit, + ): Flow = flow { + userService.patchPolicyFilter( + region = policyFilters.regions.joinToString(",") { it.toCode() }, + classification = policyFilters.classifications.joinToString(",") { it.toCode() }, + ).suspendMapSuccess { + emit(Unit) + }.handleApiFailure(onError) + } + + override fun getPolicyFilter(onError: suspend (CheonghaError) -> Unit): Flow = + flow { + userService.getPolicyFilter().suspendMapSuccess { + emit(this.data.toDomain()) + }.handleApiFailure(onError) + } + override fun withdraw(onError: suspend (CheonghaError) -> Unit): Flow = flow { userService.withdraw().suspendMapSuccess { diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultYouthPolicyRepository.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultYouthPolicyRepository.kt index d99b29c3..90ebdcdd 100644 --- a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultYouthPolicyRepository.kt +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultYouthPolicyRepository.kt @@ -84,6 +84,24 @@ class DefaultYouthPolicyRepository @Inject constructor( } } + override fun getRecommendPolicy(onError: suspend (CheonghaError) -> Unit): Flow> = + flow { + youthPolicyService.getRecommendations().suspendMapSuccess { + emit(data.map { it.toDomain() }) + }.handleApiFailure { + onErrorWithAuthExpired(it, onError) + } + } + + override fun getHotPolicy(onError: suspend (CheonghaError) -> Unit): Flow> = + flow { + youthPolicyService.getHots().suspendMapSuccess { + emit(data.map { it.toDomain() }) + }.handleApiFailure { + onErrorWithAuthExpired(it, onError) + } + } + private suspend fun onErrorWithAuthExpired( it: ResponseError, onError: suspend (CheonghaError) -> Unit, diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/post/RecentPost.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/post/RecentPost.kt new file mode 100644 index 00000000..e22513f1 --- /dev/null +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/post/RecentPost.kt @@ -0,0 +1,7 @@ +package com.withpeace.withpeace.core.domain.model.post + +data class RecentPost( + val id: Long, + val title: String, + val type: PostTopic, +) diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/PostRepository.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/PostRepository.kt index 7221785c..65888846 100644 --- a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/PostRepository.kt +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/PostRepository.kt @@ -5,6 +5,7 @@ import com.withpeace.withpeace.core.domain.model.error.CheonghaError import com.withpeace.withpeace.core.domain.model.post.Post import com.withpeace.withpeace.core.domain.model.post.PostDetail import com.withpeace.withpeace.core.domain.model.post.PostTopic +import com.withpeace.withpeace.core.domain.model.post.RecentPost import com.withpeace.withpeace.core.domain.model.post.RegisterPost import com.withpeace.withpeace.core.domain.model.post.ReportType import kotlinx.coroutines.flow.Flow @@ -48,4 +49,8 @@ interface PostRepository { reportType: ReportType, onError: suspend (CheonghaError) -> Unit, ): Flow + + fun getRecentPost( + onError: suspend (CheonghaError) -> Unit, + ): Flow> } diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/UserRepository.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/UserRepository.kt index 65f1cdbc..8fb08545 100644 --- a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/UserRepository.kt +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/UserRepository.kt @@ -1,8 +1,8 @@ package com.withpeace.withpeace.core.domain.repository -import com.withpeace.withpeace.core.domain.model.error.ResponseError import com.withpeace.withpeace.core.domain.model.SignUpInfo import com.withpeace.withpeace.core.domain.model.error.CheonghaError +import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters import com.withpeace.withpeace.core.domain.model.profile.ChangedProfile import com.withpeace.withpeace.core.domain.model.profile.Nickname import com.withpeace.withpeace.core.domain.model.profile.ProfileInfo @@ -42,4 +42,11 @@ interface UserRepository { fun withdraw( onError: suspend (CheonghaError) -> Unit, ): Flow + + fun updatePolicyFilter( + policyFilters: PolicyFilters, + onError: suspend (CheonghaError) -> Unit, + ): Flow + + fun getPolicyFilter(onError: suspend (CheonghaError) -> Unit): Flow } diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/YouthPolicyRepository.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/YouthPolicyRepository.kt index 3792666d..b057e585 100644 --- a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/YouthPolicyRepository.kt +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/YouthPolicyRepository.kt @@ -32,4 +32,12 @@ interface YouthPolicyRepository { policyId: String, onError: suspend (CheonghaError) -> Unit, ): Flow + + fun getRecommendPolicy( + onError: suspend (CheonghaError) -> Unit, + ): Flow> + + fun getHotPolicy( + onError: suspend (CheonghaError) -> Unit, + ): Flow> } \ No newline at end of file diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetHotPoliciesUseCase.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetHotPoliciesUseCase.kt new file mode 100644 index 00000000..8884b8ed --- /dev/null +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetHotPoliciesUseCase.kt @@ -0,0 +1,12 @@ +package com.withpeace.withpeace.core.domain.usecase + +import com.withpeace.withpeace.core.domain.model.error.CheonghaError +import com.withpeace.withpeace.core.domain.repository.YouthPolicyRepository +import javax.inject.Inject + +class GetHotPoliciesUseCase @Inject constructor( + private val policyRepository: YouthPolicyRepository, +) { + operator fun invoke(onError: suspend (CheonghaError) -> Unit) = + policyRepository.getHotPolicy(onError) +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetRecentPostUseCase.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetRecentPostUseCase.kt new file mode 100644 index 00000000..017a815e --- /dev/null +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetRecentPostUseCase.kt @@ -0,0 +1,12 @@ +package com.withpeace.withpeace.core.domain.usecase + +import com.withpeace.withpeace.core.domain.model.error.CheonghaError +import com.withpeace.withpeace.core.domain.repository.PostRepository +import javax.inject.Inject + +class GetRecentPostUseCase @Inject constructor( + private val postRepository: PostRepository, +) { + operator fun invoke(onError: suspend (CheonghaError) -> Unit) = + postRepository.getRecentPost(onError) +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetRecommendPoliciesUseCase.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetRecommendPoliciesUseCase.kt new file mode 100644 index 00000000..6d1ed602 --- /dev/null +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetRecommendPoliciesUseCase.kt @@ -0,0 +1,12 @@ +package com.withpeace.withpeace.core.domain.usecase + +import com.withpeace.withpeace.core.domain.model.error.CheonghaError +import com.withpeace.withpeace.core.domain.repository.YouthPolicyRepository +import javax.inject.Inject + +class GetRecommendPoliciesUseCase @Inject constructor( + private val policyRepository: YouthPolicyRepository, +) { + operator fun invoke(onError: suspend (CheonghaError) -> Unit) = + policyRepository.getRecommendPolicy(onError) +} \ No newline at end of file diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/policy/UserPolicyFilterResponse.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/policy/UserPolicyFilterResponse.kt new file mode 100644 index 00000000..fa7723f5 --- /dev/null +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/policy/UserPolicyFilterResponse.kt @@ -0,0 +1,9 @@ +package com.withpeace.withpeace.core.network.di.response.policy + +import kotlinx.serialization.Serializable + +@Serializable +data class UserPolicyFilterResponse( + val classification: List, + val region: List, +) diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/post/RecentPostResponse.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/post/RecentPostResponse.kt new file mode 100644 index 00000000..e672a1a0 --- /dev/null +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/post/RecentPostResponse.kt @@ -0,0 +1,10 @@ +package com.withpeace.withpeace.core.network.di.response.post + +import kotlinx.serialization.Serializable + +@Serializable +data class RecentPostResponse( + val type: PostTopicResponse, + val postId: Long, + val title: String, +) diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/PolicyService.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/PolicyService.kt deleted file mode 100644 index d499a4c9..00000000 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/PolicyService.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.withpeace.withpeace.core.network.di.service - -import com.skydoves.sandwich.ApiResponse -import com.withpeace.withpeace.core.network.di.response.BaseResponse -import com.withpeace.withpeace.core.network.di.response.policy.BookmarkedPolicyResponse -import com.withpeace.withpeace.core.network.di.response.policy.PolicyDetailResponse -import com.withpeace.withpeace.core.network.di.response.policy.PolicyResponse -import retrofit2.http.DELETE -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Path -import retrofit2.http.Query - -interface PolicyService { - @GET("/api/v1/policies") - suspend fun getPolicies( - @Query("region") region: String, - @Query("classification") classification: String, - @Query("pageIndex") pageIndex: Int, - @Query("display") display: Int, - ): ApiResponse>> - - @GET("/api/v1/policies/{policyId}") - suspend fun getPolicyDetail(@Path("policyId") policyId: String): ApiResponse> - - @POST("/api/v1/policies/{policyId}/favorites") - suspend fun bookmarkPolicy(@Path("policyId") policyId: String): ApiResponse> - - @DELETE("/api/v1/policies/{policyId}/favorites") - suspend fun unBookmarkPolicy(@Path("policyId") policyId: String): ApiResponse> - - @GET("/api/v1/policies/favorites") - suspend fun getBookmarkPolicies(): ApiResponse>> -} \ No newline at end of file diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/PostService.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/PostService.kt index b910ee02..ba2e9aff 100644 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/PostService.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/PostService.kt @@ -7,6 +7,7 @@ import com.withpeace.withpeace.core.network.di.response.BaseResponse import com.withpeace.withpeace.core.network.di.response.post.PostDetailResponse import com.withpeace.withpeace.core.network.di.response.post.PostIdResponse import com.withpeace.withpeace.core.network.di.response.post.PostResponse +import com.withpeace.withpeace.core.network.di.response.post.RecentPostResponse import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body @@ -71,4 +72,7 @@ interface PostService { @Path("commentId") commentId: Long, @Body reportTypeRequest: ReportTypeRequest, ): ApiResponse> + + @GET("/api/v1/posts/recents") + suspend fun getRecentPost(): ApiResponse>> } diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/UserService.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/UserService.kt index 580d1429..b3b60b55 100644 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/UserService.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/UserService.kt @@ -6,6 +6,7 @@ import com.withpeace.withpeace.core.network.di.response.BaseResponse import com.withpeace.withpeace.core.network.di.response.ChangedProfileResponse import com.withpeace.withpeace.core.network.di.response.ProfileResponse import com.withpeace.withpeace.core.network.di.response.TokenResponse +import com.withpeace.withpeace.core.network.di.response.policy.UserPolicyFilterResponse import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body @@ -57,4 +58,13 @@ interface UserService { @DELETE("/api/v1/users") suspend fun withdraw(): ApiResponse> + + @PATCH("/api/v1/users/profile/policy-filter") + suspend fun patchPolicyFilter( + @Query("region") region: String, + @Query("classification") classification: String, + ): ApiResponse> + + @GET("/api/v1/users/profile/policy-filter") + suspend fun getPolicyFilter(): ApiResponse> } diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/YouthPolicyService.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/YouthPolicyService.kt index f818165b..ec46aaec 100644 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/YouthPolicyService.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/YouthPolicyService.kt @@ -31,4 +31,10 @@ interface YouthPolicyService { @GET("/api/v1/policies/favorites") suspend fun getBookmarkedPolicies(): ApiResponse>> + + @GET("/api/v1/policies/recommendations") + suspend fun getRecommendations(): ApiResponse>> + + @GET("/api/v1/policies/hot") + suspend fun getHots(): ApiResponse>> } \ No newline at end of file diff --git a/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/FilterBottomSheet.kt b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/FilterBottomSheet.kt index d6173e88..18642f44 100644 --- a/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/FilterBottomSheet.kt +++ b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/FilterBottomSheet.kt @@ -147,7 +147,7 @@ private fun ScrollableFilterSection( ) { Spacer(modifier = modifier.height(16.dp)) Text( - text = stringResource(R.string.policy_classification_image), + text = stringResource(R.string.classification), style = WithpeaceTheme.typography.title2, color = WithpeaceTheme.colors.SnackbarBlack, ) diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index f0ff3d48..7b910434 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -37,6 +37,7 @@ 더보기 정책 분류 이미지 지역 + 정책분야 전체 해제 검색하기 diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt index e6cb6ab2..140fcfb3 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt @@ -57,6 +57,10 @@ import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel import com.withpeace.withpeace.core.ui.policy.RegionUiModel import com.withpeace.withpeace.core.ui.policy.filtersetting.FilterBottomSheet import com.withpeace.withpeace.core.ui.policy.filtersetting.PolicyFiltersUiModel +import com.withpeace.withpeace.core.ui.post.PostTopicUiModel +import com.withpeace.withpeace.feature.home.uistate.HotPolicyUiState +import com.withpeace.withpeace.feature.home.uistate.RecentPostsUiState +import com.withpeace.withpeace.feature.home.uistate.RecommendPolicyUiState import kotlinx.coroutines.launch @Composable @@ -65,8 +69,12 @@ fun HomeRoute( viewModel: HomeViewModel = hiltViewModel(), onNavigationSnackBar: (message: String) -> Unit = {}, onPolicyClick: (String) -> Unit, + onPostClick: (PostTopicUiModel) -> Unit, ) { val selectedFilterUiState = viewModel.selectingFilters.collectAsStateWithLifecycle() + val recentPosts = viewModel.recentPostsUiState.collectAsStateWithLifecycle() + val hotPolicies = viewModel.hotPolicyUiState.collectAsStateWithLifecycle() + val recommendPolicies = viewModel.recommendPolicyUiState.collectAsStateWithLifecycle() HomeScreen( selectedFilterUiState = selectedFilterUiState.value, onClassificationCheckChanged = viewModel::onCheckClassification, @@ -75,11 +83,19 @@ fun HomeRoute( onSearchWithFilter = viewModel::onCompleteFilter, onCloseFilter = viewModel::onCancelFilter, onDismissRequest = viewModel::onCancelFilter, + recentPosts = recentPosts.value, + onPostClick = onPostClick, + onPolicyClick = onPolicyClick, + hotPolicyUiState = hotPolicies.value, + recommendPolicyUiState = recommendPolicies.value, ) } @Composable fun HomeScreen( + recentPosts: RecentPostsUiState, + hotPolicyUiState: HotPolicyUiState, + recommendPolicyUiState: RecommendPolicyUiState, modifier: Modifier = Modifier, selectedFilterUiState: PolicyFiltersUiModel, onDismissRequest: () -> Unit, @@ -88,6 +104,8 @@ fun HomeScreen( onFilterAllOff: () -> Unit, onSearchWithFilter: () -> Unit, onCloseFilter: () -> Unit, + onPostClick: (PostTopicUiModel) -> Unit, + onPolicyClick: (String) -> Unit, ) { Column(modifier = modifier.fillMaxSize()) { HomeHeader( @@ -98,6 +116,7 @@ fun HomeScreen( color = WithpeaceTheme.colors.SystemGray3, ) ScrollSection( + recentPosts = recentPosts, selectedFilterUiState = selectedFilterUiState, onDismissRequest = onDismissRequest, onClassificationCheckChanged = onClassificationCheckChanged, @@ -105,6 +124,11 @@ fun HomeScreen( onFilterAllOff = onFilterAllOff, onSearchWithFilter = onSearchWithFilter, onCloseFilter = onCloseFilter, + onPostClick = onPostClick, + onPolicyClick = onPolicyClick, + hotPolicyUiState = hotPolicyUiState, + recommendPolicyUiState = recommendPolicyUiState, + ) } @@ -114,6 +138,8 @@ fun HomeScreen( @OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable private fun ScrollSection( + onPolicyClick: (String) -> Unit, + recentPosts: RecentPostsUiState, modifier: Modifier = Modifier, selectedFilterUiState: PolicyFiltersUiModel, onDismissRequest: () -> Unit, @@ -122,6 +148,9 @@ private fun ScrollSection( onFilterAllOff: () -> Unit, onSearchWithFilter: () -> Unit, onCloseFilter: () -> Unit, + onPostClick: (PostTopicUiModel) -> Unit, + hotPolicyUiState: HotPolicyUiState, + recommendPolicyUiState: RecommendPolicyUiState, ) { val builder = rememberBalloonBuilder { setIsVisibleArrow(false) @@ -235,20 +264,20 @@ private fun ScrollSection( }, contentDescription = "", ) - List(5) { //TODO("데이터 변경") - Spacer(modifier = modifier.width(8.dp)) - Text( - text = "#부산", - style = WithpeaceTheme.typography.Tag, - color = WithpeaceTheme.colors.MainPurple, - modifier = modifier - .background( - color = WithpeaceTheme.colors.SubPurple, - shape = RoundedCornerShape(7.dp), - ) - .padding(6.dp), - ) - } + // List(5) { //TODO("데이터 변경") + // Spacer(modifier = modifier.width(8.dp)) + // Text( + // text = "#부산", + // style = WithpeaceTheme.typography.Tag, + // color = WithpeaceTheme.colors.MainPurple, + // modifier = modifier + // .background( + // color = WithpeaceTheme.colors.SubPurple, + // shape = RoundedCornerShape(7.dp), + // ) + // .padding(6.dp), + // ) + // } } } Spacer(modifier = modifier.height(16.dp)) @@ -263,22 +292,54 @@ private fun ScrollSection( contentPadding = PaddingValues(horizontal = 24.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), ) { - items(6) { - Column { - Image( - modifier = modifier - .size(140.dp) - .clip(RoundedCornerShape(10.dp)), - painter = painterResource(id = com.withpeace.withpeace.core.ui.R.drawable.ic_policy_participation_right), - contentDescription = "", - ) - Spacer(modifier = modifier.height(8.dp)) - Text( - modifier = modifier.width(140.dp), - text = "울산대학교 대학일자리플러스센터(거점형)", - style = WithpeaceTheme.typography.caption, - color = WithpeaceTheme.colors.SnackbarBlack, - ) + if (hotPolicyUiState is HotPolicyUiState.Success) { + items(hotPolicyUiState.policies.size) { + val hotPolicy = hotPolicyUiState.policies[it] + Column( + modifier.clickable { + onPolicyClick(hotPolicy.id) + }, + ) { + Image( + modifier = modifier + .size(140.dp) + .clip(RoundedCornerShape(10.dp)), + painter = painterResource(id = hotPolicy.classification.drawableResId), + contentDescription = "", + ) + Spacer(modifier = modifier.height(8.dp)) + Text( + overflow = TextOverflow.Ellipsis, + maxLines = 2, + modifier = modifier.width(140.dp), + text = hotPolicy.title, + style = WithpeaceTheme.typography.caption, + color = WithpeaceTheme.colors.SnackbarBlack, + ) + } + } + } else { + items(6) { + Column( + modifier.clickable { + onPolicyClick("R2024092726752") + }, + ) { + Image( + modifier = modifier + .size(140.dp) + .clip(RoundedCornerShape(10.dp)), + painter = painterResource(id = com.withpeace.withpeace.core.ui.R.drawable.ic_policy_participation_right), + contentDescription = "", + ) + Spacer(modifier = modifier.height(8.dp)) + Text( + modifier = modifier.width(140.dp), + text = "울산대학교 대학일자리플러스센터(거점형)", + style = WithpeaceTheme.typography.caption, + color = WithpeaceTheme.colors.SnackbarBlack, + ) + } } } } @@ -301,22 +362,54 @@ private fun ScrollSection( contentPadding = PaddingValues(horizontal = 24.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), ) { - items(6) { - Column { - Image( - modifier = modifier - .size(140.dp) - .clip(RoundedCornerShape(10.dp)), - painter = painterResource(id = com.withpeace.withpeace.core.ui.R.drawable.ic_policy_participation_right), - contentDescription = "", - ) - Spacer(modifier = modifier.height(8.dp)) - Text( - modifier = modifier.width(140.dp), - text = "울산대학교 대학일자리플러스센터(거점형)", - style = WithpeaceTheme.typography.caption, - color = WithpeaceTheme.colors.SnackbarBlack, - ) + if (recommendPolicyUiState is RecommendPolicyUiState.Success) { + items(recommendPolicyUiState.policies.size) { + val recommentPolicy = recommendPolicyUiState.policies[it] + Column( + modifier.clickable { + onPolicyClick(recommentPolicy.id) + }, + ) { + Image( + modifier = modifier + .size(140.dp) + .clip(RoundedCornerShape(10.dp)), + painter = painterResource(id = recommentPolicy.classification.drawableResId), + contentDescription = "", + ) + Spacer(modifier = modifier.height(8.dp)) + Text( + overflow = TextOverflow.Ellipsis, + maxLines = 2, + modifier = modifier.width(140.dp), + text = recommentPolicy.title, + style = WithpeaceTheme.typography.caption, + color = WithpeaceTheme.colors.SnackbarBlack, + ) + } + } + } else { + items(6) { + Column( + modifier.clickable { + onPolicyClick("R2024092726752") + }, + ) { + Image( + modifier = modifier + .size(140.dp) + .clip(RoundedCornerShape(10.dp)), + painter = painterResource(id = com.withpeace.withpeace.core.ui.R.drawable.ic_policy_participation_right), + contentDescription = "", + ) + Spacer(modifier = modifier.height(8.dp)) + Text( + modifier = modifier.width(140.dp), + text = "울산대학교 대학일자리플러스센터(거점형)", + style = WithpeaceTheme.typography.caption, + color = WithpeaceTheme.colors.SnackbarBlack, + ) + } } } } @@ -329,21 +422,34 @@ private fun ScrollSection( ) Spacer(modifier = modifier.height(16.dp)) } - items(6) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier.padding(start = 24.dp, end = 17.dp), - ) { - Text(text = "자유 게시판") - Spacer(modifier = modifier.width(16.dp)) - Text( - style = WithpeaceTheme.typography.caption, - text = "토트넘 vs K리그 누가 이길 것 같나요? 토트넘 vs K리그 누가 이길 것 같나요?", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + if (recentPosts is RecentPostsUiState.Success) { + items(PostTopicUiModel.entries.size) { + val postTopic = PostTopicUiModel.entries[it] + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .padding(start = 24.dp, end = 17.dp) + .clickable { + onPostClick(postTopic) + }, + ) { + Text( + text = stringResource(id = PostTopicUiModel.entries[it].textResId) + "게시판", + style = WithpeaceTheme.typography.body, + color = WithpeaceTheme.colors.SnackbarBlack, + ) + Spacer(modifier = modifier.width(16.dp)) + Text( + color = WithpeaceTheme.colors.SystemGray1, + style = WithpeaceTheme.typography.caption, + text = recentPosts.recentPosts.find { postTopic == it.type }?.title ?: "-", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } } + } } diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt index 82ae027d..7dd18756 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt @@ -3,24 +3,48 @@ package com.withpeace.withpeace.feature.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters +import com.withpeace.withpeace.core.domain.usecase.GetHotPoliciesUseCase +import com.withpeace.withpeace.core.domain.usecase.GetRecentPostUseCase +import com.withpeace.withpeace.core.domain.usecase.GetRecommendPoliciesUseCase import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel import com.withpeace.withpeace.core.ui.policy.RegionUiModel import com.withpeace.withpeace.core.ui.policy.filtersetting.PolicyFiltersUiModel import com.withpeace.withpeace.core.ui.policy.filtersetting.toDomain import com.withpeace.withpeace.core.ui.policy.filtersetting.toUiModel import com.withpeace.withpeace.core.ui.policy.toDomain +import com.withpeace.withpeace.feature.home.uistate.HotPolicyUiState +import com.withpeace.withpeace.feature.home.uistate.RecentPostsUiState +import com.withpeace.withpeace.feature.home.uistate.RecommendPolicyUiState +import com.withpeace.withpeace.feature.home.uistate.toUiModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( + private val getRecentPostUseCase: GetRecentPostUseCase, + private val getRecommendPoliciesUseCase: GetRecommendPoliciesUseCase, + private val getHotPoliciesUseCase: GetHotPoliciesUseCase, ) : ViewModel() { + private val _recentPostsUiState: MutableStateFlow = + MutableStateFlow(RecentPostsUiState.Loading) + val recentPostsUiState = _recentPostsUiState.asStateFlow() + + private val _recommendPolicyUiState: MutableStateFlow = + MutableStateFlow(RecommendPolicyUiState.Loading) + val recommendPolicyUiState = _recommendPolicyUiState.asStateFlow() + + private val _hotPolicyUiState: MutableStateFlow = + MutableStateFlow(HotPolicyUiState.Loading) + val hotPolicyUiState = _hotPolicyUiState.asStateFlow() + private val _selectingFilters = MutableStateFlow(PolicyFilters()) val selectingFilters: StateFlow = _selectingFilters.map { it.toUiModel() }.stateIn( @@ -32,6 +56,47 @@ class HomeViewModel @Inject constructor( private var completedFilters = PolicyFilters() init { + viewModelScope.launch { + launch { + getRecentPostUseCase( + onError = { + + }, + ).collect { data -> + _recentPostsUiState.update { + RecentPostsUiState.Success( + data.map { it.toUiModel() }, + ) + } + } + } + launch { + getRecommendPoliciesUseCase( + onError = { + _recommendPolicyUiState.update { + RecommendPolicyUiState.Failure + } + }, + ).collect { data -> + _recommendPolicyUiState.update { + RecommendPolicyUiState.Success(data.map { it.toUiModel() }) + } + } + } + launch { + getHotPoliciesUseCase( + onError = { + _hotPolicyUiState.update { + HotPolicyUiState.Failure + } + }, + ).collect { data -> + _hotPolicyUiState.update { + HotPolicyUiState.Success(data.map { it.toUiModel() }) + } + } + } + } } fun onCheckClassification(classification: ClassificationUiModel) { diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/navigation/HomeNavigation.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/navigation/HomeNavigation.kt index 7efef378..1b619349 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/navigation/HomeNavigation.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/navigation/HomeNavigation.kt @@ -6,6 +6,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable +import com.withpeace.withpeace.core.ui.post.PostTopicUiModel import com.withpeace.withpeace.feature.home.HomeRoute const val HOME_ROUTE = "homeRoute" @@ -18,6 +19,7 @@ fun NavGraphBuilder.homeNavGraph( onShowSnackBar: (message: String) -> Unit, onNavigationSnackBar: (message: String) -> Unit = {}, onPolicyClick: (String) -> Unit, + onPostClick: (PostTopicUiModel) -> Unit, ) { composable( route = HOME_ROUTE, @@ -28,6 +30,7 @@ fun NavGraphBuilder.homeNavGraph( onNavigationSnackBar = onNavigationSnackBar, onShowSnackBar = onShowSnackBar, onPolicyClick = onPolicyClick, + onPostClick = onPostClick, ) } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/HotPolicyUiState.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/HotPolicyUiState.kt new file mode 100644 index 00000000..7d51cce8 --- /dev/null +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/HotPolicyUiState.kt @@ -0,0 +1,7 @@ +package com.withpeace.withpeace.feature.home.uistate + +sealed interface HotPolicyUiState { + data class Success(val policies: List) : HotPolicyUiState + data object Loading : HotPolicyUiState + data object Failure : HotPolicyUiState +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/RecentPostsUiMapper.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/RecentPostsUiMapper.kt new file mode 100644 index 00000000..1805425e --- /dev/null +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/RecentPostsUiMapper.kt @@ -0,0 +1,10 @@ +package com.withpeace.withpeace.feature.home.uistate + +import com.withpeace.withpeace.core.domain.model.post.RecentPost +import com.withpeace.withpeace.core.ui.post.toUi + +fun RecentPost.toUiModel(): RecentPostUiModel { + return RecentPostUiModel( + id = id, title = title, type = type.toUi(), + ) +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/RecentPostsUiState.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/RecentPostsUiState.kt new file mode 100644 index 00000000..c528bf6f --- /dev/null +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/RecentPostsUiState.kt @@ -0,0 +1,16 @@ +package com.withpeace.withpeace.feature.home.uistate + +import com.withpeace.withpeace.core.domain.model.post.PostTopic +import com.withpeace.withpeace.core.ui.post.PostTopicUiModel + +sealed interface RecentPostsUiState { + data class Success(val recentPosts: List) : RecentPostsUiState + data object Loading : RecentPostsUiState + data object Failure : RecentPostsUiState +} + +data class RecentPostUiModel( + val id: Long, + val title: String, + val type: PostTopicUiModel, +) \ No newline at end of file diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/RecommendPolicyUiState.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/RecommendPolicyUiState.kt new file mode 100644 index 00000000..3e878beb --- /dev/null +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/RecommendPolicyUiState.kt @@ -0,0 +1,7 @@ +package com.withpeace.withpeace.feature.home.uistate + +sealed interface RecommendPolicyUiState { + data class Success(val policies: List) : RecommendPolicyUiState + data object Loading : RecommendPolicyUiState + data object Failure : RecommendPolicyUiState +} \ No newline at end of file diff --git a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListViewModel.kt b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListViewModel.kt index d85fd858..eaae2753 100644 --- a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListViewModel.kt +++ b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListViewModel.kt @@ -1,6 +1,5 @@ package com.withpeace.withpeace.feature.postlist -import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -14,17 +13,15 @@ import com.withpeace.withpeace.core.ui.post.PostTopicUiModel import com.withpeace.withpeace.core.ui.post.PostUiModel import com.withpeace.withpeace.core.ui.post.toDomain import com.withpeace.withpeace.core.ui.post.toPostUiModel -import com.withpeace.withpeace.feature.postlist.navigation.POST_LIST_DELETED_POST_ID_ARGUMENT +import com.withpeace.withpeace.feature.postlist.navigation.POST_TYPE_ARGUMENT import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -32,11 +29,14 @@ import javax.inject.Inject @HiltViewModel class PostListViewModel @Inject constructor( private val getPostsUseCase: GetPostsUseCase, + savedStateHandle: SavedStateHandle, ) : ViewModel() { private val _uiEvent = Channel() val uiEvent = _uiEvent.receiveAsFlow() - private val _currentTopic = MutableStateFlow(PostTopicUiModel.FREEDOM) + private val _currentTopic = MutableStateFlow(PostTopicUiModel.entries.find { + it.name == savedStateHandle.get(POST_TYPE_ARGUMENT) + } ?: PostTopicUiModel.FREEDOM) val currentTopic = _currentTopic.asStateFlow() private val _postListPagingFlow = MutableStateFlow(PagingData.empty()) diff --git a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/navigation/PostListNavigation.kt b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/navigation/PostListNavigation.kt index edb3ff96..6f2fe952 100644 --- a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/navigation/PostListNavigation.kt +++ b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/navigation/PostListNavigation.kt @@ -6,15 +6,22 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions +import androidx.navigation.NavType import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.withpeace.withpeace.core.ui.post.PostTopicUiModel import com.withpeace.withpeace.feature.postlist.PostListRoute import com.withpeace.withpeace.feature.postlist.PostListViewModel const val POST_LIST_ROUTE = "post_list_route" const val POST_LIST_DELETED_POST_ID_ARGUMENT = "post_list_deleted_post_id" -fun NavController.navigateToPostList(navOptions: NavOptions? = null) = - navigate(POST_LIST_ROUTE, navOptions) +const val POST_TYPE_ARGUMENT = "youthPolicy_argument" +const val POST_LIST_ROUTE_WITH_ARGUMENT = + "$POST_LIST_ROUTE/{$POST_TYPE_ARGUMENT}" + +fun NavController.navigateToPostList(postTopic: String? = null, navOptions: NavOptions? = null) = + navigate("$POST_LIST_ROUTE/${postTopic ?: PostTopicUiModel.FREEDOM}", navOptions) fun NavGraphBuilder.postListGraph( onShowSnackBar: (String) -> Unit, @@ -23,7 +30,12 @@ fun NavGraphBuilder.postListGraph( onClickRegisterPost: () -> Unit = {}, ) { composable( - route = POST_LIST_ROUTE, + arguments = listOf( + navArgument(POST_TYPE_ARGUMENT) { + type = NavType.StringType + }, + ), + route = POST_LIST_ROUTE_WITH_ARGUMENT, enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None }, ) { From 53939c2fd99d13310430efa20ba5a7a5edae8011 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Mon, 30 Sep 2024 21:18:55 +0900 Subject: [PATCH 7/9] chore: 2.2.1 --- app/build.gradle.kts | 4 ++-- .../main/java/com/withpeace/withpeace/MainBottomNavigation.kt | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 26b82061..854cbc1a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,8 +10,8 @@ android { defaultConfig { applicationId = "com.withpeace.withpeace" targetSdk = 34 - versionCode = 12 - versionName = "2.2.0" + versionCode = 13 + versionName = "2.2.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt b/app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt index 16be7979..db86cad1 100644 --- a/app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt +++ b/app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt @@ -21,6 +21,7 @@ import com.withpeace.withpeace.feature.home.navigation.navigateHome import com.withpeace.withpeace.feature.policylist.navigation.POLICY_LIST_ROUTE import com.withpeace.withpeace.feature.policylist.navigation.navigateToPolicyList import com.withpeace.withpeace.feature.postlist.navigation.POST_LIST_ROUTE +import com.withpeace.withpeace.feature.postlist.navigation.POST_LIST_ROUTE_WITH_ARGUMENT import com.withpeace.withpeace.feature.postlist.navigation.navigateToPostList import com.withpeace.withpeace.navigation.MY_PAGE_NESTED_ROUTE @@ -106,7 +107,7 @@ enum class BottomTab( iconUnSelectedResId = R.drawable.ic_bottom_community, iconSelectedResId = R.drawable.ic_bottom_community_select, contentDescription = R.string.post, - POST_LIST_ROUTE, + POST_LIST_ROUTE_WITH_ARGUMENT, ), MY_PAGE( iconUnSelectedResId = R.drawable.ic_bottom_my_page, From aca779ee83a70ace8cc1fb04d71b8e0e2ad65fbf Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Mon, 7 Oct 2024 19:55:51 +0900 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20=EC=A0=95=EC=B1=85=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=84=A4=EC=A0=95=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../withpeace/withpeace/navigation/NavHost.kt | 22 ++++-- .../domain/usecase/GetPolicyFilterUseCase.kt | 14 ++++ .../usecase/UpdatePolicyFilterUseCase.kt | 18 +++++ .../withpeace/feature/home/HomeScreen.kt | 56 +++++++++----- .../withpeace/feature/home/HomeViewModel.kt | 77 ++++++++++++++----- .../feature/postlist/PostListViewModel.kt | 4 +- .../postlist/navigation/PostListNavigation.kt | 12 ++- 7 files changed, 155 insertions(+), 48 deletions(-) create mode 100644 core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetPolicyFilterUseCase.kt create mode 100644 core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/UpdatePolicyFilterUseCase.kt diff --git a/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt index d2a20f3c..350b5761 100644 --- a/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt +++ b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.navOptions @@ -13,12 +14,10 @@ import com.app.profileeditor.navigation.navigateProfileEditor import com.app.profileeditor.navigation.profileEditorNavGraph import com.withpeace.withpeace.core.designsystem.ui.snackbar.SnackbarState import com.withpeace.withpeace.core.designsystem.ui.snackbar.SnackbarType -import com.withpeace.withpeace.feature.disablepolicy.navigation.DISABLE_POLICY_ID_ARGUMENT import com.withpeace.withpeace.feature.disablepolicy.navigation.disabledPolicyNavGraph import com.withpeace.withpeace.feature.disablepolicy.navigation.navigateDisabledPolicy import com.withpeace.withpeace.feature.gallery.navigation.galleryNavGraph import com.withpeace.withpeace.feature.gallery.navigation.navigateToGallery -import com.withpeace.withpeace.feature.home.navigation.HOME_ROUTE import com.withpeace.withpeace.feature.home.navigation.homeNavGraph import com.withpeace.withpeace.feature.home.navigation.navigateHome import com.withpeace.withpeace.feature.login.navigation.LOGIN_ROUTE @@ -193,11 +192,22 @@ fun WithpeaceNavHost( ) }, onPolicyClick = { - navController.navigateToPolicyDetail(policyId = it) + navController.navigateToPolicyDetail( + policyId = it, + ) + }, + onPostClick = { // TODO 인스턴스가 존재할 때, argument 로딩안됨 + navController.navigateToPostList( + it.name, + navOptions = navOptions { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + }, + ) }, - onPostClick = { - navController.navigateToPostList(it.name) - } ) policyDetailNavGraph( onShowSnackBar = { onShowSnackBar(SnackbarState(it)) }, diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetPolicyFilterUseCase.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetPolicyFilterUseCase.kt new file mode 100644 index 00000000..2e83088b --- /dev/null +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetPolicyFilterUseCase.kt @@ -0,0 +1,14 @@ +package com.withpeace.withpeace.core.domain.usecase + +import com.withpeace.withpeace.core.domain.model.error.CheonghaError +import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters +import com.withpeace.withpeace.core.domain.repository.UserRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class GetPolicyFilterUseCase @Inject constructor( + private val userRepository: UserRepository, +) { + operator fun invoke(onError: suspend (CheonghaError) -> Unit): Flow = + userRepository.getPolicyFilter(onError) +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/UpdatePolicyFilterUseCase.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/UpdatePolicyFilterUseCase.kt new file mode 100644 index 00000000..a1e6af73 --- /dev/null +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/UpdatePolicyFilterUseCase.kt @@ -0,0 +1,18 @@ +package com.withpeace.withpeace.core.domain.usecase + +import com.withpeace.withpeace.core.domain.model.error.CheonghaError +import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters +import com.withpeace.withpeace.core.domain.repository.UserRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class UpdatePolicyFilterUseCase @Inject constructor( + private val userRepository: UserRepository, +) { + operator fun invoke( + policyFilters: PolicyFilters, + onError: (CheonghaError) -> Unit, + ): Flow { + return userRepository.updatePolicyFilter(policyFilters = policyFilters, onError = onError) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt index 140fcfb3..8fc7b70d 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt @@ -71,12 +71,13 @@ fun HomeRoute( onPolicyClick: (String) -> Unit, onPostClick: (PostTopicUiModel) -> Unit, ) { - val selectedFilterUiState = viewModel.selectingFilters.collectAsStateWithLifecycle() + val selectingFilterUiState = viewModel.selectingFilters.collectAsStateWithLifecycle() val recentPosts = viewModel.recentPostsUiState.collectAsStateWithLifecycle() val hotPolicies = viewModel.hotPolicyUiState.collectAsStateWithLifecycle() val recommendPolicies = viewModel.recommendPolicyUiState.collectAsStateWithLifecycle() + val completedFilterUiState = viewModel.completedFilters.collectAsStateWithLifecycle() HomeScreen( - selectedFilterUiState = selectedFilterUiState.value, + selectedFilterUiState = selectingFilterUiState.value, onClassificationCheckChanged = viewModel::onCheckClassification, onRegionCheckChanged = viewModel::onCheckRegion, onFilterAllOff = viewModel::onFilterAllOff, @@ -88,6 +89,7 @@ fun HomeRoute( onPolicyClick = onPolicyClick, hotPolicyUiState = hotPolicies.value, recommendPolicyUiState = recommendPolicies.value, + completedFilterState = completedFilterUiState.value, ) } @@ -106,6 +108,7 @@ fun HomeScreen( onCloseFilter: () -> Unit, onPostClick: (PostTopicUiModel) -> Unit, onPolicyClick: (String) -> Unit, + completedFilterState: PolicyFiltersUiModel, ) { Column(modifier = modifier.fillMaxSize()) { HomeHeader( @@ -128,7 +131,7 @@ fun HomeScreen( onPolicyClick = onPolicyClick, hotPolicyUiState = hotPolicyUiState, recommendPolicyUiState = recommendPolicyUiState, - + completedFilterState = completedFilterState, ) } @@ -151,6 +154,7 @@ private fun ScrollSection( onPostClick: (PostTopicUiModel) -> Unit, hotPolicyUiState: HotPolicyUiState, recommendPolicyUiState: RecommendPolicyUiState, + completedFilterState: PolicyFiltersUiModel, ) { val builder = rememberBalloonBuilder { setIsVisibleArrow(false) @@ -249,7 +253,7 @@ private fun ScrollSection( } Spacer(modifier = modifier.height(8.dp)) - FlowRow { + FlowRow(verticalArrangement = Arrangement.spacedBy(8.dp)) { Image( painter = painterResource(id = R.drawable.ic_filter), modifier = modifier @@ -264,20 +268,34 @@ private fun ScrollSection( }, contentDescription = "", ) - // List(5) { //TODO("데이터 변경") - // Spacer(modifier = modifier.width(8.dp)) - // Text( - // text = "#부산", - // style = WithpeaceTheme.typography.Tag, - // color = WithpeaceTheme.colors.MainPurple, - // modifier = modifier - // .background( - // color = WithpeaceTheme.colors.SubPurple, - // shape = RoundedCornerShape(7.dp), - // ) - // .padding(6.dp), - // ) - // } + List(completedFilterState.classifications.size) { //TODO("데이터 변경") + Spacer(modifier = modifier.width(8.dp)) + Text( + text = "#${stringResource(id = completedFilterState.classifications[it].stringResId)}", + style = WithpeaceTheme.typography.Tag, + color = WithpeaceTheme.colors.MainPurple, + modifier = modifier + .background( + color = WithpeaceTheme.colors.SubPurple, + shape = RoundedCornerShape(7.dp), + ) + .padding(6.dp), + ) + } + List(completedFilterState.regions.size) { //TODO("데이터 변경") + Spacer(modifier = modifier.width(8.dp)) + Text( + text = "#${completedFilterState.regions[it].name}", + style = WithpeaceTheme.typography.Tag, + color = WithpeaceTheme.colors.MainPurple, + modifier = modifier + .background( + color = WithpeaceTheme.colors.SubPurple, + shape = RoundedCornerShape(7.dp), + ) + .padding(6.dp), + ) + } } } Spacer(modifier = modifier.height(16.dp)) @@ -319,6 +337,7 @@ private fun ScrollSection( } } } else { + //TODO 실패 UI items(6) { Column( modifier.clickable { @@ -389,6 +408,7 @@ private fun ScrollSection( } } } else { + //TODO 실패 UI items(6) { Column( modifier.clickable { diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt index 7dd18756..55794f2b 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt @@ -4,8 +4,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters import com.withpeace.withpeace.core.domain.usecase.GetHotPoliciesUseCase +import com.withpeace.withpeace.core.domain.usecase.GetPolicyFilterUseCase import com.withpeace.withpeace.core.domain.usecase.GetRecentPostUseCase import com.withpeace.withpeace.core.domain.usecase.GetRecommendPoliciesUseCase +import com.withpeace.withpeace.core.domain.usecase.UpdatePolicyFilterUseCase import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel import com.withpeace.withpeace.core.ui.policy.RegionUiModel import com.withpeace.withpeace.core.ui.policy.filtersetting.PolicyFiltersUiModel @@ -17,6 +19,7 @@ import com.withpeace.withpeace.feature.home.uistate.RecentPostsUiState import com.withpeace.withpeace.feature.home.uistate.RecommendPolicyUiState import com.withpeace.withpeace.feature.home.uistate.toUiModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -32,6 +35,8 @@ class HomeViewModel @Inject constructor( private val getRecentPostUseCase: GetRecentPostUseCase, private val getRecommendPoliciesUseCase: GetRecommendPoliciesUseCase, private val getHotPoliciesUseCase: GetHotPoliciesUseCase, + private val getPolicyFilterUseCase: GetPolicyFilterUseCase, + private val updatePolicyFilterUseCase: UpdatePolicyFilterUseCase, ) : ViewModel() { private val _recentPostsUiState: MutableStateFlow = MutableStateFlow(RecentPostsUiState.Loading) @@ -53,7 +58,14 @@ class HomeViewModel @Inject constructor( PolicyFiltersUiModel(), ) - private var completedFilters = PolicyFilters() + private val _completedFilters = MutableStateFlow(PolicyFilters()) + val completedFilters: StateFlow = + _completedFilters.map { it.toUiModel() }.stateIn( + scope = viewModelScope, + SharingStarted.WhileSubscribed(), + PolicyFiltersUiModel(), + ) + init { viewModelScope.launch { @@ -70,30 +82,47 @@ class HomeViewModel @Inject constructor( } } } + getRecommendPolicy() + getHotPolicy() launch { - getRecommendPoliciesUseCase( + getPolicyFilterUseCase( onError = { - _recommendPolicyUiState.update { - RecommendPolicyUiState.Failure - } }, ).collect { data -> - _recommendPolicyUiState.update { - RecommendPolicyUiState.Success(data.map { it.toUiModel() }) - } + _selectingFilters.update { data } + _completedFilters.update { data } } } - launch { - getHotPoliciesUseCase( - onError = { - _hotPolicyUiState.update { - HotPolicyUiState.Failure - } - }, - ).collect { data -> + } + } + + private fun CoroutineScope.getHotPolicy() { + launch { + getHotPoliciesUseCase( + onError = { _hotPolicyUiState.update { - HotPolicyUiState.Success(data.map { it.toUiModel() }) + HotPolicyUiState.Failure + } + }, + ).collect { data -> + _hotPolicyUiState.update { + HotPolicyUiState.Success(data.map { it.toUiModel() }) + } + } + } + } + + private fun CoroutineScope.getRecommendPolicy() { + launch { + getRecommendPoliciesUseCase( + onError = { + _recommendPolicyUiState.update { + RecommendPolicyUiState.Failure } + }, + ).collect { data -> + _recommendPolicyUiState.update { + RecommendPolicyUiState.Success(data.map { it.toUiModel() }) } } } @@ -112,12 +141,20 @@ class HomeViewModel @Inject constructor( } fun onCompleteFilter() { - completedFilters = selectingFilters.value.toDomain() - // api + viewModelScope.launch { + updatePolicyFilterUseCase( + policyFilters = selectingFilters.value.toDomain(), + onError = {}, + ).collect { + _completedFilters.update { selectingFilters.value.toDomain() } + this.launch { getHotPolicy() } + this.launch { getRecommendPolicy() } + } + } } fun onCancelFilter() { - _selectingFilters.update { completedFilters } + _selectingFilters.update { completedFilters.value.toDomain() } } fun onFilterAllOff() { diff --git a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListViewModel.kt b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListViewModel.kt index eaae2753..25f61017 100644 --- a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListViewModel.kt +++ b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListViewModel.kt @@ -34,9 +34,7 @@ class PostListViewModel @Inject constructor( private val _uiEvent = Channel() val uiEvent = _uiEvent.receiveAsFlow() - private val _currentTopic = MutableStateFlow(PostTopicUiModel.entries.find { - it.name == savedStateHandle.get(POST_TYPE_ARGUMENT) - } ?: PostTopicUiModel.FREEDOM) + private val _currentTopic = MutableStateFlow(PostTopicUiModel.FREEDOM) val currentTopic = _currentTopic.asStateFlow() private val _postListPagingFlow = MutableStateFlow(PagingData.empty()) diff --git a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/navigation/PostListNavigation.kt b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/navigation/PostListNavigation.kt index 6f2fe952..328ec835 100644 --- a/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/navigation/PostListNavigation.kt +++ b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/navigation/PostListNavigation.kt @@ -1,5 +1,6 @@ package com.withpeace.withpeace.feature.postlist.navigation +import android.util.Log import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.hilt.navigation.compose.hiltViewModel @@ -39,8 +40,17 @@ fun NavGraphBuilder.postListGraph( enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None }, ) { - val deletedId = it.savedStateHandle.get(POST_LIST_DELETED_POST_ID_ARGUMENT) val viewModel: PostListViewModel = hiltViewModel() + val deletedId = it.savedStateHandle.get(POST_LIST_DELETED_POST_ID_ARGUMENT) + + val postType = it.savedStateHandle.get(POST_TYPE_ARGUMENT) + // postType?.let { // recomposition이 안되는건가 + // viewModel.onTopicChanged( + // PostTopicUiModel.entries.find { + // it.name == postType + // } ?: PostTopicUiModel.FREEDOM, + // ) + // } deletedId?.let { id -> viewModel.updateDeletedId(id) } From 99df02aac1157c9e46f63b558bea6d5b05949bbf Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Wed, 9 Oct 2024 18:38:18 +0900 Subject: [PATCH 9/9] =?UTF-8?q?chore:=20=EB=B2=84=EC=A0=84=20=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 854cbc1a..09ff1086 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,8 +10,8 @@ android { defaultConfig { applicationId = "com.withpeace.withpeace" targetSdk = 34 - versionCode = 13 - versionName = "2.2.1" + versionCode = 14 + versionName = "2.2.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables {