diff --git a/Prezel/app/build.gradle.kts b/Prezel/app/build.gradle.kts
index 780cc16d..e99a933f 100644
--- a/Prezel/app/build.gradle.kts
+++ b/Prezel/app/build.gradle.kts
@@ -50,6 +50,8 @@ dependencies {
implementation(projects.featureHistoryImpl)
implementation(projects.featureMyApi)
implementation(projects.featureMyImpl)
+ implementation(projects.featureSettingApi)
+ implementation(projects.featureSettingImpl)
implementation(projects.featureProfileImpl)
implementation(libs.androidx.core.ktx)
diff --git a/Prezel/app/src/main/java/com/team/prezel/ui/DoubleBackToExitHandler.kt b/Prezel/app/src/main/java/com/team/prezel/ui/DoubleBackToExitHandler.kt
index 3aadb420..d663fd8e 100644
--- a/Prezel/app/src/main/java/com/team/prezel/ui/DoubleBackToExitHandler.kt
+++ b/Prezel/app/src/main/java/com/team/prezel/ui/DoubleBackToExitHandler.kt
@@ -45,9 +45,8 @@ internal fun DoubleBackToExitHandler(navigationState: NavigationState) {
snackbarState.showPrezelSnackbar(
id = SNACKBAR_IDENTIFIER,
message = resources.getString(R.string.double_back_to_exit_snackbar_message),
- actionLabel = resources.getString(R.string.double_back_to_exit_snackbar_action_label),
- onAction = { backPressState = BackPressState.Idle },
onDismiss = { backPressState = BackPressState.Idle },
+ useRaisedPosition = false,
)
}
diff --git a/Prezel/app/src/main/res/values/strings.xml b/Prezel/app/src/main/res/values/strings.xml
index b8740117..dff017bb 100644
--- a/Prezel/app/src/main/res/values/strings.xml
+++ b/Prezel/app/src/main/res/values/strings.xml
@@ -5,6 +5,5 @@
히스토리
프로필
- 한 번 더 누르면 앱을 종료합니다
- 닫기
+ 한 번 더 누르면 앱을 종료합니다.
diff --git a/Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.kt b/Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.kt
index 4049363a..dc432761 100644
--- a/Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.kt
+++ b/Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/AuthRepositoryImpl.kt
@@ -37,30 +37,24 @@ internal class AuthRepositoryImpl @Inject constructor(
override suspend fun withdraw(reason: WithdrawReason): Result =
runCatching {
authRemoteDataSource.withdraw(
- reasonCategory = reason.toCategory(),
- reasonText = reason.toReasonText(),
+ reasonCategory = reason.toReasonCategory(),
+ reasonText = (reason as? WithdrawReason.Etc)?.text,
)
clearLocalSession()
}.mapDomainFailure()
- private fun WithdrawReason.toCategory(): String =
- when (this) {
- WithdrawReason.NotUsedOften -> "NOT_USED_OFTEN"
- WithdrawReason.NoLongerNeeded -> "NO_LONGER_NEEDED"
- WithdrawReason.TooDifficultOrComplex -> "TOO_COMPLEX"
- WithdrawReason.AnalysisResultInaccurate -> "INACCURATE_ANALYSIS"
- WithdrawReason.TooManyErrors -> "MANY_ERRORS"
- is WithdrawReason.Other -> "ETC"
- }
-
- private fun WithdrawReason.toReasonText(): String =
- when (this) {
- is WithdrawReason.Other -> text
- else -> ""
- }
-
private suspend fun clearLocalSession() {
authLocalDataSource.clearTokens()
authSessionCache.clear()
}
+
+ private fun WithdrawReason.toReasonCategory(): String =
+ when (this) {
+ WithdrawReason.NotUsedOften -> "NOT_USED_OFTEN"
+ WithdrawReason.NoLongerNeeded -> "NO_LONGER_NEEDED"
+ WithdrawReason.TooComplex -> "TOO_COMPLEX"
+ WithdrawReason.InaccurateAnalysis -> "INACCURATE_ANALYSIS"
+ WithdrawReason.ManyErrors -> "MANY_ERRORS"
+ is WithdrawReason.Etc -> "ETC"
+ }
}
diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/actions/area/PrezelButtonArea.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/actions/area/PrezelButtonArea.kt
index c77af2d0..8928b2e1 100644
--- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/actions/area/PrezelButtonArea.kt
+++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/actions/area/PrezelButtonArea.kt
@@ -22,6 +22,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.team.prezel.core.designsystem.component.PrezelDividerType
import com.team.prezel.core.designsystem.component.PrezelHorizontalDivider
+import com.team.prezel.core.designsystem.component.actions.button.PrezelButton
+import com.team.prezel.core.designsystem.component.actions.button.config.ButtonHierarchy
+import com.team.prezel.core.designsystem.component.actions.button.config.ButtonType
import com.team.prezel.core.designsystem.icon.PrezelIcons
import com.team.prezel.core.designsystem.preview.PreviewSection
import com.team.prezel.core.designsystem.theme.PrezelTheme
@@ -40,16 +43,9 @@ fun PrezelButtonArea(
showBackground: Boolean = false,
isNested: Boolean = false,
config: PrezelButtonAreaDefault = PrezelButtonAreaDefaults.getDefault(),
- content: ButtonAreaScope.() -> Unit,
+ mainButton: @Composable (Modifier) -> Unit,
+ subButton: @Composable ((Modifier) -> Unit)? = null,
) {
- val scope = DefaultButtonAreaScope().apply {
- this.content()
- }
-
- val mainButton = requireNotNull(scope.buttons.firstOrNull()) {
- "PrezelButtonArea에는 최소 1개의 버튼이 필요합니다."
- }
- val subButton = scope.buttons.getOrNull(1)
val contentModifier = if (isNested) Modifier else Modifier.padding(config.contentPadding)
Column(
@@ -247,21 +243,29 @@ private fun ButtonAreaPreviewSample(
isVertical = variant.isVertical,
isStrongStrength = variant.isStrongStrength,
showBackground = showBackground,
- ) {
- MainButton(
- iconResId = PrezelIcons.Blank,
- label = "Label",
- enabled = enabled,
- onClick = {},
- )
-
- SubButton(
- iconResId = PrezelIcons.Blank,
- label = "Label",
- enabled = enabled,
- onClick = {},
- )
- }
+ mainButton = { modifier ->
+ PrezelButton(
+ modifier = modifier,
+ text = "Label",
+ iconResId = PrezelIcons.Blank,
+ onClick = { },
+ enabled = enabled,
+ type = ButtonType.FILLED,
+ hierarchy = ButtonHierarchy.PRIMARY,
+ )
+ },
+ subButton = { modifier ->
+ PrezelButton(
+ modifier = modifier,
+ text = "Label",
+ iconResId = PrezelIcons.Blank,
+ onClick = { },
+ enabled = enabled,
+ type = ButtonType.FILLED,
+ hierarchy = ButtonHierarchy.SECONDARY,
+ )
+ },
+ )
}
@Composable
diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/datepicker/PrezelDatePicker.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/datepicker/PrezelDatePicker.kt
index 0d07bb72..bf4bc363 100644
--- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/datepicker/PrezelDatePicker.kt
+++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/datepicker/PrezelDatePicker.kt
@@ -33,6 +33,9 @@ import com.team.prezel.core.designsystem.component.PrezelDividerType
import com.team.prezel.core.designsystem.component.PrezelHorizontalDivider
import com.team.prezel.core.designsystem.component.PrezelTopAppBar
import com.team.prezel.core.designsystem.component.actions.area.PrezelButtonArea
+import com.team.prezel.core.designsystem.component.actions.button.PrezelButton
+import com.team.prezel.core.designsystem.component.actions.button.config.ButtonHierarchy
+import com.team.prezel.core.designsystem.component.actions.button.config.ButtonType
import com.team.prezel.core.designsystem.component.datepicker.config.DatePickerDefault
import com.team.prezel.core.designsystem.component.datepicker.config.DatePickerDefaults
import com.team.prezel.core.designsystem.component.datepicker.config.DatePickerMonth
@@ -120,18 +123,20 @@ private fun DatePickerFooter(
enabled: Boolean,
onClick: () -> Unit,
) {
- val buttonLabel = stringResource(R.string.core_designsystem_date_picker_confirm_btn)
-
PrezelButtonArea(
isVertical = false,
showBackground = true,
- ) {
- MainButton(
- label = buttonLabel,
- enabled = enabled,
- onClick = onClick,
- )
- }
+ mainButton = { modifier ->
+ PrezelButton(
+ modifier = modifier,
+ text = stringResource(R.string.core_designsystem_date_picker_confirm_btn),
+ onClick = onClick,
+ enabled = enabled,
+ type = ButtonType.FILLED,
+ hierarchy = ButtonHierarchy.PRIMARY,
+ )
+ },
+ )
}
@Composable
diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/feedback/dialog/PrezelDialog.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/feedback/dialog/PrezelDialog.kt
index 6e65db45..26d93cd0 100644
--- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/feedback/dialog/PrezelDialog.kt
+++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/feedback/dialog/PrezelDialog.kt
@@ -24,9 +24,9 @@ import com.team.prezel.core.designsystem.theme.PrezelTheme
@Composable
fun PrezelDialog(
title: String,
- description: String,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
+ description: String? = null,
content: @Composable PrezelDialogScope.() -> Unit,
) {
val scope = remember { PrezelDialogScope() }
@@ -59,7 +59,7 @@ fun PrezelDialog(
@Composable
private fun DialogContent(
title: String,
- description: String,
+ description: String?,
modifier: Modifier = Modifier,
) {
Column(
@@ -67,7 +67,9 @@ private fun DialogContent(
) {
Text(text = title, style = PrezelTheme.typography.title2Bold, color = PrezelTheme.colors.textLarge)
Spacer(modifier = Modifier.height(PrezelTheme.spacing.V12))
- Text(text = description, style = PrezelTheme.typography.body3Medium, color = PrezelTheme.colors.textRegular)
+ description?.let { text ->
+ Text(text = text, style = PrezelTheme.typography.body3Medium, color = PrezelTheme.colors.textRegular)
+ }
}
}
diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/feedback/snackbar/SnackbarHost.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/feedback/snackbar/SnackbarHost.kt
index 498c8bf1..c708229c 100644
--- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/feedback/snackbar/SnackbarHost.kt
+++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/feedback/snackbar/SnackbarHost.kt
@@ -28,7 +28,7 @@ suspend fun SnackbarHostState.showPrezelSnackbar(
@DrawableRes leadingIconResId: Int? = null,
duration: SnackbarDuration = SnackbarDuration.Short,
id: String? = null,
- offsetY: Dp = 0.dp,
+ useRaisedPosition: Boolean = true,
onDismiss: (() -> Unit)? = null,
) {
val result = showSnackbar(
@@ -38,7 +38,7 @@ suspend fun SnackbarHostState.showPrezelSnackbar(
duration = duration,
id = id,
leadingIconResId = leadingIconResId,
- offsetY = offsetY,
+ offsetY = if (useRaisedPosition) (-98).dp else 0.dp,
),
)
diff --git a/Prezel/core/model/src/main/java/com/team/prezel/core/model/auth/WithdrawReason.kt b/Prezel/core/model/src/main/java/com/team/prezel/core/model/auth/WithdrawReason.kt
index 0505c618..796dc961 100644
--- a/Prezel/core/model/src/main/java/com/team/prezel/core/model/auth/WithdrawReason.kt
+++ b/Prezel/core/model/src/main/java/com/team/prezel/core/model/auth/WithdrawReason.kt
@@ -5,13 +5,13 @@ sealed interface WithdrawReason {
data object NoLongerNeeded : WithdrawReason
- data object TooDifficultOrComplex : WithdrawReason
+ data object TooComplex : WithdrawReason
- data object AnalysisResultInaccurate : WithdrawReason
+ data object InaccurateAnalysis : WithdrawReason
- data object TooManyErrors : WithdrawReason
+ data object ManyErrors : WithdrawReason
- data class Other(
+ data class Etc(
val text: String,
) : WithdrawReason
}
diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSource.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSource.kt
index 12fb38f0..b72c01b5 100644
--- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSource.kt
+++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSource.kt
@@ -12,6 +12,6 @@ interface AuthRemoteDataSource {
suspend fun withdraw(
reasonCategory: String,
- reasonText: String,
+ reasonText: String?,
)
}
diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.kt
index 5de6b66b..41ddf90a 100644
--- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.kt
+++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/AuthRemoteDataSourceImpl.kt
@@ -24,11 +24,10 @@ internal class AuthRemoteDataSourceImpl @Inject constructor(
override suspend fun withdraw(
reasonCategory: String,
- reasonText: String,
+ reasonText: String?,
) {
authService
- .withdraw(
- request = WithdrawRequest(reasonCategory = reasonCategory, reasonText = reasonText),
- ).requireSuccess()
+ .withdraw(request = WithdrawRequest(reasonCategory = reasonCategory, reasonText = reasonText))
+ .requireSuccess()
}
}
diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/WithdrawRequest.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/WithdrawRequest.kt
index 4f6fb74a..490c61e7 100644
--- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/WithdrawRequest.kt
+++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/auth/WithdrawRequest.kt
@@ -8,5 +8,5 @@ data class WithdrawRequest(
@SerialName("reasonCategory")
val reasonCategory: String,
@SerialName("reasonText")
- val reasonText: String,
+ val reasonText: String?,
)
diff --git a/Prezel/core/ui/src/main/java/com/team/prezel/core/ui/util/NoRippleClickable.kt b/Prezel/core/ui/src/main/java/com/team/prezel/core/ui/util/NoRippleClickable.kt
new file mode 100644
index 00000000..30716e9b
--- /dev/null
+++ b/Prezel/core/ui/src/main/java/com/team/prezel/core/ui/util/NoRippleClickable.kt
@@ -0,0 +1,16 @@
+package com.team.prezel.core.ui.util
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+
+fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier =
+ composed {
+ clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = onClick,
+ )
+ }
diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt
index 67f15b36..9fa7fc43 100644
--- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt
+++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/LoginScreen.kt
@@ -31,6 +31,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.ui.LocalNavAnimatedContentScope
import com.team.prezel.core.auth.AuthManager
import com.team.prezel.core.designsystem.component.actions.area.PrezelButtonArea
+import com.team.prezel.core.designsystem.component.actions.button.PrezelButton
import com.team.prezel.core.designsystem.component.actions.button.config.ButtonHierarchy
import com.team.prezel.core.designsystem.component.actions.button.config.ButtonSize
import com.team.prezel.core.designsystem.component.actions.button.config.ButtonType
@@ -153,16 +154,6 @@ private fun LoginFooter(
modifier: Modifier = Modifier,
) {
var isButtonVisible by remember { mutableStateOf(false) }
- val startWithKakaoLabel = stringResource(R.string.feature_login_impl_start_with_kakao)
- val kakaoButtonConfig = PrezelButtonDefaults.getDefault(
- isIconOnly = false,
- type = ButtonType.FILLED,
- size = ButtonSize.REGULAR,
- hierarchy = ButtonHierarchy.SECONDARY,
- isRounded = false,
- backgroundColor = Color(0xFFFEE500),
- contentColor = PrezelTheme.colors.textLarge,
- )
LaunchedEffect(Unit) {
isButtonVisible = true
@@ -180,15 +171,26 @@ private fun LoginFooter(
),
exit = ExitTransition.None,
) {
- PrezelButtonArea {
- CustomButton(
- iconResId = PrezelIcons.Kakao,
- label = startWithKakaoLabel,
- enabled = enabled,
- onClick = onLogin,
- config = kakaoButtonConfig,
- )
- }
+ PrezelButtonArea(
+ mainButton = { modifier ->
+ PrezelButton(
+ modifier = modifier,
+ text = stringResource(R.string.feature_login_impl_start_with_kakao),
+ iconResId = PrezelIcons.Kakao,
+ enabled = enabled,
+ onClick = onLogin,
+ config = PrezelButtonDefaults.getDefault(
+ isIconOnly = false,
+ type = ButtonType.FILLED,
+ size = ButtonSize.REGULAR,
+ hierarchy = ButtonHierarchy.SECONDARY,
+ isRounded = false,
+ backgroundColor = Color(0xFFFEE500),
+ contentColor = PrezelTheme.colors.textLarge,
+ ),
+ )
+ },
+ )
}
}
diff --git a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.kt b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.kt
index aeccef0f..d9660fff 100644
--- a/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.kt
+++ b/Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.kt
@@ -27,7 +27,7 @@ internal fun EntryProviderScope.featureLoginEntryBuilder(authManager: Au
navigator.replaceRoot(HomeNavKey)
},
navigateToTerms = {
- navigator.navigate(TermsNavKey)
+ navigator.navigate(TermsNavKey.List)
},
navigateToCreateProfile = {
navigator.navigate(ProfileNavKey.Create)
diff --git a/Prezel/feature/my/impl/build.gradle.kts b/Prezel/feature/my/impl/build.gradle.kts
index 2f8f09c5..3f8a2585 100644
--- a/Prezel/feature/my/impl/build.gradle.kts
+++ b/Prezel/feature/my/impl/build.gradle.kts
@@ -11,6 +11,7 @@ dependencies {
implementation(projects.coreModel)
implementation(projects.featureMyApi)
+ implementation(projects.featureSettingApi)
implementation(projects.featureProfileApi)
implementation(libs.kotlinx.collections.immutable)
diff --git a/Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/MyTopAppBar.kt b/Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/MyTopAppBar.kt
index 7b1fce04..7cc5603a 100644
--- a/Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/MyTopAppBar.kt
+++ b/Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/component/MyTopAppBar.kt
@@ -2,14 +2,14 @@ package com.team.prezel.feature.my.impl.component
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.team.prezel.core.designsystem.component.PrezelTopAppBar
-import com.team.prezel.core.designsystem.component.actions.button.PrezelIconButton
-import com.team.prezel.core.designsystem.component.actions.button.config.ButtonHierarchy
-import com.team.prezel.core.designsystem.component.actions.button.config.ButtonType
import com.team.prezel.core.designsystem.icon.PrezelIcons
import com.team.prezel.core.designsystem.preview.BasicPreview
import com.team.prezel.core.designsystem.theme.PrezelTheme
@@ -22,12 +22,12 @@ internal fun MyTopAppBar(onClickSetting: () -> Unit) {
modifier = Modifier.fillMaxWidth(),
title = { Text(text = stringResource(R.string.feature_my_impl_title)) },
trailingIcons = {
- PrezelIconButton(
- iconResId = PrezelIcons.Setting,
- type = ButtonType.GHOST,
- hierarchy = ButtonHierarchy.SECONDARY,
- onClick = onClickSetting,
- )
+ IconButton(onClick = onClickSetting) {
+ Icon(
+ painter = painterResource(PrezelIcons.Setting),
+ contentDescription = null,
+ )
+ }
},
)
}
diff --git a/Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/navigation/MyEntryBuilder.kt b/Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/navigation/MyEntryBuilder.kt
index b128397a..69373811 100644
--- a/Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/navigation/MyEntryBuilder.kt
+++ b/Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/navigation/MyEntryBuilder.kt
@@ -6,6 +6,7 @@ import com.team.prezel.core.navigation.LocalNavigator
import com.team.prezel.feature.my.api.MyNavKey
import com.team.prezel.feature.my.impl.MyScreen
import com.team.prezel.feature.profile.api.ProfileNavKey
+import com.team.prezel.feature.setting.api.SettingNavKey
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -18,7 +19,7 @@ internal fun EntryProviderScope.featureMyEntryBuilder() {
MyScreen(
navigateToEditProfile = { navigator.navigate(ProfileNavKey.Edit) },
- navigateToSetting = { /* navigator.navigate(ProfileNavKey.Setting) */ },
+ navigateToSetting = { navigator.navigate(SettingNavKey) },
navigateToBadge = { /* navigator.navigate(ProfileNavKey.Badge) */ },
)
}
diff --git a/Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.kt b/Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.kt
index 8277ed87..67046e02 100644
--- a/Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.kt
+++ b/Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.kt
@@ -26,6 +26,9 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.team.prezel.core.designsystem.component.actions.area.PrezelButtonArea
+import com.team.prezel.core.designsystem.component.actions.button.PrezelButton
+import com.team.prezel.core.designsystem.component.actions.button.config.ButtonHierarchy
+import com.team.prezel.core.designsystem.component.actions.button.config.ButtonType
import com.team.prezel.core.designsystem.component.feedback.snackbar.showPrezelSnackbar
import com.team.prezel.core.designsystem.preview.BasicPreview
import com.team.prezel.core.designsystem.theme.PrezelTheme
@@ -99,7 +102,6 @@ private fun ProfileScreen(
modifier: Modifier = Modifier,
) {
val contentState = uiState as? ProfileUiState.Content
- val submitButtonText = stringResource(R.string.feature_profile_impl_submit_button_text)
Column(modifier = modifier.fillMaxSize()) {
ProfileScreenTopAppBar(
@@ -121,13 +123,17 @@ private fun ProfileScreen(
PrezelButtonArea(
showBackground = true,
modifier = Modifier.advancedImePadding(),
- ) {
- MainButton(
- label = submitButtonText,
- enabled = contentState?.submitButtonEnabled ?: false,
- onClick = onClickSubmit,
- )
- }
+ mainButton = { modifier ->
+ PrezelButton(
+ modifier = modifier,
+ text = stringResource(R.string.feature_profile_impl_submit_button_text),
+ onClick = onClickSubmit,
+ enabled = contentState?.submitButtonEnabled ?: false,
+ type = ButtonType.FILLED,
+ hierarchy = ButtonHierarchy.PRIMARY,
+ )
+ },
+ )
}
}
diff --git a/Prezel/feature/setting/api/build.gradle.kts b/Prezel/feature/setting/api/build.gradle.kts
new file mode 100644
index 00000000..169ab6b4
--- /dev/null
+++ b/Prezel/feature/setting/api/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+ alias(libs.plugins.prezel.android.feature.api)
+}
+
+android {
+ namespace = "com.team.prezel.feature.setting.api"
+}
diff --git a/Prezel/feature/setting/api/consumer-rules.pro b/Prezel/feature/setting/api/consumer-rules.pro
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/Prezel/feature/setting/api/consumer-rules.pro
@@ -0,0 +1 @@
+
diff --git a/Prezel/feature/setting/api/proguard-rules.pro b/Prezel/feature/setting/api/proguard-rules.pro
new file mode 100644
index 00000000..f1b42451
--- /dev/null
+++ b/Prezel/feature/setting/api/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
diff --git a/Prezel/feature/setting/api/src/main/java/com/team/prezel/feature/setting/api/DeleteAccountNavKey.kt b/Prezel/feature/setting/api/src/main/java/com/team/prezel/feature/setting/api/DeleteAccountNavKey.kt
new file mode 100644
index 00000000..609ee04a
--- /dev/null
+++ b/Prezel/feature/setting/api/src/main/java/com/team/prezel/feature/setting/api/DeleteAccountNavKey.kt
@@ -0,0 +1,7 @@
+package com.team.prezel.feature.setting.api
+
+import androidx.navigation3.runtime.NavKey
+import kotlinx.serialization.Serializable
+
+@Serializable
+data object DeleteAccountNavKey : NavKey
diff --git a/Prezel/feature/setting/api/src/main/java/com/team/prezel/feature/setting/api/SettingNavKey.kt b/Prezel/feature/setting/api/src/main/java/com/team/prezel/feature/setting/api/SettingNavKey.kt
new file mode 100644
index 00000000..36805dbe
--- /dev/null
+++ b/Prezel/feature/setting/api/src/main/java/com/team/prezel/feature/setting/api/SettingNavKey.kt
@@ -0,0 +1,7 @@
+package com.team.prezel.feature.setting.api
+
+import androidx.navigation3.runtime.NavKey
+import kotlinx.serialization.Serializable
+
+@Serializable
+data object SettingNavKey : NavKey
diff --git a/Prezel/feature/setting/impl/build.gradle.kts b/Prezel/feature/setting/impl/build.gradle.kts
new file mode 100644
index 00000000..961e8a91
--- /dev/null
+++ b/Prezel/feature/setting/impl/build.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+ alias(libs.plugins.prezel.android.feature.impl)
+}
+
+android {
+ namespace = "com.team.prezel.feature.setting.impl"
+}
+
+dependencies {
+ implementation(projects.coreDomain)
+ implementation(projects.coreModel)
+
+ implementation(projects.featureSettingApi)
+ implementation(projects.featureSplashApi)
+ implementation(projects.featureTermsApi)
+
+ implementation(libs.kotlinx.collections.immutable)
+}
diff --git a/Prezel/feature/setting/impl/consumer-rules.pro b/Prezel/feature/setting/impl/consumer-rules.pro
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/Prezel/feature/setting/impl/consumer-rules.pro
@@ -0,0 +1 @@
+
diff --git a/Prezel/feature/setting/impl/proguard-rules.pro b/Prezel/feature/setting/impl/proguard-rules.pro
new file mode 100644
index 00000000..f1b42451
--- /dev/null
+++ b/Prezel/feature/setting/impl/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
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/DeleteAccountScreen.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/DeleteAccountScreen.kt
new file mode 100644
index 00000000..eb34badf
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/DeleteAccountScreen.kt
@@ -0,0 +1,188 @@
+package com.team.prezel.feature.setting.impl.delete
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalResources
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.team.prezel.core.designsystem.component.feedback.snackbar.showPrezelSnackbar
+import com.team.prezel.core.designsystem.preview.BasicPreview
+import com.team.prezel.core.designsystem.theme.PrezelTheme
+import com.team.prezel.core.ui.state.LocalSnackbarHostState
+import com.team.prezel.feature.setting.impl.R
+import com.team.prezel.feature.setting.impl.delete.component.DeleteAccountActionSection
+import com.team.prezel.feature.setting.impl.delete.component.DeleteAccountConfirmDialog
+import com.team.prezel.feature.setting.impl.delete.component.DeleteAccountNoticeStep
+import com.team.prezel.feature.setting.impl.delete.component.DeleteAccountReasonStep
+import com.team.prezel.feature.setting.impl.delete.component.DeleteAccountTopAppBar
+import com.team.prezel.feature.setting.impl.delete.contract.DeleteAccountUiEffect
+import com.team.prezel.feature.setting.impl.delete.contract.DeleteAccountUiIntent
+import com.team.prezel.feature.setting.impl.delete.contract.DeleteAccountUiState
+import com.team.prezel.feature.setting.impl.delete.model.DeleteAccountReasonOption
+import com.team.prezel.feature.setting.impl.delete.model.DeleteAccountStep
+import com.team.prezel.feature.setting.impl.delete.model.DeleteAccountUiMessage
+
+@Composable
+internal fun DeleteAccountScreen(
+ navigateBack: () -> Unit,
+ navigateToSplash: () -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel: DeleteAccountViewModel = hiltViewModel(),
+) {
+ val snackbarHostState = LocalSnackbarHostState.current
+ val resources = LocalResources.current
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(viewModel) {
+ viewModel.uiEffect.collect { effect ->
+ when (effect) {
+ DeleteAccountUiEffect.NavigateToSplash -> {
+ navigateToSplash()
+ snackbarHostState.showPrezelSnackbar(message = resources.getString(R.string.feature_setting_impl_delete_account_withdraw_success))
+ }
+ is DeleteAccountUiEffect.ShowMessage -> {
+ snackbarHostState.showPrezelSnackbar(message = resources.getString(effect.message.toMessageRes()))
+ }
+ }
+ }
+ }
+
+ BackHandler {
+ navigateBack()
+ }
+
+ DeleteAccountScreen(
+ uiState = uiState,
+ onClickClose = navigateBack,
+ onToggleNoticeChecked = { checked ->
+ viewModel.onIntent(DeleteAccountUiIntent.ToggleNoticeChecked(checked))
+ },
+ onClickNext = { viewModel.onIntent(DeleteAccountUiIntent.ClickNext) },
+ onSelectReason = { reason -> viewModel.onIntent(DeleteAccountUiIntent.SelectReason(reason)) },
+ onOtherReasonChanged = { value -> viewModel.onIntent(DeleteAccountUiIntent.ChangeOtherReason(value)) },
+ onClickWithdraw = { viewModel.onIntent(DeleteAccountUiIntent.ClickWithdraw) },
+ onDismissDialog = { viewModel.onIntent(DeleteAccountUiIntent.DismissDialog) },
+ onConfirmWithdraw = { viewModel.onIntent(DeleteAccountUiIntent.ConfirmWithdraw) },
+ modifier = modifier,
+ )
+}
+
+private fun DeleteAccountUiMessage.toMessageRes(): Int =
+ when (this) {
+ DeleteAccountUiMessage.WITHDRAW_FAILED -> R.string.feature_setting_impl_delete_account_withdraw_failed
+ }
+
+@Composable
+private fun DeleteAccountScreen(
+ uiState: DeleteAccountUiState,
+ onClickClose: () -> Unit,
+ onToggleNoticeChecked: (Boolean) -> Unit,
+ onClickNext: () -> Unit,
+ onSelectReason: (DeleteAccountReasonOption) -> Unit,
+ onOtherReasonChanged: (String) -> Unit,
+ onClickWithdraw: () -> Unit,
+ onDismissDialog: () -> Unit,
+ onConfirmWithdraw: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ if (uiState.isConfirmDialogVisible) {
+ DeleteAccountConfirmDialog(
+ onDismissDialog = onDismissDialog,
+ onConfirmWithdraw = onConfirmWithdraw,
+ )
+ }
+
+ Column(modifier = modifier.fillMaxSize()) {
+ DeleteAccountTopAppBar(onClickClose = onClickClose)
+
+ DeleteAccountContent(
+ uiState = uiState,
+ onToggleNoticeChecked = onToggleNoticeChecked,
+ onSelectReason = onSelectReason,
+ onOtherReasonChanged = onOtherReasonChanged,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ )
+
+ DeleteAccountActionSection(
+ step = uiState.step,
+ enabled = uiState.isPrimaryActionEnabled,
+ onClickNext = onClickNext,
+ onClickWithdraw = onClickWithdraw,
+ )
+ }
+}
+
+@Composable
+private fun DeleteAccountContent(
+ uiState: DeleteAccountUiState,
+ onToggleNoticeChecked: (Boolean) -> Unit,
+ onSelectReason: (DeleteAccountReasonOption) -> Unit,
+ onOtherReasonChanged: (String) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Box(modifier = modifier) {
+ when (uiState.step) {
+ DeleteAccountStep.NOTICE -> DeleteAccountNoticeStep(
+ isChecked = uiState.isNoticeChecked,
+ onCheckedChange = onToggleNoticeChecked,
+ )
+
+ DeleteAccountStep.REASON -> DeleteAccountReasonStep(
+ selectedReason = uiState.selectedReason,
+ otherReasonText = uiState.otherReasonText,
+ onSelectReason = onSelectReason,
+ onOtherReasonChanged = onOtherReasonChanged,
+ )
+ }
+ }
+}
+
+@BasicPreview
+@Composable
+private fun DeleteAccountNoticeStepScreenPreview() {
+ PrezelTheme {
+ DeleteAccountScreen(
+ uiState = DeleteAccountUiState(
+ step = DeleteAccountStep.NOTICE,
+ ),
+ onClickClose = {},
+ onToggleNoticeChecked = {},
+ onClickNext = {},
+ onSelectReason = {},
+ onOtherReasonChanged = {},
+ onClickWithdraw = {},
+ onDismissDialog = {},
+ onConfirmWithdraw = {},
+ )
+ }
+}
+
+@BasicPreview
+@Composable
+private fun DeleteAccountReasonStepScreenPreview() {
+ PrezelTheme {
+ DeleteAccountScreen(
+ uiState = DeleteAccountUiState(
+ step = DeleteAccountStep.REASON,
+ selectedReason = DeleteAccountReasonOption.Etc,
+ ),
+ onClickClose = {},
+ onToggleNoticeChecked = {},
+ onClickNext = {},
+ onSelectReason = {},
+ onOtherReasonChanged = {},
+ onClickWithdraw = {},
+ onDismissDialog = {},
+ onConfirmWithdraw = {},
+ )
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/DeleteAccountViewModel.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/DeleteAccountViewModel.kt
new file mode 100644
index 00000000..2adc9e2a
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/DeleteAccountViewModel.kt
@@ -0,0 +1,92 @@
+package com.team.prezel.feature.setting.impl.delete
+
+import androidx.lifecycle.viewModelScope
+import com.team.prezel.core.domain.usecase.auth.WithdrawUseCase
+import com.team.prezel.core.model.auth.WithdrawReason
+import com.team.prezel.core.ui.base.BaseViewModel
+import com.team.prezel.feature.setting.impl.delete.contract.DeleteAccountUiEffect
+import com.team.prezel.feature.setting.impl.delete.contract.DeleteAccountUiIntent
+import com.team.prezel.feature.setting.impl.delete.contract.DeleteAccountUiState
+import com.team.prezel.feature.setting.impl.delete.model.DeleteAccountReasonOption
+import com.team.prezel.feature.setting.impl.delete.model.DeleteAccountStep
+import com.team.prezel.feature.setting.impl.delete.model.DeleteAccountUiMessage
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import javax.inject.Inject
+
+@HiltViewModel
+internal class DeleteAccountViewModel @Inject constructor(
+ private val withdrawUseCase: WithdrawUseCase,
+) : BaseViewModel(DeleteAccountUiState()) {
+ override fun onIntent(intent: DeleteAccountUiIntent) {
+ when (intent) {
+ DeleteAccountUiIntent.ClickNext -> moveToReasonStep()
+ DeleteAccountUiIntent.ClickWithdraw -> showConfirmDialog()
+ DeleteAccountUiIntent.ConfirmWithdraw -> withdraw()
+ DeleteAccountUiIntent.DismissDialog -> dismissDialog()
+ is DeleteAccountUiIntent.ChangeOtherReason -> updateState { copy(otherReasonText = intent.value) }
+ is DeleteAccountUiIntent.SelectReason -> selectReason(intent.reason)
+ is DeleteAccountUiIntent.ToggleNoticeChecked -> updateState { copy(isNoticeChecked = intent.checked) }
+ }
+ }
+
+ private fun moveToReasonStep() {
+ if (currentState.isNextEnabled) {
+ updateState { copy(step = DeleteAccountStep.REASON) }
+ }
+ }
+
+ private fun selectReason(reason: DeleteAccountReasonOption) {
+ updateState {
+ copy(
+ selectedReason = reason,
+ otherReasonText = if (reason == DeleteAccountReasonOption.Etc) otherReasonText else "",
+ )
+ }
+ }
+
+ private fun showConfirmDialog() {
+ if (currentState.isWithdrawEnabled) {
+ updateState { copy(isConfirmDialogVisible = true) }
+ }
+ }
+
+ private fun dismissDialog() {
+ updateState { copy(isConfirmDialogVisible = false) }
+ }
+
+ private fun withdraw() {
+ val reason = currentState.toWithdrawReason() ?: return
+
+ updateState {
+ copy(
+ isConfirmDialogVisible = false,
+ isSubmitting = true,
+ )
+ }
+
+ viewModelScope.launch {
+ withdrawUseCase(reason)
+ .onSuccess {
+ sendEffect(DeleteAccountUiEffect.NavigateToSplash)
+ }.onFailure { exception ->
+ Timber.e(t = exception)
+ updateState { copy(isSubmitting = false) }
+ sendEffect(DeleteAccountUiEffect.ShowMessage(DeleteAccountUiMessage.WITHDRAW_FAILED))
+ }
+ }
+ }
+
+ private fun DeleteAccountUiState.toWithdrawReason(): WithdrawReason? =
+ when (selectedReason) {
+ DeleteAccountReasonOption.NotUsedOften -> WithdrawReason.NotUsedOften
+ DeleteAccountReasonOption.NoLongerNeeded -> WithdrawReason.NoLongerNeeded
+ DeleteAccountReasonOption.TooComplex -> WithdrawReason.TooComplex
+ DeleteAccountReasonOption.InaccurateAnalysis -> WithdrawReason.InaccurateAnalysis
+ DeleteAccountReasonOption.ManyErrors -> WithdrawReason.ManyErrors
+ DeleteAccountReasonOption.Etc -> WithdrawReason.Etc(text = otherReasonText.trim())
+
+ null -> null
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountActionSection.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountActionSection.kt
new file mode 100644
index 00000000..c97d416d
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountActionSection.kt
@@ -0,0 +1,66 @@
+package com.team.prezel.feature.setting.impl.delete.component
+
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.ime
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
+import com.team.prezel.core.designsystem.component.actions.area.PrezelButtonArea
+import com.team.prezel.core.designsystem.component.actions.button.PrezelTextButton
+import com.team.prezel.core.designsystem.preview.BasicPreview
+import com.team.prezel.core.designsystem.theme.PrezelTheme
+import com.team.prezel.core.ui.util.advancedImePadding
+import com.team.prezel.feature.setting.impl.R
+import com.team.prezel.feature.setting.impl.delete.model.DeleteAccountStep
+
+@Composable
+internal fun DeleteAccountActionSection(
+ step: DeleteAccountStep,
+ enabled: Boolean,
+ onClickNext: () -> Unit,
+ onClickWithdraw: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ PrezelButtonArea(
+ isVertical = false,
+ isStrongStrength = true,
+ isNested = false,
+ showBackground = isShowBackground(),
+ mainButton = { modifier ->
+ PrezelTextButton(
+ modifier = modifier,
+ text = step.buttonText(),
+ enabled = enabled,
+ onClick = if (step == DeleteAccountStep.NOTICE) onClickNext else onClickWithdraw,
+ )
+ },
+ modifier = modifier.advancedImePadding(),
+ )
+}
+
+@Composable
+private fun isShowBackground(): Boolean {
+ val density = LocalDensity.current
+ return WindowInsets.ime.getBottom(density) > 0
+}
+
+@Composable
+private fun DeleteAccountStep.buttonText(): String =
+ when (this) {
+ DeleteAccountStep.NOTICE -> R.string.feature_setting_impl_delete_account_next
+ DeleteAccountStep.REASON -> R.string.feature_setting_impl_delete_account_withdraw
+ }.let { resId -> stringResource(resId) }
+
+@BasicPreview
+@Composable
+private fun DeleteAccountActionSectionNoticePreview() {
+ PrezelTheme {
+ DeleteAccountActionSection(
+ step = DeleteAccountStep.NOTICE,
+ enabled = true,
+ onClickNext = {},
+ onClickWithdraw = {},
+ )
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountConfirmDialog.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountConfirmDialog.kt
new file mode 100644
index 00000000..edb162da
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountConfirmDialog.kt
@@ -0,0 +1,44 @@
+package com.team.prezel.feature.setting.impl.delete.component
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.team.prezel.core.designsystem.component.feedback.dialog.PrezelDialog
+import com.team.prezel.core.designsystem.component.feedback.dialog.PrezelDialogScope.ActionType
+import com.team.prezel.core.designsystem.preview.BasicPreview
+import com.team.prezel.core.designsystem.theme.PrezelTheme
+import com.team.prezel.feature.setting.impl.R
+
+@Composable
+internal fun DeleteAccountConfirmDialog(
+ onDismissDialog: () -> Unit,
+ onConfirmWithdraw: () -> Unit,
+) {
+ PrezelDialog(
+ title = stringResource(R.string.feature_setting_impl_delete_account_dialog_title),
+ onDismiss = onDismissDialog,
+ ) {
+ Action(label = stringResource(R.string.feature_setting_impl_delete_account_dialog_cancel)) { onDismissDialog() }
+ Action(
+ label = stringResource(R.string.feature_setting_impl_delete_account_dialog_confirm),
+ type = ActionType.BAD,
+ ) {
+ onConfirmWithdraw()
+ }
+ }
+}
+
+@BasicPreview
+@Composable
+private fun DeleteAccountConfirmDialogPreview() {
+ PrezelTheme {
+ Box(modifier = Modifier.fillMaxSize()) {
+ DeleteAccountConfirmDialog(
+ onDismissDialog = {},
+ onConfirmWithdraw = {},
+ )
+ }
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountNoticeStep.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountNoticeStep.kt
new file mode 100644
index 00000000..56d0a152
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountNoticeStep.kt
@@ -0,0 +1,330 @@
+package com.team.prezel.feature.setting.impl.delete.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.Bullet
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.em
+import com.team.prezel.core.designsystem.component.PrezelCheckbox
+import com.team.prezel.core.designsystem.preview.BasicPreview
+import com.team.prezel.core.designsystem.theme.PrezelTheme
+import com.team.prezel.core.ui.util.noRippleClickable
+import com.team.prezel.feature.setting.impl.R
+
+private const val HIGHLIGHT_START_TAG = ""
+private const val HIGHLIGHT_END_TAG = ""
+private const val BOLD_START_TAG = ""
+private const val BOLD_END_TAG = ""
+
+private val supportedTags = listOf(
+ HIGHLIGHT_START_TAG,
+ HIGHLIGHT_END_TAG,
+ BOLD_START_TAG,
+ BOLD_END_TAG,
+)
+
+@Composable
+internal fun DeleteAccountNoticeStep(
+ isChecked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(horizontal = PrezelTheme.spacing.V20)
+ .padding(top = PrezelTheme.spacing.V16, bottom = PrezelTheme.spacing.V24),
+ ) {
+ DeleteAccountNoticeHeader()
+
+ Spacer(modifier = Modifier.height(PrezelTheme.spacing.V16))
+
+ DeleteAccountNoticeDetailBox()
+
+ Spacer(modifier = Modifier.height(PrezelTheme.spacing.V36))
+
+ DeletionAgreement(isChecked = isChecked, onCheckedChange = onCheckedChange)
+ }
+}
+
+@Composable
+private fun DeleteAccountNoticeHeader(modifier: Modifier = Modifier) {
+ Column(modifier = modifier.fillMaxWidth()) {
+ Text(
+ text = stringResource(R.string.feature_setting_impl_delete_account_notice_title),
+ style = PrezelTheme.typography.title2Bold,
+ color = PrezelTheme.colors.textLarge,
+ )
+
+ Spacer(modifier = Modifier.height(PrezelTheme.spacing.V8))
+
+ DeleteAccountNoticeDescription()
+ }
+}
+
+@Composable
+private fun DeletionAgreement(
+ isChecked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .noRippleClickable { onCheckedChange(!isChecked) },
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ PrezelCheckbox(
+ checked = isChecked,
+ onCheckedChange = onCheckedChange,
+ extraTouchPadding = PaddingValues(),
+ )
+
+ Spacer(modifier = Modifier.width(PrezelTheme.spacing.V8))
+
+ Text(
+ text = stringResource(R.string.feature_setting_impl_delete_account_notice_check),
+ style = PrezelTheme.typography.body3Regular,
+ color = PrezelTheme.colors.textRegular,
+ )
+ }
+}
+
+@Composable
+private fun DeleteAccountNoticeDescription(modifier: Modifier = Modifier) {
+ val highlightStyle = SpanStyle(color = PrezelTheme.colors.feedbackBadRegular)
+ val boldStyle = SpanStyle(
+ fontSize = PrezelTheme.typography.body2Bold.fontSize,
+ fontWeight = PrezelTheme.typography.body2Bold.fontWeight,
+ letterSpacing = PrezelTheme.typography.body2Bold.letterSpacing,
+ )
+
+ Text(
+ text = buildAnnotatedString {
+ appendTaggedText(
+ text = stringResource(R.string.feature_setting_impl_delete_account_notice_description),
+ highlightStyle = highlightStyle,
+ boldStyle = boldStyle,
+ )
+ },
+ modifier = modifier.fillMaxWidth(),
+ style = PrezelTheme.typography.body2Regular,
+ color = PrezelTheme.colors.textMedium,
+ )
+}
+
+@Composable
+private fun DeleteAccountNoticeDetailBox(modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(
+ color = PrezelTheme.colors.bgMedium,
+ shape = PrezelTheme.shapes.V8,
+ ).padding(horizontal = PrezelTheme.spacing.V8, vertical = PrezelTheme.spacing.V12),
+ verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V8),
+ ) {
+ DeleteAccountNoticeDetailItem(
+ text = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_1),
+ )
+ DeleteAccountNoticeDetailItem(
+ text = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_2),
+ )
+ DeleteAccountNoticeDetailItem(
+ text = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_3),
+ )
+ DeleteAccountNoticeDetailItem(
+ text = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_4),
+ )
+ }
+}
+
+@Composable
+private fun DeleteAccountNoticeDetailItem(
+ text: String,
+ modifier: Modifier = Modifier,
+) {
+ val highlightStyle = SpanStyle(color = PrezelTheme.colors.feedbackBadRegular)
+ val boldStyle = SpanStyle(
+ fontSize = PrezelTheme.typography.body3Regular.fontSize,
+ fontWeight = PrezelTheme.typography.body3Regular.fontWeight,
+ letterSpacing = PrezelTheme.typography.body3Regular.letterSpacing,
+ )
+
+ Text(
+ text = buildAnnotatedString {
+ withBulletList(bullet = Bullet.Default.copy(padding = 0.5.em)) {
+ withBulletListItem {
+ appendTaggedText(
+ text = text,
+ highlightStyle = highlightStyle,
+ boldStyle = boldStyle,
+ )
+ }
+ }
+ },
+ modifier = modifier.fillMaxWidth(),
+ style = PrezelTheme.typography.body3Regular,
+ color = PrezelTheme.colors.textMedium,
+ )
+}
+
+private fun AnnotatedString.Builder.appendTaggedText(
+ text: String,
+ highlightStyle: SpanStyle,
+ boldStyle: SpanStyle,
+) {
+ if (!isValidTaggedText(text)) {
+ append(text)
+ return
+ }
+
+ var currentIndex = 0
+ var tagState = TagState()
+
+ while (currentIndex < text.length) {
+ val nextTag = findNextTag(text = text, startIndex = currentIndex)
+
+ if (nextTag == null) {
+ appendWithStyle(
+ text = text.substring(currentIndex),
+ tagState = tagState,
+ highlightStyle = highlightStyle,
+ boldStyle = boldStyle,
+ )
+ return
+ }
+
+ val (tag, tagIndex) = nextTag
+ appendWithCurrentStyle(
+ text = text,
+ currentIndex = currentIndex,
+ tagIndex = tagIndex,
+ tagState = tagState,
+ highlightStyle = highlightStyle,
+ boldStyle = boldStyle,
+ )
+
+ tagState = tagState.next(tag) ?: return
+ currentIndex = tagIndex + tag.length
+ }
+}
+
+private fun findNextTag(
+ text: String,
+ startIndex: Int,
+): Pair? =
+ supportedTags
+ .map { tag -> tag to text.indexOf(tag, startIndex = startIndex) }
+ .filter { (_, index) -> index >= 0 }
+ .minByOrNull { (_, index) -> index }
+
+private fun AnnotatedString.Builder.appendWithCurrentStyle(
+ text: String,
+ currentIndex: Int,
+ tagIndex: Int,
+ tagState: TagState,
+ highlightStyle: SpanStyle,
+ boldStyle: SpanStyle,
+) {
+ appendWithStyle(
+ text = text.substring(currentIndex, tagIndex),
+ tagState = tagState,
+ highlightStyle = highlightStyle,
+ boldStyle = boldStyle,
+ )
+}
+
+private fun isValidTaggedText(text: String): Boolean {
+ var currentIndex = 0
+ var tagState = TagState()
+
+ while (currentIndex < text.length) {
+ val nextTag = findNextTag(text = text, startIndex = currentIndex) ?: break
+ val (tag, tagIndex) = nextTag
+
+ tagState = tagState.next(tag) ?: return false
+ currentIndex = tagIndex + tag.length
+ }
+
+ return !tagState.isHighlighting && !tagState.isBold
+}
+
+private fun AnnotatedString.Builder.appendWithStyle(
+ text: String,
+ tagState: TagState,
+ highlightStyle: SpanStyle,
+ boldStyle: SpanStyle,
+) {
+ if (text.isEmpty()) return
+
+ val style = tagState.toSpanStyle(
+ highlightStyle = highlightStyle,
+ boldStyle = boldStyle,
+ )
+
+ if (style == null) {
+ append(text)
+ return
+ }
+
+ withStyle(style) {
+ append(text)
+ }
+}
+
+private data class TagState(
+ val isHighlighting: Boolean = false,
+ val isBold: Boolean = false,
+) {
+ fun next(tag: String): TagState? =
+ when (tag) {
+ HIGHLIGHT_START_TAG -> if (isHighlighting) null else copy(isHighlighting = true)
+ HIGHLIGHT_END_TAG -> if (!isHighlighting || isBold) null else copy(isHighlighting = false)
+ BOLD_START_TAG -> if (isBold) null else copy(isBold = true)
+ BOLD_END_TAG -> if (!isBold) null else copy(isBold = false)
+ else -> null
+ }
+
+ fun toSpanStyle(
+ highlightStyle: SpanStyle,
+ boldStyle: SpanStyle,
+ ): SpanStyle? =
+ when {
+ isHighlighting && isBold -> boldStyle.merge(highlightStyle)
+ isHighlighting -> highlightStyle
+ isBold -> boldStyle
+ else -> null
+ }
+}
+
+@BasicPreview
+@Composable
+private fun DeleteAccountNoticeStepPreview() {
+ PrezelTheme {
+ DeleteAccountNoticeStep(
+ isChecked = false,
+ onCheckedChange = {},
+ )
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountReasonStep.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountReasonStep.kt
new file mode 100644
index 00000000..d95dffde
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountReasonStep.kt
@@ -0,0 +1,160 @@
+package com.team.prezel.feature.setting.impl.delete.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+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.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardType
+import com.team.prezel.core.designsystem.component.PrezelRadio
+import com.team.prezel.core.designsystem.component.list.PrezelList
+import com.team.prezel.core.designsystem.component.textfield.PrezelTextArea
+import com.team.prezel.core.designsystem.preview.BasicPreview
+import com.team.prezel.core.designsystem.theme.PrezelTheme
+import com.team.prezel.core.ui.util.noRippleClickable
+import com.team.prezel.feature.setting.impl.R
+import com.team.prezel.feature.setting.impl.delete.model.DeleteAccountReasonOption
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+
+@Composable
+internal fun DeleteAccountReasonStep(
+ selectedReason: DeleteAccountReasonOption?,
+ otherReasonText: String,
+ onSelectReason: (DeleteAccountReasonOption) -> Unit,
+ onOtherReasonChanged: (String) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(horizontal = PrezelTheme.spacing.V12)
+ .padding(top = PrezelTheme.spacing.V16, bottom = PrezelTheme.spacing.V24),
+ ) {
+ DeleteAccountReasonHeader(modifier = Modifier.padding(horizontal = PrezelTheme.spacing.V8))
+
+ Spacer(modifier = Modifier.height(PrezelTheme.spacing.V32))
+
+ DeleteAccountReasons(
+ selectedReason = selectedReason,
+ otherReasonText = otherReasonText,
+ onSelectReason = onSelectReason,
+ onOtherReasonChanged = onOtherReasonChanged,
+ )
+ }
+}
+
+@Composable
+private fun DeleteAccountReasonHeader(modifier: Modifier = Modifier) {
+ Column(modifier = modifier.fillMaxWidth()) {
+ Text(
+ text = stringResource(R.string.feature_setting_impl_delete_account_reason_title),
+ style = PrezelTheme.typography.title2Bold,
+ color = PrezelTheme.colors.textLarge,
+ )
+ Spacer(modifier = Modifier.height(PrezelTheme.spacing.V8))
+ Text(
+ text = stringResource(R.string.feature_setting_impl_delete_account_reason_description),
+ style = PrezelTheme.typography.body3Regular,
+ color = PrezelTheme.colors.textRegular,
+ )
+ }
+}
+
+@Composable
+private fun DeleteAccountReasons(
+ selectedReason: DeleteAccountReasonOption?,
+ otherReasonText: String,
+ onSelectReason: (DeleteAccountReasonOption) -> Unit,
+ onOtherReasonChanged: (String) -> Unit,
+ reasonOptions: ImmutableList> = persistentListOf(
+ DeleteAccountReasonOption.Etc to stringResource(R.string.feature_setting_impl_delete_account_reason_other),
+ DeleteAccountReasonOption.NotUsedOften to stringResource(R.string.feature_setting_impl_delete_account_reason_not_used_often),
+ DeleteAccountReasonOption.NoLongerNeeded to stringResource(R.string.feature_setting_impl_delete_account_reason_no_longer_needed),
+ DeleteAccountReasonOption.TooComplex to stringResource(R.string.feature_setting_impl_delete_account_reason_too_difficult_or_complex),
+ DeleteAccountReasonOption.InaccurateAnalysis to stringResource(
+ R.string.feature_setting_impl_delete_account_reason_analysis_result_inaccurate,
+ ),
+ DeleteAccountReasonOption.ManyErrors to stringResource(R.string.feature_setting_impl_delete_account_reason_too_many_errors),
+ ),
+) {
+ Column(verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V12)) {
+ CompositionLocalProvider(LocalContentColor provides PrezelTheme.colors.textLarge) {
+ reasonOptions.forEach { (reason, label) ->
+ DeleteAccountReasonOptionItem(
+ reason = reason,
+ label = label,
+ selectedReason = selectedReason,
+ otherReasonText = otherReasonText,
+ onSelectReason = onSelectReason,
+ onOtherReasonChanged = onOtherReasonChanged,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun DeleteAccountReasonOptionItem(
+ reason: DeleteAccountReasonOption,
+ label: String,
+ selectedReason: DeleteAccountReasonOption?,
+ otherReasonText: String,
+ onSelectReason: (DeleteAccountReasonOption) -> Unit,
+ onOtherReasonChanged: (String) -> Unit,
+) {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ PrezelList(
+ title = label,
+ titleTextColor = PrezelTheme.colors.textLarge,
+ nested = true,
+ leadingContent = {
+ PrezelRadio(
+ checked = selectedReason == reason,
+ onCheckedChange = { checked -> if (checked) onSelectReason(reason) },
+ )
+ },
+ modifier = Modifier.noRippleClickable { onSelectReason(reason) },
+ )
+
+ if (reason == DeleteAccountReasonOption.Etc && selectedReason == DeleteAccountReasonOption.Etc) {
+ Spacer(modifier = Modifier.height(PrezelTheme.spacing.V8))
+ PrezelTextArea(
+ value = otherReasonText,
+ onValueChange = onOtherReasonChanged,
+ placeholder = stringResource(R.string.feature_setting_impl_delete_account_reason_other_placeholder),
+ maxLength = 200,
+ showCount = false,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = PrezelTheme.spacing.V8),
+ )
+ }
+ }
+}
+
+@BasicPreview
+@Composable
+private fun DeleteAccountReasonStepPreview() {
+ PrezelTheme {
+ DeleteAccountReasonStep(
+ selectedReason = DeleteAccountReasonOption.Etc,
+ otherReasonText = "",
+ onSelectReason = {},
+ onOtherReasonChanged = {},
+ )
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountTopAppBar.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountTopAppBar.kt
new file mode 100644
index 00000000..e110f33d
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountTopAppBar.kt
@@ -0,0 +1,41 @@
+package com.team.prezel.feature.setting.impl.delete.component
+
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import com.team.prezel.core.designsystem.component.PrezelTopAppBar
+import com.team.prezel.core.designsystem.icon.PrezelIcons
+import com.team.prezel.core.designsystem.preview.BasicPreview
+import com.team.prezel.core.designsystem.theme.PrezelTheme
+import com.team.prezel.feature.setting.impl.R
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun DeleteAccountTopAppBar(
+ onClickClose: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ PrezelTopAppBar(
+ modifier = modifier,
+ trailingIcons = {
+ IconButton(onClick = onClickClose) {
+ Icon(
+ painter = painterResource(PrezelIcons.Cancel),
+ contentDescription = stringResource(R.string.feature_setting_impl_close_icon_content_description),
+ )
+ }
+ },
+ )
+}
+
+@BasicPreview
+@Composable
+private fun DeleteAccountTopAppBarPreview() {
+ PrezelTheme {
+ DeleteAccountTopAppBar(onClickClose = {})
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiEffect.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiEffect.kt
new file mode 100644
index 00000000..48c1646e
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiEffect.kt
@@ -0,0 +1,12 @@
+package com.team.prezel.feature.setting.impl.delete.contract
+
+import com.team.prezel.core.ui.base.UiEffect
+import com.team.prezel.feature.setting.impl.delete.model.DeleteAccountUiMessage
+
+internal sealed interface DeleteAccountUiEffect : UiEffect {
+ data object NavigateToSplash : DeleteAccountUiEffect
+
+ data class ShowMessage(
+ val message: DeleteAccountUiMessage,
+ ) : DeleteAccountUiEffect
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiIntent.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiIntent.kt
new file mode 100644
index 00000000..57f7ff02
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiIntent.kt
@@ -0,0 +1,26 @@
+package com.team.prezel.feature.setting.impl.delete.contract
+
+import com.team.prezel.core.ui.base.UiIntent
+import com.team.prezel.feature.setting.impl.delete.model.DeleteAccountReasonOption
+
+internal sealed interface DeleteAccountUiIntent : UiIntent {
+ data class ToggleNoticeChecked(
+ val checked: Boolean,
+ ) : DeleteAccountUiIntent
+
+ data object ClickNext : DeleteAccountUiIntent
+
+ data class SelectReason(
+ val reason: DeleteAccountReasonOption,
+ ) : DeleteAccountUiIntent
+
+ data class ChangeOtherReason(
+ val value: String,
+ ) : DeleteAccountUiIntent
+
+ data object ClickWithdraw : DeleteAccountUiIntent
+
+ data object DismissDialog : DeleteAccountUiIntent
+
+ data object ConfirmWithdraw : DeleteAccountUiIntent
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiState.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiState.kt
new file mode 100644
index 00000000..e9bb29cd
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiState.kt
@@ -0,0 +1,22 @@
+package com.team.prezel.feature.setting.impl.delete.contract
+
+import androidx.compose.runtime.Immutable
+import com.team.prezel.core.ui.base.UiState
+import com.team.prezel.feature.setting.impl.delete.model.DeleteAccountReasonOption
+import com.team.prezel.feature.setting.impl.delete.model.DeleteAccountStep
+
+@Immutable
+internal data class DeleteAccountUiState(
+ val step: DeleteAccountStep = DeleteAccountStep.NOTICE,
+ val isNoticeChecked: Boolean = false,
+ val selectedReason: DeleteAccountReasonOption? = null,
+ val otherReasonText: String = "",
+ val isConfirmDialogVisible: Boolean = false,
+ val isSubmitting: Boolean = false,
+) : UiState {
+ val isNextEnabled: Boolean = isNoticeChecked
+
+ val isWithdrawEnabled: Boolean = selectedReason != null && !isSubmitting
+
+ val isPrimaryActionEnabled: Boolean = if (step == DeleteAccountStep.NOTICE) isNextEnabled else isWithdrawEnabled
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/model/DeleteAccountReasonOption.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/model/DeleteAccountReasonOption.kt
new file mode 100644
index 00000000..3d1d8cab
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/model/DeleteAccountReasonOption.kt
@@ -0,0 +1,10 @@
+package com.team.prezel.feature.setting.impl.delete.model
+
+internal enum class DeleteAccountReasonOption {
+ NotUsedOften,
+ NoLongerNeeded,
+ TooComplex,
+ InaccurateAnalysis,
+ ManyErrors,
+ Etc,
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/model/DeleteAccountStep.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/model/DeleteAccountStep.kt
new file mode 100644
index 00000000..8cc80dcc
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/model/DeleteAccountStep.kt
@@ -0,0 +1,6 @@
+package com.team.prezel.feature.setting.impl.delete.model
+
+internal enum class DeleteAccountStep {
+ NOTICE,
+ REASON,
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/model/DeleteAccountUiMessage.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/model/DeleteAccountUiMessage.kt
new file mode 100644
index 00000000..5ad49a58
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/model/DeleteAccountUiMessage.kt
@@ -0,0 +1,5 @@
+package com.team.prezel.feature.setting.impl.delete.model
+
+internal enum class DeleteAccountUiMessage {
+ WITHDRAW_FAILED,
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/navigation/SettingEntryBuilder.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/navigation/SettingEntryBuilder.kt
new file mode 100644
index 00000000..1a4c55e4
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/navigation/SettingEntryBuilder.kt
@@ -0,0 +1,55 @@
+package com.team.prezel.feature.setting.impl.navigation
+
+import androidx.navigation3.runtime.EntryProviderScope
+import androidx.navigation3.runtime.NavKey
+import com.team.prezel.core.navigation.LocalNavigator
+import com.team.prezel.feature.setting.api.DeleteAccountNavKey
+import com.team.prezel.feature.setting.api.SettingNavKey
+import com.team.prezel.feature.setting.impl.delete.DeleteAccountScreen
+import com.team.prezel.feature.setting.impl.setting.SettingScreen
+import com.team.prezel.feature.splash.api.SplashNavKey
+import com.team.prezel.feature.terms.api.TermsDocumentType
+import com.team.prezel.feature.terms.api.TermsNavKey
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityRetainedComponent
+import dagger.multibindings.IntoSet
+
+internal fun EntryProviderScope.featureSettingEntryBuilder() {
+ entry {
+ val navigator = LocalNavigator.current
+
+ SettingScreen(
+ navigateBack = { navigator.goBack() },
+ navigateToDeleteAccount = { navigator.navigate(DeleteAccountNavKey) },
+ navigateToSplash = { navigator.replaceRoot(SplashNavKey) },
+ navigateToTermsOfService = {
+ navigator.navigate(TermsNavKey.Detail(TermsDocumentType.TERMS_OF_SERVICE))
+ },
+ navigateToPrivacyPolicy = {
+ navigator.navigate(TermsNavKey.Detail(TermsDocumentType.PRIVACY_POLICY))
+ },
+ )
+ }
+
+ entry {
+ val navigator = LocalNavigator.current
+
+ DeleteAccountScreen(
+ navigateBack = { navigator.goBack() },
+ navigateToSplash = { navigator.replaceRoot(SplashNavKey) },
+ )
+ }
+}
+
+@Module
+@InstallIn(ActivityRetainedComponent::class)
+object FeatureSettingModule {
+ @IntoSet
+ @Provides
+ fun provideFeatureSettingEntryBuilder(): EntryProviderScope.() -> Unit =
+ {
+ featureSettingEntryBuilder()
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/SettingScreen.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/SettingScreen.kt
new file mode 100644
index 00000000..49989a74
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/SettingScreen.kt
@@ -0,0 +1,138 @@
+package com.team.prezel.feature.setting.impl.setting
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+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.platform.LocalResources
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.team.prezel.core.designsystem.component.feedback.snackbar.showPrezelSnackbar
+import com.team.prezel.core.designsystem.preview.BasicPreview
+import com.team.prezel.core.designsystem.theme.PrezelTheme
+import com.team.prezel.core.ui.state.LocalSnackbarHostState
+import com.team.prezel.feature.setting.impl.R
+import com.team.prezel.feature.setting.impl.setting.component.LogoutDialog
+import com.team.prezel.feature.setting.impl.setting.component.SettingActionSection
+import com.team.prezel.feature.setting.impl.setting.component.SettingBodySection
+import com.team.prezel.feature.setting.impl.setting.component.SettingTopAppBar
+import com.team.prezel.feature.setting.impl.setting.contract.SettingUiEffect
+import com.team.prezel.feature.setting.impl.setting.contract.SettingUiIntent
+import com.team.prezel.feature.setting.impl.setting.contract.SettingUiState
+import com.team.prezel.feature.setting.impl.setting.model.SettingUiMessage
+
+@Composable
+internal fun SettingScreen(
+ navigateBack: () -> Unit,
+ navigateToDeleteAccount: () -> Unit,
+ navigateToSplash: () -> Unit,
+ navigateToTermsOfService: () -> Unit,
+ navigateToPrivacyPolicy: () -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel: SettingViewModel = hiltViewModel(),
+) {
+ val snackbarHostState = LocalSnackbarHostState.current
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val resource = LocalResources.current
+ var shouldShowLogoutDialog by remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ viewModel.onIntent(SettingUiIntent.FetchData)
+
+ viewModel.uiEffect.collect { effect ->
+ when (effect) {
+ SettingUiEffect.NavigateToSplash -> {
+ shouldShowLogoutDialog = false
+ navigateToSplash()
+ snackbarHostState.showPrezelSnackbar(message = resource.getString(R.string.feature_setting_impl_logout_success))
+ }
+
+ is SettingUiEffect.ShowMessage -> {
+ snackbarHostState.showPrezelSnackbar(message = resource.getString(effect.message.toMessageRes()))
+ }
+ }
+ }
+ }
+
+ if (shouldShowLogoutDialog) {
+ LogoutDialog(
+ onDismiss = { shouldShowLogoutDialog = false },
+ onConfirmLogout = { viewModel.onIntent(SettingUiIntent.ClickLogout) },
+ )
+ }
+
+ SettingScreen(
+ uiState = uiState,
+ onClickBack = navigateBack,
+ onClickTermsOfService = navigateToTermsOfService,
+ onClickPrivacyPolicy = navigateToPrivacyPolicy,
+ onClickLogout = { shouldShowLogoutDialog = true },
+ onClickWithdraw = navigateToDeleteAccount,
+ modifier = modifier,
+ )
+}
+
+private fun SettingUiMessage.toMessageRes(): Int =
+ when (this) {
+ SettingUiMessage.FETCH_USER_INFO_FAILED -> R.string.feature_setting_impl_fetch_user_info_failed
+ SettingUiMessage.LOGOUT_FAILED -> R.string.feature_setting_impl_logout_failed
+ }
+
+@Composable
+private fun SettingScreen(
+ uiState: SettingUiState,
+ onClickBack: () -> Unit,
+ onClickTermsOfService: () -> Unit,
+ onClickPrivacyPolicy: () -> Unit,
+ onClickLogout: () -> Unit,
+ onClickWithdraw: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(modifier = modifier.fillMaxSize()) {
+ SettingHeaderSection(onClickBack = onClickBack)
+ SettingBodySection(
+ uiState = uiState,
+ onClickTermsOfService = onClickTermsOfService,
+ onClickPrivacyPolicy = onClickPrivacyPolicy,
+ modifier = Modifier.weight(1f),
+ )
+ SettingActionSection(
+ onClickWithdraw = onClickWithdraw,
+ onClickLogout = onClickLogout,
+ )
+ }
+}
+
+@Composable
+private fun SettingHeaderSection(
+ onClickBack: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ SettingTopAppBar(
+ onBack = onClickBack,
+ modifier = modifier,
+ )
+}
+
+@BasicPreview
+@Composable
+private fun SettingScreenPreview() {
+ PrezelTheme {
+ SettingScreen(
+ uiState = SettingUiState(
+ nickname = "프레젤",
+ email = "prezel@email.com",
+ ),
+ onClickBack = {},
+ onClickTermsOfService = {},
+ onClickPrivacyPolicy = {},
+ onClickLogout = {},
+ onClickWithdraw = {},
+ )
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/SettingViewModel.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/SettingViewModel.kt
new file mode 100644
index 00000000..194708d4
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/SettingViewModel.kt
@@ -0,0 +1,60 @@
+package com.team.prezel.feature.setting.impl.setting
+
+import androidx.lifecycle.viewModelScope
+import com.team.prezel.core.domain.usecase.auth.LogoutUseCase
+import com.team.prezel.core.domain.usecase.user.FetchUserInfoUseCase
+import com.team.prezel.core.ui.base.BaseViewModel
+import com.team.prezel.feature.setting.impl.setting.contract.SettingUiEffect
+import com.team.prezel.feature.setting.impl.setting.contract.SettingUiIntent
+import com.team.prezel.feature.setting.impl.setting.contract.SettingUiState
+import com.team.prezel.feature.setting.impl.setting.model.SettingUiMessage
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import javax.inject.Inject
+
+@HiltViewModel
+internal class SettingViewModel @Inject constructor(
+ private val fetchUserInfoUseCase: FetchUserInfoUseCase,
+ private val logoutUseCase: LogoutUseCase,
+) : BaseViewModel(SettingUiState()) {
+ override fun onIntent(intent: SettingUiIntent) {
+ when (intent) {
+ SettingUiIntent.FetchData -> fetchUserInfo()
+ SettingUiIntent.ClickLogout -> logout()
+ }
+ }
+
+ private fun fetchUserInfo() {
+ updateState { copy(isLoading = true) }
+
+ viewModelScope
+ .launch {
+ fetchUserInfoUseCase()
+ .onSuccess { user ->
+ updateState {
+ copy(
+ profileImageUrl = user.profileImageUrl,
+ nickname = user.nickname,
+ email = user.email,
+ )
+ }
+ }.onFailure { exception ->
+ Timber.e(t = exception)
+ sendEffect(SettingUiEffect.ShowMessage(SettingUiMessage.FETCH_USER_INFO_FAILED))
+ }
+ }.invokeOnCompletion { updateState { copy(isLoading = false) } }
+ }
+
+ private fun logout() {
+ viewModelScope.launch {
+ logoutUseCase()
+ .onSuccess {
+ sendEffect(SettingUiEffect.NavigateToSplash)
+ }.onFailure { exception ->
+ Timber.e(t = exception)
+ sendEffect(SettingUiEffect.ShowMessage(SettingUiMessage.LOGOUT_FAILED))
+ }
+ }
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/LogoutDialog.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/LogoutDialog.kt
new file mode 100644
index 00000000..b675b246
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/LogoutDialog.kt
@@ -0,0 +1,46 @@
+package com.team.prezel.feature.setting.impl.setting.component
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.team.prezel.core.designsystem.component.feedback.dialog.PrezelDialog
+import com.team.prezel.core.designsystem.component.feedback.dialog.PrezelDialogScope.ActionType
+import com.team.prezel.core.designsystem.preview.BasicPreview
+import com.team.prezel.core.designsystem.theme.PrezelTheme
+import com.team.prezel.feature.setting.impl.R
+
+@Composable
+internal fun LogoutDialog(
+ onDismiss: () -> Unit,
+ onConfirmLogout: () -> Unit,
+) {
+ PrezelDialog(
+ title = stringResource(R.string.feature_setting_impl_logout_dialog_title),
+ onDismiss = onDismiss,
+ ) {
+ Action(label = stringResource(R.string.feature_setting_impl_cancel)) {
+ onDismiss()
+ }
+ Action(
+ label = stringResource(R.string.feature_setting_impl_logout),
+ type = ActionType.BAD,
+ ) {
+ onConfirmLogout()
+ }
+ }
+}
+
+@BasicPreview
+@Composable
+private fun LogoutDialogPreview() {
+ PrezelTheme {
+ Box(modifier = Modifier.fillMaxSize()) {
+ LogoutDialog(
+ onDismiss = {},
+ onConfirmLogout = {},
+ )
+ }
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingActionSection.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingActionSection.kt
new file mode 100644
index 00000000..647281c0
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingActionSection.kt
@@ -0,0 +1,55 @@
+package com.team.prezel.feature.setting.impl.setting.component
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.team.prezel.core.designsystem.component.actions.area.PrezelButtonArea
+import com.team.prezel.core.designsystem.component.actions.button.PrezelTextButton
+import com.team.prezel.core.designsystem.component.actions.button.config.ButtonHierarchy
+import com.team.prezel.core.designsystem.component.actions.button.config.ButtonSize
+import com.team.prezel.core.designsystem.component.actions.button.config.ButtonType
+import com.team.prezel.core.designsystem.preview.BasicPreview
+import com.team.prezel.core.designsystem.theme.PrezelTheme
+import com.team.prezel.feature.setting.impl.R
+
+@Composable
+internal fun SettingActionSection(
+ onClickWithdraw: () -> Unit,
+ onClickLogout: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ PrezelButtonArea(
+ modifier = modifier,
+ mainButton = { modifier ->
+ PrezelTextButton(
+ modifier = modifier,
+ text = stringResource(R.string.feature_setting_impl_withdraw),
+ type = ButtonType.GHOST,
+ size = ButtonSize.XSMALL,
+ hierarchy = ButtonHierarchy.SECONDARY,
+ onClick = onClickWithdraw,
+ )
+ },
+ subButton = { modifier ->
+ PrezelTextButton(
+ modifier = modifier,
+ text = stringResource(R.string.feature_setting_impl_logout),
+ type = ButtonType.FILLED,
+ size = ButtonSize.SMALL,
+ hierarchy = ButtonHierarchy.SECONDARY,
+ onClick = onClickLogout,
+ )
+ },
+ )
+}
+
+@BasicPreview
+@Composable
+private fun SettingActionSectionPreview() {
+ PrezelTheme {
+ SettingActionSection(
+ onClickWithdraw = {},
+ onClickLogout = {},
+ )
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingBodySection.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingBodySection.kt
new file mode 100644
index 00000000..64fad349
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingBodySection.kt
@@ -0,0 +1,168 @@
+package com.team.prezel.feature.setting.impl.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.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+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.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.team.prezel.core.designsystem.component.PrezelAvatar
+import com.team.prezel.core.designsystem.component.PrezelAvatarSize
+import com.team.prezel.core.designsystem.component.PrezelHorizontalDivider
+import com.team.prezel.core.designsystem.component.list.PrezelList
+import com.team.prezel.core.designsystem.icon.PrezelIcons
+import com.team.prezel.core.designsystem.preview.BasicPreview
+import com.team.prezel.core.designsystem.theme.PrezelTheme
+import com.team.prezel.core.ui.util.noRippleClickable
+import com.team.prezel.feature.setting.impl.R
+import com.team.prezel.feature.setting.impl.setting.contract.SettingUiState
+
+@Composable
+internal fun SettingBodySection(
+ uiState: SettingUiState,
+ onClickTermsOfService: () -> Unit,
+ onClickPrivacyPolicy: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(modifier = modifier) {
+ AccountSection(
+ profileImageUrl = uiState.profileImageUrl,
+ nickname = uiState.nickname,
+ email = uiState.email,
+ )
+
+ PrezelHorizontalDivider(color = PrezelTheme.colors.bgMedium)
+
+ PolicySection(
+ onClickTermsOfService = onClickTermsOfService,
+ onClickPrivacyPolicy = onClickPrivacyPolicy,
+ )
+ }
+}
+
+@Composable
+internal fun AccountSection(
+ profileImageUrl: String?,
+ nickname: String,
+ email: String,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = PrezelTheme.spacing.V20,
+ vertical = PrezelTheme.spacing.V16,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ PrezelAvatar(
+ imageUrl = profileImageUrl,
+ contentDescription = stringResource(R.string.feature_setting_impl_profile_image_content_description),
+ size = PrezelAvatarSize.SMALL,
+ )
+
+ Spacer(modifier = Modifier.width(PrezelTheme.spacing.V12))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = nickname,
+ color = PrezelTheme.colors.textLarge,
+ style = PrezelTheme.typography.body2Medium,
+ )
+
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = email,
+ color = PrezelTheme.colors.textRegular,
+ style = PrezelTheme.typography.body3Medium,
+ )
+ Spacer(modifier = Modifier.width(PrezelTheme.spacing.V4))
+ Icon(
+ painter = painterResource(PrezelIcons.Kakao),
+ contentDescription = stringResource(R.string.feature_setting_impl_kakao_account_content_description),
+ tint = PrezelTheme.colors.solidBlack,
+ modifier = Modifier
+ .size(16.dp)
+ .clip(PrezelTheme.shapes.V1000)
+ .background(Color(0xFFFEE500))
+ .padding(3.dp),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+internal fun PolicySection(
+ onClickTermsOfService: () -> Unit,
+ onClickPrivacyPolicy: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.padding(
+ horizontal = PrezelTheme.spacing.V20,
+ vertical = PrezelTheme.spacing.V28,
+ ),
+ verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V20),
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides PrezelTheme.colors.textLarge,
+ ) {
+ val trailingIcon = @Composable {
+ Icon(
+ painter = painterResource(PrezelIcons.ChevronRight),
+ contentDescription = null,
+ tint = PrezelTheme.colors.iconRegular,
+ )
+ }
+
+ PrezelList(
+ title = stringResource(R.string.feature_setting_impl_terms_of_service),
+ nested = true,
+ trailingContent = trailingIcon,
+ modifier = Modifier.noRippleClickable(
+ onClick = onClickTermsOfService,
+ ),
+ )
+ PrezelList(
+ title = stringResource(R.string.feature_setting_impl_privacy_policy),
+ nested = true,
+ trailingContent = trailingIcon,
+ modifier = Modifier.noRippleClickable(
+ onClick = onClickPrivacyPolicy,
+ ),
+ )
+ }
+ }
+}
+
+@BasicPreview
+@Composable
+private fun SettingBodySectionPreview() {
+ PrezelTheme {
+ SettingBodySection(
+ uiState = SettingUiState(
+ nickname = "발표잘하고싶어요",
+ email = "email@email.com",
+ ),
+ onClickTermsOfService = {},
+ onClickPrivacyPolicy = {},
+ )
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingTopAppBar.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingTopAppBar.kt
new file mode 100644
index 00000000..47ce2003
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingTopAppBar.kt
@@ -0,0 +1,45 @@
+package com.team.prezel.feature.setting.impl.setting.component
+
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import com.team.prezel.core.designsystem.component.PrezelTopAppBar
+import com.team.prezel.core.designsystem.icon.PrezelIcons
+import com.team.prezel.core.designsystem.preview.BasicPreview
+import com.team.prezel.core.designsystem.theme.PrezelTheme
+import com.team.prezel.feature.setting.impl.R
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun SettingTopAppBar(
+ onBack: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ PrezelTopAppBar(
+ modifier = modifier,
+ title = {
+ Text(text = stringResource(R.string.feature_setting_impl_title))
+ },
+ leadingIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ painter = painterResource(PrezelIcons.ArrowLeft),
+ contentDescription = stringResource(R.string.feature_setting_impl_back_icon_content_description),
+ )
+ }
+ },
+ )
+}
+
+@BasicPreview
+@Composable
+private fun SettingTopAppBarPreview() {
+ PrezelTheme {
+ SettingTopAppBar(onBack = {})
+ }
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/contract/SettingUiEffect.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/contract/SettingUiEffect.kt
new file mode 100644
index 00000000..de89c4b2
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/contract/SettingUiEffect.kt
@@ -0,0 +1,12 @@
+package com.team.prezel.feature.setting.impl.setting.contract
+
+import com.team.prezel.core.ui.base.UiEffect
+import com.team.prezel.feature.setting.impl.setting.model.SettingUiMessage
+
+internal sealed interface SettingUiEffect : UiEffect {
+ data object NavigateToSplash : SettingUiEffect
+
+ data class ShowMessage(
+ val message: SettingUiMessage,
+ ) : SettingUiEffect
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/contract/SettingUiIntent.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/contract/SettingUiIntent.kt
new file mode 100644
index 00000000..5722f31e
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/contract/SettingUiIntent.kt
@@ -0,0 +1,9 @@
+package com.team.prezel.feature.setting.impl.setting.contract
+
+import com.team.prezel.core.ui.base.UiIntent
+
+internal sealed interface SettingUiIntent : UiIntent {
+ data object FetchData : SettingUiIntent
+
+ data object ClickLogout : SettingUiIntent
+}
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/contract/SettingUiState.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/contract/SettingUiState.kt
new file mode 100644
index 00000000..db19cca0
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/contract/SettingUiState.kt
@@ -0,0 +1,12 @@
+package com.team.prezel.feature.setting.impl.setting.contract
+
+import androidx.compose.runtime.Immutable
+import com.team.prezel.core.ui.base.UiState
+
+@Immutable
+internal data class SettingUiState(
+ val isLoading: Boolean = false,
+ val profileImageUrl: String? = null,
+ val nickname: String = "",
+ val email: String = "",
+) : UiState
diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/model/SettingUiMessage.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/model/SettingUiMessage.kt
new file mode 100644
index 00000000..243f86eb
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/model/SettingUiMessage.kt
@@ -0,0 +1,6 @@
+package com.team.prezel.feature.setting.impl.setting.model
+
+enum class SettingUiMessage {
+ FETCH_USER_INFO_FAILED,
+ LOGOUT_FAILED,
+}
diff --git a/Prezel/feature/setting/impl/src/main/res/values/strings.xml b/Prezel/feature/setting/impl/src/main/res/values/strings.xml
new file mode 100644
index 00000000..113445dd
--- /dev/null
+++ b/Prezel/feature/setting/impl/src/main/res/values/strings.xml
@@ -0,0 +1,46 @@
+
+
+ 설정
+ 뒤로가기
+ 닫기
+ 프로필 이미지
+ 카카오 계정
+ 유저 정보 조회에 실패했어요.
+ 로그아웃되었어요.
+ 로그아웃에 실패했어요. 다시 시도해 주세요.
+ 로그아웃 하시겠어요?
+ 취소
+ 로그아웃
+ 이용약관
+ 개인정보 정책
+ 탈퇴하기
+
+
+ 회원을 탈퇴하시면
+ 추가한 발표, 발표 연습 기록, 획득한 뱃지 등을\n포함한 이용 내역 및 개인정보가 <highlight><bold>영구 삭제</bold></highlight>되며,\n<bold>다시 복구할 수 없어요.</bold>
+ 회원 탈퇴 시 즉시 탈퇴 처리되며, <highlight>이후 서비스 이용이 불가합니다.</highlight> 탈퇴 후에는 동일 계정으로 재가입하더라도 기존 정보는 복구되지 않습니다.
+ 회원 탈퇴 시 아래 정보의 복구가 불가능합니다. <highlight>연습 녹음 기록, 음성 분석 결과 및 리포트, 발표 대본 및 연습 히스토리 등의 데이터가 삭제</highlight>되며, 탈퇴 후에는 삭제된 데이터에 대한 복구 요청이 불가합니다.
+ 탈퇴 시 진행 중인 발표 연습, 분석 요청은 모두 종료됩니다. 미완료된 분석 결과는 제공되지 않습니다.
+ 탈퇴 후 동일한 카카오 계정으로 재가입은 가능하나, <highlight>이전 이용 기록 및 연습 데이터는 새로 생성되지 않습니다.</highlight>
+ 탈퇴 시 주의사항을 모두 확인했습니다.
+ 다음
+
+
+ 탈퇴하시는 이유가 궁금해요
+ 더 나은 서비스를 만들기 위한 참고 자료로만 사용돼요.
+ 자주 이용하지 않아요
+ 더 이상 필요하지 않아요
+ 사용법이 어렵거나 복잡해요
+ 분석 결과가 정확하지 않다고 느꼈어요
+ 오류가 많아요
+ 기타
+ (선택) 탈퇴 사유를 작성해주세요
+ 탈퇴하기
+
+
+ 정말 탈퇴하시겠어요?
+ 취소
+ 탈퇴
+ 회원탈퇴가 완료되었어요.
+ 회원탈퇴에 실패했어요. 다시 시도해 주세요.
+
diff --git a/Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/navigation/SplashEntryBuilder.kt b/Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/navigation/SplashEntryBuilder.kt
index 91f04879..7d552aa2 100644
--- a/Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/navigation/SplashEntryBuilder.kt
+++ b/Prezel/feature/splash/impl/src/main/java/com/team/prezel/feature/splash/impl/navigation/SplashEntryBuilder.kt
@@ -32,7 +32,7 @@ internal fun EntryProviderScope.featureSplashEntryBuilder() {
},
navigateToTerms = {
navigator.replaceRoot(LoginNavKey)
- navigator.navigate(TermsNavKey)
+ navigator.navigate(TermsNavKey.List)
},
navigateToCreateProfile = {
navigator.replaceRoot(LoginNavKey)
diff --git a/Prezel/feature/terms/api/src/main/java/com/team/prezel/feature/terms/api/TermsDocumentType.kt b/Prezel/feature/terms/api/src/main/java/com/team/prezel/feature/terms/api/TermsDocumentType.kt
new file mode 100644
index 00000000..48c87dd9
--- /dev/null
+++ b/Prezel/feature/terms/api/src/main/java/com/team/prezel/feature/terms/api/TermsDocumentType.kt
@@ -0,0 +1,9 @@
+package com.team.prezel.feature.terms.api
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+enum class TermsDocumentType {
+ TERMS_OF_SERVICE,
+ PRIVACY_POLICY,
+}
diff --git a/Prezel/feature/terms/api/src/main/java/com/team/prezel/feature/terms/api/TermsNavKey.kt b/Prezel/feature/terms/api/src/main/java/com/team/prezel/feature/terms/api/TermsNavKey.kt
index c34c996f..fea30c17 100644
--- a/Prezel/feature/terms/api/src/main/java/com/team/prezel/feature/terms/api/TermsNavKey.kt
+++ b/Prezel/feature/terms/api/src/main/java/com/team/prezel/feature/terms/api/TermsNavKey.kt
@@ -4,4 +4,12 @@ import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
@Serializable
-data object TermsNavKey : NavKey
+sealed interface TermsNavKey : NavKey {
+ @Serializable
+ data object List : TermsNavKey
+
+ @Serializable
+ data class Detail(
+ val document: TermsDocumentType,
+ ) : TermsNavKey
+}
diff --git a/Prezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/TermsScreen.kt b/Prezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/TermsScreen.kt
index 2a7bfe49..5b3fe132 100644
--- a/Prezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/TermsScreen.kt
+++ b/Prezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/TermsScreen.kt
@@ -36,14 +36,16 @@ import com.team.prezel.core.designsystem.component.PrezelDividerType
import com.team.prezel.core.designsystem.component.PrezelHorizontalDivider
import com.team.prezel.core.designsystem.component.PrezelTopAppBar
import com.team.prezel.core.designsystem.component.actions.area.PrezelButtonArea
+import com.team.prezel.core.designsystem.component.actions.button.PrezelButton
import com.team.prezel.core.designsystem.component.actions.button.PrezelHyperlinkButton
+import com.team.prezel.core.designsystem.component.actions.button.config.ButtonHierarchy
+import com.team.prezel.core.designsystem.component.actions.button.config.ButtonType
import com.team.prezel.core.designsystem.component.feedback.snackbar.showPrezelSnackbar
import com.team.prezel.core.designsystem.component.list.PrezelList
import com.team.prezel.core.designsystem.component.list.PrezelListSize
import com.team.prezel.core.designsystem.icon.PrezelIcons
import com.team.prezel.core.designsystem.theme.PrezelTheme
import com.team.prezel.core.ui.state.LocalSnackbarHostState
-import com.team.prezel.feature.terms.impl.component.TermsDetailModal
import com.team.prezel.feature.terms.impl.contract.TermsUiEffect
import com.team.prezel.feature.terms.impl.contract.TermsUiIntent
import com.team.prezel.feature.terms.impl.contract.TermsUiState
@@ -52,6 +54,8 @@ import com.team.prezel.feature.terms.impl.model.TermsUiMessage
@Composable
internal fun TermsScreen(
navigateBack: () -> Unit,
+ navigateToTermsOfServiceDetail: () -> Unit,
+ navigateToPrivacyPolicyDetail: () -> Unit,
navigateToProfile: () -> Unit,
modifier: Modifier = Modifier,
viewModel: TermsViewModel = hiltViewModel(),
@@ -59,7 +63,6 @@ internal fun TermsScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val resources = LocalResources.current
val snackbarHostState = LocalSnackbarHostState.current
- var activeDetailUrl by remember { mutableStateOf(null) }
LaunchedEffect(Unit) {
viewModel.uiEffect.collect { effect ->
@@ -79,13 +82,6 @@ internal fun TermsScreen(
}
}
- activeDetailUrl?.let { url ->
- TermsDetailModal(
- url = url,
- onDismiss = { activeDetailUrl = null },
- )
- }
-
TermsScreenScreen(
uiState = uiState,
onBack = navigateBack,
@@ -93,8 +89,8 @@ internal fun TermsScreen(
onToggleTermsOfService = { viewModel.onIntent(TermsUiIntent.ToggleTermsOfService) },
onTogglePrivacyPolicy = { viewModel.onIntent(TermsUiIntent.TogglePrivacyPolicy) },
onToggleMarketingConsent = { viewModel.onIntent(TermsUiIntent.ToggleMarketingConsent) },
- onClickTermsOfServiceDetail = { activeDetailUrl = BuildConfig.TERMS_OF_SERVICE_URL },
- onClickPrivacyPolicyDetail = { activeDetailUrl = BuildConfig.PRIVACY_POLICY_URL },
+ onClickTermsOfServiceDetail = navigateToTermsOfServiceDetail,
+ onClickPrivacyPolicyDetail = navigateToPrivacyPolicyDetail,
onContinue = { viewModel.onIntent(TermsUiIntent.ClickContinue) },
modifier = modifier,
)
@@ -114,8 +110,6 @@ private fun TermsScreenScreen(
onContinue: () -> Unit,
modifier: Modifier = Modifier,
) {
- val resources = LocalResources.current
-
Column(modifier = modifier.fillMaxSize()) {
TermsScreenTopAppBar(onBack = onBack)
@@ -130,13 +124,18 @@ private fun TermsScreenScreen(
onToggleMarketingConsent = onToggleMarketingConsent,
)
- PrezelButtonArea {
- MainButton(
- label = resources.getString(R.string.feature_terms_impl_terms_continue_button_text),
- enabled = uiState.isRequiredChecked && !uiState.isLoading,
- onClick = onContinue,
- )
- }
+ PrezelButtonArea(
+ mainButton = { modifier ->
+ PrezelButton(
+ modifier = modifier,
+ text = stringResource(R.string.feature_terms_impl_terms_continue_button_text),
+ onClick = onContinue,
+ enabled = uiState.isRequiredChecked && !uiState.isLoading,
+ type = ButtonType.FILLED,
+ hierarchy = ButtonHierarchy.PRIMARY,
+ )
+ },
+ )
}
}
diff --git a/Prezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/navigation/TermsEntryBuilder.kt b/Prezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/navigation/TermsEntryBuilder.kt
index ebb66d09..5bf6efa7 100644
--- a/Prezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/navigation/TermsEntryBuilder.kt
+++ b/Prezel/feature/terms/impl/src/main/java/com/team/prezel/feature/terms/impl/navigation/TermsEntryBuilder.kt
@@ -4,8 +4,11 @@ import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import com.team.prezel.core.navigation.LocalNavigator
import com.team.prezel.feature.profile.api.ProfileNavKey
+import com.team.prezel.feature.terms.api.TermsDocumentType
import com.team.prezel.feature.terms.api.TermsNavKey
+import com.team.prezel.feature.terms.impl.BuildConfig
import com.team.prezel.feature.terms.impl.TermsScreen
+import com.team.prezel.feature.terms.impl.component.TermsDetailModal
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -13,20 +16,41 @@ import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.multibindings.IntoSet
internal fun EntryProviderScope.featureTermsEntryBuilder() {
- entry {
+ entry {
val navigator = LocalNavigator.current
TermsScreen(
navigateBack = {
navigator.goBack()
},
+ navigateToTermsOfServiceDetail = {
+ navigator.navigate(TermsNavKey.Detail(TermsDocumentType.TERMS_OF_SERVICE))
+ },
+ navigateToPrivacyPolicyDetail = {
+ navigator.navigate(TermsNavKey.Detail(TermsDocumentType.PRIVACY_POLICY))
+ },
navigateToProfile = {
navigator.navigate(ProfileNavKey.Create)
},
)
}
+
+ entry { key ->
+ val navigator = LocalNavigator.current
+
+ TermsDetailModal(
+ url = key.document.toUrl(),
+ onDismiss = { navigator.goBack() },
+ )
+ }
}
+private fun TermsDocumentType.toUrl(): String =
+ when (this) {
+ TermsDocumentType.TERMS_OF_SERVICE -> BuildConfig.TERMS_OF_SERVICE_URL
+ TermsDocumentType.PRIVACY_POLICY -> BuildConfig.PRIVACY_POLICY_URL
+ }
+
@Module
@InstallIn(ActivityRetainedComponent::class)
object FeatureTermsModule {
diff --git a/Prezel/settings.gradle.kts b/Prezel/settings.gradle.kts
index 6e39fb67..825400ea 100644
--- a/Prezel/settings.gradle.kts
+++ b/Prezel/settings.gradle.kts
@@ -55,6 +55,8 @@ includeAuto(
":feature:history:impl",
":feature:my:api",
":feature:my:impl",
+ ":feature:setting:api",
+ ":feature:setting:impl",
":feature:profile:api",
":feature:profile:impl",
":feature:login:api",