From 98b32bb8b42ef6b291d9e8a211c9fb90e1f60ab0 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Wed, 6 May 2026 01:45:48 +0900 Subject: [PATCH 01/15] =?UTF-8?q?refactor:=20PrezelDialog=20=EB=82=B4=20de?= =?UTF-8?q?scription=20=ED=95=84=EB=93=9C=20=EC=84=A0=ED=83=9D=20=EC=82=AC?= =?UTF-8?q?=ED=95=AD=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **refactor: `PrezelDialog`의 설명(description) 매개변수 Nullable 처리** * `description` 타입을 `String`에서 `String?`로 변경하고 기본값을 `null`로 지정하여, 설명 문구가 없는 다이얼로그도 구성할 수 있도록 개선했습니다. * `DialogContent` 내부에서 `description`이 존재할 때만 `Text` 컴포넌트가 렌더링되도록 로직을 수정했습니다. --- .../component/feedback/dialog/PrezelDialog.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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) + } } } From 72eeebf993194816b9bdfe06bb4a3b2feacedbdb Mon Sep 17 00:00:00 2001 From: moondev03 Date: Wed, 6 May 2026 01:47:03 +0900 Subject: [PATCH 02/15] =?UTF-8?q?refactor:=20`PrezelButtonArea`=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20DSL=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **refactor: `PrezelButtonArea` 인터페이스 및 내부 로직 변경** * 기존의 `ButtonAreaScope`를 통한 DSL 방식의 버튼 배치 로직을 제거했습니다. * 버튼 구성을 명시적인 `mainButton` 및 `subButton` Composable 람다 매개변수 방식으로 변경하여 제어권을 강화했습니다. * `DefaultButtonAreaScope` 등 내부 유효성 검사 로직을 제거하여 컴포넌트 구조를 단순화했습니다. * **refactor: 각 Feature 모듈 내 버튼 영역 구현부 업데이트** * `TermsScreen`, `ProfileScreen`, `LoginScreen`, `PrezelDatePicker` 등 프로젝트 전반에서 변경된 `PrezelButtonArea` 형식을 적용했습니다. * 기존의 `MainButton`, `SubButton`, `CustomButton` 호출 방식을 `PrezelButton` 직접 주입 방식으로 전환했습니다. * 버튼 생성 시 `ButtonType.FILLED`, `ButtonHierarchy.PRIMARY` 등 명시적인 스타일 설정을 적용하여 디자인 시스템 일관성을 높였습니다. * **style: 불필요한 지역 변수 및 리소스 참조 정리** * `ProfileScreen` 및 `PrezelDatePicker` 등에서 버튼 라벨을 위해 사용하던 불필요한 지역 변수를 제거하고 `stringResource`를 직접 사용하도록 수정했습니다. --- .../actions/area/PrezelButtonArea.kt | 52 ++++++++++--------- .../component/datepicker/PrezelDatePicker.kt | 23 ++++---- .../prezel/feature/login/impl/LoginScreen.kt | 40 +++++++------- .../feature/profile/impl/ProfileScreen.kt | 22 +++++--- .../prezel/feature/terms/impl/TermsScreen.kt | 19 ++++--- 5 files changed, 89 insertions(+), 67 deletions(-) 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/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/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/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..ad653413 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 @@ -130,13 +130,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, + ) + }, + ) } } From a5754a085e25bc33154314faa5fb8eb536af4d13 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Wed, 6 May 2026 01:47:38 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20UI=20=EC=9C=A0=ED=8B=B8=EB=A6=AC?= =?UTF-8?q?=ED=8B=B0=20`noRippleClickable`=20=ED=99=95=EC=9E=A5=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **feat: `Modifier.noRippleClickable` 확장 함수 추가** * 클릭 시 리플(Ripple) 효과가 발생하지 않는 `noRippleClickable` 유틸리티를 `core.ui.util` 패키지에 추가했습니다. * `indication = null`과 `MutableInteractionSource`를 사용하여 시각적 피드백 없이 클릭 이벤트만 처리할 수 있도록 구현했습니다. --- .../prezel/core/ui/util/NoRippleClickable.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Prezel/core/ui/src/main/java/com/team/prezel/core/ui/util/NoRippleClickable.kt 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, + ) + } From 1387d7f3d0d8dae2b6e1ab84add3af7620581a97 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Wed, 6 May 2026 02:28:52 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=82=B4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **refactor: `TermsNavKey` 구조 개선 및 상세 화면 경로 추가** * `TermsNavKey`를 `data object`에서 `sealed interface`로 변경하고, 약관 목록(`List`)과 상세 화면(`Detail`)으로 구분했습니다. * 상세 화면 이동 시 약관 타입을 전달하기 위한 `TermsDocumentType` Enum을 정의했습니다. * **refactor: 내비게이션 엔트리 구성 및 상세 모달 분리** * `TermsEntryBuilder`에서 `TermsNavKey.Detail`에 대한 엔트리를 추가하여 `TermsDetailModal`을 독립적인 내비게이션 목적지로 처리하도록 변경했습니다. * `TermsDocumentType`에 따라 `BuildConfig`에 정의된 URL을 매핑하는 로직을 추가했습니다. * **refactor: `TermsScreen` 내 상세 표시 로직 제거** * `TermsScreen` 내부에서 `activeDetailUrl` 상태로 관리하던 모달 표시 로직을 제거했습니다. * 상세 화면 이동을 위한 콜백(`navigateToTermsOfServiceDetail`, `navigateToPrivacyPolicyDetail`)을 추가하여 내비게이션 처리를 위임했습니다. * **fix: 외부 모듈의 약관 화면 참조 수정** * `login` 및 `splash` 기능 모듈에서 기존 `TermsNavKey` 참조를 `TermsNavKey.List`로 업데이트했습니다. --- .../impl/navigation/LoginEntryBuilder.kt | 2 +- .../impl/navigation/SplashEntryBuilder.kt | 2 +- .../feature/terms/api/TermsDocumentType.kt | 9 +++++++ .../prezel/feature/terms/api/TermsNavKey.kt | 10 ++++++- .../prezel/feature/terms/impl/TermsScreen.kt | 20 +++++--------- .../impl/navigation/TermsEntryBuilder.kt | 26 ++++++++++++++++++- 6 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 Prezel/feature/terms/api/src/main/java/com/team/prezel/feature/terms/api/TermsDocumentType.kt 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/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 ad653413..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) 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 { From 726bde6a84e9a8e231305be78ad243db7cf4b28f Mon Sep 17 00:00:00 2001 From: moondev03 Date: Wed, 6 May 2026 02:30:40 +0900 Subject: [PATCH 05/15] =?UTF-8?q?feat:=20Setting=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EB=84=A4?= =?UTF-8?q?=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **feat: Setting feature 모듈(`api`, `impl`) 추가** * 설정 기능을 위한 `:feature:setting:api` 및 `:feature:setting:impl` 모듈을 신규 생성하고 프로젝트 구성(`settings.gradle.kts`)에 추가했습니다. * `app` 모듈의 의존성에 설정 관련 모듈을 포함했습니다. * `feature:setting:impl` 모듈에 `coreAuth`, `coreDomain`, `coreModel` 및 관련 feature api 의존성을 설정했습니다. * **feat: 설정 관련 Navigation Key 정의** * 설정 메인 화면 이동을 위한 `SettingNavKey`를 추가했습니다. * 회원 탈퇴 화면 이동을 위한 `DeleteAccountNavKey`를 추가했습니다. * **feat: My feature 모듈 내 설정 화면 이동 연결** * `MyEntryBuilder`에서 주석 처리되어 있던 `navigateToSetting` 로직을 `SettingNavKey`를 사용하여 실제 네비게이션이 동작하도록 구현했습니다. * `feature:my:impl` 모듈에서 `SettingNavKey` 참조를 위해 `feature:setting:api` 의존성을 추가했습니다. --- Prezel/app/build.gradle.kts | 2 ++ Prezel/feature/my/impl/build.gradle.kts | 1 + .../my/impl/navigation/MyEntryBuilder.kt | 3 ++- Prezel/feature/setting/api/build.gradle.kts | 7 +++++++ Prezel/feature/setting/api/consumer-rules.pro | 1 + Prezel/feature/setting/api/proguard-rules.pro | 21 +++++++++++++++++++ .../setting/api/DeleteAccountNavKey.kt | 7 +++++++ .../feature/setting/api/SettingNavKey.kt | 7 +++++++ Prezel/feature/setting/impl/build.gradle.kts | 17 +++++++++++++++ .../feature/setting/impl/consumer-rules.pro | 1 + .../feature/setting/impl/proguard-rules.pro | 21 +++++++++++++++++++ Prezel/settings.gradle.kts | 2 ++ 12 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 Prezel/feature/setting/api/build.gradle.kts create mode 100644 Prezel/feature/setting/api/consumer-rules.pro create mode 100644 Prezel/feature/setting/api/proguard-rules.pro create mode 100644 Prezel/feature/setting/api/src/main/java/com/team/prezel/feature/setting/api/DeleteAccountNavKey.kt create mode 100644 Prezel/feature/setting/api/src/main/java/com/team/prezel/feature/setting/api/SettingNavKey.kt create mode 100644 Prezel/feature/setting/impl/build.gradle.kts create mode 100644 Prezel/feature/setting/impl/consumer-rules.pro create mode 100644 Prezel/feature/setting/impl/proguard-rules.pro 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/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/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/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..1cc7ccd6 --- /dev/null +++ b/Prezel/feature/setting/impl/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(libs.plugins.prezel.android.feature.impl) +} + +android { + namespace = "com.team.prezel.feature.setting.impl" +} + +dependencies { + implementation(projects.coreAuth) + implementation(projects.coreDomain) + implementation(projects.coreModel) + + implementation(projects.featureSettingApi) + implementation(projects.featureSplashApi) + implementation(projects.featureTermsApi) +} 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/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", From ffac9872dfdc4033b2937c013c45f69f98b9f8cc Mon Sep 17 00:00:00 2001 From: moondev03 Date: Wed, 6 May 2026 02:47:00 +0900 Subject: [PATCH 06/15] =?UTF-8?q?refactor:=20MyTopAppBar=20=EB=82=B4=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B2=84=ED=8A=BC=EC=9D=84=20Material=203?= =?UTF-8?q?=20=ED=91=9C=EC=A4=80=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **refactor: `MyTopAppBar`의 아이콘 버튼 구현 방식 변경** * 디자인 시스템의 커스텀 컴포넌트인 `PrezelIconButton` 대신 Compose Material 3의 표준 `IconButton` 및 `Icon`을 사용하도록 수정했습니다. * `ButtonType.GHOST`, `ButtonHierarchy.SECONDARY` 등 기존의 커스텀 속성 설정을 제거하고 표준 접근 방식으로 단순화했습니다. * 불필요해진 디자인 시스템 버튼 관련 import 문을 정리했습니다. --- .../feature/my/impl/component/MyTopAppBar.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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, + ) + } }, ) } From 48ba5ad94e3bce7069878392bd46bd25629c5bd9 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Wed, 6 May 2026 23:50:36 +0900 Subject: [PATCH 07/15] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=ED=83=88=ED=87=B4=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **feat: 설정(Setting) 화면 및 기능 구현** * 사용자 정보(닉네임, 이메일, 프로필 이미지) 조회 및 표시 기능을 구현했습니다. * 로그아웃 기능을 구현하고 확인 다이얼로그를 추가했습니다. * 이용약관 및 개인정보 처리방침 화면으로의 네비게이션 경로를 연결했습니다. * **feat: 회원 탈퇴(Delete Account) 프로세스 구현** * 유의사항 확인(`NOTICE`)과 탈퇴 사유 선택(`REASON`)으로 구성된 2단계 탈퇴 플로우를 구현했습니다. * `WithdrawUseCase`를 연동하여 사유별 탈퇴 처리 로직을 구현했습니다. * 기타 사유 선택 시 직접 입력할 수 있는 텍스트 필드를 제공합니다. * 탈퇴 전 최종 확인을 위한 다이얼로그를 추가했습니다. * **feat: MVI 기반 아키텍처 및 네비게이션 설정** * `SettingViewModel` 및 `DeleteAccountViewModel`을 추가하고 `UiState`, `UiIntent`, `UiEffect`를 정의했습니다. * `SettingEntryBuilder`를 통해 `SettingNavKey`와 `DeleteAccountNavKey`에 대한 네비게이션 그래프를 구성했습니다. * Hilt를 사용하여 UseCase 및 ViewModel 의존성을 주입했습니다. * **style: UI 컴포넌트 및 리소스 정의** * 설정 및 탈퇴 화면에서 공통으로 사용하는 `TopAppBar`, `ActionSection`, `BodySection` 등 커스텀 컴포넌트를 구현했습니다. * 기능 구현에 필요한 다국어 문자열 리소스를 `strings.xml`에 추가했습니다. --- .../impl/delete/DeleteAccountScreen.kt | 153 ++++++++++++ .../impl/delete/DeleteAccountViewModel.kt | 91 +++++++ .../component/DeleteAccountActionSection.kt | 79 +++++++ .../component/DeleteAccountConfirmDialog.kt | 44 ++++ .../component/DeleteAccountContentSection.kt | 67 ++++++ .../component/DeleteAccountNoticeStep.kt | 222 ++++++++++++++++++ .../component/DeleteAccountReasonStep.kt | 158 +++++++++++++ .../component/DeleteAccountTopAppBar.kt | 41 ++++ .../delete/contract/DeleteAccountUiEffect.kt | 12 + .../delete/contract/DeleteAccountUiIntent.kt | 26 ++ .../delete/contract/DeleteAccountUiState.kt | 28 +++ .../delete/model/DeleteAccountReasonOption.kt | 10 + .../impl/delete/model/DeleteAccountStep.kt | 6 + .../delete/model/DeleteAccountUiMessage.kt | 5 + .../impl/navigation/SettingEntryBuilder.kt | 55 +++++ .../setting/impl/setting/SettingScreen.kt | 138 +++++++++++ .../setting/impl/setting/SettingViewModel.kt | 60 +++++ .../impl/setting/component/LogoutDialog.kt | 46 ++++ .../setting/component/SettingActionSection.kt | 55 +++++ .../setting/component/SettingBodySection.kt | 190 +++++++++++++++ .../setting/component/SettingTopAppBar.kt | 45 ++++ .../impl/setting/contract/SettingUiEffect.kt | 12 + .../impl/setting/contract/SettingUiIntent.kt | 9 + .../impl/setting/contract/SettingUiState.kt | 12 + .../impl/setting/model/SettingUiMessage.kt | 6 + .../impl/src/main/res/values/strings.xml | 52 ++++ 26 files changed, 1622 insertions(+) create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/DeleteAccountScreen.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/DeleteAccountViewModel.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountActionSection.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountConfirmDialog.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountContentSection.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountNoticeStep.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountReasonStep.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountTopAppBar.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiEffect.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiIntent.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiState.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/model/DeleteAccountReasonOption.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/model/DeleteAccountStep.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/model/DeleteAccountUiMessage.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/navigation/SettingEntryBuilder.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/SettingScreen.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/SettingViewModel.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/LogoutDialog.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingActionSection.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingBodySection.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingTopAppBar.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/contract/SettingUiEffect.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/contract/SettingUiIntent.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/contract/SettingUiState.kt create mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/model/SettingUiMessage.kt create mode 100644 Prezel/feature/setting/impl/src/main/res/values/strings.xml 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..21c17f6e --- /dev/null +++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/DeleteAccountScreen.kt @@ -0,0 +1,153 @@ +package com.team.prezel.feature.setting.impl.delete + +import androidx.activity.compose.BackHandler +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.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.DeleteAccountContentSection +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() + is DeleteAccountUiEffect.ShowMessage -> { + val message = when (effect.message) { + DeleteAccountUiMessage.WITHDRAW_FAILED -> R.string.feature_setting_impl_delete_account_withdraw_failed + } + snackbarHostState.showPrezelSnackbar(message = resources.getString(message)) + } + } + } + } + + 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, + ) +} + +@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) + + DeleteAccountContentSection( + uiState = uiState, + onToggleNoticeChecked = onToggleNoticeChecked, + onSelectReason = onSelectReason, + onOtherReasonChanged = onOtherReasonChanged, + modifier = Modifier.weight(1f), + ) + + DeleteAccountActionSection( + step = uiState.step, + enabled = uiState.isPrimaryActionEnabled, + onClickNext = onClickNext, + onClickWithdraw = onClickWithdraw, + ) + } +} + +@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.OTHER, + ), + 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..2cde4f3a --- /dev/null +++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/DeleteAccountViewModel.kt @@ -0,0 +1,91 @@ +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.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 = com.team.prezel.feature.setting.impl.delete.model.DeleteAccountStep.REASON) } + } + } + + private fun selectReason(reason: DeleteAccountReasonOption) { + updateState { + copy( + selectedReason = reason, + otherReasonText = if (reason == DeleteAccountReasonOption.OTHER) 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.NOT_USED_OFTEN -> WithdrawReason.NotUsedOften + DeleteAccountReasonOption.NO_LONGER_NEEDED -> WithdrawReason.NoLongerNeeded + DeleteAccountReasonOption.TOO_DIFFICULT_OR_COMPLEX -> WithdrawReason.TooDifficultOrComplex + DeleteAccountReasonOption.ANALYSIS_RESULT_INACCURATE -> WithdrawReason.AnalysisResultInaccurate + DeleteAccountReasonOption.TOO_MANY_ERRORS -> WithdrawReason.TooManyErrors + DeleteAccountReasonOption.OTHER -> WithdrawReason.Other(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..39d8bfb8 --- /dev/null +++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountActionSection.kt @@ -0,0 +1,79 @@ +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 = {}, + ) + } +} + +@BasicPreview +@Composable +private fun DeleteAccountActionSectionReasonPreview() { + PrezelTheme { + DeleteAccountActionSection( + step = DeleteAccountStep.REASON, + 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/DeleteAccountContentSection.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountContentSection.kt new file mode 100644 index 00000000..3b8c7bdb --- /dev/null +++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountContentSection.kt @@ -0,0 +1,67 @@ +package com.team.prezel.feature.setting.impl.delete.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.team.prezel.core.designsystem.preview.BasicPreview +import com.team.prezel.core.designsystem.theme.PrezelTheme +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 + +@Composable +internal fun DeleteAccountContentSection( + uiState: DeleteAccountUiState, + onToggleNoticeChecked: (Boolean) -> Unit, + onSelectReason: (DeleteAccountReasonOption) -> Unit, + onOtherReasonChanged: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier.fillMaxWidth()) { + 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 DeleteAccountContentSectionNoticePreview() { + PrezelTheme { + DeleteAccountContentSection( + uiState = DeleteAccountUiState( + step = DeleteAccountStep.NOTICE, + ), + onToggleNoticeChecked = {}, + onSelectReason = {}, + onOtherReasonChanged = {}, + ) + } +} + +@BasicPreview +@Composable +private fun DeleteAccountContentSectionReasonPreview() { + PrezelTheme { + DeleteAccountContentSection( + uiState = DeleteAccountUiState( + step = DeleteAccountStep.REASON, + selectedReason = DeleteAccountReasonOption.OTHER, + ), + onToggleNoticeChecked = {}, + onSelectReason = {}, + onOtherReasonChanged = {}, + ) + } +} 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..34623056 --- /dev/null +++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountNoticeStep.kt @@ -0,0 +1,222 @@ +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.dp +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 + +@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), + verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V16), + ) { + Column(verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V8)) { + Text( + text = stringResource(R.string.feature_setting_impl_delete_account_notice_title), + style = PrezelTheme.typography.title2Bold, + color = PrezelTheme.colors.textLarge, + ) + DeleteAccountNoticeDescription() + } + + DeleteAccountNoticeDetailBox() + + Row( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable { onCheckedChange(!isChecked) }, + verticalAlignment = Alignment.CenterVertically, + ) { + PrezelCheckbox( + checked = isChecked, + onCheckedChange = onCheckedChange, + extraTouchPadding = PaddingValues(end = 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 regularStyle = SpanStyle( + color = PrezelTheme.colors.textMedium, + fontSize = PrezelTheme.typography.body2Regular.fontSize, + fontWeight = PrezelTheme.typography.body2Regular.fontWeight, + letterSpacing = PrezelTheme.typography.body2Regular.letterSpacing, + ) + val highlightStyle = SpanStyle( + color = PrezelTheme.colors.feedbackBadRegular, + fontSize = PrezelTheme.typography.body2Bold.fontSize, + fontWeight = PrezelTheme.typography.body2Bold.fontWeight, + letterSpacing = PrezelTheme.typography.body2Bold.letterSpacing, + ) + + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(0.dp), + ) { + Text( + text = stringResource(R.string.feature_setting_impl_delete_account_notice_description_1), + style = PrezelTheme.typography.body2Regular, + color = PrezelTheme.colors.textMedium, + ) + Text( + text = buildAnnotatedString { + withStyle(regularStyle) { + append(stringResource(R.string.feature_setting_impl_delete_account_notice_description_2_prefix)) + append(" ") + } + withStyle(highlightStyle) { + append(stringResource(R.string.feature_setting_impl_delete_account_notice_description_2_highlight)) + } + withStyle(regularStyle) { + append(stringResource(R.string.feature_setting_impl_delete_account_notice_description_2_suffix)) + } + }, + style = PrezelTheme.typography.body2Regular, + ) + Text( + text = stringResource(R.string.feature_setting_impl_delete_account_notice_description_3), + style = PrezelTheme.typography.body2Bold, + 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( + prefix = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_1_prefix), + highlight = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_1_highlight), + suffix = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_1_suffix), + ) + DeleteAccountNoticeDetailItem( + prefix = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_2_prefix), + highlight = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_2_highlight), + suffix = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_2_suffix), + ) + DeleteAccountNoticeDetailItem( + text = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_3), + ) + DeleteAccountNoticeDetailItem( + prefix = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_4_prefix), + highlight = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_4_highlight), + ) + } +} + +@Composable +private fun DeleteAccountNoticeDetailItem( + modifier: Modifier = Modifier, + text: String? = null, + prefix: String = "", + highlight: String = "", + suffix: String = "", +) { + val regularStyle = SpanStyle( + color = PrezelTheme.colors.textMedium, + fontSize = PrezelTheme.typography.body3Regular.fontSize, + fontWeight = PrezelTheme.typography.body3Regular.fontWeight, + letterSpacing = PrezelTheme.typography.body3Regular.letterSpacing, + ) + val highlightStyle = SpanStyle( + color = PrezelTheme.colors.feedbackBadRegular, + fontSize = PrezelTheme.typography.body3Regular.fontSize, + fontWeight = PrezelTheme.typography.body2Bold.fontWeight, + letterSpacing = PrezelTheme.typography.body3Regular.letterSpacing, + ) + val detailText: AnnotatedString = buildAnnotatedString { + withBulletList(bullet = Bullet.Default.copy(padding = 0.5.em)) { + withBulletListItem { + if (text != null) { + append(text) + } else { + withStyle(regularStyle) { + append(prefix) + if (prefix.isNotEmpty()) append(" ") + } + withStyle(highlightStyle) { append(highlight) } + withStyle(regularStyle) { + if (suffix.isNotEmpty()) append(" ") + append(suffix) + } + } + } + } + } + + Text( + text = detailText, + modifier = modifier.fillMaxWidth(), + style = PrezelTheme.typography.body3Regular, + color = PrezelTheme.colors.textMedium, + ) +} + +@BasicPreview +@Composable +private fun DeleteAccountNoticeStepPreview() { + PrezelTheme { + DeleteAccountNoticeStep( + isChecked = false, + onCheckedChange = {}, + ) + } +} + +@BasicPreview +@Composable +private fun DeleteAccountNoticeStepCheckedPreview() { + PrezelTheme { + DeleteAccountNoticeStep( + isChecked = true, + 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..b8c20384 --- /dev/null +++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountReasonStep.kt @@ -0,0 +1,158 @@ +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 + +@Composable +internal fun DeleteAccountReasonStep( + selectedReason: DeleteAccountReasonOption?, + otherReasonText: String, + onSelectReason: (DeleteAccountReasonOption) -> Unit, + onOtherReasonChanged: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val reasonOptions = listOf( + DeleteAccountReasonOption.OTHER to stringResource(R.string.feature_setting_impl_delete_account_reason_other), + DeleteAccountReasonOption.NOT_USED_OFTEN to stringResource(R.string.feature_setting_impl_delete_account_reason_not_used_often), + DeleteAccountReasonOption.NO_LONGER_NEEDED to stringResource(R.string.feature_setting_impl_delete_account_reason_no_longer_needed), + DeleteAccountReasonOption.TOO_DIFFICULT_OR_COMPLEX to + stringResource(R.string.feature_setting_impl_delete_account_reason_too_difficult_or_complex), + DeleteAccountReasonOption.ANALYSIS_RESULT_INACCURATE to + stringResource(R.string.feature_setting_impl_delete_account_reason_analysis_result_inaccurate), + DeleteAccountReasonOption.TOO_MANY_ERRORS to stringResource(R.string.feature_setting_impl_delete_account_reason_too_many_errors), + ) + + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = PrezelTheme.spacing.V12) + .padding(top = PrezelTheme.spacing.V16, bottom = PrezelTheme.spacing.V24), + ) { + DeleteAccountReasonHeader() + + Spacer(modifier = Modifier.height(PrezelTheme.spacing.V32)) + + 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 DeleteAccountReasonHeader() { + Column(verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V8)) { + Text( + text = stringResource(R.string.feature_setting_impl_delete_account_reason_title), + style = PrezelTheme.typography.title2Bold, + color = PrezelTheme.colors.textLarge, + ) + Text( + text = stringResource(R.string.feature_setting_impl_delete_account_reason_description), + style = PrezelTheme.typography.body3Regular, + color = PrezelTheme.colors.textRegular, + ) + } +} + +@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( + onClick = { onSelectReason(reason) }, + ), + ) + + if (reason == DeleteAccountReasonOption.OTHER && selectedReason == DeleteAccountReasonOption.OTHER) { + 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.NOT_USED_OFTEN, + otherReasonText = "", + onSelectReason = {}, + onOtherReasonChanged = {}, + ) + } +} + +@BasicPreview +@Composable +private fun DeleteAccountReasonStepOtherPreview() { + PrezelTheme { + DeleteAccountReasonStep( + selectedReason = DeleteAccountReasonOption.OTHER, + 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..0b4561c1 --- /dev/null +++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/contract/DeleteAccountUiState.kt @@ -0,0 +1,28 @@ +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 isOtherReasonSelected: Boolean + get() = selectedReason == DeleteAccountReasonOption.OTHER + + val isNextEnabled: Boolean + get() = isNoticeChecked + + val isWithdrawEnabled: Boolean + get() = selectedReason != null && !isSubmitting + + val isPrimaryActionEnabled: Boolean + get() = 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..2ef98ccb --- /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 { + NOT_USED_OFTEN, + NO_LONGER_NEEDED, + TOO_DIFFICULT_OR_COMPLEX, + ANALYSIS_RESULT_INACCURATE, + TOO_MANY_ERRORS, + OTHER, +} 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..efe5b92a --- /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() + } + + is SettingUiEffect.ShowMessage -> { + val messageRes = when (effect.message) { + SettingUiMessage.FETCH_USER_INFO_FAILED -> R.string.feature_setting_impl_title + SettingUiMessage.LOGOUT_FAILED -> R.string.feature_setting_impl_logout_failed + } + + snackbarHostState.showPrezelSnackbar( + message = resource.getString(messageRes), + ) + } + } + } + } + + 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, + ) +} + +@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..7773a22c --- /dev/null +++ b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingBodySection.kt @@ -0,0 +1,190 @@ +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.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, + ) + } +} + +@BasicPreview +@Composable +private fun SettingBodySectionPreview() { + PrezelTheme { + SettingBodySection( + uiState = SettingUiState( + nickname = "발표잘하고싶어요", + email = "email@email.com", + ), + onClickTermsOfService = {}, + 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 = androidx.compose.ui.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 = androidx.compose.ui.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), + ) + } + } + } +} + +@BasicPreview +@Composable +private fun AccountSectionPreview() { + PrezelTheme { + AccountSection( + profileImageUrl = null, + nickname = "발표잘하고싶어요", + email = "email@email.com", + ) + } +} + +@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 PolicySectionPreview() { + PrezelTheme { + PolicySection( + 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..1c0cfce6 --- /dev/null +++ b/Prezel/feature/setting/impl/src/main/res/values/strings.xml @@ -0,0 +1,52 @@ + + + 설정 + 뒤로가기 + 닫기 + 프로필 이미지 + 카카오 계정 + 로그아웃에 실패했어요. 다시 시도해 주세요. + 로그아웃 하시겠어요? + 취소 + 로그아웃 + 이용약관 + 개인정보 정책 + 탈퇴하기 + + + 회원을 탈퇴하시면 + 추가한 발표, 발표 연습 기록, 획득한 뱃지 등을 + 포함한 이용 내역 및 개인정보가 + 영구 삭제 + 되며, + 다시 복구할 수 없어요. + 회원 탈퇴 시 즉시 탈퇴 처리되며, + 이후 서비스 이용이 불가합니다. + 탈퇴 후에는 동일 계정으로 재가입하더라도 기존 정보는 복구되지 않습니다. + 회원 탈퇴 시 아래 정보의 복구가 불가능합니다. + 연습 녹음 기록, 음성 분석 결과 및 리포트, 발표 대본 및 연습 히스토리 등의 데이터가 삭제 + 되며, 탈퇴 후에는 삭제된 데이터에 대한 복구 요청이 불가합니다. + 탈퇴 시 진행 중인 발표 연습, 분석 요청은 모두 종료됩니다. 미완료된 분석 결과는 제공되지 않습니다. + 탈퇴 후 동일한 카카오 계정으로 재가입은 가능하나, + 이전 이용 기록 및 연습 데이터는 새로 생성되지 않습니다. + 탈퇴 시 주의사항을 모두 확인했습니다. + 다음 + + + 탈퇴하시는 이유가 궁금해요 + 더 나은 서비스를 만들기 위한 참고 자료로만 사용돼요. + 자주 이용하지 않아요 + 더 이상 필요하지 않아요 + 사용법이 어렵거나 복잡해요 + 분석 결과가 정확하지 않다고 느꼈어요 + 오류가 많아요 + 기타 + (선택) 탈퇴 사유를 작성해주세요 + 탈퇴하기 + + + 정말 탈퇴하시겠어요? + 취소 + 탈퇴 + 회원탈퇴에 실패했어요. 다시 시도해 주세요. + From 68fd6c33d86f074c8a8b669356b722ef5b204232 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 7 May 2026 00:06:35 +0900 Subject: [PATCH 08/15] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=ED=83=88=ED=87=B4=20=EB=B0=8F=20=EC=84=A4=EC=A0=95=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20UI=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **refactor: 회원 탈퇴 화면 구성 요소 정리 및 통합** * `DeleteAccountContentSection.kt` 파일을 삭제하고, 해당 로직을 `DeleteAccountScreen.kt` 내의 `DeleteAccountContent` 프라이빗 컴포저블로 통합하여 구조를 단순화했습니다. * `DeleteAccountActionSection`, `DeleteAccountNoticeStep`, `DeleteAccountReasonStep` 등 여러 컴포넌트 파일에 분산되어 있던 중복 Preview 코드를 삭제했습니다. * **refactor: `DeleteAccountUiState` 모델 개선** * `isNextEnabled`, `isWithdrawEnabled`, `isPrimaryActionEnabled` 프로퍼티를 커스텀 게터(`get()`) 방식에서 일반 프로퍼티(`val`) 초기화 방식으로 변경했습니다. * 사용하지 않는 `isOtherReasonSelected` 프로퍼티를 제거했습니다. * **refactor: 설정(Setting) UI 컴포넌트 및 코드 정체성 개선** * `SettingBodySection.kt` 내의 `AccountSectionPreview`, `PolicySectionPreview` 등을 삭제하고 `SettingBodySectionPreview` 하나로 통합하여 관리하도록 수정했습니다. * `Alignment` 관련 불필요한 전체 패키지 경로 참조를 import 문으로 대체했습니다. * **style: 코드 가독성 및 import 최적화** * `DeleteAccountViewModel`에서 `DeleteAccountStep` 참조 시 사용된 불필요한 전체 패키지 경로를 정리했습니다. * `DeleteAccountScreen` 내 레이아웃 배치를 위해 `fillMaxWidth` 및 `weight` 수식어를 조정했습니다. --- .../impl/delete/DeleteAccountScreen.kt | 36 +++++++++- .../impl/delete/DeleteAccountViewModel.kt | 3 +- .../component/DeleteAccountActionSection.kt | 13 ---- .../component/DeleteAccountContentSection.kt | 67 ------------------- .../component/DeleteAccountNoticeStep.kt | 11 --- .../component/DeleteAccountReasonStep.kt | 13 ---- .../delete/contract/DeleteAccountUiState.kt | 12 +--- .../setting/component/SettingBodySection.kt | 40 +++-------- 8 files changed, 47 insertions(+), 148 deletions(-) delete mode 100644 Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountContentSection.kt 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 index 21c17f6e..85c95aa5 100644 --- 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 @@ -1,8 +1,10 @@ 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 @@ -17,7 +19,8 @@ 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.DeleteAccountContentSection +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 @@ -94,12 +97,14 @@ private fun DeleteAccountScreen( Column(modifier = modifier.fillMaxSize()) { DeleteAccountTopAppBar(onClickClose = onClickClose) - DeleteAccountContentSection( + DeleteAccountContent( uiState = uiState, onToggleNoticeChecked = onToggleNoticeChecked, onSelectReason = onSelectReason, onOtherReasonChanged = onOtherReasonChanged, - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .fillMaxWidth(), ) DeleteAccountActionSection( @@ -111,6 +116,31 @@ private fun DeleteAccountScreen( } } +@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() { 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 index 2cde4f3a..ef0e8a26 100644 --- 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 @@ -8,6 +8,7 @@ import com.team.prezel.feature.setting.impl.delete.contract.DeleteAccountUiEffec 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 @@ -32,7 +33,7 @@ internal class DeleteAccountViewModel @Inject constructor( private fun moveToReasonStep() { if (currentState.isNextEnabled) { - updateState { copy(step = com.team.prezel.feature.setting.impl.delete.model.DeleteAccountStep.REASON) } + updateState { copy(step = DeleteAccountStep.REASON) } } } 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 index 39d8bfb8..c97d416d 100644 --- 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 @@ -64,16 +64,3 @@ private fun DeleteAccountActionSectionNoticePreview() { ) } } - -@BasicPreview -@Composable -private fun DeleteAccountActionSectionReasonPreview() { - PrezelTheme { - DeleteAccountActionSection( - step = DeleteAccountStep.REASON, - enabled = true, - onClickNext = {}, - onClickWithdraw = {}, - ) - } -} diff --git a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountContentSection.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountContentSection.kt deleted file mode 100644 index 3b8c7bdb..00000000 --- a/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountContentSection.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.team.prezel.feature.setting.impl.delete.component - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.team.prezel.core.designsystem.preview.BasicPreview -import com.team.prezel.core.designsystem.theme.PrezelTheme -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 - -@Composable -internal fun DeleteAccountContentSection( - uiState: DeleteAccountUiState, - onToggleNoticeChecked: (Boolean) -> Unit, - onSelectReason: (DeleteAccountReasonOption) -> Unit, - onOtherReasonChanged: (String) -> Unit, - modifier: Modifier = Modifier, -) { - Box(modifier = modifier.fillMaxWidth()) { - 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 DeleteAccountContentSectionNoticePreview() { - PrezelTheme { - DeleteAccountContentSection( - uiState = DeleteAccountUiState( - step = DeleteAccountStep.NOTICE, - ), - onToggleNoticeChecked = {}, - onSelectReason = {}, - onOtherReasonChanged = {}, - ) - } -} - -@BasicPreview -@Composable -private fun DeleteAccountContentSectionReasonPreview() { - PrezelTheme { - DeleteAccountContentSection( - uiState = DeleteAccountUiState( - step = DeleteAccountStep.REASON, - selectedReason = DeleteAccountReasonOption.OTHER, - ), - onToggleNoticeChecked = {}, - onSelectReason = {}, - onOtherReasonChanged = {}, - ) - } -} 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 index 34623056..0ff3ef85 100644 --- 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 @@ -209,14 +209,3 @@ private fun DeleteAccountNoticeStepPreview() { ) } } - -@BasicPreview -@Composable -private fun DeleteAccountNoticeStepCheckedPreview() { - PrezelTheme { - DeleteAccountNoticeStep( - isChecked = true, - 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 index b8c20384..db95b966 100644 --- 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 @@ -143,16 +143,3 @@ private fun DeleteAccountReasonStepPreview() { ) } } - -@BasicPreview -@Composable -private fun DeleteAccountReasonStepOtherPreview() { - PrezelTheme { - DeleteAccountReasonStep( - selectedReason = DeleteAccountReasonOption.OTHER, - otherReasonText = "기타 사유를 입력합니다.", - onSelectReason = {}, - onOtherReasonChanged = {}, - ) - } -} 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 index 0b4561c1..e9bb29cd 100644 --- 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 @@ -14,15 +14,9 @@ internal data class DeleteAccountUiState( val isConfirmDialogVisible: Boolean = false, val isSubmitting: Boolean = false, ) : UiState { - val isOtherReasonSelected: Boolean - get() = selectedReason == DeleteAccountReasonOption.OTHER + val isNextEnabled: Boolean = isNoticeChecked - val isNextEnabled: Boolean - get() = isNoticeChecked + val isWithdrawEnabled: Boolean = selectedReason != null && !isSubmitting - val isWithdrawEnabled: Boolean - get() = selectedReason != null && !isSubmitting - - val isPrimaryActionEnabled: Boolean - get() = if (step == DeleteAccountStep.NOTICE) isNextEnabled else isWithdrawEnabled + 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/setting/component/SettingBodySection.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/setting/component/SettingBodySection.kt index 7773a22c..64fad349 100644 --- 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 @@ -14,6 +14,7 @@ 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 @@ -54,21 +55,6 @@ internal fun SettingBodySection( } } -@BasicPreview -@Composable -private fun SettingBodySectionPreview() { - PrezelTheme { - SettingBodySection( - uiState = SettingUiState( - nickname = "발표잘하고싶어요", - email = "email@email.com", - ), - onClickTermsOfService = {}, - onClickPrivacyPolicy = {}, - ) - } -} - @Composable internal fun AccountSection( profileImageUrl: String?, @@ -83,7 +69,7 @@ internal fun AccountSection( horizontal = PrezelTheme.spacing.V20, vertical = PrezelTheme.spacing.V16, ), - verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically, ) { PrezelAvatar( imageUrl = profileImageUrl, @@ -100,7 +86,7 @@ internal fun AccountSection( style = PrezelTheme.typography.body2Medium, ) - Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) { + Row(verticalAlignment = Alignment.CenterVertically) { Text( text = email, color = PrezelTheme.colors.textRegular, @@ -122,18 +108,6 @@ internal fun AccountSection( } } -@BasicPreview -@Composable -private fun AccountSectionPreview() { - PrezelTheme { - AccountSection( - profileImageUrl = null, - nickname = "발표잘하고싶어요", - email = "email@email.com", - ) - } -} - @Composable internal fun PolicySection( onClickTermsOfService: () -> Unit, @@ -180,9 +154,13 @@ internal fun PolicySection( @BasicPreview @Composable -private fun PolicySectionPreview() { +private fun SettingBodySectionPreview() { PrezelTheme { - PolicySection( + SettingBodySection( + uiState = SettingUiState( + nickname = "발표잘하고싶어요", + email = "email@email.com", + ), onClickTermsOfService = {}, onClickPrivacyPolicy = {}, ) From c12fe8150dad0ef8b0df287c5a8d9b36d7c05823 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 7 May 2026 00:52:13 +0900 Subject: [PATCH 09/15] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=ED=83=88=ED=87=B4=20=EC=95=88=EB=82=B4=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A7=81=20=EA=B4=80=EB=A6=AC=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=ED=83=9C=EA=B7=B8?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=20=EC=8A=A4=ED=83=80=EC=9D=BC=EB=A7=81=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **refactor: 파편화된 스트링 리소스를 태그 포함 단일 스트링으로 통합** * `strings.xml` 내 여러 개로 쪼개져 있던 안내 문구들을 ``, `` 등 커스텀 태그를 포함한 하나의 스트링 리소스로 병합했습니다. * 줄바꿈(`\n`) 및 태그를 활용하여 번역 및 문구 수정 시의 유연성을 높였습니다. * **feat: `AnnotatedString` 내 커스텀 태그 해석 로직 구현** * ``, `` 태그를 분석하여 각각 `feedbackBadRegular` 색상 및 `body2Bold` 스타일을 적용하는 `appendTaggedText` 함수를 추가했습니다. * `TagState`를 통해 태그 중첩(Bold + Highlight) 및 유효성 검사를 처리하도록 개선했습니다. * **refactor: `DeleteAccountNoticeStep` UI 구조 및 컴포넌트 리팩터링** * 기존에 코드로 조합하던 `AnnotatedString` 생성 로직을 `appendTaggedText`를 사용하는 방식으로 단순화했습니다. * `DeleteAccountNoticeHeader`, `DeletionAgreement`, `DeleteAccountNoticeDetailBox` 등 하위 컴포넌트를 분리하여 가독성을 높였습니다. * `Arrangement.spacedBy` 대신 `Spacer`를 사용하여 컴포넌트 간 간격 조절 방식을 명확하게 수정했습니다. * `PrezelCheckbox`의 터치 영역 및 간격 설정을 `Spacer`와 `PaddingValues`를 활용하도록 조정했습니다. --- .../component/DeleteAccountNoticeStep.kt | 327 ++++++++++++------ .../impl/src/main/res/values/strings.xml | 17 +- 2 files changed, 227 insertions(+), 117 deletions(-) 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 index 0ff3ef85..56d0a152 100644 --- 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 @@ -5,9 +5,12 @@ 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 @@ -20,7 +23,6 @@ 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.dp import androidx.compose.ui.unit.em import com.team.prezel.core.designsystem.component.PrezelCheckbox import com.team.prezel.core.designsystem.preview.BasicPreview @@ -28,6 +30,18 @@ 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, @@ -40,84 +54,83 @@ internal fun DeleteAccountNoticeStep( .verticalScroll(rememberScrollState()) .padding(horizontal = PrezelTheme.spacing.V20) .padding(top = PrezelTheme.spacing.V16, bottom = PrezelTheme.spacing.V24), - verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V16), ) { - Column(verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V8)) { - Text( - text = stringResource(R.string.feature_setting_impl_delete_account_notice_title), - style = PrezelTheme.typography.title2Bold, - color = PrezelTheme.colors.textLarge, - ) - DeleteAccountNoticeDescription() - } + DeleteAccountNoticeHeader() + + Spacer(modifier = Modifier.height(PrezelTheme.spacing.V16)) DeleteAccountNoticeDetailBox() - Row( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { onCheckedChange(!isChecked) }, - verticalAlignment = Alignment.CenterVertically, - ) { - PrezelCheckbox( - checked = isChecked, - onCheckedChange = onCheckedChange, - extraTouchPadding = PaddingValues(end = PrezelTheme.spacing.V8), - ) - Text( - text = stringResource(R.string.feature_setting_impl_delete_account_notice_check), - style = PrezelTheme.typography.body3Regular, - color = PrezelTheme.colors.textRegular, - ) - } + 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 regularStyle = SpanStyle( - color = PrezelTheme.colors.textMedium, - fontSize = PrezelTheme.typography.body2Regular.fontSize, - fontWeight = PrezelTheme.typography.body2Regular.fontWeight, - letterSpacing = PrezelTheme.typography.body2Regular.letterSpacing, - ) - val highlightStyle = SpanStyle( - color = PrezelTheme.colors.feedbackBadRegular, + 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, ) - Column( + Text( + text = buildAnnotatedString { + appendTaggedText( + text = stringResource(R.string.feature_setting_impl_delete_account_notice_description), + highlightStyle = highlightStyle, + boldStyle = boldStyle, + ) + }, modifier = modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(0.dp), - ) { - Text( - text = stringResource(R.string.feature_setting_impl_delete_account_notice_description_1), - style = PrezelTheme.typography.body2Regular, - color = PrezelTheme.colors.textMedium, - ) - Text( - text = buildAnnotatedString { - withStyle(regularStyle) { - append(stringResource(R.string.feature_setting_impl_delete_account_notice_description_2_prefix)) - append(" ") - } - withStyle(highlightStyle) { - append(stringResource(R.string.feature_setting_impl_delete_account_notice_description_2_highlight)) - } - withStyle(regularStyle) { - append(stringResource(R.string.feature_setting_impl_delete_account_notice_description_2_suffix)) - } - }, - style = PrezelTheme.typography.body2Regular, - ) - Text( - text = stringResource(R.string.feature_setting_impl_delete_account_notice_description_3), - style = PrezelTheme.typography.body2Bold, - color = PrezelTheme.colors.textMedium, - ) - } + style = PrezelTheme.typography.body2Regular, + color = PrezelTheme.colors.textMedium, + ) } @Composable @@ -132,73 +145,179 @@ private fun DeleteAccountNoticeDetailBox(modifier: Modifier = Modifier) { verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V8), ) { DeleteAccountNoticeDetailItem( - prefix = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_1_prefix), - highlight = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_1_highlight), - suffix = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_1_suffix), + text = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_1), ) DeleteAccountNoticeDetailItem( - prefix = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_2_prefix), - highlight = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_2_highlight), - suffix = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_2_suffix), + 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( - prefix = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_4_prefix), - highlight = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_4_highlight), + text = stringResource(R.string.feature_setting_impl_delete_account_notice_detail_4), ) } } @Composable private fun DeleteAccountNoticeDetailItem( + text: String, modifier: Modifier = Modifier, - text: String? = null, - prefix: String = "", - highlight: String = "", - suffix: String = "", ) { - val regularStyle = SpanStyle( - color = PrezelTheme.colors.textMedium, + 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, ) - val highlightStyle = SpanStyle( - color = PrezelTheme.colors.feedbackBadRegular, - fontSize = PrezelTheme.typography.body3Regular.fontSize, - fontWeight = PrezelTheme.typography.body2Bold.fontWeight, - letterSpacing = PrezelTheme.typography.body3Regular.letterSpacing, - ) - val detailText: AnnotatedString = buildAnnotatedString { - withBulletList(bullet = Bullet.Default.copy(padding = 0.5.em)) { - withBulletListItem { - if (text != null) { - append(text) - } else { - withStyle(regularStyle) { - append(prefix) - if (prefix.isNotEmpty()) append(" ") - } - withStyle(highlightStyle) { append(highlight) } - withStyle(regularStyle) { - if (suffix.isNotEmpty()) append(" ") - append(suffix) - } - } - } - } - } Text( - text = detailText, + 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() { diff --git a/Prezel/feature/setting/impl/src/main/res/values/strings.xml b/Prezel/feature/setting/impl/src/main/res/values/strings.xml index 1c0cfce6..903a9977 100644 --- a/Prezel/feature/setting/impl/src/main/res/values/strings.xml +++ b/Prezel/feature/setting/impl/src/main/res/values/strings.xml @@ -15,20 +15,11 @@ 회원을 탈퇴하시면 - 추가한 발표, 발표 연습 기록, 획득한 뱃지 등을 - 포함한 이용 내역 및 개인정보가 - 영구 삭제 - 되며, - 다시 복구할 수 없어요. - 회원 탈퇴 시 즉시 탈퇴 처리되며, - 이후 서비스 이용이 불가합니다. - 탈퇴 후에는 동일 계정으로 재가입하더라도 기존 정보는 복구되지 않습니다. - 회원 탈퇴 시 아래 정보의 복구가 불가능합니다. - 연습 녹음 기록, 음성 분석 결과 및 리포트, 발표 대본 및 연습 히스토리 등의 데이터가 삭제 - 되며, 탈퇴 후에는 삭제된 데이터에 대한 복구 요청이 불가합니다. + 추가한 발표, 발표 연습 기록, 획득한 뱃지 등을\n포함한 이용 내역 및 개인정보가 <highlight><bold>영구 삭제</bold></highlight>되며,\n<bold>다시 복구할 수 없어요.</bold> + 회원 탈퇴 시 즉시 탈퇴 처리되며, <highlight>이후 서비스 이용이 불가합니다.</highlight> 탈퇴 후에는 동일 계정으로 재가입하더라도 기존 정보는 복구되지 않습니다. + 회원 탈퇴 시 아래 정보의 복구가 불가능합니다. <highlight>연습 녹음 기록, 음성 분석 결과 및 리포트, 발표 대본 및 연습 히스토리 등의 데이터가 삭제</highlight>되며, 탈퇴 후에는 삭제된 데이터에 대한 복구 요청이 불가합니다. 탈퇴 시 진행 중인 발표 연습, 분석 요청은 모두 종료됩니다. 미완료된 분석 결과는 제공되지 않습니다. - 탈퇴 후 동일한 카카오 계정으로 재가입은 가능하나, - 이전 이용 기록 및 연습 데이터는 새로 생성되지 않습니다. + 탈퇴 후 동일한 카카오 계정으로 재가입은 가능하나, <highlight>이전 이용 기록 및 연습 데이터는 새로 생성되지 않습니다.</highlight> 탈퇴 시 주의사항을 모두 확인했습니다. 다음 From a672b7f1391e62470f518ed4251de3250e08740d Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 7 May 2026 01:15:49 +0900 Subject: [PATCH 10/15] =?UTF-8?q?refactor:=20=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=EC=82=AC=EC=9C=A0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/common/extensions/StringExt.kt | 7 ++ .../data/repository/AuthRepositoryImpl.kt | 21 +---- .../prezel/core/model/auth/WithdrawReason.kt | 8 +- .../datasource/AuthRemoteDataSource.kt | 2 +- .../datasource/AuthRemoteDataSourceImpl.kt | 7 +- .../network/model/auth/WithdrawRequest.kt | 2 +- Prezel/feature/setting/impl/build.gradle.kts | 2 + .../impl/delete/DeleteAccountScreen.kt | 2 +- .../impl/delete/DeleteAccountViewModel.kt | 14 ++-- .../component/DeleteAccountReasonStep.kt | 81 +++++++++++-------- .../delete/model/DeleteAccountReasonOption.kt | 12 +-- 11 files changed, 83 insertions(+), 75 deletions(-) create mode 100644 Prezel/core/common/src/main/kotlin/com/team/prezel/core/common/extensions/StringExt.kt diff --git a/Prezel/core/common/src/main/kotlin/com/team/prezel/core/common/extensions/StringExt.kt b/Prezel/core/common/src/main/kotlin/com/team/prezel/core/common/extensions/StringExt.kt new file mode 100644 index 00000000..275955c7 --- /dev/null +++ b/Prezel/core/common/src/main/kotlin/com/team/prezel/core/common/extensions/StringExt.kt @@ -0,0 +1,7 @@ +package com.team.prezel.core.common.extensions + +fun String.toSnakeCase(): String = + fold(StringBuilder()) { acc, c -> + if (c.isUpperCase() && acc.isNotEmpty()) acc.append('_') + acc.append(c.uppercaseChar()) + }.toString() 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..3aaab81c 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 @@ -1,5 +1,6 @@ package com.team.prezel.core.data.repository +import com.team.prezel.core.common.extensions.toSnakeCase import com.team.prezel.core.data.error.mapDomainFailure import com.team.prezel.core.datastore.auth.AuthLocalDataSource import com.team.prezel.core.domain.repository.auth.AuthRepository @@ -37,28 +38,12 @@ internal class AuthRepositoryImpl @Inject constructor( override suspend fun withdraw(reason: WithdrawReason): Result = runCatching { authRemoteDataSource.withdraw( - reasonCategory = reason.toCategory(), - reasonText = reason.toReasonText(), + reasonCategory = reason.javaClass.simpleName.toSnakeCase(), + 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() 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/feature/setting/impl/build.gradle.kts b/Prezel/feature/setting/impl/build.gradle.kts index 1cc7ccd6..604aa005 100644 --- a/Prezel/feature/setting/impl/build.gradle.kts +++ b/Prezel/feature/setting/impl/build.gradle.kts @@ -14,4 +14,6 @@ dependencies { implementation(projects.featureSettingApi) implementation(projects.featureSplashApi) implementation(projects.featureTermsApi) + + implementation(libs.kotlinx.collections.immutable) } 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 index 85c95aa5..52bfc2d9 100644 --- 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 @@ -168,7 +168,7 @@ private fun DeleteAccountReasonStepScreenPreview() { DeleteAccountScreen( uiState = DeleteAccountUiState( step = DeleteAccountStep.REASON, - selectedReason = DeleteAccountReasonOption.OTHER, + selectedReason = DeleteAccountReasonOption.Etc, ), onClickClose = {}, onToggleNoticeChecked = {}, 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 index ef0e8a26..2adc9e2a 100644 --- 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 @@ -41,7 +41,7 @@ internal class DeleteAccountViewModel @Inject constructor( updateState { copy( selectedReason = reason, - otherReasonText = if (reason == DeleteAccountReasonOption.OTHER) otherReasonText else "", + otherReasonText = if (reason == DeleteAccountReasonOption.Etc) otherReasonText else "", ) } } @@ -80,12 +80,12 @@ internal class DeleteAccountViewModel @Inject constructor( private fun DeleteAccountUiState.toWithdrawReason(): WithdrawReason? = when (selectedReason) { - DeleteAccountReasonOption.NOT_USED_OFTEN -> WithdrawReason.NotUsedOften - DeleteAccountReasonOption.NO_LONGER_NEEDED -> WithdrawReason.NoLongerNeeded - DeleteAccountReasonOption.TOO_DIFFICULT_OR_COMPLEX -> WithdrawReason.TooDifficultOrComplex - DeleteAccountReasonOption.ANALYSIS_RESULT_INACCURATE -> WithdrawReason.AnalysisResultInaccurate - DeleteAccountReasonOption.TOO_MANY_ERRORS -> WithdrawReason.TooManyErrors - DeleteAccountReasonOption.OTHER -> WithdrawReason.Other(text = otherReasonText.trim()) + 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/DeleteAccountReasonStep.kt b/Prezel/feature/setting/impl/src/main/java/com/team/prezel/feature/setting/impl/delete/component/DeleteAccountReasonStep.kt index db95b966..d95dffde 100644 --- 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 @@ -25,6 +25,8 @@ 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( @@ -34,17 +36,6 @@ internal fun DeleteAccountReasonStep( onOtherReasonChanged: (String) -> Unit, modifier: Modifier = Modifier, ) { - val reasonOptions = listOf( - DeleteAccountReasonOption.OTHER to stringResource(R.string.feature_setting_impl_delete_account_reason_other), - DeleteAccountReasonOption.NOT_USED_OFTEN to stringResource(R.string.feature_setting_impl_delete_account_reason_not_used_often), - DeleteAccountReasonOption.NO_LONGER_NEEDED to stringResource(R.string.feature_setting_impl_delete_account_reason_no_longer_needed), - DeleteAccountReasonOption.TOO_DIFFICULT_OR_COMPLEX to - stringResource(R.string.feature_setting_impl_delete_account_reason_too_difficult_or_complex), - DeleteAccountReasonOption.ANALYSIS_RESULT_INACCURATE to - stringResource(R.string.feature_setting_impl_delete_account_reason_analysis_result_inaccurate), - DeleteAccountReasonOption.TOO_MANY_ERRORS to stringResource(R.string.feature_setting_impl_delete_account_reason_too_many_errors), - ) - Column( modifier = modifier .fillMaxSize() @@ -52,35 +43,28 @@ internal fun DeleteAccountReasonStep( .padding(horizontal = PrezelTheme.spacing.V12) .padding(top = PrezelTheme.spacing.V16, bottom = PrezelTheme.spacing.V24), ) { - DeleteAccountReasonHeader() + DeleteAccountReasonHeader(modifier = Modifier.padding(horizontal = PrezelTheme.spacing.V8)) Spacer(modifier = Modifier.height(PrezelTheme.spacing.V32)) - 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, - ) - } - } - } + DeleteAccountReasons( + selectedReason = selectedReason, + otherReasonText = otherReasonText, + onSelectReason = onSelectReason, + onOtherReasonChanged = onOtherReasonChanged, + ) } } @Composable -private fun DeleteAccountReasonHeader() { - Column(verticalArrangement = Arrangement.spacedBy(PrezelTheme.spacing.V8)) { +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, @@ -89,6 +73,39 @@ private fun DeleteAccountReasonHeader() { } } +@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, @@ -109,12 +126,10 @@ private fun DeleteAccountReasonOptionItem( onCheckedChange = { checked -> if (checked) onSelectReason(reason) }, ) }, - modifier = Modifier.noRippleClickable( - onClick = { onSelectReason(reason) }, - ), + modifier = Modifier.noRippleClickable { onSelectReason(reason) }, ) - if (reason == DeleteAccountReasonOption.OTHER && selectedReason == DeleteAccountReasonOption.OTHER) { + if (reason == DeleteAccountReasonOption.Etc && selectedReason == DeleteAccountReasonOption.Etc) { Spacer(modifier = Modifier.height(PrezelTheme.spacing.V8)) PrezelTextArea( value = otherReasonText, @@ -136,7 +151,7 @@ private fun DeleteAccountReasonOptionItem( private fun DeleteAccountReasonStepPreview() { PrezelTheme { DeleteAccountReasonStep( - selectedReason = DeleteAccountReasonOption.NOT_USED_OFTEN, + selectedReason = DeleteAccountReasonOption.Etc, otherReasonText = "", onSelectReason = {}, onOtherReasonChanged = {}, 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 index 2ef98ccb..3d1d8cab 100644 --- 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 @@ -1,10 +1,10 @@ package com.team.prezel.feature.setting.impl.delete.model internal enum class DeleteAccountReasonOption { - NOT_USED_OFTEN, - NO_LONGER_NEEDED, - TOO_DIFFICULT_OR_COMPLEX, - ANALYSIS_RESULT_INACCURATE, - TOO_MANY_ERRORS, - OTHER, + NotUsedOften, + NoLongerNeeded, + TooComplex, + InaccurateAnalysis, + ManyErrors, + Etc, } From e62bfb3f190755aaa1dd4de92b6376f8f6b5c182 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 7 May 2026 01:21:38 +0900 Subject: [PATCH 11/15] =?UTF-8?q?refactor:=20=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=EC=82=AC=EC=9C=A0=20=EB=A7=A4=ED=95=91=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EC=8B=A4=ED=8C=A8=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **refactor: `WithdrawReason` 카테고리 매핑 방식 변경 및 코드 정리** * 기존 reflection과 `toSnakeCase()`를 사용하여 클래스명을 변환하던 방식을 명시적인 `toReasonCategory()` 매핑 함수로 변경하여 로직의 안정성을 높였습니다. * 더 이상 사용되지 않는 `String.toSnakeCase()` 확장 함수를 삭제했습니다. * **feat: 유저 정보 조회 실패 관련 스트링 리소스 추가 및 적용** * `SettingScreen`에서 유저 정보 조회 실패 시 표시할 구체적인 에러 메시지(`feature_setting_impl_fetch_user_info_failed`)를 추가했습니다. * 기존에 유저 정보 조회 실패 시 잘못 참조하고 있던 공통 타이틀 리소스를 새로 추가된 스트링 리소스로 수정했습니다. --- .../team/prezel/core/common/extensions/StringExt.kt | 7 ------- .../core/data/repository/AuthRepositoryImpl.kt | 13 +++++++++++-- .../feature/setting/impl/setting/SettingScreen.kt | 2 +- .../setting/impl/src/main/res/values/strings.xml | 1 + 4 files changed, 13 insertions(+), 10 deletions(-) delete mode 100644 Prezel/core/common/src/main/kotlin/com/team/prezel/core/common/extensions/StringExt.kt diff --git a/Prezel/core/common/src/main/kotlin/com/team/prezel/core/common/extensions/StringExt.kt b/Prezel/core/common/src/main/kotlin/com/team/prezel/core/common/extensions/StringExt.kt deleted file mode 100644 index 275955c7..00000000 --- a/Prezel/core/common/src/main/kotlin/com/team/prezel/core/common/extensions/StringExt.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.team.prezel.core.common.extensions - -fun String.toSnakeCase(): String = - fold(StringBuilder()) { acc, c -> - if (c.isUpperCase() && acc.isNotEmpty()) acc.append('_') - acc.append(c.uppercaseChar()) - }.toString() 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 3aaab81c..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 @@ -1,6 +1,5 @@ package com.team.prezel.core.data.repository -import com.team.prezel.core.common.extensions.toSnakeCase import com.team.prezel.core.data.error.mapDomainFailure import com.team.prezel.core.datastore.auth.AuthLocalDataSource import com.team.prezel.core.domain.repository.auth.AuthRepository @@ -38,7 +37,7 @@ internal class AuthRepositoryImpl @Inject constructor( override suspend fun withdraw(reason: WithdrawReason): Result = runCatching { authRemoteDataSource.withdraw( - reasonCategory = reason.javaClass.simpleName.toSnakeCase(), + reasonCategory = reason.toReasonCategory(), reasonText = (reason as? WithdrawReason.Etc)?.text, ) clearLocalSession() @@ -48,4 +47,14 @@ internal class AuthRepositoryImpl @Inject constructor( 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/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 index efe5b92a..5d24e53a 100644 --- 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 @@ -53,7 +53,7 @@ internal fun SettingScreen( is SettingUiEffect.ShowMessage -> { val messageRes = when (effect.message) { - SettingUiMessage.FETCH_USER_INFO_FAILED -> R.string.feature_setting_impl_title + SettingUiMessage.FETCH_USER_INFO_FAILED -> R.string.feature_setting_impl_fetch_user_info_failed SettingUiMessage.LOGOUT_FAILED -> R.string.feature_setting_impl_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 index 903a9977..e623a798 100644 --- a/Prezel/feature/setting/impl/src/main/res/values/strings.xml +++ b/Prezel/feature/setting/impl/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ 닫기 프로필 이미지 카카오 계정 + 유저 정보 조회에 실패했어요. 로그아웃에 실패했어요. 다시 시도해 주세요. 로그아웃 하시겠어요? 취소 From e44e85abff4c85499cfe72654c0e534d443aca81 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 7 May 2026 01:37:02 +0900 Subject: [PATCH 12/15] =?UTF-8?q?refactor:=20SnackbarHost=EC=9D=98=20?= =?UTF-8?q?=EC=8A=A4=EB=82=B5=EB=B0=94=20=EB=85=B8=EC=B6=9C=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=A0=9C=EC=96=B4=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **refactor: `showPrezelSnackbar` 매개변수 및 위치 계산 방식 변경** * 기존의 구체적인 수치(`offsetY: Dp`)를 직접 받던 매개변수를 `useRaisedPosition: Boolean`으로 변경하여 인터페이스를 단순화했습니다. * `useRaisedPosition` 값에 따라 스낵바 오프셋을 기본 위치(`0.dp`) 또는 위로 플로팅된 위치(`-98.dp`)로 자동 결정하도록 로직을 수정했습니다. --- .../designsystem/component/feedback/snackbar/SnackbarHost.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, ), ) From e2520935f68325e48b152da2c1bb8aa2a29a9f29 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 7 May 2026 01:37:59 +0900 Subject: [PATCH 13/15] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=20=ED=83=88=ED=87=B4?= =?UTF-8?q?=20=EC=84=B1=EA=B3=B5=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20UI=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **feat: 로그아웃 및 회원 탈퇴 성공 시 스낵바 알림 추가** * 로그아웃 처리가 완료되어 스플래시 화면으로 이동할 때 "로그아웃되었어요." 메시지를 출력하도록 수정했습니다. * 회원 탈퇴 처리가 완료되어 스플래시 화면으로 이동할 때 "회원탈퇴가 완료되었어요." 메시지를 출력하도록 수정했습니다. * 이에 필요한 문자열 리소스(`feature_setting_impl_logout_success`, `feature_setting_impl_delete_account_withdraw_success`)를 추가했습니다. * **refactor: UI 메시지 리소스 매핑 로직 개선** * `SettingScreen` 및 `DeleteAccountScreen`에서 `UiMessage`를 리소스 ID로 변환하는 로직을 `toMessageRes()` 확장 함수로 추출하여 `LaunchedEffect` 내부의 가독성을 높였습니다. --- .../setting/impl/delete/DeleteAccountScreen.kt | 15 ++++++++++----- .../setting/impl/setting/SettingScreen.kt | 16 ++++++++-------- .../setting/impl/src/main/res/values/strings.xml | 2 ++ 3 files changed, 20 insertions(+), 13 deletions(-) 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 index 52bfc2d9..eb34badf 100644 --- 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 @@ -43,12 +43,12 @@ internal fun DeleteAccountScreen( LaunchedEffect(viewModel) { viewModel.uiEffect.collect { effect -> when (effect) { - DeleteAccountUiEffect.NavigateToSplash -> navigateToSplash() + DeleteAccountUiEffect.NavigateToSplash -> { + navigateToSplash() + snackbarHostState.showPrezelSnackbar(message = resources.getString(R.string.feature_setting_impl_delete_account_withdraw_success)) + } is DeleteAccountUiEffect.ShowMessage -> { - val message = when (effect.message) { - DeleteAccountUiMessage.WITHDRAW_FAILED -> R.string.feature_setting_impl_delete_account_withdraw_failed - } - snackbarHostState.showPrezelSnackbar(message = resources.getString(message)) + snackbarHostState.showPrezelSnackbar(message = resources.getString(effect.message.toMessageRes())) } } } @@ -74,6 +74,11 @@ internal fun DeleteAccountScreen( ) } +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, 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 index 5d24e53a..49989a74 100644 --- 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 @@ -49,17 +49,11 @@ internal fun SettingScreen( SettingUiEffect.NavigateToSplash -> { shouldShowLogoutDialog = false navigateToSplash() + snackbarHostState.showPrezelSnackbar(message = resource.getString(R.string.feature_setting_impl_logout_success)) } is SettingUiEffect.ShowMessage -> { - val messageRes = when (effect.message) { - SettingUiMessage.FETCH_USER_INFO_FAILED -> R.string.feature_setting_impl_fetch_user_info_failed - SettingUiMessage.LOGOUT_FAILED -> R.string.feature_setting_impl_logout_failed - } - - snackbarHostState.showPrezelSnackbar( - message = resource.getString(messageRes), - ) + snackbarHostState.showPrezelSnackbar(message = resource.getString(effect.message.toMessageRes())) } } } @@ -83,6 +77,12 @@ internal fun SettingScreen( ) } +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, diff --git a/Prezel/feature/setting/impl/src/main/res/values/strings.xml b/Prezel/feature/setting/impl/src/main/res/values/strings.xml index e623a798..113445dd 100644 --- a/Prezel/feature/setting/impl/src/main/res/values/strings.xml +++ b/Prezel/feature/setting/impl/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ 프로필 이미지 카카오 계정 유저 정보 조회에 실패했어요. + 로그아웃되었어요. 로그아웃에 실패했어요. 다시 시도해 주세요. 로그아웃 하시겠어요? 취소 @@ -40,5 +41,6 @@ 정말 탈퇴하시겠어요? 취소 탈퇴 + 회원탈퇴가 완료되었어요. 회원탈퇴에 실패했어요. 다시 시도해 주세요. From 1946bfe729ecdcf0466880f84bd2030d204b46da Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 7 May 2026 01:55:39 +0900 Subject: [PATCH 14/15] =?UTF-8?q?build:=20feature:setting=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EB=82=B4=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **cleanup: `coreAuth` 의존성 삭제** * `setting` 기능 구현 모듈(`feature:setting:impl`)의 `build.gradle.kts`에서 사용하지 않는 `coreAuth` 모듈 의존성을 제거했습니다. --- Prezel/feature/setting/impl/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/Prezel/feature/setting/impl/build.gradle.kts b/Prezel/feature/setting/impl/build.gradle.kts index 604aa005..961e8a91 100644 --- a/Prezel/feature/setting/impl/build.gradle.kts +++ b/Prezel/feature/setting/impl/build.gradle.kts @@ -7,7 +7,6 @@ android { } dependencies { - implementation(projects.coreAuth) implementation(projects.coreDomain) implementation(projects.coreModel) From de94a5c47741ee8bb2372f841e6cbce4648e7bb5 Mon Sep 17 00:00:00 2001 From: moondev03 Date: Thu, 7 May 2026 01:56:17 +0900 Subject: [PATCH 15/15] =?UTF-8?q?refactor:=20=EC=A2=85=EB=A3=8C=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EC=8A=A4=EB=82=B5=EB=B0=94=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **refactor: `DoubleBackToExitHandler` 내 스낵바 위치 조정** * 앱 종료 확인을 위한 스낵바 호출 시 `useRaisedPosition` 파라미터를 `false`로 설정하여 UI 표시 위치를 조정했습니다. --- .../main/java/com/team/prezel/ui/DoubleBackToExitHandler.kt | 3 +-- Prezel/app/src/main/res/values/strings.xml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) 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 @@ 히스토리 프로필 - 한 번 더 누르면 앱을 종료합니다 - 닫기 + 한 번 더 누르면 앱을 종료합니다.