diff --git a/core/src/main/java/in/koreatech/koin/core/util/ContextUtil.kt b/core/src/main/java/in/koreatech/koin/core/util/ContextUtil.kt
new file mode 100644
index 000000000..4d8794bfe
--- /dev/null
+++ b/core/src/main/java/in/koreatech/koin/core/util/ContextUtil.kt
@@ -0,0 +1,9 @@
+package `in`.koreatech.koin.core.util
+
+import android.content.Context
+import android.content.Intent
+import androidx.core.net.toUri
+
+fun Context.goToContactUrl() = startActivity(Intent(Intent.ACTION_VIEW, KOIN_ASK_FORM.toUri()))
+
+const val KOIN_ASK_FORM = "https://forms.gle/Yo1WNR5mLQdi1pMh6"
diff --git a/feature/setting/.gitignore b/feature/setting/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/feature/setting/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/setting/build.gradle.kts b/feature/setting/build.gradle.kts
new file mode 100644
index 000000000..34be694d4
--- /dev/null
+++ b/feature/setting/build.gradle.kts
@@ -0,0 +1,26 @@
+plugins {
+ alias(libs.plugins.koin.feature)
+ alias(libs.plugins.koin.hilt)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "in.koreatech.koin.feature.setting"
+}
+
+dependencies {
+
+ implementation(projects.core)
+ implementation(projects.domain)
+ implementation(projects.core.designsystem)
+ implementation(projects.core.navigation)
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+
+ implementation(libs.kotlinx.collections.immutable)
+
+ implementation(libs.timber)
+ implementation(libs.play.services.oss.licenses)
+}
diff --git a/feature/setting/consumer-rules.pro b/feature/setting/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/feature/setting/proguard-rules.pro b/feature/setting/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/feature/setting/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/setting/src/main/AndroidManifest.xml b/feature/setting/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..22e77079a
--- /dev/null
+++ b/feature/setting/src/main/AndroidManifest.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/component/SettingItem.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/component/SettingItem.kt
new file mode 100644
index 000000000..4e1201b40
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/component/SettingItem.kt
@@ -0,0 +1,61 @@
+package `in`.koreatech.koin.feature.setting.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+import `in`.koreatech.koin.feature.setting.R
+
+@Composable
+fun SettingItem(
+ text: String,
+ modifier: Modifier = Modifier,
+ showIcon: Boolean = false,
+ textStyle: TextStyle = KoinTheme.typography.regular16,
+ backgroundColor: Color = KoinTheme.colors.neutral0,
+ onClick: () -> Unit = {}
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = backgroundColor)
+ .clickable(onClick = onClick)
+ .padding(vertical = 13.dp, horizontal = 24.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BasicText(
+ text = text,
+ style = textStyle
+ )
+ if (showIcon) {
+ Icon(
+ painter = painterResource(R.drawable.ic_arrow_right),
+ contentDescription = ""
+ )
+ }
+ }
+ HorizontalDivider(color = KoinTheme.colors.neutral100)
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SettingItemPreview() {
+ SettingItem(
+ text = "프로필"
+ )
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/component/SettingTitle.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/component/SettingTitle.kt
new file mode 100644
index 000000000..08cdeda50
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/component/SettingTitle.kt
@@ -0,0 +1,38 @@
+package `in`.koreatech.koin.feature.setting.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+
+@Composable
+fun SettingTitle(
+ text: String,
+ modifier: Modifier = Modifier,
+ textStyle: TextStyle = KoinTheme.typography.medium14,
+ backgroundColor: Color = KoinTheme.colors.neutral50
+) {
+ BasicText(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = backgroundColor)
+ .padding(vertical = 8.dp, horizontal = 24.dp),
+ text = text,
+ style = textStyle
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SettingTitlePreview() {
+ SettingTitle(
+ text = "일반"
+ )
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/component/SettingVersionItem.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/component/SettingVersionItem.kt
new file mode 100644
index 000000000..2dd27383b
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/component/SettingVersionItem.kt
@@ -0,0 +1,102 @@
+package `in`.koreatech.koin.feature.setting.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+import `in`.koreatech.koin.feature.setting.R
+
+@Composable
+fun SettingVersionItem(
+ currentVersion: String,
+ latestVersion: String,
+ showVersionInfo: Boolean,
+ modifier: Modifier = Modifier,
+ textStyle: TextStyle = KoinTheme.typography.regular16,
+ backgroundColor: Color = KoinTheme.colors.neutral0
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = backgroundColor)
+ .padding(
+ vertical = if (showVersionInfo) 5.dp else 13.dp,
+ horizontal = 24.dp
+ ),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BasicText(
+ text = stringResource(R.string.setting_item_app_version),
+ style = textStyle
+ )
+ if (showVersionInfo) {
+ Column(
+ horizontalAlignment = Alignment.End
+ ) {
+ BasicText(
+ text = currentVersion,
+ style = KoinTheme.typography.regular14
+ )
+ BasicText(
+ text = if (currentVersion == latestVersion) {
+ stringResource(R.string.setting_item_newest_version_info)
+ } else {
+ stringResource(R.string.setting_item_not_newest_version_info, latestVersion)
+ },
+ style = KoinTheme.typography.regular12.copy(
+ color = if (currentVersion == latestVersion) {
+ KoinTheme.colors.neutral500
+ } else {
+ KoinTheme.colors.primary500
+ }
+ )
+ )
+ }
+ }
+ }
+ HorizontalDivider(color = KoinTheme.colors.neutral100)
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SettingVersionItemPreviewVersion() {
+ SettingVersionItem(
+ currentVersion = "4.2.3",
+ latestVersion = "4.2.4",
+ showVersionInfo = true
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SettingVersionItemPreviewLatest() {
+ SettingVersionItem(
+ currentVersion = "4.2.2",
+ latestVersion = "4.2.2",
+ showVersionInfo = true
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SettingVersionItemPreviewNotShow() {
+ SettingVersionItem(
+ currentVersion = "4.2.2",
+ latestVersion = "4.2.2",
+ showVersionInfo = false
+ )
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/component/switch/KoinSwitch.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/component/switch/KoinSwitch.kt
new file mode 100644
index 000000000..5e37d312d
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/component/switch/KoinSwitch.kt
@@ -0,0 +1,116 @@
+package `in`.koreatech.koin.feature.setting.component.switch
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.shape.CircleShape
+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.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+
+@Composable
+fun KoinSwitch(
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ switchColors: KoinSwitchColors = KoinSwitchDefaults.koinSwitchColors()
+) {
+ var isChecked by remember(checked) { mutableStateOf(checked) }
+ val thumbOffset = remember { Animatable(if (isChecked) 1f else 0f) }
+
+ LaunchedEffect(isChecked) {
+ thumbOffset.animateTo(if (isChecked) 1f else 0f, animationSpec = tween(durationMillis = 300))
+ }
+
+ Box(
+ modifier = modifier
+ .height(24.dp)
+ .aspectRatio(2.1f)
+ .background(Color.Gray, CircleShape)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null
+ ) {
+ isChecked = !isChecked
+ onCheckedChange(isChecked)
+ }
+ ) {
+ Canvas(modifier = Modifier.matchParentSize()) {
+ val trackWidth = size.width
+ val trackHeight = size.height
+ val thumbMargin = size.height / 8
+ val thumbRadius = trackHeight / 2 - thumbMargin
+
+ // Draw track
+ drawRoundRect(
+ color = if (isChecked) switchColors.selectedContainerColor else switchColors.unselectedContainerColor,
+ size = size,
+ cornerRadius = CornerRadius(trackHeight / 2, trackHeight / 2)
+ )
+
+ // Draw thumb
+ val thumbX = (thumbOffset.value * (trackWidth - (thumbRadius + thumbMargin) * 2)) + (thumbRadius + thumbMargin)
+ drawCircle(
+ color = if (isChecked) switchColors.selectedContentColor else switchColors.unselectedContentColor,
+ radius = thumbRadius,
+ center = Offset(thumbX, trackHeight / 2)
+ )
+ }
+ }
+}
+
+object KoinSwitchDefaults {
+ @Composable
+ fun koinSwitchColors(
+ selectedContainerColor: Color = KoinTheme.colors.primary500,
+ selectedContentColor: Color = Color.White,
+ unselectedContainerColor: Color = KoinTheme.colors.neutral300,
+ unselectedContentColor: Color = KoinTheme.colors.neutral0
+ ) = KoinSwitchColors(
+ selectedContainerColor = selectedContainerColor,
+ selectedContentColor = selectedContentColor,
+ unselectedContainerColor = unselectedContainerColor,
+ unselectedContentColor = unselectedContentColor
+ )
+}
+
+class KoinSwitchColors internal constructor(
+ val selectedContainerColor: Color,
+ val selectedContentColor: Color,
+ val unselectedContainerColor: Color,
+ val unselectedContentColor: Color
+)
+
+@Preview
+@Composable
+private fun KoinSwitchCheckedPreview() {
+ KoinSwitch(
+ checked = true,
+ onCheckedChange = {}
+ )
+}
+
+@Preview
+@Composable
+private fun KoinSwitchUnCheckedPreview() {
+ KoinSwitch(
+ checked = false,
+ onCheckedChange = {}
+ )
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/constant/Constant.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/constant/Constant.kt
new file mode 100644
index 000000000..0b665c0c0
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/constant/Constant.kt
@@ -0,0 +1,3 @@
+package `in`.koreatech.koin.feature.setting.constant
+
+val ARTICLE_KEYWORD_URL = "koin://article/activity?fragment=article_keyword"
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/constant/TermConstant.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/constant/TermConstant.kt
new file mode 100644
index 000000000..9d6baa116
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/constant/TermConstant.kt
@@ -0,0 +1,10 @@
+package `in`.koreatech.koin.feature.setting.constant
+
+enum class TermConstant(
+ val type: String
+) {
+ TERM_UNKNOWN("termUnknown"),
+ TERM_KOIN("termKoin"),
+ TERM_PRIVACY_POLICY("termPrivacyPolicy"),
+ TERM_MARKETING("termMarketing")
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/navigation/Navigation.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/navigation/Navigation.kt
new file mode 100644
index 000000000..db09991b5
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/navigation/Navigation.kt
@@ -0,0 +1,83 @@
+package `in`.koreatech.koin.feature.setting.navigation
+
+import android.app.Activity
+import androidx.compose.ui.platform.LocalContext
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import `in`.koreatech.koin.feature.setting.constant.TermConstant
+import `in`.koreatech.koin.feature.setting.ui.SettingScreen
+import `in`.koreatech.koin.feature.setting.ui.notification.NotificationScreen
+import `in`.koreatech.koin.feature.setting.ui.term.TermScreen
+
+fun NavGraphBuilder.koinSettingGraph(
+ navController: NavController
+) {
+ composable(
+ route = SettingNavType.Setting.route
+ ) {
+ val context = LocalContext.current
+
+ SettingScreen(
+ onTopbarBackClick = {
+ if (!navController.popBackStack()) {
+ (context as? Activity)?.finish()
+ }
+ },
+ onNotificationClick = {
+ navController.navigate(SettingNavType.Notification.route)
+ },
+ onPrivacyPolicyClick = {
+ navController.navigate("${SettingNavType.Term.route}/${TermConstant.TERM_PRIVACY_POLICY.type}")
+ },
+ onKoinTermsClick = {
+ navController.navigate("${SettingNavType.Term.route}/${TermConstant.TERM_KOIN.type}")
+ },
+ onMarketingTermsClick = {
+ navController.navigate("${SettingNavType.Term.route}/${TermConstant.TERM_MARKETING.type}")
+ }
+ )
+ }
+
+ composable(
+ route = "${SettingNavType.Term.route}/{${TERM_TYPE}}",
+ arguments = listOf(
+ navArgument(TERM_TYPE) { type = NavType.StringType }
+ )
+ ) {
+ val context = LocalContext.current
+ val termType = when (it.arguments?.getString(TERM_TYPE)) {
+ TermConstant.TERM_KOIN.type -> TermConstant.TERM_KOIN
+ TermConstant.TERM_PRIVACY_POLICY.type -> TermConstant.TERM_PRIVACY_POLICY
+ TermConstant.TERM_MARKETING.type -> TermConstant.TERM_MARKETING
+ else -> TermConstant.TERM_UNKNOWN
+ }
+
+ TermScreen(
+ termType = termType,
+ onTopbarBackClick = {
+ if (!navController.popBackStack()) {
+ (context as? Activity)?.finish()
+ }
+ }
+ )
+ }
+
+ composable(
+ route = SettingNavType.Notification.route
+ ) {
+ val context = LocalContext.current
+
+ NotificationScreen(
+ onTopbarBackClick = {
+ if (!navController.popBackStack()) {
+ (context as? Activity)?.finish()
+ }
+ }
+ )
+ }
+}
+
+const val TERM_TYPE = "termType"
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/navigation/SettingNavType.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/navigation/SettingNavType.kt
new file mode 100644
index 000000000..72a12f5b1
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/navigation/SettingNavType.kt
@@ -0,0 +1,7 @@
+package `in`.koreatech.koin.feature.setting.navigation
+
+sealed class SettingNavType(val route: String) {
+ data object Setting : SettingNavType("Setting")
+ data object Term : SettingNavType("Term")
+ data object Notification : SettingNavType("Notification")
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/SettingActivity.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/SettingActivity.kt
new file mode 100644
index 000000000..4cb3598e1
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/SettingActivity.kt
@@ -0,0 +1,44 @@
+package `in`.koreatech.koin.feature.setting.ui
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.rememberNavController
+import dagger.hilt.android.AndroidEntryPoint
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+import `in`.koreatech.koin.core.designsystem.util.enableEdgeToEdgeWithDarkStatusBar
+import `in`.koreatech.koin.feature.setting.navigation.SettingNavType
+import `in`.koreatech.koin.feature.setting.navigation.koinSettingGraph
+
+@AndroidEntryPoint
+class SettingActivity : ComponentActivity() {
+ private lateinit var navController: NavHostController
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdgeWithDarkStatusBar()
+
+ setContent {
+ KoinTheme {
+ var startDestination by remember { mutableStateOf(SettingNavType.Setting.route) }
+ navController = rememberNavController()
+ NavHost(
+ modifier = Modifier.Companion,
+ navController = navController,
+ startDestination = startDestination
+ ) {
+ koinSettingGraph(
+ navController = navController
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/SettingScreen.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/SettingScreen.kt
new file mode 100644
index 000000000..fbf9b9e9f
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/SettingScreen.kt
@@ -0,0 +1,222 @@
+package `in`.koreatech.koin.feature.setting.ui
+
+import android.content.Context
+import android.content.Intent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+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.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
+import `in`.koreatech.koin.core.designsystem.component.topbar.KoinTopAppBar
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+import `in`.koreatech.koin.core.navigation.Navigator
+import `in`.koreatech.koin.core.navigation.utils.rememberNavigator
+import `in`.koreatech.koin.core.util.goToContactUrl
+import `in`.koreatech.koin.feature.setting.R
+import `in`.koreatech.koin.feature.setting.component.SettingItem
+import `in`.koreatech.koin.feature.setting.component.SettingTitle
+import `in`.koreatech.koin.feature.setting.component.SettingVersionItem
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingScreen(
+ modifier: Modifier = Modifier,
+ viewModel: SettingViewModel = hiltViewModel(),
+ onTopbarBackClick: () -> Unit = {},
+ onProfileClick: () -> Unit = {},
+ onChangePasswordClick: () -> Unit = {},
+ onNotificationClick: () -> Unit = {},
+ onPrivacyPolicyClick: () -> Unit = {},
+ onKoinTermsClick: () -> Unit = {},
+ onMarketingTermsClick: () -> Unit = {}
+) {
+ val versionState by viewModel.versionState.collectAsState()
+
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ val navigator = rememberNavigator()
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current
+
+ Scaffold(
+ containerColor = KoinTheme.colors.neutral0,
+ topBar = {
+ KoinTopAppBar(
+ title = stringResource(R.string.setting_appbar_title),
+ colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
+ containerColor = KoinTheme.colors.primary500,
+ navigationIconContentColor = Color.White,
+ titleContentColor = Color.White,
+ actionIconContentColor = Color.White
+ ),
+ onNavigationIconClick = onTopbarBackClick
+ )
+ },
+ snackbarHost = {
+ SnackbarHost(
+ hostState = snackbarHostState,
+ modifier = Modifier.systemBarsPadding()
+ )
+ },
+ contentWindowInsets = WindowInsets(0, 0, 0, 0)
+ ) { contentPadding ->
+ var currentVersionName by remember { mutableStateOf("") }
+ var latestVersionName by remember { mutableStateOf("") }
+ when (versionState) { // smartcast not working
+ is VersionState.Outdated -> {
+ currentVersionName = (versionState as VersionState.Outdated).currentVersion
+ latestVersionName = (versionState as VersionState.Outdated).latestVersion
+ }
+ is VersionState.Latest -> {
+ currentVersionName = (versionState as VersionState.Latest).currentVersion
+ latestVersionName = currentVersionName
+ }
+ else -> {}
+ }
+ SettingScreenImpl(
+ currentVersionName = currentVersionName,
+ latestVersionName = latestVersionName,
+ modifier = modifier
+ .padding(contentPadding)
+ .consumeWindowInsets(contentPadding)
+ .systemBarsPadding(),
+ onNotificationClick = {
+ if (viewModel.isLoggedIn) {
+ onNotificationClick()
+ } else {
+ scope.launch {
+ showLoginSnackBar(
+ context = context,
+ navigator = navigator,
+ snackbarHostState = snackbarHostState
+ )
+ }
+ }
+ },
+ onPrivacyPolicyClick = onPrivacyPolicyClick,
+ onKoinTermsClick = onKoinTermsClick,
+ onMarketingTermsClick = onMarketingTermsClick
+ )
+ }
+}
+
+private suspend fun showLoginSnackBar(
+ context: Context,
+ navigator: Navigator,
+ snackbarHostState: SnackbarHostState
+) {
+ val result = snackbarHostState.showSnackbar(
+ message = context.getString(R.string.setting_snackbar_login),
+ actionLabel = context.getString(R.string.setting_snackbar_login_button),
+ duration = SnackbarDuration.Short
+ )
+ if (result == SnackbarResult.ActionPerformed) {
+ navigator.navigateToSignIn(context).let {
+ context.startActivity(it)
+ }
+ }
+}
+
+@Composable
+private fun SettingScreenImpl(
+ currentVersionName: String,
+ latestVersionName: String,
+ modifier: Modifier = Modifier,
+ onProfileClick: () -> Unit = {},
+ onChangePasswordClick: () -> Unit = {},
+ onNotificationClick: () -> Unit = {},
+ onPrivacyPolicyClick: () -> Unit = {},
+ onKoinTermsClick: () -> Unit = {},
+ onMarketingTermsClick: () -> Unit = {}
+) {
+ val context = LocalContext.current
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ ) {
+ SettingTitle(
+ text = stringResource(R.string.setting_title_normal)
+ )
+ SettingItem(
+ text = stringResource(R.string.setting_item_profile),
+ showIcon = true,
+ onClick = {}
+ )
+ SettingItem(
+ text = stringResource(R.string.setting_item_change_password),
+ showIcon = true,
+ onClick = {}
+ )
+ SettingItem(
+ text = stringResource(R.string.setting_item_notification),
+ showIcon = true,
+ onClick = onNotificationClick
+ )
+ SettingTitle(
+ text = stringResource(R.string.setting_title_service)
+ )
+ SettingItem(
+ text = stringResource(R.string.setting_item_privacy_policy),
+ onClick = onPrivacyPolicyClick
+ )
+ SettingItem(
+ text = stringResource(R.string.setting_item_koin_terms),
+ onClick = onKoinTermsClick
+ )
+ SettingItem(
+ text = stringResource(R.string.setting_item_marketing_terms),
+ onClick = onMarketingTermsClick
+ )
+ SettingItem(
+ text = stringResource(R.string.setting_item_open_source_license),
+ onClick = {
+ context.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
+ }
+ )
+ SettingVersionItem(
+ currentVersion = currentVersionName,
+ latestVersion = latestVersionName,
+ showVersionInfo = currentVersionName.isNotEmpty() && latestVersionName.isNotEmpty()
+ )
+ Spacer(Modifier.weight(1f))
+ SettingItem(
+ text = stringResource(R.string.setting_item_contact),
+ onClick = {
+ context.goToContactUrl()
+ }
+ )
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+private fun SettingScreenPreview() {
+ SettingScreenImpl(
+ currentVersionName = "4.5.2",
+ latestVersionName = "4.5.3"
+ )
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/SettingViewModel.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/SettingViewModel.kt
new file mode 100644
index 000000000..b9f94ddf5
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/SettingViewModel.kt
@@ -0,0 +1,55 @@
+package `in`.koreatech.koin.feature.setting.ui
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import `in`.koreatech.koin.core.viewmodel.BaseViewModel
+import `in`.koreatech.koin.domain.model.user.User
+import `in`.koreatech.koin.domain.usecase.user.GetUserInfoUseCase
+import `in`.koreatech.koin.domain.usecase.version.GetLatestVersionUseCase
+import `in`.koreatech.koin.domain.util.onSuccess
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+@HiltViewModel
+class SettingViewModel @Inject constructor(
+ private val getUserInfoUseCase: GetUserInfoUseCase,
+ private val getLatestVersionUseCase: GetLatestVersionUseCase
+) : BaseViewModel() {
+ private val _versionState: MutableStateFlow =
+ MutableStateFlow(VersionState.Init)
+ val versionState: StateFlow get() = _versionState.asStateFlow()
+
+ private val _userInfo: MutableStateFlow = MutableStateFlow(User.Anonymous)
+
+ val isLoggedIn: Boolean get() = _userInfo.value.isStudent || _userInfo.value.isGeneral
+
+ init {
+ fetchVersion()
+ fetchUserInfo()
+ }
+
+ fun fetchUserInfo() = viewModelScope.launch {
+ getUserInfoUseCase()
+ .onSuccess {
+ _userInfo.value = it
+ }
+ }
+
+ fun fetchVersion() = viewModelScope.launch {
+ getLatestVersionUseCase()
+ .onSuccess { (currentVersion, latestVersion) ->
+ _versionState.value =
+ if (currentVersion == latestVersion) {
+ VersionState.Latest(currentVersion)
+ } else {
+ VersionState.Outdated(currentVersion, latestVersion)
+ }
+ }
+ .onFailure {
+ _versionState.value = VersionState.Failure
+ }
+ }
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/VersionState.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/VersionState.kt
new file mode 100644
index 000000000..f931e197b
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/VersionState.kt
@@ -0,0 +1,11 @@
+package `in`.koreatech.koin.feature.setting.ui
+
+sealed class VersionState {
+ data class Outdated(val currentVersion: String, val latestVersion: String) : VersionState()
+
+ data class Latest(val currentVersion: String) : VersionState()
+
+ data object Init : VersionState()
+
+ data object Failure : VersionState()
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/NotificationScreen.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/NotificationScreen.kt
new file mode 100644
index 000000000..11c4370d6
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/NotificationScreen.kt
@@ -0,0 +1,284 @@
+package `in`.koreatech.koin.feature.setting.ui.notification
+
+import android.content.Intent
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.core.net.toUri
+import androidx.hilt.navigation.compose.hiltViewModel
+import `in`.koreatech.koin.core.designsystem.component.topbar.KoinTopAppBar
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+import `in`.koreatech.koin.domain.model.notification.SubscribesDetailType
+import `in`.koreatech.koin.domain.model.notification.SubscribesType
+import `in`.koreatech.koin.feature.setting.R
+import `in`.koreatech.koin.feature.setting.component.SettingTitle
+import `in`.koreatech.koin.feature.setting.constant.ARTICLE_KEYWORD_URL
+import `in`.koreatech.koin.feature.setting.ui.notification.component.NotificationItem
+import `in`.koreatech.koin.feature.setting.ui.notification.component.NotificationSwitchItem
+import `in`.koreatech.koin.feature.setting.ui.notification.component.NotificationSwitchSubItem
+import `in`.koreatech.koin.feature.setting.util.isDetailTypePermitted
+import `in`.koreatech.koin.feature.setting.util.isTypePermitted
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun NotificationScreen(
+ modifier: Modifier = Modifier,
+ viewModel: NotificationViewModel = hiltViewModel(),
+ onTopbarBackClick: () -> Unit = {}
+) {
+ LaunchedEffect(Unit) {
+ viewModel.getPermissionInfo()
+ }
+ val notificationState by viewModel.notificationUiState.collectAsState()
+
+ Scaffold(
+ containerColor = KoinTheme.colors.neutral0,
+ topBar = {
+ KoinTopAppBar(
+ title = stringResource(R.string.notification_appbar_title),
+ colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
+ containerColor = KoinTheme.colors.primary500,
+ navigationIconContentColor = Color.White,
+ titleContentColor = Color.White,
+ actionIconContentColor = Color.White
+ ),
+ onNavigationIconClick = onTopbarBackClick
+ )
+ },
+ contentWindowInsets = WindowInsets(0, 0, 0, 0)
+ ) { contentPadding ->
+ when (notificationState) {
+ is NotificationUiState.Success -> {
+ val notificationInfos = remember { (notificationState as NotificationUiState.Success).notificationPermissionInfo }
+ NotificationScreenImpl(
+ isMarketingSubscribed = notificationInfos.subscribes.isTypePermitted(SubscribesType.MARKETING),
+ isSoldoutSubscribed = notificationInfos.subscribes.isTypePermitted(SubscribesType.DINING_SOLD_OUT),
+ isBreakfastSubscribed = notificationInfos.subscribes.isDetailTypePermitted(SubscribesDetailType.BREAKFAST),
+ isLunchSubscribed = notificationInfos.subscribes.isDetailTypePermitted(SubscribesDetailType.LUNCH),
+ isDinnerSubscribed = notificationInfos.subscribes.isDetailTypePermitted(SubscribesDetailType.BREAKFAST),
+ isDiningImageUploadedSubscribed = notificationInfos.subscribes.isTypePermitted(SubscribesType.DINING_IMAGE_UPLOAD),
+ isChatSubscribed = notificationInfos.subscribes.isTypePermitted(SubscribesType.LOST_ITEM_CHAT),
+ isEventSubscribed = notificationInfos.subscribes.isTypePermitted(SubscribesType.SHOP_EVENT),
+ isReviewSubscribed = notificationInfos.subscribes.isTypePermitted(SubscribesType.REVIEW_PROMPT),
+ updateSubscription = viewModel::updateSubscription,
+ deleteSubscription = viewModel::deleteSubscription,
+ updateSubscriptionDetail = viewModel::updateSubscriptionDetail,
+ deleteSubscriptionDetail = viewModel::deleteSubscriptionDetail,
+ modifier = modifier
+ .padding(contentPadding)
+ .consumeWindowInsets(contentPadding)
+ .systemBarsPadding()
+ )
+ }
+ else -> {}
+ }
+ }
+}
+
+@Composable
+private fun NotificationScreenImpl(
+ isMarketingSubscribed: Boolean,
+ isSoldoutSubscribed: Boolean,
+ isBreakfastSubscribed: Boolean,
+ isLunchSubscribed: Boolean,
+ isDinnerSubscribed: Boolean,
+ isDiningImageUploadedSubscribed: Boolean,
+ isChatSubscribed: Boolean,
+ isEventSubscribed: Boolean,
+ isReviewSubscribed: Boolean,
+ modifier: Modifier = Modifier,
+ updateSubscription: (SubscribesType) -> Unit = {},
+ deleteSubscription: (SubscribesType) -> Unit = {},
+ updateSubscriptionDetail: (SubscribesDetailType) -> Unit = {},
+ deleteSubscriptionDetail: (SubscribesDetailType) -> Unit = {}
+) {
+ val context = LocalContext.current
+ var isMarketingSubscribed by remember { mutableStateOf(isMarketingSubscribed) }
+ var isSoldoutSubscribed by remember { mutableStateOf(isSoldoutSubscribed) }
+ var isBreakfastSubscribed by remember { mutableStateOf(isBreakfastSubscribed) }
+ var isLunchSubscribed by remember { mutableStateOf(isLunchSubscribed) }
+ var isDinnerSubscribed by remember { mutableStateOf(isDinnerSubscribed) }
+ var isDiningImageUploadedSubscribed by remember { mutableStateOf(isDiningImageUploadedSubscribed) }
+ var isChatSubscribed by remember { mutableStateOf(isChatSubscribed) }
+ var isEventSubscribed by remember { mutableStateOf(isEventSubscribed) }
+ var isReviewSubscribed by remember { mutableStateOf(isReviewSubscribed) }
+ LazyColumn(
+ modifier = modifier
+ .fillMaxSize()
+ ) {
+ item {
+ NotificationSwitchItem(
+ text = stringResource(R.string.notification_item_marketing),
+ description = stringResource(R.string.notification_item_marketing_description),
+ checked = isMarketingSubscribed,
+ onClick = {
+ if (isMarketingSubscribed) {
+ deleteSubscription(SubscribesType.MARKETING)
+ } else {
+ updateSubscription(SubscribesType.MARKETING)
+ }
+ isMarketingSubscribed = !isMarketingSubscribed
+ }
+ )
+ }
+ item {
+ SettingTitle(
+ text = stringResource(R.string.notification_item_dining)
+ )
+ NotificationSwitchItem(
+ text = stringResource(R.string.notification_item_dining_soldout),
+ description = stringResource(R.string.notification_item_dining_soldout_description),
+ checked = isSoldoutSubscribed,
+ onClick = {
+ if (isSoldoutSubscribed) {
+ deleteSubscription(SubscribesType.DINING_SOLD_OUT)
+ } else {
+ updateSubscription(SubscribesType.DINING_SOLD_OUT)
+ }
+ isSoldoutSubscribed = !isSoldoutSubscribed
+ }
+ )
+ if (isSoldoutSubscribed) {
+ NotificationSwitchSubItem(
+ text = stringResource(R.string.notification_item_dining_breakfast),
+ checked = isBreakfastSubscribed,
+ onClick = {
+ if (isBreakfastSubscribed) {
+ deleteSubscriptionDetail(SubscribesDetailType.BREAKFAST)
+ } else {
+ updateSubscriptionDetail(SubscribesDetailType.BREAKFAST)
+ }
+ isBreakfastSubscribed = !isBreakfastSubscribed
+ }
+ )
+ NotificationSwitchSubItem(
+ text = stringResource(R.string.notification_item_dining_launch),
+ checked = isLunchSubscribed,
+ onClick = {
+ if (isLunchSubscribed) {
+ deleteSubscriptionDetail(SubscribesDetailType.LUNCH)
+ } else {
+ updateSubscriptionDetail(SubscribesDetailType.LUNCH)
+ }
+ isLunchSubscribed = !isLunchSubscribed
+ }
+ )
+ NotificationSwitchSubItem(
+ text = stringResource(R.string.notification_item_dining_dinner),
+ checked = isDinnerSubscribed,
+ onClick = {
+ if (isDinnerSubscribed) {
+ deleteSubscriptionDetail(SubscribesDetailType.DINNER)
+ } else {
+ updateSubscriptionDetail(SubscribesDetailType.DINNER)
+ }
+ isDinnerSubscribed = !isDinnerSubscribed
+ }
+ )
+ }
+ NotificationSwitchItem(
+ text = stringResource(R.string.notification_item_dining_image_uploaded),
+ description = stringResource(R.string.notification_item_dining_image_uploaded_description),
+ checked = isDiningImageUploadedSubscribed,
+ onClick = {
+ if (isDiningImageUploadedSubscribed) {
+ deleteSubscription(SubscribesType.DINING_IMAGE_UPLOAD)
+ } else {
+ updateSubscription(SubscribesType.DINING_IMAGE_UPLOAD)
+ }
+ isDiningImageUploadedSubscribed = !isDiningImageUploadedSubscribed
+ }
+ )
+ }
+ item {
+ SettingTitle(
+ text = stringResource(R.string.notification_item_article)
+ )
+ NotificationItem(
+ text = stringResource(R.string.notification_item_article_keyword),
+ description = stringResource(R.string.notification_item_article_keyword_description),
+ onClick = {
+ context.startActivity(Intent(Intent.ACTION_VIEW, ARTICLE_KEYWORD_URL.toUri()))
+ }
+ )
+ NotificationSwitchItem(
+ text = stringResource(R.string.notification_item_chat),
+ description = stringResource(R.string.notification_item_chat_description),
+ checked = isChatSubscribed,
+ onClick = {
+ if (isChatSubscribed) {
+ deleteSubscription(SubscribesType.LOST_ITEM_CHAT)
+ } else {
+ updateSubscription(SubscribesType.LOST_ITEM_CHAT)
+ }
+ isChatSubscribed = !isChatSubscribed
+ }
+ )
+ }
+ item {
+ SettingTitle(
+ text = stringResource(R.string.notification_item_nearby)
+ )
+ NotificationSwitchItem(
+ text = stringResource(R.string.notification_item_nearby_event),
+ description = stringResource(R.string.notification_item_nearby_event_description),
+ checked = isEventSubscribed,
+ onClick = {
+ if (isEventSubscribed) {
+ deleteSubscription(SubscribesType.SHOP_EVENT)
+ } else {
+ updateSubscription(SubscribesType.SHOP_EVENT)
+ }
+ isEventSubscribed = !isEventSubscribed
+ }
+ )
+ NotificationSwitchItem(
+ text = stringResource(R.string.notification_item_nearby_review),
+ description = stringResource(R.string.notification_item_nearby_review_description),
+ checked = isReviewSubscribed,
+ onClick = {
+ if (isReviewSubscribed) {
+ deleteSubscription(SubscribesType.REVIEW_PROMPT)
+ } else {
+ updateSubscription(SubscribesType.REVIEW_PROMPT)
+ }
+ isReviewSubscribed = !isReviewSubscribed
+ }
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun NotificationScreenPreview() {
+ NotificationScreenImpl(
+ isMarketingSubscribed = false,
+ isSoldoutSubscribed = true,
+ isBreakfastSubscribed = false,
+ isLunchSubscribed = false,
+ isDinnerSubscribed = false,
+ isDiningImageUploadedSubscribed = false,
+ isChatSubscribed = false,
+ isEventSubscribed = false,
+ isReviewSubscribed = false
+ )
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/NotificationViewModel.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/NotificationViewModel.kt
new file mode 100644
index 000000000..3bba93cfb
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/NotificationViewModel.kt
@@ -0,0 +1,69 @@
+package `in`.koreatech.koin.feature.setting.ui.notification
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import `in`.koreatech.koin.domain.model.notification.NotificationPermissionInfo
+import `in`.koreatech.koin.domain.model.notification.SubscribesDetailType
+import `in`.koreatech.koin.domain.model.notification.SubscribesType
+import `in`.koreatech.koin.domain.usecase.notification.DeleteNotificationSubscriptionDetailUseCase
+import `in`.koreatech.koin.domain.usecase.notification.DeleteNotificationSubscriptionUseCase
+import `in`.koreatech.koin.domain.usecase.notification.GetNotificationPermissionInfoUseCase
+import `in`.koreatech.koin.domain.usecase.notification.UpdateNotificationSubscriptionDetailUseCase
+import `in`.koreatech.koin.domain.usecase.notification.UpdateNotificationSubscriptionUseCase
+import `in`.koreatech.koin.domain.util.onFailure
+import `in`.koreatech.koin.domain.util.onSuccess
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+@HiltViewModel
+class NotificationViewModel @Inject constructor(
+ private val getNotificationPermissionInfoUseCase: GetNotificationPermissionInfoUseCase,
+ private val updateNotificationSubscriptionUseCase: UpdateNotificationSubscriptionUseCase,
+ private val updateNotificationSubscriptionDetailUseCase: UpdateNotificationSubscriptionDetailUseCase,
+ private val deleteNotificationSubscriptionUseCase: DeleteNotificationSubscriptionUseCase,
+ private val deleteNotificationSubscriptionDetailUseCase: DeleteNotificationSubscriptionDetailUseCase
+) : ViewModel() {
+ private val _notificationUiState =
+ MutableStateFlow(NotificationUiState.Nothing)
+ val notificationUiState = _notificationUiState.asStateFlow()
+
+ fun getPermissionInfo() = viewModelScope.launch {
+ getNotificationPermissionInfoUseCase().onSuccess { info ->
+ _notificationUiState.update {
+ NotificationUiState.Success(info)
+ }
+ }.onFailure {
+ _notificationUiState.update { NotificationUiState.Failed }
+ }
+ }
+
+ fun updateSubscription(type: SubscribesType) = viewModelScope.launch {
+ updateNotificationSubscriptionUseCase(type)
+ }
+
+ fun updateSubscriptionDetail(type: SubscribesDetailType) = viewModelScope.launch {
+ updateNotificationSubscriptionDetailUseCase(type)
+ }
+
+ fun deleteSubscription(type: SubscribesType) = viewModelScope.launch {
+ deleteNotificationSubscriptionUseCase(type)
+ }
+
+ fun deleteSubscriptionDetail(type: SubscribesDetailType) = viewModelScope.launch {
+ deleteNotificationSubscriptionDetailUseCase(type)
+ }
+}
+
+sealed class NotificationUiState {
+ data class Success(
+ val notificationPermissionInfo: NotificationPermissionInfo
+ ) : NotificationUiState()
+
+ data object Failed : NotificationUiState()
+
+ data object Nothing : NotificationUiState()
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/component/NotificationItem.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/component/NotificationItem.kt
new file mode 100644
index 000000000..c2c9a62f7
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/component/NotificationItem.kt
@@ -0,0 +1,78 @@
+package `in`.koreatech.koin.feature.setting.ui.notification.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+import `in`.koreatech.koin.feature.setting.R
+
+@Composable
+fun NotificationItem(
+ text: String,
+ modifier: Modifier = Modifier,
+ description: String = "",
+ textStyle: TextStyle = KoinTheme.typography.medium18,
+ descriptionTextStyle: TextStyle = KoinTheme.typography.regular16,
+ descriptionColor: Color = KoinTheme.colors.neutral500,
+ backgroundColor: Color = KoinTheme.colors.neutral0,
+ onClick: () -> Unit = {}
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = backgroundColor)
+ .clickable(onClick = onClick)
+ .padding(vertical = 13.dp, horizontal = 24.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BasicText(
+ text = text,
+ style = textStyle
+ )
+ Icon(
+ painter = painterResource(R.drawable.ic_arrow_right),
+ contentDescription = ""
+ )
+ }
+ if (description.isNotEmpty()) {
+ Spacer(Modifier.height(5.dp))
+ BasicText(
+ text = description,
+ style = descriptionTextStyle.copy(
+ color = descriptionColor
+ )
+ )
+ }
+ }
+ HorizontalDivider(color = KoinTheme.colors.neutral100)
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun NotificationItemPreview() {
+ NotificationItem(
+ text = "title",
+ description = "description"
+ )
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/component/NotificationSwitchItem.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/component/NotificationSwitchItem.kt
new file mode 100644
index 000000000..6674e00e7
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/component/NotificationSwitchItem.kt
@@ -0,0 +1,85 @@
+package `in`.koreatech.koin.feature.setting.ui.notification.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+import `in`.koreatech.koin.feature.setting.component.switch.KoinSwitch
+
+@Composable
+fun NotificationSwitchItem(
+ text: String,
+ checked: Boolean,
+ modifier: Modifier = Modifier,
+ description: String = "",
+ textStyle: TextStyle = KoinTheme.typography.medium18,
+ descriptionTextStyle: TextStyle = KoinTheme.typography.regular16,
+ descriptionColor: Color = KoinTheme.colors.neutral500,
+ backgroundColor: Color = KoinTheme.colors.neutral0,
+ onClick: (Boolean) -> Unit = {}
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = backgroundColor)
+ .padding(vertical = 13.dp, horizontal = 24.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BasicText(
+ text = text,
+ style = textStyle
+ )
+ KoinSwitch(
+ checked = checked,
+ onCheckedChange = onClick
+ )
+ }
+ if (description.isNotEmpty()) {
+ Spacer(Modifier.height(5.dp))
+ BasicText(
+ text = description,
+ style = descriptionTextStyle.copy(
+ color = descriptionColor
+ )
+ )
+ }
+ }
+ HorizontalDivider(color = KoinTheme.colors.neutral100)
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun NotificationSwitchItemPreview() {
+ NotificationSwitchItem(
+ text = "title",
+ checked = false
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun NotificationSwitchItemDescriptionPreview() {
+ NotificationSwitchItem(
+ text = "title",
+ checked = true,
+ description = "description"
+ )
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/component/NotificationSwitchSubItem.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/component/NotificationSwitchSubItem.kt
new file mode 100644
index 000000000..0eb4a25a4
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/notification/component/NotificationSwitchSubItem.kt
@@ -0,0 +1,57 @@
+package `in`.koreatech.koin.feature.setting.ui.notification.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+import `in`.koreatech.koin.feature.setting.component.switch.KoinSwitch
+
+@Composable
+fun NotificationSwitchSubItem(
+ text: String,
+ checked: Boolean,
+ modifier: Modifier = Modifier,
+ textStyle: TextStyle = KoinTheme.typography.regular16,
+ backgroundColor: Color = KoinTheme.colors.neutral0,
+ onClick: (Boolean) -> Unit = {}
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = backgroundColor)
+ .padding(vertical = 16.dp, horizontal = 24.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BasicText(
+ modifier = Modifier.padding(start = 8.dp),
+ text = text,
+ style = textStyle
+ )
+ KoinSwitch(
+ checked = checked,
+ onCheckedChange = onClick
+ )
+ }
+ HorizontalDivider(color = KoinTheme.colors.neutral100)
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun NotificationSwitchSubItemPreview() {
+ NotificationSwitchSubItem(
+ text = "title",
+ checked = false
+ )
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/TermScreen.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/TermScreen.kt
new file mode 100644
index 000000000..2842fe1fb
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/TermScreen.kt
@@ -0,0 +1,186 @@
+package `in`.koreatech.koin.feature.setting.ui.term
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import `in`.koreatech.koin.core.designsystem.component.topbar.KoinTopAppBar
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+import `in`.koreatech.koin.domain.model.term.TermArticle
+import `in`.koreatech.koin.feature.setting.R
+import `in`.koreatech.koin.feature.setting.constant.TermConstant
+import `in`.koreatech.koin.feature.setting.ui.term.component.TermDescriptionItem
+import `in`.koreatech.koin.feature.setting.ui.term.component.TermMenuItem
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TermScreen(
+ termType: TermConstant,
+ modifier: Modifier = Modifier,
+ viewModel: TermViewModel = hiltViewModel(),
+ onTopbarBackClick: () -> Unit = {}
+) {
+ val snackbarHostState = remember { SnackbarHostState() }
+ val termUnknownMessage = stringResource(R.string.term_unknown_message)
+ LaunchedEffect(termType) {
+ when (termType) {
+ TermConstant.TERM_UNKNOWN -> {
+ snackbarHostState.showSnackbar(
+ message = termUnknownMessage,
+ duration = SnackbarDuration.Short
+ )
+ }
+ TermConstant.TERM_KOIN -> {
+ viewModel.loadKoinTerm()
+ }
+ TermConstant.TERM_PRIVACY_POLICY -> {
+ viewModel.loadPrivacyTerm()
+ }
+ TermConstant.TERM_MARKETING -> {
+ viewModel.loadMarketingTerm()
+ }
+ }
+ }
+ val termState by viewModel.term.collectAsState()
+ LaunchedEffect(termState) {
+ if (termState is TermState.Failure) { // smartcast not working
+ snackbarHostState.showSnackbar(
+ message = (termState as TermState.Failure).message,
+ duration = SnackbarDuration.Short
+ )
+ }
+ }
+ Scaffold(
+ containerColor = KoinTheme.colors.neutral0,
+ topBar = {
+ KoinTopAppBar(
+ title = stringResource(R.string.term_appbar_title),
+ colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
+ containerColor = KoinTheme.colors.primary500,
+ navigationIconContentColor = Color.White,
+ titleContentColor = Color.White,
+ actionIconContentColor = Color.White
+ ),
+ onNavigationIconClick = onTopbarBackClick
+ )
+ },
+ snackbarHost = {
+ SnackbarHost(
+ hostState = snackbarHostState,
+ modifier = Modifier.systemBarsPadding()
+ )
+ },
+ contentWindowInsets = WindowInsets(0, 0, 0, 0)
+ ) { contentPadding ->
+ if (termState is TermState.Success) { // smartcast not working
+ TermScreenImpl(
+ modifier = modifier
+ .padding(contentPadding)
+ .consumeWindowInsets(contentPadding)
+ .systemBarsPadding(),
+ title = (termState as TermState.Success).term.header,
+ articles = (termState as TermState.Success).term.articles
+ )
+ }
+ }
+}
+
+@Composable
+private fun TermScreenImpl(
+ title: String,
+ articles: List,
+ modifier: Modifier = Modifier
+) {
+ val scope = rememberCoroutineScope()
+
+ val termLazyState = rememberLazyListState()
+
+ LazyColumn(
+ modifier = modifier
+ .fillMaxSize()
+ .background(color = KoinTheme.colors.neutral0),
+ state = termLazyState
+ ) {
+ item {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 13.dp, horizontal = 24.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BasicText(
+ text = title,
+ style = KoinTheme.typography.regular16
+ )
+ }
+ HorizontalDivider(color = KoinTheme.colors.neutral100)
+ }
+ item {
+ articles.forEachIndexed { index, article ->
+ TermMenuItem(
+ text = article.article,
+ onClick = {
+ scope.launch {
+ termLazyState.animateScrollToItem(index + 2)
+ }
+ }
+ )
+ }
+ HorizontalDivider(
+ modifier = Modifier
+ .padding(vertical = 28.dp, horizontal = 24.dp),
+ color = KoinTheme.colors.neutral800
+ )
+ }
+ items(items = articles) {
+ TermDescriptionItem(
+ title = it.article,
+ description = it.content
+ )
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+private fun TermScreenPreview() {
+ TermScreenImpl(
+ title = "코인 이용약관",
+ articles = listOf(
+ TermArticle("제 1조 ---", listOf("1조 내용")),
+ TermArticle("제 2조 ---", listOf("2조 내용1", "2조 내용2")),
+ TermArticle("제 3조 ---", listOf("3조 내용1", "3조 내용2", "3조 내용3"))
+ )
+ )
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/TermState.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/TermState.kt
new file mode 100644
index 000000000..91c436988
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/TermState.kt
@@ -0,0 +1,11 @@
+package `in`.koreatech.koin.feature.setting.ui.term
+
+import `in`.koreatech.koin.domain.model.term.Term
+
+sealed class TermState {
+ data object Init : TermState()
+
+ data class Success(val term: Term) : TermState()
+
+ data class Failure(val message: String) : TermState()
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/TermViewModel.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/TermViewModel.kt
new file mode 100644
index 000000000..21ce99385
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/TermViewModel.kt
@@ -0,0 +1,59 @@
+package `in`.koreatech.koin.feature.setting.ui.term
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import `in`.koreatech.koin.domain.usecase.signup.GetKoinTermUseCase
+import `in`.koreatech.koin.domain.usecase.signup.GetMarketingTermUseCase
+import `in`.koreatech.koin.domain.usecase.signup.GetPrivacyTermUseCase
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+@HiltViewModel
+class TermViewModel @Inject constructor(
+ private val getKoinTermUseCase: GetKoinTermUseCase,
+ private val getPrivacyTermUseCase: GetPrivacyTermUseCase,
+ private val getMarketingTermUseCase: GetMarketingTermUseCase
+) : ViewModel() {
+ private val _term: MutableStateFlow = MutableStateFlow(TermState.Init)
+ val term: StateFlow get() = _term.asStateFlow()
+
+ fun loadKoinTerm() {
+ viewModelScope.launch {
+ getKoinTermUseCase()
+ .onSuccess {
+ _term.value = TermState.Success(it)
+ }
+ .onFailure {
+ _term.value = TermState.Failure(it.message ?: "")
+ }
+ }
+ }
+
+ fun loadPrivacyTerm() {
+ viewModelScope.launch {
+ getPrivacyTermUseCase()
+ .onSuccess {
+ _term.value = TermState.Success(it)
+ }
+ .onFailure {
+ _term.value = TermState.Failure(it.message ?: "")
+ }
+ }
+ }
+
+ fun loadMarketingTerm() {
+ viewModelScope.launch {
+ getMarketingTermUseCase()
+ .onSuccess {
+ _term.value = TermState.Success(it)
+ }
+ .onFailure {
+ _term.value = TermState.Failure(it.message ?: "")
+ }
+ }
+ }
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/component/TermDescriptionItem.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/component/TermDescriptionItem.kt
new file mode 100644
index 000000000..636e31df9
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/component/TermDescriptionItem.kt
@@ -0,0 +1,62 @@
+package `in`.koreatech.koin.feature.setting.ui.term.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+
+@Composable
+fun TermDescriptionItem(
+ title: String,
+ description: List,
+ modifier: Modifier = Modifier,
+ backgroundColor: Color = KoinTheme.colors.neutral0
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = backgroundColor)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp, horizontal = 24.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BasicText(
+ text = title,
+ style = KoinTheme.typography.bold15
+ )
+ }
+ HorizontalDivider(color = KoinTheme.colors.neutral100)
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp, horizontal = 26.dp)
+ ) {
+ BasicText(
+ text = description.joinToString(separator = "\n"),
+ style = KoinTheme.typography.regular12
+ )
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+private fun TermDescriptionItemPreview() {
+ TermDescriptionItem(
+ title = "제 1조 ---",
+ description = listOf("① ('koreatech.in'이하 '코인')은(는) 다음의 개인정보 항목을 처리하고 있습니다.\n 어쩌구 저쩌구..")
+ )
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/component/TermMenuItem.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/component/TermMenuItem.kt
new file mode 100644
index 000000000..f0b3c6de7
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/ui/term/component/TermMenuItem.kt
@@ -0,0 +1,58 @@
+package `in`.koreatech.koin.feature.setting.ui.term.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.runtime.Composable
+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.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import `in`.koreatech.koin.core.designsystem.theme.KoinTheme
+
+@Composable
+fun TermMenuItem(
+ text: String,
+ modifier: Modifier = Modifier,
+ textStyle: TextStyle = KoinTheme.typography.regular14,
+ backgroundColor: Color = KoinTheme.colors.neutral0,
+ onClick: () -> Unit = {}
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = backgroundColor)
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) {
+ onClick()
+ }
+ .padding(vertical = 10.dp, horizontal = 48.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BasicText(
+ text = text,
+ style = textStyle
+ )
+ }
+ HorizontalDivider(color = KoinTheme.colors.neutral100)
+}
+
+@Composable
+@Preview(showBackground = true)
+private fun TermMenuItemPreview() {
+ TermMenuItem(
+ text = "제 1조 ---"
+ )
+}
diff --git a/feature/setting/src/main/java/in/koreatech/koin/feature/setting/util/NotificationInfoUtil.kt b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/util/NotificationInfoUtil.kt
new file mode 100644
index 000000000..56d5d79ba
--- /dev/null
+++ b/feature/setting/src/main/java/in/koreatech/koin/feature/setting/util/NotificationInfoUtil.kt
@@ -0,0 +1,21 @@
+package `in`.koreatech.koin.feature.setting.util
+
+import androidx.compose.ui.util.fastForEach
+import `in`.koreatech.koin.domain.model.notification.Subscribes
+import `in`.koreatech.koin.domain.model.notification.SubscribesDetailType
+import `in`.koreatech.koin.domain.model.notification.SubscribesType
+
+fun List.isTypePermitted(type: SubscribesType): Boolean {
+ this.fastForEach { subscribe ->
+ if (subscribe.type == type) return subscribe.isPermit
+ }
+ return false
+}
+fun List.isDetailTypePermitted(type: SubscribesDetailType): Boolean {
+ this.fastForEach { subscribe ->
+ subscribe.detailSubscribes.forEach { subscribeDetail ->
+ if (subscribeDetail.type == type) return subscribeDetail.isPermit
+ }
+ }
+ return false
+}
diff --git a/feature/setting/src/main/res/values/strings.xml b/feature/setting/src/main/res/values/strings.xml
new file mode 100644
index 000000000..71b5dc330
--- /dev/null
+++ b/feature/setting/src/main/res/values/strings.xml
@@ -0,0 +1,49 @@
+
+ 설정
+ 일반
+ 프로필
+ 비밀번호 변경
+ 알림
+
+ 로그인이 필요한 기능이에요.
+ 로그인 하기
+
+ 서비스
+ 개인정보 처리방침
+ 코인 이용약관
+ 마케팅 수신 동의 약관
+ 오픈소스 라이선스
+ 앱 버전
+ 문의하기
+
+ 코인 이용약관
+ 약관 타입을 명시해야 합니다.
+
+ 알림 설정
+ 마케팅 정보 수신 동의
+ 마케팅 정보 수신 동의/해제
+
+ 식단
+ 품절 알림
+ 식단이 품절될 경우 알림을 받습니다.
+ 아침 식단
+ 점심 식단
+ 저녁 식단
+ 식단사진 업로드 알림
+ 식단사진이 업로드 될 경우 알림을 받습니다.
+
+ 게시판
+ 공지사항 키워드 알림
+ 키워드가 포함된 글이 게시될 경우 알림을 받습니다.
+ 쪽지 알림
+ 쪽지가 도착하면 알림을 받습니다.
+
+ 주변상점
+ 이벤트 알림
+ 모든 주변상점의 이벤트 알림을 받습니다.
+ 리뷰 작성 요청 알림
+ 전화 주문 시 리뷰 작성 요청 알림을 받습니다.
+
+ 최신 버전 %1$s
+ 현재 최신 버전 입니다.
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 00e4ef378..9b52e155a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -69,6 +69,7 @@ stickyScrollView = "1.0.2"
strictVersionMatcherPlugin = "1.2.4"
timber = "5.0.1"
turbine = "1.2.0"
+playServicesOssLicenses = "17.3.0"
[libraries]
android-gradle-tool = { module = "com.android.tools.build:gradle", version.ref = "androidGradle" }
@@ -182,6 +183,7 @@ stickyScrollView = { module = "com.github.amarjain07:StickyScrollView", version.
strict-version-matcher-plugin = { module = "com.google.android.gms:strict-version-matcher-plugin", version.ref = "strictVersionMatcherPlugin" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
+play-services-oss-licenses = { group = "com.google.android.gms", name = "play-services-oss-licenses", version.ref = "playServicesOssLicenses" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradle" }
diff --git a/koin/build.gradle.kts b/koin/build.gradle.kts
index 9a2e8c62d..4ccaa985d 100644
--- a/koin/build.gradle.kts
+++ b/koin/build.gradle.kts
@@ -112,6 +112,7 @@ dependencies {
implementation(projects.feature.user)
implementation(projects.feature.club)
implementation(projects.feature.dining)
+ implementation(projects.feature.setting)
implementation(libs.guava)
diff --git a/settings.gradle b/settings.gradle
index 9e5dd10e1..bc15d2e31 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -37,3 +37,4 @@ include ':feature:user'
include ':feature:club'
include ':core:network'
include ':feature:dining'
+include ':feature:setting'