From 8e520b183e11967e5d374db5074b76324b8088bb Mon Sep 17 00:00:00 2001 From: dogmania Date: Tue, 19 May 2026 18:05:20 +0900 Subject: [PATCH 01/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20API=20?= =?UTF-8?q?=ED=86=B5=EC=8B=A0=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/twix/network/service/OnboardingService.kt | 6 ++++++ .../com/twix/data/repository/DefaultOnboardingRepository.kt | 2 ++ .../java/com/twix/domain/repository/OnBoardingRepository.kt | 2 ++ 3 files changed, 10 insertions(+) diff --git a/core/network/src/main/java/com/twix/network/service/OnboardingService.kt b/core/network/src/main/java/com/twix/network/service/OnboardingService.kt index 74a26141b..31f81de07 100644 --- a/core/network/src/main/java/com/twix/network/service/OnboardingService.kt +++ b/core/network/src/main/java/com/twix/network/service/OnboardingService.kt @@ -7,6 +7,7 @@ import com.twix.network.model.response.onboarding.InviteCodeResponse import com.twix.network.model.response.onboarding.OnBoardingStatusResponse import de.jensklingenberg.ktorfit.http.Body import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.PATCH import de.jensklingenberg.ktorfit.http.POST interface OnboardingService { @@ -30,4 +31,9 @@ interface OnboardingService { @GET("api/v1/onboarding/status") suspend fun fetchOnBoardingStatus(): OnBoardingStatusResponse + + @PATCH("api/v1/onboarding/profile") + suspend fun updateProfile( + @Body request: ProfileRequest, + ) } diff --git a/data/src/main/java/com/twix/data/repository/DefaultOnboardingRepository.kt b/data/src/main/java/com/twix/data/repository/DefaultOnboardingRepository.kt index 4d017c508..8451ef784 100644 --- a/data/src/main/java/com/twix/data/repository/DefaultOnboardingRepository.kt +++ b/data/src/main/java/com/twix/data/repository/DefaultOnboardingRepository.kt @@ -34,4 +34,6 @@ class DefaultOnboardingRepository( safeApiCall { service.coupleConnection(CoupleConnectionRequest(request)) } override suspend fun profileSetup(request: String): AppResult = safeApiCall { service.profileSetup(ProfileRequest(request)) } + + override suspend fun updateProfile(request: String): AppResult = safeApiCall { service.updateProfile(ProfileRequest(request)) } } diff --git a/domain/src/main/java/com/twix/domain/repository/OnBoardingRepository.kt b/domain/src/main/java/com/twix/domain/repository/OnBoardingRepository.kt index 24e58b1bd..136205587 100644 --- a/domain/src/main/java/com/twix/domain/repository/OnBoardingRepository.kt +++ b/domain/src/main/java/com/twix/domain/repository/OnBoardingRepository.kt @@ -14,4 +14,6 @@ interface OnBoardingRepository { suspend fun fetchInviteCode(): AppResult suspend fun fetchOnboardingStatus(): AppResult + + suspend fun updateProfile(request: String): AppResult } From a2553836c7ed42d209266a22599eb6242a8dd7d5 Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 20 May 2026 21:40:57 +0900 Subject: [PATCH 02/44] =?UTF-8?q?=F0=9F=8D=B1=20Chore:=20String=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/design-system/src/main/res/values/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 235114872..1713241c8 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -100,6 +100,8 @@ 개인정보 처리방침 나의 버전 알림 설정 + 닉네임을 입력해 주세요. + 닉네임 2-8자 스탬프 통계 From a4c21ac8fccdcf93e54e5bde9c322c9a76b427a1 Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 20 May 2026 21:41:13 +0900 Subject: [PATCH 03/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EA=B3=B5=EC=9A=A9=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=ED=95=84=EB=93=9C=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../text_field/ValidateUnderlineTextField.kt | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 core/design-system/src/main/java/com/twix/designsystem/components/text_field/ValidateUnderlineTextField.kt diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/text_field/ValidateUnderlineTextField.kt b/core/design-system/src/main/java/com/twix/designsystem/components/text_field/ValidateUnderlineTextField.kt new file mode 100644 index 000000000..bfc2d20fd --- /dev/null +++ b/core/design-system/src/main/java/com/twix/designsystem/components/text_field/ValidateUnderlineTextField.kt @@ -0,0 +1,150 @@ +package com.twix.designsystem.components.text_field + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import com.twix.designsystem.R +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.theme.SystemColor +import com.twix.domain.model.enums.AppTextStyle +import com.twix.ui.extension.noRippleClickable + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ValidateUnderlineTextField( + value: String, + modifier: Modifier = Modifier, + textFieldModifier: Modifier = Modifier, + placeholder: String, + guideText: String, + validLengthRange: IntRange = 2..14, + maxLength: Int = validLengthRange.last, + showGuideWhenValid: Boolean = true, + onCommit: (String) -> Unit, +) { + val focusManager = LocalFocusManager.current + val density = LocalDensity.current + + var internalValue by rememberSaveable(value) { mutableStateOf(value) } + var isFocused by remember { mutableStateOf(false) } + var lastCommitted by remember(value) { mutableStateOf(value.trim()) } + + fun commitIfChanged() { + val trimmed = internalValue.trim() + + if (trimmed != lastCommitted) { + lastCommitted = trimmed + onCommit(trimmed) + } + } + + val imeVisibleState = remember { mutableStateOf(false) } + imeVisibleState.value = WindowInsets.ime.getBottom(density) > 0 + + LaunchedEffect(isFocused) { + var prev = imeVisibleState.value + + snapshotFlow { imeVisibleState.value } + .collect { now -> + if (prev && !now && isFocused) { + commitIfChanged() + focusManager.clearFocus(force = true) + } + + prev = now + } + } + + val isValidLength = internalValue.length in validLengthRange + + Column( + modifier = modifier.height(96.dp), + verticalArrangement = Arrangement.Top, + ) { + UnderlineTextField( + modifier = + textFieldModifier + .padding(horizontal = 20.dp) + .fillMaxWidth() + .onFocusChanged { state -> + isFocused = state.isFocused + + if (!state.isFocused) { + commitIfChanged() + } + }, + value = internalValue, + placeHolder = placeholder, + maxLength = maxLength, + showTrailing = internalValue.isNotBlank(), + onValueChange = { internalValue = it }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = + KeyboardActions( + onDone = { + commitIfChanged() + focusManager.clearFocus(force = true) + }, + ), + trailing = { + Image( + painter = painterResource(R.drawable.ic_clear_text), + contentDescription = null, + modifier = + Modifier.noRippleClickable { + internalValue = "" + }, + ) + }, + ) + + if (showGuideWhenValid && isValidLength) { + Row( + modifier = + Modifier + .padding(top = 8.dp, start = 20.dp) + .height(17.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Image( + painter = painterResource(R.drawable.ic_check_success), + contentDescription = null, + modifier = Modifier.size(16.dp), + ) + + AppText( + text = guideText, + style = AppTextStyle.C2, + color = SystemColor.Success, + ) + } + } + } +} From 732dd734eb1746c8d44ddf4cc09a8006c8946f1a Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 20 May 2026 21:42:11 +0900 Subject: [PATCH 04/44] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20GoalText?= =?UTF-8?q?Field=20->=20ValidateUnderlineTextField=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/twix/goal_editor/GoalEditorScreen.kt | 11 +- .../goal_editor/component/GoalTextField.kt | 140 ------------------ 2 files changed, 8 insertions(+), 143 deletions(-) delete mode 100644 feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt index f399856c1..87ce6458e 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt @@ -47,6 +47,7 @@ import com.twix.designsystem.components.button.AppButton import com.twix.designsystem.components.calendar.Calendar import com.twix.designsystem.components.dialog.CommonDialog import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.components.text_field.ValidateUnderlineTextField import com.twix.designsystem.components.toast.ToastManager import com.twix.designsystem.components.toast.model.ToastData import com.twix.designsystem.components.topbar.CommonTopBar @@ -61,7 +62,6 @@ import com.twix.domain.model.enums.RepeatCycle import com.twix.domain.model.goal.RecommendedGoalPresets import com.twix.goal_editor.component.EmojiPicker import com.twix.goal_editor.component.GoalInfoCard -import com.twix.goal_editor.component.GoalTextField import com.twix.goal_editor.model.GoalEditorUiState import com.twix.ui.extension.dismissKeyboardOnTap import com.twix.ui.extension.noRippleClickable @@ -200,9 +200,14 @@ fun GoalEditorScreen( Spacer(Modifier.height(44.dp)) - GoalTextField( + ValidateUnderlineTextField( + modifier = + Modifier + .height(96.dp), value = uiState.goalTitle, - onCommitTitle = onCommitTitle, + onCommit = onCommitTitle, + placeholder = stringResource(R.string.goal_editor_text_field_placeholder), + guideText = stringResource(R.string.goal_editor_text_filed_guide), ) GoalInfoCard( diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt deleted file mode 100644 index 14542af6d..000000000 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.twix.goal_editor.component - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -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.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.unit.dp -import com.twix.designsystem.R -import com.twix.designsystem.components.text.AppText -import com.twix.designsystem.components.text_field.UnderlineTextField -import com.twix.designsystem.theme.SystemColor -import com.twix.domain.model.enums.AppTextStyle -import com.twix.ui.extension.noRippleClickable - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun GoalTextField( - value: String, - onCommitTitle: (String) -> Unit, -) { - val focusManager = LocalFocusManager.current - val density = LocalDensity.current - var internalValue by rememberSaveable(value) { mutableStateOf(value) } - var isFocused by remember { mutableStateOf(false) } - var lastCommitted by remember(value) { mutableStateOf(value.trim()) } - - fun commitIfChanged() { - val trimmed = internalValue.trim() - if (trimmed != lastCommitted) { - lastCommitted = trimmed - onCommitTitle(trimmed) - } - } - - val imeVisibleState = - remember { - mutableStateOf(false) - } - - imeVisibleState.value = WindowInsets.ime.getBottom(density) > 0 - LaunchedEffect(isFocused) { - var prev = imeVisibleState.value - snapshotFlow { imeVisibleState.value } - .collect { now -> - if (prev && !now && isFocused) { - commitIfChanged() - focusManager.clearFocus(force = true) - } - prev = now - } - } - - Column( - modifier = - Modifier - .height(96.dp), - verticalArrangement = Arrangement.Top, - ) { - UnderlineTextField( - modifier = - Modifier - .padding(horizontal = 20.dp) - .fillMaxWidth() - .onFocusChanged { state -> - isFocused = state.isFocused - if (!state.isFocused) commitIfChanged() - }, - value = internalValue, - placeHolder = stringResource(R.string.goal_editor_text_field_placeholder), - maxLength = 14, - showTrailing = internalValue.isNotBlank(), - onValueChange = { internalValue = it }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = - KeyboardActions( - onDone = { - commitIfChanged() - focusManager.clearFocus(force = true) - }, - ), - trailing = { - Image( - painter = painterResource(R.drawable.ic_clear_text), - contentDescription = null, - modifier = Modifier.noRippleClickable { internalValue = "" }, - ) - }, - ) - - if (internalValue.length in 2..14) { - Row( - modifier = - Modifier - .padding(top = 8.dp, start = 20.dp) - .height(17.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Image( - painter = painterResource(R.drawable.ic_check_success), - contentDescription = null, - modifier = - Modifier - .size(16.dp), - ) - - AppText( - text = stringResource(R.string.goal_editor_text_filed_guide), - style = AppTextStyle.C2, - color = SystemColor.Success, - ) - } - } - } -} From 362b5daf9edac9ba071821bb02898ae2167694ef Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 20 May 2026 21:42:33 +0900 Subject: [PATCH 05/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EB=B3=80=EA=B2=BD=20=EB=B9=84=EC=A6=88=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/twix/settings/SettingsViewModel.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt index 724377e5b..178393ce4 100644 --- a/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt @@ -3,6 +3,7 @@ package com.twix.settings import com.twix.designsystem.R import com.twix.designsystem.components.toast.model.ToastType import com.twix.domain.repository.AuthRepository +import com.twix.domain.repository.OnBoardingRepository import com.twix.domain.repository.UserRepository import com.twix.notification.token.NotificationTokenRegistrar import com.twix.settings.model.SettingsUiState @@ -12,6 +13,7 @@ class SettingsViewModel( private val userRepository: UserRepository, private val authRepository: AuthRepository, private val tokenRegistrar: NotificationTokenRegistrar, + private val onBoardingRepository: OnBoardingRepository, ) : BaseViewModel(SettingsUiState()) { init { fetchUserInfo() @@ -33,7 +35,14 @@ class SettingsViewModel( } private fun setNickName(nickName: String) { + val originalNickName = currentState.nickName reduce { copy(nickName = nickName) } + + launchResult( + block = { onBoardingRepository.updateProfile(nickName) }, + onSuccess = {}, + onError = { reduce { copy(nickName = originalNickName) } }, + ) } private fun logout() { From 1378854d3f90d996adfaccf630e4817c615608f8 Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 20 May 2026 21:42:41 +0900 Subject: [PATCH 06/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/twix/settings/SettingsScreen.kt | 95 ++++++++++++++++--- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt index b60511cf1..8efb76045 100644 --- a/feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt @@ -2,6 +2,7 @@ package com.twix.settings import androidx.compose.foundation.Image 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 @@ -14,21 +15,28 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.twix.designsystem.R import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.components.text_field.ValidateUnderlineTextField import com.twix.designsystem.components.topbar.CommonTopBar import com.twix.designsystem.theme.CommonColor import com.twix.designsystem.theme.GrayColor +import com.twix.designsystem.theme.TwixTheme import com.twix.domain.model.enums.AppTextStyle import com.twix.settings.component.SettingsMenuFrame import com.twix.settings.component.SettingsMenuItem import com.twix.settings.model.SettingsUiState +import com.twix.ui.extension.dismissKeyboardOnTap import com.twix.ui.extension.noRippleClickable import org.koin.androidx.compose.koinViewModel @@ -46,20 +54,25 @@ fun SettingsRoute( onBack = popBackStack, onAccountClick = navigateToSettingsAccount, onAboutClick = navigateToSettingsAbout, + onCommitNickName = { viewModel.dispatch(SettingsIntent.SetNickName(it)) }, ) } @Composable private fun SettingsScreen( - uiState: SettingsUiState, - onBack: () -> Unit, - onAccountClick: () -> Unit, - onAboutClick: () -> Unit, + uiState: SettingsUiState = SettingsUiState(), + onBack: () -> Unit = {}, + onAccountClick: () -> Unit = {}, + onAboutClick: () -> Unit = {}, + onCommitNickName: (String) -> Unit = {}, ) { + var isEditMode by remember { mutableStateOf(false) } + Column( modifier = Modifier .fillMaxSize() + .dismissKeyboardOnTap(onDismiss = { isEditMode = false }) .background(CommonColor.White), ) { CommonTopBar( @@ -85,7 +98,15 @@ private fun SettingsScreen( .fillMaxWidth() .padding(horizontal = 20.dp), ) { - ProfileInfo(nickname = uiState.nickName) + ProfileInfo( + nickname = uiState.nickName, + isEditMode = isEditMode, + onCommitNickName = { + onCommitNickName(it) + isEditMode = false + }, + onEditModeChange = { isEditMode = it }, + ) Spacer(Modifier.height(24.dp)) @@ -109,12 +130,17 @@ private fun SettingsScreen( } @Composable -private fun ProfileInfo(nickname: String) { +private fun ProfileInfo( + nickname: String, + isEditMode: Boolean, + onCommitNickName: (String) -> Unit, + onEditModeChange: (Boolean) -> Unit, +) { Row( modifier = Modifier .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.Top, ) { Image( painter = painterResource(R.drawable.ic_profile), @@ -124,12 +150,55 @@ private fun ProfileInfo(nickname: String) { .size(52.dp), ) - Spacer(Modifier.width(16.dp)) + Column( + horizontalAlignment = Alignment.Start, + ) { + if (isEditMode) { + ValidateUnderlineTextField( + modifier = + Modifier + .height(77.dp), + value = nickname, + onCommit = onCommitNickName, + placeholder = stringResource(R.string.settings_nickname_placeholder), + guideText = stringResource(R.string.settings_nickname_text_filed_guide), + validLengthRange = 2..8, + ) + } else { + Row( + modifier = + Modifier + .height(52.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + Spacer(Modifier.width(16.dp)) - AppText( - text = nickname, - style = AppTextStyle.T1, - color = GrayColor.C500, - ) + AppText( + text = nickname, + style = AppTextStyle.T1, + color = GrayColor.C500, + ) + + Image( + painter = painterResource(R.drawable.ic_edit), + contentDescription = null, + modifier = + Modifier + .padding(10.dp) + .size(24.dp) + .noRippleClickable { onEditModeChange(true) }, + ) + } + } + } + } +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +private fun Preview() { + TwixTheme { + SettingsScreen() } } From f3a9b6a3241c54b5398ccc151e091e3bd5283d06 Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 28 May 2026 22:25:20 +0900 Subject: [PATCH 07/44] =?UTF-8?q?=F0=9F=8D=B1=20Chore:=20=EB=A6=AC?= =?UTF-8?q?=EC=86=8C=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/res/drawable/ic_arrow_down_circle.xml | 16 ++++++++++++++++ .../src/main/res/drawable/ic_language.xml | 9 +++++++++ .../src/main/res/drawable/ic_notification.xml | 12 ++++++++++++ .../src/main/res/drawable/ic_question.xml | 9 +++++++++ .../src/main/res/values/strings.xml | 3 +++ 5 files changed, 49 insertions(+) create mode 100644 core/design-system/src/main/res/drawable/ic_arrow_down_circle.xml create mode 100644 core/design-system/src/main/res/drawable/ic_language.xml create mode 100644 core/design-system/src/main/res/drawable/ic_notification.xml create mode 100644 core/design-system/src/main/res/drawable/ic_question.xml diff --git a/core/design-system/src/main/res/drawable/ic_arrow_down_circle.xml b/core/design-system/src/main/res/drawable/ic_arrow_down_circle.xml new file mode 100644 index 000000000..2528c7a15 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_arrow_down_circle.xml @@ -0,0 +1,16 @@ + + + + diff --git a/core/design-system/src/main/res/drawable/ic_language.xml b/core/design-system/src/main/res/drawable/ic_language.xml new file mode 100644 index 000000000..a2819377f --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_language.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_notification.xml b/core/design-system/src/main/res/drawable/ic_notification.xml new file mode 100644 index 000000000..4fa2a51d2 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_question.xml b/core/design-system/src/main/res/drawable/ic_question.xml new file mode 100644 index 000000000..e05384fe2 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_question.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 1713241c8..20817c397 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -100,6 +100,9 @@ 개인정보 처리방침 나의 버전 알림 설정 + 언어 설정 + 문의하기 + 평일 오전 9시 - 오후 6시 운영 닉네임을 입력해 주세요. 닉네임 2-8자 From fe8d485fb9f46a7b8eb9b0ce90cc5a74afb8813a Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 28 May 2026 22:25:34 +0900 Subject: [PATCH 08/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20SettingMenuUiModel=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/twix/settings/model/SettingMenuUiModel.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 feature/settings/src/main/java/com/twix/settings/model/SettingMenuUiModel.kt diff --git a/feature/settings/src/main/java/com/twix/settings/model/SettingMenuUiModel.kt b/feature/settings/src/main/java/com/twix/settings/model/SettingMenuUiModel.kt new file mode 100644 index 000000000..3fd4b6959 --- /dev/null +++ b/feature/settings/src/main/java/com/twix/settings/model/SettingMenuUiModel.kt @@ -0,0 +1,9 @@ +package com.twix.settings.model + +data class SettingMenuUiModel( + val iconResId: Int, + val title: String, + val trailingText: String? = null, + val trailingIconResId: Int? = null, + val onClick: () -> Unit, +) From 66461da9828a83ccf533b13208e959870fbb2e55 Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 28 May 2026 22:26:37 +0900 Subject: [PATCH 09/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20SettingsLanguage=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/twix/settings/model/SettingsLanguage.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 feature/settings/src/main/java/com/twix/settings/model/SettingsLanguage.kt diff --git a/feature/settings/src/main/java/com/twix/settings/model/SettingsLanguage.kt b/feature/settings/src/main/java/com/twix/settings/model/SettingsLanguage.kt new file mode 100644 index 000000000..50eb92c7b --- /dev/null +++ b/feature/settings/src/main/java/com/twix/settings/model/SettingsLanguage.kt @@ -0,0 +1,7 @@ +package com.twix.settings.model + +enum class SettingsLanguage( + val displayName: String, +) { + Korean("한국어"), +} From 5f73901fa4f5552cfdfcc48b30879add07422314 Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 28 May 2026 22:28:23 +0900 Subject: [PATCH 10/44] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20ProfileI?= =?UTF-8?q?nfo=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../twix/settings/component/ProfileInfo.kt | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 feature/settings/src/main/java/com/twix/settings/component/ProfileInfo.kt diff --git a/feature/settings/src/main/java/com/twix/settings/component/ProfileInfo.kt b/feature/settings/src/main/java/com/twix/settings/component/ProfileInfo.kt new file mode 100644 index 000000000..7af53e006 --- /dev/null +++ b/feature/settings/src/main/java/com/twix/settings/component/ProfileInfo.kt @@ -0,0 +1,90 @@ +package com.twix.settings.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.twix.designsystem.R +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.components.text_field.ValidateUnderlineTextField +import com.twix.designsystem.theme.GrayColor +import com.twix.domain.model.enums.AppTextStyle +import com.twix.ui.extension.noRippleClickable + +@Composable +fun ProfileInfo( + nickname: String, + isEditMode: Boolean, + onCommitNickName: (String) -> Unit, + onEditModeChange: (Boolean) -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.Top, + ) { + Image( + painter = painterResource(R.drawable.ic_profile), + contentDescription = "profile", + modifier = + Modifier + .size(52.dp), + ) + + Column( + horizontalAlignment = Alignment.Start, + ) { + if (isEditMode) { + ValidateUnderlineTextField( + modifier = + Modifier + .height(77.dp), + value = nickname, + onCommit = onCommitNickName, + placeholder = stringResource(R.string.settings_nickname_placeholder), + guideText = stringResource(R.string.settings_nickname_text_filed_guide), + validLengthRange = 2..8, + ) + } else { + Row( + modifier = + Modifier + .height(52.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + Spacer(Modifier.width(16.dp)) + + AppText( + text = nickname, + style = AppTextStyle.T1, + color = GrayColor.C500, + ) + + Image( + painter = painterResource(R.drawable.ic_edit), + contentDescription = null, + modifier = + Modifier + .padding(10.dp) + .size(24.dp) + .noRippleClickable { onEditModeChange(true) }, + ) + } + } + } + } +} \ No newline at end of file From 0fad5cb483416261a7ab87ea9e1e71f755fbf586 Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 28 May 2026 22:39:56 +0900 Subject: [PATCH 11/44] =?UTF-8?q?=F0=9F=8D=B1=20Chore:=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EC=97=B4=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/design-system/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 20817c397..70189d743 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -105,6 +105,7 @@ 평일 오전 9시 - 오후 6시 운영 닉네임을 입력해 주세요. 닉네임 2-8자 + 이미 앱 내에 저장된 언어는 변경되지 않아요 스탬프 통계 From 6c54243822b1f3632af38ee3421361eedb4aa54d Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 28 May 2026 22:40:12 +0900 Subject: [PATCH 12/44] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20lint=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/twix/settings/component/ProfileInfo.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/settings/src/main/java/com/twix/settings/component/ProfileInfo.kt b/feature/settings/src/main/java/com/twix/settings/component/ProfileInfo.kt index 7af53e006..52cc6369c 100644 --- a/feature/settings/src/main/java/com/twix/settings/component/ProfileInfo.kt +++ b/feature/settings/src/main/java/com/twix/settings/component/ProfileInfo.kt @@ -87,4 +87,4 @@ fun ProfileInfo( } } } -} \ No newline at end of file +} From 5d085855fc8890a1563f0080e6873916cbbf7926 Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 28 May 2026 22:40:30 +0900 Subject: [PATCH 13/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=96=B8=EC=96=B4=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EB=A1=9C=EC=A7=81=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 --- .../java/com/twix/settings/SettingsScreen.kt | 288 ++++++++++++------ 1 file changed, 193 insertions(+), 95 deletions(-) diff --git a/feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt index 8efb76045..b827ef2e9 100644 --- a/feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt @@ -2,7 +2,7 @@ package com.twix.settings import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -26,15 +26,17 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.twix.designsystem.R +import com.twix.designsystem.components.dialog.CommonDialog import com.twix.designsystem.components.text.AppText -import com.twix.designsystem.components.text_field.ValidateUnderlineTextField import com.twix.designsystem.components.topbar.CommonTopBar import com.twix.designsystem.theme.CommonColor import com.twix.designsystem.theme.GrayColor import com.twix.designsystem.theme.TwixTheme import com.twix.domain.model.enums.AppTextStyle +import com.twix.settings.component.ProfileInfo import com.twix.settings.component.SettingsMenuFrame import com.twix.settings.component.SettingsMenuItem +import com.twix.settings.model.SettingsLanguage import com.twix.settings.model.SettingsUiState import com.twix.ui.extension.dismissKeyboardOnTap import com.twix.ui.extension.noRippleClickable @@ -46,6 +48,8 @@ fun SettingsRoute( popBackStack: () -> Unit, navigateToSettingsAccount: () -> Unit, navigateToSettingsAbout: () -> Unit, + navigateToSettingsInquiry: () -> Unit, + navigateToSettingsNotification: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -54,6 +58,8 @@ fun SettingsRoute( onBack = popBackStack, onAccountClick = navigateToSettingsAccount, onAboutClick = navigateToSettingsAbout, + onInquiryClick = navigateToSettingsInquiry, + onNotificationClick = navigateToSettingsNotification, onCommitNickName = { viewModel.dispatch(SettingsIntent.SetNickName(it)) }, ) } @@ -64,134 +70,226 @@ private fun SettingsScreen( onBack: () -> Unit = {}, onAccountClick: () -> Unit = {}, onAboutClick: () -> Unit = {}, + onInquiryClick: () -> Unit = {}, + onNotificationClick: () -> Unit = {}, onCommitNickName: (String) -> Unit = {}, ) { var isEditMode by remember { mutableStateOf(false) } + var showLanguageDialog by remember { mutableStateOf(false) } - Column( + Box( modifier = Modifier .fillMaxSize() .dismissKeyboardOnTap(onDismiss = { isEditMode = false }) .background(CommonColor.White), ) { - CommonTopBar( - title = stringResource(R.string.word_setting), - left = { - Image( - painter = painterResource(R.drawable.ic_arrow3_left), - contentDescription = "back", - modifier = - Modifier - .padding(18.dp) - .size(24.dp) - .noRippleClickable(onClick = onBack), - ) - }, - ) - - Spacer(Modifier.height(20.dp)) - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), + modifier = Modifier.fillMaxSize(), ) { - ProfileInfo( - nickname = uiState.nickName, - isEditMode = isEditMode, - onCommitNickName = { - onCommitNickName(it) - isEditMode = false + CommonTopBar( + title = stringResource(R.string.word_setting), + left = { + Image( + painter = painterResource(R.drawable.ic_arrow3_left), + contentDescription = "back", + modifier = + Modifier + .padding(18.dp) + .size(24.dp) + .noRippleClickable(onClick = onBack), + ) }, - onEditModeChange = { isEditMode = it }, ) - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(20.dp)) - SettingsMenuFrame { - SettingsMenuItem( - resId = R.drawable.ic_profile_small, - title = stringResource(R.string.word_account), - onClick = onAccountClick, + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) { + ProfileInfo( + nickname = uiState.nickName, + isEditMode = isEditMode, + onCommitNickName = { + onCommitNickName(it) + isEditMode = false + }, + onEditModeChange = { isEditMode = it }, ) - HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) + Spacer(Modifier.height(24.dp)) - SettingsMenuItem( - resId = R.drawable.ic_info, - title = stringResource(R.string.word_information), - onClick = onAboutClick, - ) + SettingsMenuFrame { + SettingsMenuItem( + resId = R.drawable.ic_language, + title = stringResource(R.string.settings_language), + right = { + LanguageMenuRight(language = SettingsLanguage.Korean) + }, + onClick = { showLanguageDialog = true }, + ) + + SettingsDivider() + + SettingsMenuItem( + resId = R.drawable.ic_profile_small, + title = stringResource(R.string.word_account), + onClick = onAccountClick, + ) + + SettingsDivider() + + SettingsMenuItem( + resId = R.drawable.ic_info, + title = stringResource(R.string.word_information), + onClick = onAboutClick, + ) + + SettingsDivider() + + SettingsMenuItem( + resId = R.drawable.ic_question, + title = stringResource(R.string.settings_inquiry), + right = { + AppText( + text = stringResource(R.string.settings_inquiry_time), + style = AppTextStyle.B2, + color = GrayColor.C500, + ) + }, + onClick = onInquiryClick, + ) + + SettingsDivider() + + SettingsMenuItem( + resId = R.drawable.ic_notification, + title = stringResource(R.string.settings_notification), + onClick = onNotificationClick, + ) + } } } + + LanguageSettingDialog( + visible = showLanguageDialog, + selectedLanguage = SettingsLanguage.Korean, + onDismissRequest = { + showLanguageDialog = false + }, + onConfirm = { + showLanguageDialog = false + }, + onDismiss = { + showLanguageDialog = false + }, + ) } } @Composable -private fun ProfileInfo( - nickname: String, - isEditMode: Boolean, - onCommitNickName: (String) -> Unit, - onEditModeChange: (Boolean) -> Unit, +private fun LanguageSettingDialog( + visible: Boolean, + selectedLanguage: SettingsLanguage, + onDismissRequest: () -> Unit, + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + CommonDialog( + visible = visible, + confirmText = stringResource(R.string.word_completion), + dismissText = stringResource(R.string.word_cancel), + onDismissRequest = onDismissRequest, + onConfirm = onConfirm, + onDismiss = onDismiss, + content = { + AppText( + text = stringResource(R.string.settings_language), + style = AppTextStyle.T1, + color = GrayColor.C500, + ) + + Spacer(Modifier.height(8.dp)) + + AppText( + text = stringResource(R.string.settings_language_description), + style = AppTextStyle.B2, + color = GrayColor.C400, + ) + + Spacer(Modifier.height(24.dp)) + + LanguageDialogItem( + language = SettingsLanguage.Korean, + selected = selectedLanguage == SettingsLanguage.Korean, + ) + }, + ) +} + +@Composable +private fun LanguageDialogItem( + language: SettingsLanguage, + selected: Boolean, ) { Row( modifier = Modifier .fillMaxWidth(), - verticalAlignment = Alignment.Top, + verticalAlignment = Alignment.CenterVertically, ) { Image( - painter = painterResource(R.drawable.ic_profile), - contentDescription = "profile", - modifier = - Modifier - .size(52.dp), + painter = + painterResource( + if (selected) { + R.drawable.ic_checked_you + } else { + R.drawable.ic_empty_check + }, + ), + contentDescription = null, + modifier = Modifier.size(28.dp), ) - Column( - horizontalAlignment = Alignment.Start, - ) { - if (isEditMode) { - ValidateUnderlineTextField( - modifier = - Modifier - .height(77.dp), - value = nickname, - onCommit = onCommitNickName, - placeholder = stringResource(R.string.settings_nickname_placeholder), - guideText = stringResource(R.string.settings_nickname_text_filed_guide), - validLengthRange = 2..8, - ) - } else { - Row( - modifier = - Modifier - .height(52.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - ) { - Spacer(Modifier.width(16.dp)) - - AppText( - text = nickname, - style = AppTextStyle.T1, - color = GrayColor.C500, - ) + Spacer(Modifier.width(8.dp)) - Image( - painter = painterResource(R.drawable.ic_edit), - contentDescription = null, - modifier = - Modifier - .padding(10.dp) - .size(24.dp) - .noRippleClickable { onEditModeChange(true) }, - ) - } - } - } + AppText( + text = language.displayName, + style = AppTextStyle.B2, + color = GrayColor.C500, + ) + } +} + +@Composable +private fun SettingsDivider() { + HorizontalDivider( + thickness = 1.dp, + color = GrayColor.C500, + ) +} + +@Composable +private fun LanguageMenuRight(language: SettingsLanguage = SettingsLanguage.Korean) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + AppText( + text = language.displayName, + style = AppTextStyle.B2, + color = GrayColor.C500, + ) + + Spacer(Modifier.width(8.dp)) + + Image( + painter = painterResource(R.drawable.ic_arrow_down_circle), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) } } From a6c334983bfc0076e3590c60e54b0bcf9c5650aa Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 28 May 2026 23:56:24 +0900 Subject: [PATCH 14/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20openExternalUrl=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/twix/util/extension/ContextExt.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 core/util/src/main/java/com/twix/util/extension/ContextExt.kt diff --git a/core/util/src/main/java/com/twix/util/extension/ContextExt.kt b/core/util/src/main/java/com/twix/util/extension/ContextExt.kt new file mode 100644 index 000000000..a89545ab8 --- /dev/null +++ b/core/util/src/main/java/com/twix/util/extension/ContextExt.kt @@ -0,0 +1,29 @@ +package com.twix.util.extension + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import androidx.core.net.toUri + +fun Context.openExternalUrl( + url: String, + onFailed: () -> Unit = {}, +) { + if (url.isBlank()) { + onFailed() + return + } + + val intent = + Intent(Intent.ACTION_VIEW, url.toUri()).apply { + addCategory(Intent.CATEGORY_BROWSABLE) + } + + try { + startActivity(intent) + } catch (e: ActivityNotFoundException) { + onFailed() + } catch (e: Exception) { + onFailed() + } +} From 791223c91c67a35cf6803603fbf114233c4e2bf8 Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 28 May 2026 23:56:36 +0900 Subject: [PATCH 15/44] =?UTF-8?q?=F0=9F=8D=B1=20Chore:=20string=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/design-system/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 70189d743..856d45011 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -106,6 +106,7 @@ 닉네임을 입력해 주세요. 닉네임 2-8자 이미 앱 내에 저장된 언어는 변경되지 않아요 + 문의하기 화면을 열 수 없어요. 잠시 후 다시 시도해 주세요. 스탬프 통계 From 10d054b542b009b965cf8cc804e8c66f8736660b Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 28 May 2026 23:56:47 +0900 Subject: [PATCH 16/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=98=A4=ED=94=88=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20url=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/settings/build.gradle.kts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index dd2d77af0..b558d35e7 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -24,9 +24,15 @@ android { ?: providers.gradleProperty("privacy_policy_url").orNull ?: "https://incongruous-sweatshirt-b32.notion.site/Keepliuv-3024eb2e10638051824ef9ac7f9a522f" + val kakaoOpenChatUrl = + localProperties.getProperty("kakao_open_chat_url") + ?: providers.gradleProperty("kakao_open_chat_url").orNull + ?: "https://open.kakao.com/o/sTwixHelp" + buildTypes { all { buildConfigField("String", "PRIVACY_POLICY_URL", "\"$privacyPolicyUrl\"") + buildConfigField("String", "KAKAO_OPEN_CHAT_URL", "\"$kakaoOpenChatUrl\"") } } } From 91e0ac832d0c3a1d62c1830c4b61363248ff86cc Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 28 May 2026 23:57:07 +0900 Subject: [PATCH 17/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=AC=B8=EC=9D=98?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EC=98=A4=ED=94=88=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EB=B0=A9=20=EC=9D=B4=EB=8F=99=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/twix/settings/SettingsScreen.kt | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt index b827ef2e9..b6958989a 100644 --- a/feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/twix/settings/SettingsScreen.kt @@ -17,9 +17,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -28,6 +30,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.twix.designsystem.R import com.twix.designsystem.components.dialog.CommonDialog import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.components.toast.ToastManager +import com.twix.designsystem.components.toast.model.ToastData +import com.twix.designsystem.components.toast.model.ToastType import com.twix.designsystem.components.topbar.CommonTopBar import com.twix.designsystem.theme.CommonColor import com.twix.designsystem.theme.GrayColor @@ -40,17 +45,21 @@ import com.twix.settings.model.SettingsLanguage import com.twix.settings.model.SettingsUiState import com.twix.ui.extension.dismissKeyboardOnTap import com.twix.ui.extension.noRippleClickable +import com.twix.util.extension.openExternalUrl import org.koin.androidx.compose.koinViewModel +import org.koin.compose.koinInject @Composable fun SettingsRoute( viewModel: SettingsViewModel = koinViewModel(), + toastManager: ToastManager = koinInject(), popBackStack: () -> Unit, navigateToSettingsAccount: () -> Unit, navigateToSettingsAbout: () -> Unit, - navigateToSettingsInquiry: () -> Unit, navigateToSettingsNotification: () -> Unit, ) { + val context = LocalContext.current + val currentContext by rememberUpdatedState(context) val uiState by viewModel.uiState.collectAsStateWithLifecycle() SettingsScreen( @@ -58,7 +67,19 @@ fun SettingsRoute( onBack = popBackStack, onAccountClick = navigateToSettingsAccount, onAboutClick = navigateToSettingsAbout, - onInquiryClick = navigateToSettingsInquiry, + onInquiryClick = { + currentContext.openExternalUrl( + url = BuildConfig.KAKAO_OPEN_CHAT_URL, + onFailed = { + toastManager.tryShow( + ToastData( + message = currentContext.getString(R.string.settings_inquiry_open_failed), + type = ToastType.ERROR, + ), + ) + }, + ) + }, onNotificationClick = navigateToSettingsNotification, onCommitNickName = { viewModel.dispatch(SettingsIntent.SetNickName(it)) }, ) From 68199b83860e9f23757158e24d879e56842f7c0e Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 00:23:40 +0900 Subject: [PATCH 18/44] =?UTF-8?q?=F0=9F=8D=B1=20Chore:=20String=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/design-system/src/main/res/values/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 856d45011..19807d864 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -107,6 +107,9 @@ 닉네임 2-8자 이미 앱 내에 저장된 언어는 변경되지 않아요 문의하기 화면을 열 수 없어요. 잠시 후 다시 시도해 주세요. + 찌르기 푸쉬알림 + 마케팅 정보 푸쉬알림 + 야간 마케팅 정보 푸쉬알림 스탬프 통계 From bd835dfc0fe7e1208911948357ea97f0c8bd1d58 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 00:24:10 +0900 Subject: [PATCH 19/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20Intent=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/twix/settings/SettingsIntent.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/feature/settings/src/main/java/com/twix/settings/SettingsIntent.kt b/feature/settings/src/main/java/com/twix/settings/SettingsIntent.kt index ba5cc95d5..a3f22d361 100644 --- a/feature/settings/src/main/java/com/twix/settings/SettingsIntent.kt +++ b/feature/settings/src/main/java/com/twix/settings/SettingsIntent.kt @@ -7,6 +7,18 @@ sealed interface SettingsIntent : Intent { val nickName: String, ) : SettingsIntent + data class SetPokeNotificationEnabled( + val enabled: Boolean, + ) : SettingsIntent + + data class SetMarketingNotificationEnabled( + val enabled: Boolean, + ) : SettingsIntent + + data class SetNightMarketingNotificationEnabled( + val enabled: Boolean, + ) : SettingsIntent + data object Logout : SettingsIntent data object WithdrawAccount : SettingsIntent From ac959a2f81db0d937d614314eb10705187db9afb Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 00:24:22 +0900 Subject: [PATCH 20/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=83=81=ED=83=9C=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/twix/settings/model/SettingsUiState.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt b/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt index fa50ef184..18441d629 100644 --- a/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt +++ b/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt @@ -6,6 +6,9 @@ import com.twix.ui.base.LoadableState data class SettingsUiState( val nickName: String = "", val email: String = "", + val pokeNotificationEnabled: Boolean = true, + val marketingNotificationEnabled: Boolean = false, + val nightMarketingNotificationEnabled: Boolean = false, override val isLoading: Boolean = false, override val error: AppError? = null, ) : LoadableState { From 780553049a5322cdc1ed99d60ee2cd632cbdcecb Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 00:24:32 +0900 Subject: [PATCH 21/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SettingsNotificationScreen.kt | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 feature/settings/src/main/java/com/twix/settings/notification/SettingsNotificationScreen.kt diff --git a/feature/settings/src/main/java/com/twix/settings/notification/SettingsNotificationScreen.kt b/feature/settings/src/main/java/com/twix/settings/notification/SettingsNotificationScreen.kt new file mode 100644 index 000000000..acd12280c --- /dev/null +++ b/feature/settings/src/main/java/com/twix/settings/notification/SettingsNotificationScreen.kt @@ -0,0 +1,159 @@ +package com.twix.settings.notification + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +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.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.twix.designsystem.R +import com.twix.designsystem.components.common.CommonSwitch +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.components.topbar.CommonTopBar +import com.twix.designsystem.theme.CommonColor +import com.twix.designsystem.theme.GrayColor +import com.twix.designsystem.theme.TwixTheme +import com.twix.domain.model.enums.AppTextStyle +import com.twix.settings.SettingsIntent +import com.twix.settings.SettingsViewModel +import com.twix.settings.component.SettingsMenuFrame +import com.twix.settings.model.SettingsUiState +import com.twix.ui.extension.noRippleClickable + +@Composable +fun SettingsNotificationRoute( + viewModel: SettingsViewModel, + popBackStack: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + SettingsNotificationScreen( + uiState = uiState, + onBack = popBackStack, + onPokeNotificationChange = { + viewModel.dispatch(SettingsIntent.SetPokeNotificationEnabled(it)) + }, + onMarketingNotificationChange = { + viewModel.dispatch(SettingsIntent.SetMarketingNotificationEnabled(it)) + }, + onNightMarketingNotificationChange = { + viewModel.dispatch(SettingsIntent.SetNightMarketingNotificationEnabled(it)) + }, + ) +} + +@Composable +private fun SettingsNotificationScreen( + uiState: SettingsUiState = SettingsUiState(), + onBack: () -> Unit = {}, + onPokeNotificationChange: (Boolean) -> Unit = {}, + onMarketingNotificationChange: (Boolean) -> Unit = {}, + onNightMarketingNotificationChange: (Boolean) -> Unit = {}, +) { + Column( + modifier = + Modifier + .fillMaxSize() + .background(CommonColor.White), + ) { + CommonTopBar( + title = stringResource(R.string.settings_notification), + left = { + Image( + painter = painterResource(R.drawable.ic_arrow3_left), + contentDescription = "back", + modifier = + Modifier + .padding(18.dp) + .noRippleClickable(onClick = onBack), + ) + }, + ) + + Spacer(Modifier.height(20.dp)) + + SettingsMenuFrame( + modifier = Modifier.padding(horizontal = 20.dp), + ) { + NotificationSettingItem( + title = stringResource(R.string.settings_poke_push_notification), + checked = uiState.pokeNotificationEnabled, + onCheckedChange = onPokeNotificationChange, + ) + + SettingsNotificationDivider() + + NotificationSettingItem( + title = stringResource(R.string.settings_marketing_push_notification), + checked = uiState.marketingNotificationEnabled, + onCheckedChange = onMarketingNotificationChange, + ) + + SettingsNotificationDivider() + + NotificationSettingItem( + title = stringResource(R.string.settings_night_marketing_push_notification), + checked = uiState.nightMarketingNotificationEnabled, + onCheckedChange = onNightMarketingNotificationChange, + ) + } + } +} + +@Composable +private fun NotificationSettingItem( + title: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .height(60.dp) + .padding(start = 16.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AppText( + text = title, + style = AppTextStyle.B1, + color = GrayColor.C500, + ) + + Spacer(Modifier.weight(1f)) + + CommonSwitch( + checked = checked, + onClick = onCheckedChange, + ) + } +} + +@Composable +private fun SettingsNotificationDivider() { + HorizontalDivider( + thickness = 1.dp, + color = GrayColor.C500, + ) +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +private fun SettingsNotificationScreenPreview() { + TwixTheme { + SettingsNotificationScreen() + } +} From 942e9372080d411ac05dc4ebc3c841a976a41b20 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 00:24:52 +0900 Subject: [PATCH 22/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20->=20=EC=95=8C=EB=A6=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=99=94=EB=A9=B4=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/twix/navigation/NavRoutes.kt | 2 ++ .../settings/navigation/SettingsNavGraph.kt | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt b/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt index a1c041d00..4cfa538b4 100644 --- a/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt +++ b/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt @@ -135,6 +135,8 @@ sealed class NavRoutes( object SettingsAboutRoute : NavRoutes("settings/about") + object SettingsNotificationRoute : NavRoutes("settings/notification") + /** * StatsGraph * */ diff --git a/feature/settings/src/main/java/com/twix/settings/navigation/SettingsNavGraph.kt b/feature/settings/src/main/java/com/twix/settings/navigation/SettingsNavGraph.kt index d37603d31..3fc426be1 100644 --- a/feature/settings/src/main/java/com/twix/settings/navigation/SettingsNavGraph.kt +++ b/feature/settings/src/main/java/com/twix/settings/navigation/SettingsNavGraph.kt @@ -11,6 +11,7 @@ import com.twix.settings.SettingsRoute import com.twix.settings.SettingsViewModel import com.twix.settings.about.SettingsAboutRoute import com.twix.settings.account.SettingsAccountRoute +import com.twix.settings.notification.SettingsNotificationRoute import org.koin.androidx.compose.koinViewModel object SettingsNavGraph : NavGraphContributor { @@ -46,6 +47,11 @@ object SettingsNavGraph : NavGraphContributor { launchSingleTop = true } }, + navigateToSettingsNotification = { + navController.navigate(NavRoutes.SettingsNotificationRoute.route) { + launchSingleTop = true + } + }, ) } @@ -77,6 +83,21 @@ object SettingsNavGraph : NavGraphContributor { popBackStack = { navController.popBackStack() }, ) } + + composable(NavRoutes.SettingsNotificationRoute.route) { entry -> + val graphEntry = + rememberNavGraphOwner( + navController = navController, + graphRoute = NavRoutes.SettingsGraph.route, + currentEntry = entry, + ) + val viewModel: SettingsViewModel = koinViewModel(viewModelStoreOwner = graphEntry) + + SettingsNotificationRoute( + viewModel = viewModel, + popBackStack = { navController.popBackStack() }, + ) + } } } } From 590ffdaabf57352006be7fd1a0daa4a83abf4925 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 18:10:08 +0900 Subject: [PATCH 23/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../twix/domain/model/notification/NotificationSettings.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 domain/src/main/java/com/twix/domain/model/notification/NotificationSettings.kt diff --git a/domain/src/main/java/com/twix/domain/model/notification/NotificationSettings.kt b/domain/src/main/java/com/twix/domain/model/notification/NotificationSettings.kt new file mode 100644 index 000000000..415b6f506 --- /dev/null +++ b/domain/src/main/java/com/twix/domain/model/notification/NotificationSettings.kt @@ -0,0 +1,7 @@ +package com.twix.domain.model.notification + +data class NotificationSettings( + val isPushEnabled: Boolean, + val isMarketingPushEnabled: Boolean, + val isNightPushEnabled: Boolean, +) From 680fb5870333242046253fefa7bea173d584cb70 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 18:10:21 +0900 Subject: [PATCH 24/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=9D=91=EB=8B=B5=20DTO=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/model/NotificationSettingsResponse.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 core/network/src/main/java/com/twix/network/model/response/notification/model/NotificationSettingsResponse.kt diff --git a/core/network/src/main/java/com/twix/network/model/response/notification/model/NotificationSettingsResponse.kt b/core/network/src/main/java/com/twix/network/model/response/notification/model/NotificationSettingsResponse.kt new file mode 100644 index 000000000..8cbc0d667 --- /dev/null +++ b/core/network/src/main/java/com/twix/network/model/response/notification/model/NotificationSettingsResponse.kt @@ -0,0 +1,10 @@ +package com.twix.network.model.response.notification.model + +import kotlinx.serialization.Serializable + +@Serializable +data class NotificationSettingsResponse( + val isPushEnabled: Boolean, + val isMarketingPushEnabled: Boolean, + val isNightPushEnabled: Boolean, +) From 41868468003165c163ecefc48ca59ebf0cbafec3 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 18:10:32 +0900 Subject: [PATCH 25/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?DTO=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/UpdateNotificationSettingRequest.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 core/network/src/main/java/com/twix/network/model/request/notification/UpdateNotificationSettingRequest.kt diff --git a/core/network/src/main/java/com/twix/network/model/request/notification/UpdateNotificationSettingRequest.kt b/core/network/src/main/java/com/twix/network/model/request/notification/UpdateNotificationSettingRequest.kt new file mode 100644 index 000000000..b8086f0ec --- /dev/null +++ b/core/network/src/main/java/com/twix/network/model/request/notification/UpdateNotificationSettingRequest.kt @@ -0,0 +1,8 @@ +package com.twix.network.model.request.notification + +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateNotificationSettingRequest( + val enabled: Boolean, +) From 43c83f3791a88b2565c18cf1f696ccef3bb5e4f3 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 18:10:45 +0900 Subject: [PATCH 26/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=9D=91=EB=8B=B5=20Mapper=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/notification/mapper/NotificationMapper.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/network/src/main/java/com/twix/network/model/response/notification/mapper/NotificationMapper.kt b/core/network/src/main/java/com/twix/network/model/response/notification/mapper/NotificationMapper.kt index 0df92ca63..5d7645922 100644 --- a/core/network/src/main/java/com/twix/network/model/response/notification/mapper/NotificationMapper.kt +++ b/core/network/src/main/java/com/twix/network/model/response/notification/mapper/NotificationMapper.kt @@ -3,8 +3,10 @@ package com.twix.network.model.response.notification.mapper import com.twix.domain.model.enums.NotificationType import com.twix.domain.model.notification.Notification import com.twix.domain.model.notification.NotificationPage +import com.twix.domain.model.notification.NotificationSettings import com.twix.network.model.response.notification.model.NotificationListResponse import com.twix.network.model.response.notification.model.NotificationResponse +import com.twix.network.model.response.notification.model.NotificationSettingsResponse import java.time.LocalDateTime import java.time.OffsetDateTime @@ -25,4 +27,11 @@ fun NotificationResponse.toDomain(): Notification = createdAt = createdAt.toLocalDateTimeOrNull(), ) +fun NotificationSettingsResponse.toDomain(): NotificationSettings = + NotificationSettings( + isPushEnabled = isPushEnabled, + isMarketingPushEnabled = isMarketingPushEnabled, + isNightPushEnabled = isNightPushEnabled, + ) + private fun String.toLocalDateTimeOrNull(): LocalDateTime? = runCatching { OffsetDateTime.parse(this).toLocalDateTime() }.getOrNull() From 34467481aea97baee1e716d204df0d29ecc7e7d1 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 18:10:58 +0900 Subject: [PATCH 27/44] =?UTF-8?q?=F0=9F=8D=B1=20Chore:=20string=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/design-system/src/main/res/values/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 19807d864..531306c62 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -143,6 +143,8 @@ 이미 인증되었어요. 인증샷 촬영을 위해서 카메라 권한이 필요해요. 알림 목록 조회에 실패했습니다. + 알림 설정 조회에 실패했습니다. + 알림 설정 변경에 실패했습니다. 상대방을 찔렀어요! 찌르기에 실패했습니다. %1$s 뒤에 다시 찌를 수 있어요 From a56a85a40deab7d2b8a04abd0098fb9d1bcae54d Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 18:11:19 +0900 Subject: [PATCH 28/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A1=B0=ED=9A=8C,=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20api=20=ED=86=B5=EC=8B=A0=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../network/service/NotificationService.kt | 20 ++++++++++++++++ .../DefaultNotificationRepository.kt | 24 +++++++++++++++++++ .../repository/NotificationRepository.kt | 9 +++++++ 3 files changed, 53 insertions(+) diff --git a/core/network/src/main/java/com/twix/network/service/NotificationService.kt b/core/network/src/main/java/com/twix/network/service/NotificationService.kt index 409ca05c0..f7be92ff5 100644 --- a/core/network/src/main/java/com/twix/network/service/NotificationService.kt +++ b/core/network/src/main/java/com/twix/network/service/NotificationService.kt @@ -3,7 +3,9 @@ package com.twix.network.service import com.twix.network.model.request.notification.InitNotificationSettingsRequest import com.twix.network.model.request.notification.RegisterFcmTokenRequest import com.twix.network.model.request.notification.TokenRequest +import com.twix.network.model.request.notification.UpdateNotificationSettingRequest import com.twix.network.model.response.notification.model.NotificationListResponse +import com.twix.network.model.response.notification.model.NotificationSettingsResponse import de.jensklingenberg.ktorfit.http.Body import de.jensklingenberg.ktorfit.http.DELETE import de.jensklingenberg.ktorfit.http.GET @@ -33,6 +35,24 @@ interface NotificationService { @Body request: InitNotificationSettingsRequest, ) + @GET("api/v1/notifications/settings") + suspend fun fetchNotificationSettings(): NotificationSettingsResponse + + @PATCH("api/v1/notifications/settings/poke") + suspend fun updatePokeNotificationSetting( + @Body request: UpdateNotificationSettingRequest, + ): NotificationSettingsResponse + + @PATCH("api/v1/notifications/settings/marketing") + suspend fun updateMarketingNotificationSetting( + @Body request: UpdateNotificationSettingRequest, + ): NotificationSettingsResponse + + @PATCH("api/v1/notifications/settings/night") + suspend fun updateNightNotificationSetting( + @Body request: UpdateNotificationSettingRequest, + ): NotificationSettingsResponse + @GET("api/v1/notifications") suspend fun fetchNotifications( @Query("lastId") lastId: Long? = null, diff --git a/data/src/main/java/com/twix/data/repository/DefaultNotificationRepository.kt b/data/src/main/java/com/twix/data/repository/DefaultNotificationRepository.kt index e2aa91042..cd9c54e66 100644 --- a/data/src/main/java/com/twix/data/repository/DefaultNotificationRepository.kt +++ b/data/src/main/java/com/twix/data/repository/DefaultNotificationRepository.kt @@ -5,6 +5,7 @@ import com.twix.network.execute.safeApiCall import com.twix.network.model.request.notification.InitNotificationSettingsRequest import com.twix.network.model.request.notification.RegisterFcmTokenRequest import com.twix.network.model.request.notification.TokenRequest +import com.twix.network.model.request.notification.UpdateNotificationSettingRequest import com.twix.network.model.response.notification.mapper.toDomain import com.twix.network.service.NotificationService @@ -34,6 +35,29 @@ class DefaultNotificationRepository( ) } + override suspend fun fetchNotificationSettings() = safeApiCall { service.fetchNotificationSettings().toDomain() } + + override suspend fun updatePokeNotificationSetting(enabled: Boolean) = + safeApiCall { + service + .updatePokeNotificationSetting(UpdateNotificationSettingRequest(enabled)) + .toDomain() + } + + override suspend fun updateMarketingNotificationSetting(enabled: Boolean) = + safeApiCall { + service + .updateMarketingNotificationSetting(UpdateNotificationSettingRequest(enabled)) + .toDomain() + } + + override suspend fun updateNightNotificationSetting(enabled: Boolean) = + safeApiCall { + service + .updateNightNotificationSetting(UpdateNotificationSettingRequest(enabled)) + .toDomain() + } + override suspend fun fetchNotifications( lastId: Long?, size: Int, diff --git a/domain/src/main/java/com/twix/domain/repository/NotificationRepository.kt b/domain/src/main/java/com/twix/domain/repository/NotificationRepository.kt index 9d8636dc3..6c615eae0 100644 --- a/domain/src/main/java/com/twix/domain/repository/NotificationRepository.kt +++ b/domain/src/main/java/com/twix/domain/repository/NotificationRepository.kt @@ -1,6 +1,7 @@ package com.twix.domain.repository import com.twix.domain.model.notification.NotificationPage +import com.twix.domain.model.notification.NotificationSettings import com.twix.result.AppResult interface NotificationRepository { @@ -19,6 +20,14 @@ interface NotificationRepository { isNightPushEnabled: Boolean, ): AppResult + suspend fun fetchNotificationSettings(): AppResult + + suspend fun updatePokeNotificationSetting(enabled: Boolean): AppResult + + suspend fun updateMarketingNotificationSetting(enabled: Boolean): AppResult + + suspend fun updateNightNotificationSetting(enabled: Boolean): AppResult + suspend fun fetchNotifications( lastId: Long? = null, size: Int = 20, From 9489f9c56608e7916a4e38af343a67b9e248f4f4 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 18:11:53 +0900 Subject: [PATCH 29/44] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B3=80=EC=88=98=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/twix/settings/model/SettingsUiState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt b/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt index 18441d629..ed3a01fd8 100644 --- a/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt +++ b/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt @@ -6,7 +6,7 @@ import com.twix.ui.base.LoadableState data class SettingsUiState( val nickName: String = "", val email: String = "", - val pokeNotificationEnabled: Boolean = true, + val pokeNotificationEnabled: Boolean = false, val marketingNotificationEnabled: Boolean = false, val nightMarketingNotificationEnabled: Boolean = false, override val isLoading: Boolean = false, From 079178e0a086db3156e6d784613ae1a51c42e5ee Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 18:12:11 +0900 Subject: [PATCH 30/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A1=B0=ED=9A=8C,=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20=EC=83=81=ED=83=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/twix/settings/SettingsViewModel.kt | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt index 178393ce4..3396a6cfd 100644 --- a/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt @@ -3,6 +3,7 @@ package com.twix.settings import com.twix.designsystem.R import com.twix.designsystem.components.toast.model.ToastType import com.twix.domain.repository.AuthRepository +import com.twix.domain.repository.NotificationRepository import com.twix.domain.repository.OnBoardingRepository import com.twix.domain.repository.UserRepository import com.twix.notification.token.NotificationTokenRegistrar @@ -14,19 +15,53 @@ class SettingsViewModel( private val authRepository: AuthRepository, private val tokenRegistrar: NotificationTokenRegistrar, private val onBoardingRepository: OnBoardingRepository, + private val notificationRepository: NotificationRepository, ) : BaseViewModel(SettingsUiState()) { init { fetchUserInfo() + fetchNotificationSetting() } override suspend fun handleIntent(intent: SettingsIntent) { when (intent) { is SettingsIntent.SetNickName -> setNickName(intent.nickName) + is SettingsIntent.SetPokeNotificationEnabled -> { + setPokeNotificationEnabled(intent.enabled) + } + + is SettingsIntent.SetMarketingNotificationEnabled -> { + setMarketingNotificationEnabled(intent.enabled) + } + + is SettingsIntent.SetNightMarketingNotificationEnabled -> { + setNightMarketingNotificationEnabled(intent.enabled) + } SettingsIntent.Logout -> logout() SettingsIntent.WithdrawAccount -> withdrawAccount() } } + private fun fetchNotificationSetting() { + launchResult( + block = { notificationRepository.fetchNotificationSettings() }, + onSuccess = { setting -> + reduceNotificationSettings( + isPushEnabled = setting.isPushEnabled, + isMarketingPushEnabled = setting.isMarketingPushEnabled, + isNightPushEnabled = setting.isNightPushEnabled, + ) + }, + onError = { + tryEmitSideEffect( + SettingsSideEffect.ShowToast( + R.string.toast_notification_setting_load_failed, + ToastType.ERROR, + ), + ) + }, + ) + } + private fun fetchUserInfo() { launchResult( block = { userRepository.fetchUserInfo() }, @@ -45,6 +80,112 @@ class SettingsViewModel( ) } + private fun setPokeNotificationEnabled(enabled: Boolean) { + val originalState = currentState + + reduce { + copy(pokeNotificationEnabled = enabled) + } + + launchResult( + block = { notificationRepository.updatePokeNotificationSetting(enabled) }, + onSuccess = { setting -> + reduceNotificationSettings( + isPushEnabled = setting.isPushEnabled, + isMarketingPushEnabled = setting.isMarketingPushEnabled, + isNightPushEnabled = setting.isNightPushEnabled, + ) + }, + onError = { + restoreNotificationSettings(originalState) + tryEmitSideEffect( + SettingsSideEffect.ShowToast( + R.string.toast_notification_setting_update_failed, + ToastType.ERROR, + ), + ) + }, + ) + } + + private fun setMarketingNotificationEnabled(enabled: Boolean) { + val originalState = currentState + + reduce { + copy(marketingNotificationEnabled = enabled) + } + + launchResult( + block = { notificationRepository.updateMarketingNotificationSetting(enabled) }, + onSuccess = { setting -> + reduceNotificationSettings( + isPushEnabled = setting.isPushEnabled, + isMarketingPushEnabled = setting.isMarketingPushEnabled, + isNightPushEnabled = setting.isNightPushEnabled, + ) + }, + onError = { + restoreNotificationSettings(originalState) + tryEmitSideEffect( + SettingsSideEffect.ShowToast( + R.string.toast_notification_setting_update_failed, + ToastType.ERROR, + ), + ) + }, + ) + } + + private fun setNightMarketingNotificationEnabled(enabled: Boolean) { + val originalState = currentState + + reduce { + copy(nightMarketingNotificationEnabled = enabled) + } + + launchResult( + block = { notificationRepository.updateNightNotificationSetting(enabled) }, + onSuccess = { setting -> + reduceNotificationSettings( + isPushEnabled = setting.isPushEnabled, + isMarketingPushEnabled = setting.isMarketingPushEnabled, + isNightPushEnabled = setting.isNightPushEnabled, + ) + }, + onError = { + restoreNotificationSettings(originalState) + tryEmitSideEffect( + SettingsSideEffect.ShowToast( + R.string.toast_notification_setting_update_failed, + ToastType.ERROR, + ), + ) + }, + ) + } + + private fun reduceNotificationSettings( + isPushEnabled: Boolean, + isMarketingPushEnabled: Boolean, + isNightPushEnabled: Boolean, + ) { + reduce { + copy( + pokeNotificationEnabled = isPushEnabled, + marketingNotificationEnabled = isMarketingPushEnabled, + nightMarketingNotificationEnabled = isNightPushEnabled, + ) + } + } + + private fun restoreNotificationSettings(state: SettingsUiState) { + reduceNotificationSettings( + isPushEnabled = state.pokeNotificationEnabled, + isMarketingPushEnabled = state.marketingNotificationEnabled, + isNightPushEnabled = state.nightMarketingNotificationEnabled, + ) + } + private fun logout() { launchResult( block = { authRepository.logout() }, From ca3f2f3eb90a4dd08c552973854886924da06465 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 18:41:54 +0900 Subject: [PATCH 31/44] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20enabled?= =?UTF-8?q?=20=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/twix/designsystem/components/common/CommonSwitch.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/common/CommonSwitch.kt b/core/design-system/src/main/java/com/twix/designsystem/components/common/CommonSwitch.kt index ac156819c..2e4f7275e 100644 --- a/core/design-system/src/main/java/com/twix/designsystem/components/common/CommonSwitch.kt +++ b/core/design-system/src/main/java/com/twix/designsystem/components/common/CommonSwitch.kt @@ -27,6 +27,7 @@ import kotlin.math.roundToInt fun CommonSwitch( modifier: Modifier = Modifier, checked: Boolean, + enabled: Boolean = true, onClick: (Boolean) -> Unit, ) { val density = LocalDensity.current @@ -45,7 +46,10 @@ fun CommonSwitch( .clip(RoundedCornerShape(999.dp)) .background(if (checked) GrayColor.C500 else CommonColor.White) .border(1.dp, GrayColor.C500, RoundedCornerShape(999.dp)) - .clickable(onClick = { onClick(!checked) }), + .clickable( + enabled = enabled, + onClick = { onClick(!checked) }, + ), contentAlignment = Alignment.CenterStart, ) { Box( From f7efbcd47c8a253e9de1fe25e2739e4de6e49eea Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 18:42:31 +0900 Subject: [PATCH 32/44] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=20=EA=B8=B8=EC=9D=B4=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/text_field/ValidateUnderlineTextField.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/text_field/ValidateUnderlineTextField.kt b/core/design-system/src/main/java/com/twix/designsystem/components/text_field/ValidateUnderlineTextField.kt index bfc2d20fd..6bfeeff6c 100644 --- a/core/design-system/src/main/java/com/twix/designsystem/components/text_field/ValidateUnderlineTextField.kt +++ b/core/design-system/src/main/java/com/twix/designsystem/components/text_field/ValidateUnderlineTextField.kt @@ -58,7 +58,7 @@ fun ValidateUnderlineTextField( fun commitIfChanged() { val trimmed = internalValue.trim() - if (trimmed != lastCommitted) { + if (trimmed.length in validLengthRange && trimmed != lastCommitted) { lastCommitted = trimmed onCommit(trimmed) } From 2c40bb68abd5444d81acd826a0159965c5843d57 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 18:43:20 +0900 Subject: [PATCH 33/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EC=A4=91=20=EB=B0=A9=EC=96=B4=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/twix/settings/SettingsViewModel.kt | 24 ++++++++++++++++--- .../twix/settings/model/SettingsUiState.kt | 1 + 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt index 3396a6cfd..7ef49afd3 100644 --- a/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt @@ -81,13 +81,19 @@ class SettingsViewModel( } private fun setPokeNotificationEnabled(enabled: Boolean) { + if (currentState.notificationSettingsUpdating) return + val originalState = currentState reduce { - copy(pokeNotificationEnabled = enabled) + copy( + pokeNotificationEnabled = enabled, + notificationSettingsUpdating = true, + ) } launchResult( + onFinally = { reduce { copy(notificationSettingsUpdating = false) } }, block = { notificationRepository.updatePokeNotificationSetting(enabled) }, onSuccess = { setting -> reduceNotificationSettings( @@ -109,13 +115,19 @@ class SettingsViewModel( } private fun setMarketingNotificationEnabled(enabled: Boolean) { + if (currentState.notificationSettingsUpdating) return + val originalState = currentState reduce { - copy(marketingNotificationEnabled = enabled) + copy( + marketingNotificationEnabled = enabled, + notificationSettingsUpdating = true, + ) } launchResult( + onFinally = { reduce { copy(notificationSettingsUpdating = false) } }, block = { notificationRepository.updateMarketingNotificationSetting(enabled) }, onSuccess = { setting -> reduceNotificationSettings( @@ -137,13 +149,19 @@ class SettingsViewModel( } private fun setNightMarketingNotificationEnabled(enabled: Boolean) { + if (currentState.notificationSettingsUpdating) return + val originalState = currentState reduce { - copy(nightMarketingNotificationEnabled = enabled) + copy( + nightMarketingNotificationEnabled = enabled, + notificationSettingsUpdating = true, + ) } launchResult( + onFinally = { reduce { copy(notificationSettingsUpdating = false) } }, block = { notificationRepository.updateNightNotificationSetting(enabled) }, onSuccess = { setting -> reduceNotificationSettings( diff --git a/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt b/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt index ed3a01fd8..329dd5472 100644 --- a/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt +++ b/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt @@ -9,6 +9,7 @@ data class SettingsUiState( val pokeNotificationEnabled: Boolean = false, val marketingNotificationEnabled: Boolean = false, val nightMarketingNotificationEnabled: Boolean = false, + val notificationSettingsUpdating: Boolean = false, override val isLoading: Boolean = false, override val error: AppError? = null, ) : LoadableState { From 21fb17b483623b428abc35d5db7caf4f66f03fef Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 29 May 2026 18:43:44 +0900 Subject: [PATCH 34/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20SideEffect=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SettingsNotificationScreen.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/feature/settings/src/main/java/com/twix/settings/notification/SettingsNotificationScreen.kt b/feature/settings/src/main/java/com/twix/settings/notification/SettingsNotificationScreen.kt index acd12280c..1a5765317 100644 --- a/feature/settings/src/main/java/com/twix/settings/notification/SettingsNotificationScreen.kt +++ b/feature/settings/src/main/java/com/twix/settings/notification/SettingsNotificationScreen.kt @@ -12,8 +12,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -22,23 +24,38 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.twix.designsystem.R import com.twix.designsystem.components.common.CommonSwitch import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.components.toast.ToastManager +import com.twix.designsystem.components.toast.model.ToastData import com.twix.designsystem.components.topbar.CommonTopBar import com.twix.designsystem.theme.CommonColor import com.twix.designsystem.theme.GrayColor import com.twix.designsystem.theme.TwixTheme import com.twix.domain.model.enums.AppTextStyle import com.twix.settings.SettingsIntent +import com.twix.settings.SettingsSideEffect import com.twix.settings.SettingsViewModel import com.twix.settings.component.SettingsMenuFrame import com.twix.settings.model.SettingsUiState +import com.twix.ui.base.ObserveAsEvents import com.twix.ui.extension.noRippleClickable +import org.koin.compose.koinInject @Composable fun SettingsNotificationRoute( viewModel: SettingsViewModel, + toastManager: ToastManager = koinInject(), popBackStack: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current + val currentContext by rememberUpdatedState(context) + + ObserveAsEvents(viewModel.sideEffect) { effect -> + when (effect) { + is SettingsSideEffect.ShowToast -> toastManager.tryShow(ToastData(currentContext.getString(effect.resId), effect.type)) + else -> Unit + } + } SettingsNotificationScreen( uiState = uiState, @@ -91,6 +108,7 @@ private fun SettingsNotificationScreen( NotificationSettingItem( title = stringResource(R.string.settings_poke_push_notification), checked = uiState.pokeNotificationEnabled, + enabled = !uiState.notificationSettingsUpdating, onCheckedChange = onPokeNotificationChange, ) @@ -99,6 +117,7 @@ private fun SettingsNotificationScreen( NotificationSettingItem( title = stringResource(R.string.settings_marketing_push_notification), checked = uiState.marketingNotificationEnabled, + enabled = !uiState.notificationSettingsUpdating, onCheckedChange = onMarketingNotificationChange, ) @@ -107,6 +126,7 @@ private fun SettingsNotificationScreen( NotificationSettingItem( title = stringResource(R.string.settings_night_marketing_push_notification), checked = uiState.nightMarketingNotificationEnabled, + enabled = !uiState.notificationSettingsUpdating, onCheckedChange = onNightMarketingNotificationChange, ) } @@ -117,6 +137,7 @@ private fun SettingsNotificationScreen( private fun NotificationSettingItem( title: String, checked: Boolean, + enabled: Boolean, onCheckedChange: (Boolean) -> Unit, ) { Row( @@ -137,6 +158,7 @@ private fun NotificationSettingItem( CommonSwitch( checked = checked, + enabled = enabled, onClick = onCheckedChange, ) } From 8f6df7ee8323ece5dcbe699e2d3c6b80d6b56e36 Mon Sep 17 00:00:00 2001 From: dogmania Date: Mon, 1 Jun 2026 23:44:52 +0900 Subject: [PATCH 35/44] =?UTF-8?q?=F0=9F=8D=B1=20Chore:=20String=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/design-system/src/main/res/values/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 531306c62..2ac097938 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -26,6 +26,7 @@ 찌르기 알림 NEW + 커플코드 매일 매주 @@ -56,6 +57,8 @@ 탈퇴하기 찌르기 찌르기! + 커플 끊기 + 연결 끊기 오늘 우리 목표 @@ -169,6 +172,8 @@ 저장된 인증샷은 모두 삭제됩니다. 정말 탈퇴하시겠어요? 커플 연결이 끊어집니다.\n데이터는 전부 삭제되며 복구가 불가능합니다. + 정말 커플을 끊으시겠어요? + 오늘부로 30일 후, 모든 데이터가 삭제됩니다.\n복구 가능 기간은 30일 이내입니다.\n복구 희망시 ttwixteamm@gmail.com로 문의해 주시기 바랍니다. 도움이 되는 정보를\n알림으로 받아보시겠어요? From 624a53284b63ef426b584d44ee3daa35a12e1fde Mon Sep 17 00:00:00 2001 From: dogmania Date: Mon, 1 Jun 2026 23:45:15 +0900 Subject: [PATCH 36/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20inviteCode=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/twix/settings/SettingsViewModel.kt | 2 +- .../src/main/java/com/twix/settings/model/SettingsUiState.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt index 7ef49afd3..1794307c8 100644 --- a/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/twix/settings/SettingsViewModel.kt @@ -65,7 +65,7 @@ class SettingsViewModel( private fun fetchUserInfo() { launchResult( block = { userRepository.fetchUserInfo() }, - onSuccess = { reduce { copy(nickName = it.name, email = it.email) } }, + onSuccess = { reduce { copy(nickName = it.name, email = it.email, inviteCode = it.inviteCode) } }, ) } diff --git a/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt b/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt index 329dd5472..8f0e1b49d 100644 --- a/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt +++ b/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt @@ -6,6 +6,7 @@ import com.twix.ui.base.LoadableState data class SettingsUiState( val nickName: String = "", val email: String = "", + val inviteCode: String = "", val pokeNotificationEnabled: Boolean = false, val marketingNotificationEnabled: Boolean = false, val nightMarketingNotificationEnabled: Boolean = false, From d5e317827cdad5355d12d5d1b0964739d36b81c4 Mon Sep 17 00:00:00 2001 From: dogmania Date: Mon, 1 Jun 2026 23:45:31 +0900 Subject: [PATCH 37/44] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=BB=A4=ED=94=8C=20?= =?UTF-8?q?=EB=81=8A=EA=B8=B0,=20=EC=BB=A4=ED=94=8C=EC=BD=94=EB=93=9C=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 --- .../settings/account/SettingsAccountScreen.kt | 134 +++++++++++++++--- 1 file changed, 113 insertions(+), 21 deletions(-) diff --git a/feature/settings/src/main/java/com/twix/settings/account/SettingsAccountScreen.kt b/feature/settings/src/main/java/com/twix/settings/account/SettingsAccountScreen.kt index e867c4c69..d1936e40e 100644 --- a/feature/settings/src/main/java/com/twix/settings/account/SettingsAccountScreen.kt +++ b/feature/settings/src/main/java/com/twix/settings/account/SettingsAccountScreen.kt @@ -20,7 +20,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.twix.designsystem.R import com.twix.designsystem.components.dialog.CommonDialog import com.twix.designsystem.components.text.AppText @@ -29,6 +31,7 @@ import com.twix.designsystem.components.toast.model.ToastData import com.twix.designsystem.components.topbar.CommonTopBar import com.twix.designsystem.theme.CommonColor import com.twix.designsystem.theme.GrayColor +import com.twix.designsystem.theme.TwixTheme import com.twix.domain.model.enums.AppTextStyle import com.twix.settings.SettingsIntent import com.twix.settings.SettingsSideEffect @@ -46,6 +49,7 @@ fun SettingsAccountRoute( popBackStack: () -> Unit, navigateToLogin: () -> Unit, ) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current val currentContext by rememberUpdatedState(context) @@ -57,19 +61,25 @@ fun SettingsAccountRoute( } SettingsAccountScreen( + inviteCode = uiState.inviteCode, onBack = popBackStack, onLogout = { viewModel.dispatch(SettingsIntent.Logout) }, onWithdrawAccount = { viewModel.dispatch(SettingsIntent.WithdrawAccount) }, + // TODO: 커플 끊기 API 구현 후 SettingsIntent.UnlinkCouple 로 교체 + onUnlinkCouple = { viewModel.dispatch(SettingsIntent.WithdrawAccount) }, ) } @Composable private fun SettingsAccountScreen( + inviteCode: String = "", onBack: () -> Unit, onLogout: () -> Unit, onWithdrawAccount: () -> Unit, + onUnlinkCouple: () -> Unit, ) { var showWithdrawDialog by remember { mutableStateOf(false) } + var showUnlinkCoupleDialog by remember { mutableStateOf(false) } Column( modifier = @@ -106,6 +116,26 @@ private fun SettingsAccountScreen( HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) + SettingsMenuItem( + title = stringResource(R.string.word_couple_code), + right = { + AppText( + text = inviteCode, + color = GrayColor.C500, + style = AppTextStyle.B2, + ) + }, + ) + + HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) + + SettingsMenuItem( + title = stringResource(R.string.action_unlink_couple), + onClick = { showUnlinkCoupleDialog = true }, + ) + + HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) + SettingsMenuItem( title = stringResource(R.string.action_withdraw_account), onClick = { showWithdrawDialog = true }, @@ -124,30 +154,92 @@ private fun SettingsAccountScreen( onConfirm = { showWithdrawDialog = false }, onDismissRequest = { showWithdrawDialog = false }, content = { - Image( - painter = painterResource(R.drawable.ic_warning), - contentDescription = "warning", - modifier = - Modifier - .size(60.dp), - ) + WithdrawAccountDialogContent() + }, + ) - Spacer(Modifier.height(12.dp)) + CommonDialog( + visible = showUnlinkCoupleDialog, + confirmText = stringResource(R.string.word_cancel), + dismissText = stringResource(R.string.action_unlink_couple), + onDismiss = { + showUnlinkCoupleDialog = false + onUnlinkCouple() + }, + onConfirm = { showUnlinkCoupleDialog = false }, + onDismissRequest = { showUnlinkCoupleDialog = false }, + content = { + UnlinkCoupleDialogContent() + }, + ) +} - AppText( - text = stringResource(R.string.dialog_withdraw_account_title), - color = GrayColor.C500, - style = AppTextStyle.T1, - ) +@Composable +private fun WithdrawAccountDialogContent() { + Image( + painter = painterResource(R.drawable.ic_warning), + contentDescription = "warning", + modifier = + Modifier + .size(60.dp), + ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(12.dp)) - AppText( - text = stringResource(R.string.dialog_withdraw_account_content), - color = GrayColor.C400, - style = AppTextStyle.B2, - textAlign = TextAlign.Center, - ) - }, + AppText( + text = stringResource(R.string.dialog_withdraw_account_title), + color = GrayColor.C500, + style = AppTextStyle.T1, + ) + + Spacer(Modifier.height(8.dp)) + + AppText( + text = stringResource(R.string.dialog_withdraw_account_content), + color = GrayColor.C400, + style = AppTextStyle.B2, + textAlign = TextAlign.Center, + ) +} + +@Composable +private fun UnlinkCoupleDialogContent() { + Image( + painter = painterResource(R.drawable.ic_warning), + contentDescription = "warning", + modifier = + Modifier + .size(60.dp), + ) + + Spacer(Modifier.height(12.dp)) + + AppText( + text = stringResource(R.string.dialog_unlink_couple_title), + color = GrayColor.C500, + style = AppTextStyle.T1, + ) + + Spacer(Modifier.height(8.dp)) + + AppText( + text = stringResource(R.string.dialog_unlink_couple_content), + color = GrayColor.C400, + style = AppTextStyle.B2, + textAlign = TextAlign.Center, ) } + +@Preview(showBackground = true, showSystemUi = true) +@Composable +private fun Preview() { + TwixTheme { + SettingsAccountScreen( + inviteCode = "inviteCode", + onBack = {}, + onLogout = {}, + onWithdrawAccount = {}, + onUnlinkCouple = {}, + ) + } +} From 9929d23439dcca7fbaee4f8d78374d3864c062f9 Mon Sep 17 00:00:00 2001 From: dogmania Date: Tue, 2 Jun 2026 00:21:35 +0900 Subject: [PATCH 38/44] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20isLoadin?= =?UTF-8?q?g=20=EC=B4=88=EA=B8=B0=EA=B0=92=20true=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/twix/settings/model/SettingsUiState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt b/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt index 004601051..7f4dd5d5d 100644 --- a/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt +++ b/feature/settings/src/main/java/com/twix/settings/model/SettingsUiState.kt @@ -11,7 +11,7 @@ data class SettingsUiState( val marketingNotificationEnabled: Boolean = false, val nightMarketingNotificationEnabled: Boolean = false, val notificationSettingsUpdating: Boolean = false, - override val isLoading: Boolean = false, + override val isLoading: Boolean = true, val isLoadedUserInfo: Boolean = false, val isLoadedNotificationSettings: Boolean = false, val isAccountActionInFlight: Boolean = false, From fdda525168e7307912a3e7f75bd8db83096d992f Mon Sep 17 00:00:00 2001 From: dogmania Date: Tue, 2 Jun 2026 00:28:18 +0900 Subject: [PATCH 39/44] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20GraphRou?= =?UTF-8?q?te=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/main/java/com/twix/main/navigation/MainNavGraph.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt b/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt index a100428f9..ba38be4dc 100644 --- a/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt +++ b/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt @@ -38,7 +38,7 @@ object MainNavGraph : NavGraphContributor { } }, navigateToSettings = { - navController.navigate(NavRoutes.SettingsRoute.route) { + navController.navigate(NavRoutes.SettingsGraph.route) { launchSingleTop = true } }, From c7acb3f0a4155110062b6ca36ba3ae73bcb05684 Mon Sep 17 00:00:00 2001 From: dogmania Date: Tue, 2 Jun 2026 00:31:13 +0900 Subject: [PATCH 40/44] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20Settings?= =?UTF-8?q?GraphHost=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=EC=9D=B4=ED=8E=99=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/navigation/SettingsNavGraph.kt | 182 +++++++++++------- 1 file changed, 109 insertions(+), 73 deletions(-) diff --git a/feature/settings/src/main/java/com/twix/settings/navigation/SettingsNavGraph.kt b/feature/settings/src/main/java/com/twix/settings/navigation/SettingsNavGraph.kt index 3fc426be1..fde3e9927 100644 --- a/feature/settings/src/main/java/com/twix/settings/navigation/SettingsNavGraph.kt +++ b/feature/settings/src/main/java/com/twix/settings/navigation/SettingsNavGraph.kt @@ -1,18 +1,31 @@ package com.twix.settings.navigation +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import androidx.navigation.navigation +import androidx.navigation.compose.rememberNavController +import com.twix.designsystem.components.toast.ToastManager +import com.twix.designsystem.components.toast.model.ToastData import com.twix.navigation.NavRoutes import com.twix.navigation.base.NavGraphContributor -import com.twix.navigation.owner.rememberNavGraphOwner import com.twix.settings.SettingsRoute +import com.twix.settings.SettingsSideEffect import com.twix.settings.SettingsViewModel import com.twix.settings.about.SettingsAboutRoute import com.twix.settings.account.SettingsAccountRoute import com.twix.settings.notification.SettingsNotificationRoute +import com.twix.ui.base.ObserveAsEvents import org.koin.androidx.compose.koinViewModel +import org.koin.compose.koinInject object SettingsNavGraph : NavGraphContributor { override val graphRoute: NavRoutes @@ -21,83 +34,106 @@ object SettingsNavGraph : NavGraphContributor { get() = NavRoutes.SettingsRoute.route override fun NavGraphBuilder.registerGraph(navController: NavHostController) { - navigation( - route = graphRoute.route, - startDestination = startDestination, - ) { - composable(NavRoutes.SettingsRoute.route) { entry -> - val graphEntry = - rememberNavGraphOwner( - navController = navController, - graphRoute = NavRoutes.SettingsGraph.route, - currentEntry = entry, - ) - val viewModel: SettingsViewModel = koinViewModel(viewModelStoreOwner = graphEntry) + composable(graphRoute.route) { + SettingsGraphHost(rootNavController = navController) + } + } +} - SettingsRoute( - viewModel = viewModel, - popBackStack = { navController.popBackStack() }, - navigateToSettingsAccount = { - navController.navigate(NavRoutes.SettingsAccountRoute.route) { - launchSingleTop = true - } - }, - navigateToSettingsAbout = { - navController.navigate(NavRoutes.SettingsAboutRoute.route) { - launchSingleTop = true - } - }, - navigateToSettingsNotification = { - navController.navigate(NavRoutes.SettingsNotificationRoute.route) { - launchSingleTop = true - } - }, - ) - } +@Composable +private fun SettingsGraphHost( + rootNavController: NavHostController, + viewModel: SettingsViewModel = koinViewModel(), + toastManager: ToastManager = koinInject(), +) { + val settingsNavController = rememberNavController() + val context = LocalContext.current + val currentContext by rememberUpdatedState(context) - composable(NavRoutes.SettingsAccountRoute.route) { entry -> - val graphEntry = - rememberNavGraphOwner( - navController = navController, - graphRoute = NavRoutes.SettingsGraph.route, - currentEntry = entry, - ) - val viewModel: SettingsViewModel = koinViewModel(viewModelStoreOwner = graphEntry) + ObserveAsEvents(viewModel.sideEffect) { effect -> + when (effect) { + SettingsSideEffect.NavigateToLogin -> + rootNavController.navigate(NavRoutes.LoginRoute.route) { + launchSingleTop = true + popUpTo(NavRoutes.SettingsGraph.route) { + inclusive = true + } + } + is SettingsSideEffect.ShowToast -> + toastManager.show(ToastData(currentContext.getString(effect.resId), effect.type)) + } + } - SettingsAccountRoute( - viewModel = viewModel, - popBackStack = { navController.popBackStack() }, - navigateToLogin = { - navController.navigate(NavRoutes.LoginRoute.route) { - launchSingleTop = true - popUpTo(NavRoutes.SettingsRoute.route) { - inclusive = true - } - } - }, - ) - } + NavHost( + navController = settingsNavController, + startDestination = NavRoutes.SettingsRoute.route, + enterTransition = { + slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(NAVIGATION_ANIMATION_DURATION, easing = FastOutSlowInEasing), + ) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { fullWidth -> -fullWidth }, + animationSpec = tween(NAVIGATION_ANIMATION_DURATION, easing = FastOutSlowInEasing), + ) + }, + popEnterTransition = { + slideInHorizontally( + initialOffsetX = { fullWidth -> -fullWidth }, + animationSpec = tween(NAVIGATION_ANIMATION_DURATION, easing = FastOutSlowInEasing), + ) + }, + popExitTransition = { + slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(NAVIGATION_ANIMATION_DURATION, easing = FastOutSlowInEasing), + ) + }, + ) { + composable(NavRoutes.SettingsRoute.route) { + SettingsRoute( + viewModel = viewModel, + popBackStack = { rootNavController.popBackStack() }, + navigateToSettingsAccount = { + settingsNavController.navigate(NavRoutes.SettingsAccountRoute.route) { + launchSingleTop = true + } + }, + navigateToSettingsAbout = { + settingsNavController.navigate(NavRoutes.SettingsAboutRoute.route) { + launchSingleTop = true + } + }, + navigateToSettingsNotification = { + settingsNavController.navigate(NavRoutes.SettingsNotificationRoute.route) { + launchSingleTop = true + } + }, + ) + } - composable(NavRoutes.SettingsAboutRoute.route) { - SettingsAboutRoute( - popBackStack = { navController.popBackStack() }, - ) - } + composable(NavRoutes.SettingsAccountRoute.route) { + SettingsAccountRoute( + viewModel = viewModel, + popBackStack = { settingsNavController.popBackStack() }, + ) + } - composable(NavRoutes.SettingsNotificationRoute.route) { entry -> - val graphEntry = - rememberNavGraphOwner( - navController = navController, - graphRoute = NavRoutes.SettingsGraph.route, - currentEntry = entry, - ) - val viewModel: SettingsViewModel = koinViewModel(viewModelStoreOwner = graphEntry) + composable(NavRoutes.SettingsAboutRoute.route) { + SettingsAboutRoute( + popBackStack = { settingsNavController.popBackStack() }, + ) + } - SettingsNotificationRoute( - viewModel = viewModel, - popBackStack = { navController.popBackStack() }, - ) - } + composable(NavRoutes.SettingsNotificationRoute.route) { + SettingsNotificationRoute( + viewModel = viewModel, + popBackStack = { settingsNavController.popBackStack() }, + ) } } } + +private const val NAVIGATION_ANIMATION_DURATION = 300 From c53774515b0dbcf8661b414184b3c6f89e2fe19d Mon Sep 17 00:00:00 2001 From: dogmania Date: Tue, 2 Jun 2026 00:31:31 +0900 Subject: [PATCH 41/44] =?UTF-8?q?=F0=9F=94=A5=20Remove:=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=EC=9D=B4=ED=8E=99=ED=8A=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/account/SettingsAccountScreen.kt | 18 ------------------ .../notification/SettingsNotificationScreen.kt | 17 ----------------- 2 files changed, 35 deletions(-) diff --git a/feature/settings/src/main/java/com/twix/settings/account/SettingsAccountScreen.kt b/feature/settings/src/main/java/com/twix/settings/account/SettingsAccountScreen.kt index eba1f7a0e..3a27d10a5 100644 --- a/feature/settings/src/main/java/com/twix/settings/account/SettingsAccountScreen.kt +++ b/feature/settings/src/main/java/com/twix/settings/account/SettingsAccountScreen.kt @@ -13,10 +13,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -28,40 +26,24 @@ import com.twix.designsystem.components.dialog.CommonDialog import com.twix.designsystem.components.error.ErrorScreen import com.twix.designsystem.components.loading.TwixLoadingOverlay import com.twix.designsystem.components.text.AppText -import com.twix.designsystem.components.toast.ToastManager -import com.twix.designsystem.components.toast.model.ToastData import com.twix.designsystem.components.topbar.CommonTopBar import com.twix.designsystem.theme.CommonColor import com.twix.designsystem.theme.GrayColor import com.twix.designsystem.theme.TwixTheme import com.twix.domain.model.enums.AppTextStyle import com.twix.settings.SettingsIntent -import com.twix.settings.SettingsSideEffect import com.twix.settings.SettingsViewModel import com.twix.settings.component.SettingsMenuFrame import com.twix.settings.component.SettingsMenuItem import com.twix.settings.model.SettingsUiState -import com.twix.ui.base.ObserveAsEvents import com.twix.ui.extension.noRippleClickable -import org.koin.compose.koinInject @Composable fun SettingsAccountRoute( - toastManager: ToastManager = koinInject(), viewModel: SettingsViewModel, popBackStack: () -> Unit, - navigateToLogin: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current - val currentContext by rememberUpdatedState(context) - - ObserveAsEvents(viewModel.sideEffect) { effect -> - when (effect) { - SettingsSideEffect.NavigateToLogin -> navigateToLogin() - is SettingsSideEffect.ShowToast -> toastManager.show(ToastData(currentContext.getString(effect.resId), effect.type)) - } - } SettingsAccountScreen( uiState = uiState, diff --git a/feature/settings/src/main/java/com/twix/settings/notification/SettingsNotificationScreen.kt b/feature/settings/src/main/java/com/twix/settings/notification/SettingsNotificationScreen.kt index 22fa06c42..5a4978bf8 100644 --- a/feature/settings/src/main/java/com/twix/settings/notification/SettingsNotificationScreen.kt +++ b/feature/settings/src/main/java/com/twix/settings/notification/SettingsNotificationScreen.kt @@ -12,10 +12,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -26,38 +24,23 @@ import com.twix.designsystem.components.common.CommonSwitch import com.twix.designsystem.components.error.ErrorScreen import com.twix.designsystem.components.loading.TwixLoadingOverlay import com.twix.designsystem.components.text.AppText -import com.twix.designsystem.components.toast.ToastManager -import com.twix.designsystem.components.toast.model.ToastData import com.twix.designsystem.components.topbar.CommonTopBar import com.twix.designsystem.theme.CommonColor import com.twix.designsystem.theme.GrayColor import com.twix.designsystem.theme.TwixTheme import com.twix.domain.model.enums.AppTextStyle import com.twix.settings.SettingsIntent -import com.twix.settings.SettingsSideEffect import com.twix.settings.SettingsViewModel import com.twix.settings.component.SettingsMenuFrame import com.twix.settings.model.SettingsUiState -import com.twix.ui.base.ObserveAsEvents import com.twix.ui.extension.noRippleClickable -import org.koin.compose.koinInject @Composable fun SettingsNotificationRoute( viewModel: SettingsViewModel, - toastManager: ToastManager = koinInject(), popBackStack: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current - val currentContext by rememberUpdatedState(context) - - ObserveAsEvents(viewModel.sideEffect) { effect -> - when (effect) { - is SettingsSideEffect.ShowToast -> toastManager.tryShow(ToastData(currentContext.getString(effect.resId), effect.type)) - else -> Unit - } - } SettingsNotificationScreen( uiState = uiState, From 0408f6d5e3ba5d155d31695d2892716471db11e5 Mon Sep 17 00:00:00 2001 From: dogmania Date: Tue, 2 Jun 2026 00:33:10 +0900 Subject: [PATCH 42/44] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade:=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 198827f55..c1602dd1d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,8 +26,8 @@ android { defaultConfig { applicationId = "com.yapp.twix" - versionCode = 9 - versionName = "1.0.1" + versionCode = 10 + versionName = "1.0.2" val kakaoKey = properties["kakao_dev_native_app_key"].toString() manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = kakaoKey.trim('"') From bbd7b0342c4c486b72c45a056568729b0644d477 Mon Sep 17 00:00:00 2001 From: dogmania Date: Tue, 2 Jun 2026 02:48:10 +0900 Subject: [PATCH 43/44] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20non=20Ac?= =?UTF-8?q?tivity=20Context=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../util/src/main/java/com/twix/util/extension/ContextExt.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/util/src/main/java/com/twix/util/extension/ContextExt.kt b/core/util/src/main/java/com/twix/util/extension/ContextExt.kt index a89545ab8..c98b7debb 100644 --- a/core/util/src/main/java/com/twix/util/extension/ContextExt.kt +++ b/core/util/src/main/java/com/twix/util/extension/ContextExt.kt @@ -1,5 +1,6 @@ package com.twix.util.extension +import android.app.Activity import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent @@ -17,6 +18,10 @@ fun Context.openExternalUrl( val intent = Intent(Intent.ACTION_VIEW, url.toUri()).apply { addCategory(Intent.CATEGORY_BROWSABLE) + + if (this@openExternalUrl !is Activity) { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } } try { From b667d49641b06765bafd6287ed9b277dd7b5cffd Mon Sep 17 00:00:00 2001 From: dogmania Date: Tue, 2 Jun 2026 02:55:37 +0900 Subject: [PATCH 44/44] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EC=A0=9C=EC=95=BD=20=EB=AA=85=EC=8B=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A4=91=EB=B3=B5=20Modifier=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/twix/goal_editor/GoalEditorScreen.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt index d08b289f3..b6a18077f 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt @@ -215,13 +215,11 @@ fun GoalEditorScreen( Spacer(Modifier.height(44.dp)) ValidateUnderlineTextField( - modifier = - Modifier - .height(96.dp), value = uiState.goalTitle, onCommit = onCommitTitle, placeholder = stringResource(R.string.goal_editor_text_field_placeholder), guideText = stringResource(R.string.goal_editor_text_filed_guide), + validLengthRange = 2..14, ) GoalInfoCard(