diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b2784c6a..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 = 11
- versionName = "2.1.1"
+ versionCode = 14
+ versionName = "2.2.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -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..db86cad1 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,11 @@ 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.POST_LIST_ROUTE_WITH_ARGUMENT
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 +79,8 @@ private fun NavController.navigateToTabScreen(bottomTab: BottomTab) {
when (bottomTab) {
BottomTab.HOME -> navigateHome(tabNavOptions)
- BottomTab.POST -> navigateToPostList(tabNavOptions)
- BottomTab.REGISTER_POST -> navigateToRegisterPost()
+ BottomTab.POLICY -> navigateToPolicyList(tabNavOptions)
+ BottomTab.POST -> navigateToPostList(navOptions = tabNavOptions)
BottomTab.MY_PAGE -> navigate(MY_PAGE_NESTED_ROUTE, tabNavOptions)
}
}
@@ -97,17 +97,17 @@ 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,
+ POST_LIST_ROUTE_WITH_ARGUMENT,
),
MY_PAGE(
iconUnSelectedResId = R.drawable.ic_bottom_my_page,
@@ -119,7 +119,6 @@ enum class BottomTab(
companion object {
operator fun contains(route: String): Boolean {
- if (route == REGISTER_POST_ROUTE) return false
return entries.map { it.route }.contains(route)
}
}
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..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
@@ -32,11 +31,13 @@ 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
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
@@ -191,7 +192,21 @@ 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
+ },
+ )
},
)
policyDetailNavGraph(
@@ -319,6 +334,28 @@ fun WithpeaceNavHost(
onAuthExpired = {
onAuthExpired(onShowSnackBar, navController)
},
+ onClickRegisterPost = {
+ 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/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/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/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/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/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/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/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/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
new file mode 100644
index 00000000..18642f44
--- /dev/null
+++ b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/FilterBottomSheet.kt
@@ -0,0 +1,350 @@
+package com.withpeace.withpeace.core.ui.policy.filtersetting
+
+import androidx.compose.foundation.BorderStroke
+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.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.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.lazy.LazyRow
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ButtonColors
+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.graphics.Color
+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.R
+import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel
+import com.withpeace.withpeace.core.ui.policy.RegionUiModel
+
+@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(
+ PolicyFiltersUiModel(
+ classifications = ClassificationUiModel.entries,
+ regions = RegionUiModel.entries,
+ ),
+ )
+ }
+ 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,
+ 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),
+ )
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+private fun ScrollableFilterSection(
+ modifier: Modifier,
+ filterListUiState: PolicyFiltersUiModel,
+ selectedFilterUiState: PolicyFiltersUiModel,
+ onClassificationCheckChanged: (ClassificationUiModel) -> Unit,
+ onRegionCheckChanged: (RegionUiModel) -> Unit,
+ scrollState: ScrollState,
+) {
+ Column(
+ modifier = modifier
+ .verticalScroll(scrollState)
+ .padding(horizontal = 24.dp),
+ ) {
+ Spacer(modifier = modifier.height(16.dp))
+ Text(
+ text = stringResource(R.string.classification),
+ style = WithpeaceTheme.typography.title2,
+ color = WithpeaceTheme.colors.SnackbarBlack,
+ )
+ Spacer(modifier = modifier.height(16.dp))
+ 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,
+ ),
+ 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(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(
+ 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.SnackbarBlack,
+ )
+
+ Spacer(modifier = modifier.height(16.dp))
+ 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,
+ ),
+ 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))
+ }
+}
+
+@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/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/PolicyFiltersUiModel.kt b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/PolicyFiltersUiModel.kt
similarity index 93%
rename from feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/PolicyFiltersUiModel.kt
rename to core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/filtersetting/PolicyFiltersUiModel.kt
index 88c35818..e2ad44f5 100644
--- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/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.home.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..7b910434 100644
--- a/core/ui/src/main/res/values/strings.xml
+++ b/core/ui/src/main/res/values/strings.xml
@@ -30,4 +30,14 @@
복지,문화
참여,권리
기타
+
+
+ 필터
+ 필터 닫기
+ 더보기
+ 정책 분류 이미지
+ 지역
+ 정책분야
+ 전체 해제
+ 검색하기
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 894f4943..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
@@ -5,20 +5,25 @@ 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.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState
+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
@@ -35,80 +40,65 @@ 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.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 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
fun HomeRoute(
onShowSnackBar: (message: String) -> Unit = {},
- onNavigationSnackBar: (message: String) -> Unit = {},
viewModel: HomeViewModel = hiltViewModel(),
+ onNavigationSnackBar: (message: String) -> Unit = {},
onPolicyClick: (String) -> Unit,
+ onPostClick: (PostTopicUiModel) -> 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("관심목록에서 삭제되었습니다.")
- }
- }
- }
- }
+ 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(
- youthPolicies = youthPolicyPagingData,
- selectedFilterUiState = selectedFilterUiState.value,
- onDismissRequest = viewModel::onCancelFilter,
+ selectedFilterUiState = selectingFilterUiState.value,
onClassificationCheckChanged = viewModel::onCheckClassification,
onRegionCheckChanged = viewModel::onCheckRegion,
onFilterAllOff = viewModel::onFilterAllOff,
onSearchWithFilter = viewModel::onCompleteFilter,
onCloseFilter = viewModel::onCancelFilter,
+ onDismissRequest = viewModel::onCancelFilter,
+ recentPosts = recentPosts.value,
+ onPostClick = onPostClick,
onPolicyClick = onPolicyClick,
- onBookmarkClick = viewModel::bookmark
+ hotPolicyUiState = hotPolicies.value,
+ recommendPolicyUiState = recommendPolicies.value,
+ completedFilterState = completedFilterUiState.value,
)
}
@Composable
fun HomeScreen(
+ recentPosts: RecentPostsUiState,
+ hotPolicyUiState: HotPolicyUiState,
+ recommendPolicyUiState: RecommendPolicyUiState,
modifier: Modifier = Modifier,
- youthPolicies: LazyPagingItems,
selectedFilterUiState: PolicyFiltersUiModel,
onDismissRequest: () -> Unit,
onClassificationCheckChanged: (ClassificationUiModel) -> Unit,
@@ -116,145 +106,44 @@ fun HomeScreen(
onFilterAllOff: () -> Unit,
onSearchWithFilter: () -> Unit,
onCloseFilter: () -> Unit,
+ onPostClick: (PostTopicUiModel) -> Unit,
onPolicyClick: (String) -> Unit,
- onBookmarkClick: (id: String, isChecked: Boolean) -> Unit,
-
+ completedFilterState: PolicyFiltersUiModel,
) {
Column(modifier = modifier.fillMaxSize()) {
HomeHeader(
modifier = modifier,
- onDismissRequest = onDismissRequest,
+ )
+ HorizontalDivider(
+ modifier = modifier.height(1.dp),
+ color = WithpeaceTheme.colors.SystemGray3,
+ )
+ ScrollSection(
+ recentPosts = recentPosts,
selectedFilterUiState = selectedFilterUiState,
+ onDismissRequest = onDismissRequest,
onClassificationCheckChanged = onClassificationCheckChanged,
onRegionCheckChanged = onRegionCheckChanged,
onFilterAllOff = onFilterAllOff,
onSearchWithFilter = onSearchWithFilter,
onCloseFilter = onCloseFilter,
+ onPostClick = onPostClick,
+ onPolicyClick = onPolicyClick,
+ hotPolicyUiState = hotPolicyUiState,
+ recommendPolicyUiState = recommendPolicyUiState,
+ completedFilterState = completedFilterState,
)
- 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 = "home")
}
+@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
@Composable
-private fun PolicyItems(
- modifier: Modifier,
- youthPolicies: LazyPagingItems,
+private fun ScrollSection(
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,
+ recentPosts: RecentPostsUiState,
+ modifier: Modifier = Modifier,
selectedFilterUiState: PolicyFiltersUiModel,
onDismissRequest: () -> Unit,
onClassificationCheckChanged: (ClassificationUiModel) -> Unit,
@@ -262,7 +151,21 @@ private fun HomeHeader(
onFilterAllOff: () -> Unit,
onSearchWithFilter: () -> Unit,
onCloseFilter: () -> Unit,
+ onPostClick: (PostTopicUiModel) -> Unit,
+ hotPolicyUiState: HotPolicyUiState,
+ recommendPolicyUiState: RecommendPolicyUiState,
+ completedFilterState: PolicyFiltersUiModel,
) {
+ val builder = rememberBalloonBuilder {
+ setIsVisibleArrow(false)
+ setWidth(BalloonSizeSpec.WRAP)
+ setHeight(BalloonSizeSpec.WRAP)
+ setPadding(12)
+ setCornerRadius(12f)
+ setBackgroundColor(Color(0xFFF9FBFB))
+ setBalloonAnimation(BalloonAnimation.FADE)
+ setArrowSize(0)
+ }
var showBottomSheet by remember { mutableStateOf(false) }
val bottomSheetChildScrollState = rememberScrollState()
@@ -277,30 +180,6 @@ private fun HomeHeader(
}
}
- 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,
@@ -333,180 +212,293 @@ private fun HomeHeader(
)
}
}
-}
-@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)
- },
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ contentPadding = PaddingValues(bottom = 24.dp),
) {
- ConstraintLayout(
- modifier = modifier
- .fillMaxWidth()
- .background(WithpeaceTheme.colors.SystemWhite)
- .padding(16.dp),
- ) {
- val (
- title, content,
- region, ageRange, thumbnail, heart,
- ) = createRefs()
+ item {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = WithpeaceTheme.colors.Gray3_70)
+ .padding(horizontal = 24.dp, vertical = 12.dp),
+ ) {
+ 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))
+ FlowRow(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_filter),
+ modifier = modifier
+ .background(
+ color = WithpeaceTheme.colors.SubPurple,
+ shape = CircleShape,
+ )
+ .padding(4.dp)
+ .size(16.dp)
+ .clickable {
+ showBottomSheet = true
+ },
+ contentDescription = "",
+ )
+ 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))
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,
+ 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),
+ ) {
+ 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 {
+ //TODO 실패 UI
+ 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,
+ )
+ }
+ }
+ }
+ }
+ Spacer(modifier = modifier.height(24.dp))
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,
+ modifier = modifier.padding(horizontal = 24.dp),
+ text = "맞춤 정책을 추천해드릴게요!",
+ color = WithpeaceTheme.colors.SnackbarBlack,
+ style = WithpeaceTheme.typography.title2,
)
- 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)
- },
- )
-
+ Spacer(modifier = modifier.height(8.dp))
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,
+ 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),
+ ) {
+ 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 {
+ //TODO 실패 UI
+ 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,
+ )
+ }
+ }
+ }
+ }
+ Spacer(modifier = modifier.height(24.dp))
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)
+ modifier = modifier.padding(horizontal = 24.dp),
+ text = "커뮤니티",
+ color = WithpeaceTheme.colors.SnackbarBlack,
+ style = WithpeaceTheme.typography.title2,
+ )
+ Spacer(modifier = modifier.height(16.dp))
+ }
+ 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,
)
- .background(
- color = WithpeaceTheme.colors.SystemGray3,
- shape = RoundedCornerShape(size = 5.dp),
+ 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,
)
- .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->}
+private fun HomeHeader(
+ modifier: Modifier,
+) {
+ 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),
+ // )
}
-}
+}
\ 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 a38bb3d3..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
@@ -2,65 +2,53 @@ 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.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
+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.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.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.ExperimentalCoroutinesApi
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.CoroutineScope
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 youthPoliciesUseCase: GetYouthPoliciesUseCase,
- private val bookmarkPolicyUseCase: BookmarkPolicyUseCase,
+ private val getRecentPostUseCase: GetRecentPostUseCase,
+ private val getRecommendPoliciesUseCase: GetRecommendPoliciesUseCase,
+ private val getHotPoliciesUseCase: GetHotPoliciesUseCase,
+ private val getPolicyFilterUseCase: GetPolicyFilterUseCase,
+ private val updatePolicyFilterUseCase: UpdatePolicyFilterUseCase,
) : 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 _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 =
@@ -70,63 +58,72 @@ class HomeViewModel @Inject constructor(
PolicyFiltersUiModel(),
)
- private val _uiEvent = Channel()
- val uiEvent = _uiEvent.receiveAsFlow()
-
- private val debounceFlow = MutableSharedFlow(replay = 1)
-
- private val lastByWhetherSuccessOfBookmarks =
- mutableMapOf() // optimistic UI에서 실패시에 사용할 캐시 데이터
+ private val _completedFilters = MutableStateFlow(PolicyFilters())
+ val completedFilters: StateFlow =
+ _completedFilters.map { it.toUiModel() }.stateIn(
+ scope = viewModelScope,
+ SharingStarted.WhileSubscribed(),
+ PolicyFiltersUiModel(),
+ )
- private var completedFilters = PolicyFilters()
init {
viewModelScope.launch {
- debounceFlow.groupBy { it.id }.flatMapMerge {
- it.second.debounce(300L)
- }.collectLatest { bookmarkInfo -> // policyBookmarkViewModel과 다른 이유를 찾아보기
- bookmarkPolicyUseCase(
- bookmarkInfo.id, bookmarkInfo.isBookmarked,
+ launch {
+ getRecentPostUseCase(
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)
+ ).collect { data ->
+ _recentPostsUiState.update {
+ RecentPostsUiState.Success(
+ data.map { it.toUiModel() },
+ )
}
}
-
+ }
+ getRecommendPolicy()
+ getHotPolicy()
+ launch {
+ getPolicyFilterUseCase(
+ onError = {
+ },
+ ).collect { data ->
+ _selectingFilters.update { data }
+ _completedFilters.update { data }
+ }
}
}
- fetchData()
}
+ private fun CoroutineScope.getHotPolicy() {
+ launch {
+ getHotPoliciesUseCase(
+ onError = {
+ _hotPolicyUiState.update {
+ HotPolicyUiState.Failure
+ }
+ },
+ ).collect { data ->
+ _hotPolicyUiState.update {
+ HotPolicyUiState.Success(data.map { it.toUiModel() })
+ }
+ }
+ }
+ }
- private fun fetchData() {
- viewModelScope.launch {
- _youthPolicyPagingFlow.update {
- youthPoliciesUseCase(
- filterInfo = completedFilters,
- onError = {
- when (it) {
- ResponseError.EXPIRED_TOKEN_ERROR -> {}
- else -> {}
- }
- },
- ).map {
- it.map { youthPolicy ->
- youthPolicy.toUiModel()
+ private fun CoroutineScope.getRecommendPolicy() {
+ launch {
+ getRecommendPoliciesUseCase(
+ onError = {
+ _recommendPolicyUiState.update {
+ RecommendPolicyUiState.Failure
}
- }.cachedIn(viewModelScope).firstOrNull() ?: PagingData.empty()
+ },
+ ).collect { data ->
+ _recommendPolicyUiState.update {
+ RecommendPolicyUiState.Success(data.map { it.toUiModel() })
+ }
}
}
}
@@ -144,12 +141,20 @@ class HomeViewModel @Inject constructor(
}
fun onCompleteFilter() {
- completedFilters = selectingFilters.value.toDomain()
- fetchData()
+ 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() {
@@ -157,11 +162,4 @@ class HomeViewModel @Inject constructor(
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/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/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/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/.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..c7eeba42
--- /dev/null
+++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListScreen.kt
@@ -0,0 +1,511 @@
+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.core.ui.policy.filtersetting.FilterBottomSheet
+import com.withpeace.withpeace.feature.policylist.uistate.PolicyListUiEvent
+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
+
+@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..12bec2da
--- /dev/null
+++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/PolicyListViewModel.kt
@@ -0,0 +1,168 @@
+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.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.YouthPolicyUiModel
+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/home/src/main/java/com/withpeace/withpeace/feature/home/filtersetting/FilterBottomSheet.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt
similarity index 51%
rename from feature/home/src/main/java/com/withpeace/withpeace/feature/home/filtersetting/FilterBottomSheet.kt
rename to feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/FilterBottomSheet.kt
index 3f02e149..7b45d93f 100644
--- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/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.home.filtersetting
+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,9 +40,8 @@ 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.home.R
-import com.withpeace.withpeace.feature.home.filtersetting.uistate.FilterListUiState
-import com.withpeace.withpeace.feature.home.uistate.PolicyFiltersUiModel
+import com.withpeace.withpeace.core.ui.policy.filtersetting.PolicyFiltersUiModel
+import com.withpeace.withpeace.feature.policylist.R
@Composable
fun FilterBottomSheet(
@@ -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,93 +131,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),
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 +251,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/filtersetting/PolicyFiltersUiModel.kt b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/PolicyFiltersUiModel.kt
new file mode 100644
index 00000000..d79b7c29
--- /dev/null
+++ b/feature/policylist/src/main/java/com/withpeace/withpeace/feature/policylist/filtersetting/PolicyFiltersUiModel.kt
@@ -0,0 +1,29 @@
+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
+
+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/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/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 00000000..bac4677c
Binary files /dev/null and b/feature/policylist/src/main/res/drawable/ic_home_thumbnail_example.png differ
diff --git a/feature/policylist/src/main/res/drawable/ic_no_result.xml b/feature/policylist/src/main/res/drawable/ic_no_result.xml
new file mode 100644
index 00000000..9770e928
--- /dev/null
+++ b/feature/policylist/src/main/res/drawable/ic_no_result.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
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/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..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
@@ -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
@@ -67,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()
@@ -75,6 +78,7 @@ fun PostListRoute(
postListPagingData = postListPagingData,
onTopicChanged = viewModel::onTopicChanged,
navigateToDetail = navigateToDetail,
+ onClickRegisterPost = onClickRegisterPost,
)
LaunchedEffect(null) {
@@ -93,8 +97,13 @@ fun PostListScreen(
postListPagingData: LazyPagingItems,
onTopicChanged: (PostTopicUiModel) -> Unit = {},
navigateToDetail: (postId: Long) -> Unit = {},
+ onClickRegisterPost: () -> 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 +137,24 @@ fun PostListScreen(
}
}
}
+ Box(modifier = Modifier.fillMaxSize()) {
+ FloatingActionButton(
+ shape = CircleShape,
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(bottom = 16.dp, end = 16.dp),
+ onClick = {
+ onClickRegisterPost()
+ },
+ 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/java/com/withpeace/withpeace/feature/postlist/PostListViewModel.kt b/feature/postlist/src/main/java/com/withpeace/withpeace/feature/postlist/PostListViewModel.kt
index d85fd858..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
@@ -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,6 +29,7 @@ import javax.inject.Inject
@HiltViewModel
class PostListViewModel @Inject constructor(
private val getPostsUseCase: GetPostsUseCase,
+ savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val _uiEvent = Channel()
val uiEvent = _uiEvent.receiveAsFlow()
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..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,33 +1,56 @@
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
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,
navigateToPostDetail: (postId: Long) -> Unit,
onAuthExpired: () -> Unit,
+ 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 },
) {
- 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)
}
@@ -36,6 +59,7 @@ fun NavGraphBuilder.postListGraph(
onShowSnackBar = onShowSnackBar,
navigateToDetail = navigateToPostDetail,
onAuthExpired = onAuthExpired,
+ onClickRegisterPost = onClickRegisterPost,
)
}
}
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 @@
+
+
+
+
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" }
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")