From e83ab48e699f6210c776c7a0f2982248f86f72fb Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Thu, 12 Jun 2025 23:14:13 +0900 Subject: [PATCH 01/26] =?UTF-8?q?[TNT-261]=20feat:=20feature:trainee:modif?= =?UTF-8?q?ymyinfo=20=EB=AA=A8=EB=93=88=20=EC=B5=9C=EC=B4=88=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/co/kr/tnt/navigation/RouteModel.kt | 3 ++ feature/trainee/modifymyinfo/.gitignore | 1 + feature/trainee/modifymyinfo/build.gradle.kts | 13 +++++++ .../modifymyinfo/src/main/AndroidManifest.xml | 4 +++ .../TraineeModifyMyInfoContract.kt | 15 ++++++++ .../modifymyinfo/TraineeModifyMyInfoScreen.kt | 34 +++++++++++++++++++ .../TraineeModifyMyInfoViewModel.kt | 18 ++++++++++ .../TraineeModifyMyInfoNavigation.kt | 21 ++++++++++++ settings.gradle.kts | 1 + 9 files changed, 110 insertions(+) create mode 100644 feature/trainee/modifymyinfo/.gitignore create mode 100644 feature/trainee/modifymyinfo/build.gradle.kts create mode 100644 feature/trainee/modifymyinfo/src/main/AndroidManifest.xml create mode 100644 feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt create mode 100644 feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt create mode 100644 feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt create mode 100644 feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/navigation/TraineeModifyMyInfoNavigation.kt diff --git a/core/navigation/src/main/java/co/kr/tnt/navigation/RouteModel.kt b/core/navigation/src/main/java/co/kr/tnt/navigation/RouteModel.kt index aceff7d1..03a99829 100644 --- a/core/navigation/src/main/java/co/kr/tnt/navigation/RouteModel.kt +++ b/core/navigation/src/main/java/co/kr/tnt/navigation/RouteModel.kt @@ -96,6 +96,9 @@ sealed interface Route { @Serializable data object TraineeMyPage : Route + @Serializable + data object TraineeModifyMyInfo : Route + @Serializable data object TraineeNotification : Route diff --git a/feature/trainee/modifymyinfo/.gitignore b/feature/trainee/modifymyinfo/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/trainee/modifymyinfo/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/trainee/modifymyinfo/build.gradle.kts b/feature/trainee/modifymyinfo/build.gradle.kts new file mode 100644 index 00000000..91a86101 --- /dev/null +++ b/feature/trainee/modifymyinfo/build.gradle.kts @@ -0,0 +1,13 @@ +import co.kr.tnt.setNamespace + +plugins { + id("tnt.android.feature") +} + +android { + setNamespace("feature.trainee.modifymyinfo") +} + +dependencies { + implementation(libs.kotlinx.immutable) +} diff --git a/feature/trainee/modifymyinfo/src/main/AndroidManifest.xml b/feature/trainee/modifymyinfo/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/trainee/modifymyinfo/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt new file mode 100644 index 00000000..bf90fcb5 --- /dev/null +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt @@ -0,0 +1,15 @@ +package co.kr.tnt.trainee.modifymyinfo + +import co.kr.tnt.ui.base.UiEvent +import co.kr.tnt.ui.base.UiSideEffect +import co.kr.tnt.ui.base.UiState + +internal class TraineeModifyMyInfoContract { + data object TraineeModifyMyInfoUiState : UiState + + data object TraineeModifyMyInfoUiEvent : UiEvent + + sealed interface TraineeModifyMyInfoEffect : UiSideEffect { + data class ShowToast(val message: String) : TraineeModifyMyInfoEffect + } +} diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt new file mode 100644 index 00000000..4672f6c2 --- /dev/null +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -0,0 +1,34 @@ +package co.kr.tnt.trainee.modifymyinfo + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState + +@Composable +internal fun TraineeModifyMyInfoRoute( + viewModel: TraineeModifyMyInfoViewModel = hiltViewModel(), +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + + TraineeModifyMyInfoScreen( + state = state, + ) +} + +@Composable +private fun TraineeModifyMyInfoScreen( + state: TraineeModifyMyInfoUiState, +) { + Scaffold { padding -> + Text( + "Trainee modify my info", + modifier = Modifier.padding(padding), + ) + } +} diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt new file mode 100644 index 00000000..91c91a08 --- /dev/null +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt @@ -0,0 +1,18 @@ +package co.kr.tnt.trainee.modifymyinfo + +import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoEffect +import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiEvent +import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState +import co.kr.tnt.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +internal class TraineeModifyMyInfoViewModel @Inject constructor() : + BaseViewModel( + TraineeModifyMyInfoUiState, + ) { + override suspend fun handleEvent(event: TraineeModifyMyInfoUiEvent) { + TODO("Not yet implemented") + } + } diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/navigation/TraineeModifyMyInfoNavigation.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/navigation/TraineeModifyMyInfoNavigation.kt new file mode 100644 index 00000000..363a85f8 --- /dev/null +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/navigation/TraineeModifyMyInfoNavigation.kt @@ -0,0 +1,21 @@ +package co.kr.tnt.trainee.modifymyinfo.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.compose.composable +import co.kr.tnt.navigation.Route +import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoRoute + +fun NavController.navigateToTraineeModifyMyInfo( + navOptions: NavOptionsBuilder.() -> Unit = {}, +) = navigate( + route = Route.TraineeModifyMyInfo, + builder = navOptions, +) + +fun NavGraphBuilder.traineeModifyMyInfo() { + composable { + TraineeModifyMyInfoRoute() + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 148663ae..56498a55 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,4 +70,5 @@ include( ":feature:trainee:notification", ":feature:trainee:mealrecord", ":feature:trainee:mealdetail", + ":feature:trainee:modifymyinfo", ) From 6d308bf5f1ebfdbe3031702306aaf2dee616b672 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Sun, 6 Jul 2025 15:58:54 +0900 Subject: [PATCH 02/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=EA=B0=9C=EC=9D=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마이페이지 -> 개인 정보 수정 --- feature/trainee/main/build.gradle.kts | 1 + .../co/kr/tnt/trainee/main/TraineeMainScreen.kt | 10 ++++++++-- .../tnt/trainee/mypage/TraineeMyPageContract.kt | 2 ++ .../kr/tnt/trainee/mypage/TraineeMyPageScreen.kt | 16 +++++++++++++++- .../tnt/trainee/mypage/TraineeMyPageViewModel.kt | 5 +++++ .../mypage/navigation/TraineeMyPageNavigation.kt | 4 ++++ 6 files changed, 35 insertions(+), 3 deletions(-) diff --git a/feature/trainee/main/build.gradle.kts b/feature/trainee/main/build.gradle.kts index 83f2de46..8e438b33 100644 --- a/feature/trainee/main/build.gradle.kts +++ b/feature/trainee/main/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(projects.feature.trainee.notification) implementation(projects.feature.trainee.mealrecord) implementation(projects.feature.trainee.mealdetail) + implementation(projects.feature.trainee.modifymyinfo) implementation(libs.kotlinx.immutable) } diff --git a/feature/trainee/main/src/main/java/co/kr/tnt/trainee/main/TraineeMainScreen.kt b/feature/trainee/main/src/main/java/co/kr/tnt/trainee/main/TraineeMainScreen.kt index f2cc394e..a6f149ad 100644 --- a/feature/trainee/main/src/main/java/co/kr/tnt/trainee/main/TraineeMainScreen.kt +++ b/feature/trainee/main/src/main/java/co/kr/tnt/trainee/main/TraineeMainScreen.kt @@ -15,6 +15,8 @@ import co.kr.tnt.trainee.mealdetail.navigation.navigateToTraineeMealDetail import co.kr.tnt.trainee.mealdetail.navigation.traineeMealDetail import co.kr.tnt.trainee.mealrecord.navigation.navigateToTraineeMealRecord import co.kr.tnt.trainee.mealrecord.navigation.traineeMealRecord +import co.kr.tnt.trainee.modifymyinfo.navigation.navigateToTraineeModifyMyInfo +import co.kr.tnt.trainee.modifymyinfo.navigation.traineeModifyMyInfo import co.kr.tnt.trainee.mypage.navigation.traineeMyPageNavGraph import co.kr.tnt.trainee.notification.navigation.navigateToTraineeNotification import co.kr.tnt.trainee.notification.navigation.traineeNotification @@ -49,7 +51,8 @@ private fun TraineeMainScreen( val navController = state.navController Scaffold( - containerColor = state.currentMainTab?.containerColor?.invoke() ?: TnTTheme.colors.commonColors.Common0, + containerColor = state.currentMainTab?.containerColor?.invoke() + ?: TnTTheme.colors.commonColors.Common0, modifier = Modifier.fillMaxSize(), bottomBar = { TnTBottomBar( @@ -86,8 +89,11 @@ private fun TraineeMainScreen( padding = innerPadding, navigateToLogin = navigateToLogin, navigateToWebView = navigateToWebView, + navigateToModifyMyInfo = navController::navigateToTraineeModifyMyInfo, navigateToTraineeConnect = navigateToConnect, - ) + ) { + traineeModifyMyInfo() + } } } } diff --git a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageContract.kt b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageContract.kt index 20364dfc..ad67570a 100644 --- a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageContract.kt +++ b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageContract.kt @@ -30,6 +30,7 @@ internal class TraineeMyPageContract { val shouldShowRationale: Boolean, ) : TraineeMyPageUiEvent + data object OnClickModifyMyInfo : TraineeMyPageUiEvent data object OnClickConnect : TraineeMyPageUiEvent data object OnClickTermsOfService : TraineeMyPageUiEvent data object OnClickPrivacy : TraineeMyPageUiEvent @@ -42,6 +43,7 @@ internal class TraineeMyPageContract { sealed interface TraineeMyPageEffect : UiSideEffect { data class ShowToast(val message: DisplayText) : TraineeMyPageEffect + data object NavigateToModifyMyInfo : TraineeMyPageEffect data object NavigateToConnect : TraineeMyPageEffect data object NavigateToLogin : TraineeMyPageEffect data class NavigateToWebView(val url: String) : TraineeMyPageEffect diff --git a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt index 989e70b3..1957ca78 100644 --- a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt +++ b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt @@ -43,6 +43,9 @@ import co.kr.tnt.designsystem.component.TnTIconPopupDialog import co.kr.tnt.designsystem.component.TnTProfileImage import co.kr.tnt.designsystem.component.TnTSingleButtonPopupDialog import co.kr.tnt.designsystem.component.TnTSwitch +import co.kr.tnt.designsystem.component.button.TnTTextButton +import co.kr.tnt.designsystem.component.button.model.ButtonSize +import co.kr.tnt.designsystem.component.button.model.ButtonType import co.kr.tnt.designsystem.snackbar.LocalSnackbar import co.kr.tnt.designsystem.theme.TnTTheme import co.kr.tnt.domain.model.User @@ -71,6 +74,7 @@ import java.time.LocalDate @Composable internal fun TraineeMyPageRoute( padding: PaddingValues, + navigateToModifyMyInfo: () -> Unit, navigateToConnect: (ScreenMode) -> Unit, navigateToLogin: () -> Unit, navigateToWebView: (url: String) -> Unit, @@ -86,6 +90,7 @@ internal fun TraineeMyPageRoute( state = uiState, padding = padding, appVersion = context.getAppVersion(), + onClickModifyMyInfo = { viewModel.setEvent(TraineeMyPageUiEvent.OnClickModifyMyInfo) }, onClickConnect = { viewModel.setEvent(TraineeMyPageUiEvent.OnClickConnect) }, onTogglePushNotification = { viewModel.setEvent( @@ -115,6 +120,7 @@ internal fun TraineeMyPageRoute( LaunchedEffect(viewModel.effect) { viewModel.effect.collect { effect -> when (effect) { + TraineeMyPageEffect.NavigateToModifyMyInfo -> navigateToModifyMyInfo() TraineeMyPageEffect.NavigateToConnect -> navigateToConnect(ScreenMode.BACK) TraineeMyPageEffect.NavigateToLogin -> navigateToLogin() is TraineeMyPageEffect.ShowToast -> snackbar.show(effect.message.asString(context)) @@ -140,6 +146,7 @@ private fun TraineeMyPageScreen( state: TraineeMyPageUiState, padding: PaddingValues, appVersion: String, + onClickModifyMyInfo: () -> Unit, onClickConnect: () -> Unit, onTogglePushNotification: () -> Unit, onClickTermsOfService: () -> Unit, @@ -178,7 +185,13 @@ private fun TraineeMyPageScreen( color = TnTTheme.colors.neutralColors.Neutral950, style = TnTTheme.typography.h2, ) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(8.dp)) + TnTTextButton( + text = stringResource(co.kr.tnt.core.ui.R.string.modifying_personal_info), + size = ButtonSize.Small, + type = ButtonType.Gray, + onClick = onClickModifyMyInfo, + ) Column( verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier @@ -346,6 +359,7 @@ private fun TraineeMyPageScreenPreview() { ), padding = PaddingValues(), appVersion = "1.0", + onClickModifyMyInfo = { }, onClickConnect = { }, onTogglePushNotification = { }, onClickTermsOfService = { }, diff --git a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt index 67a4f272..99ff5dfd 100644 --- a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt +++ b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt @@ -36,6 +36,7 @@ internal class TraineeMyPageViewModel @Inject constructor( override suspend fun handleEvent(event: TraineeMyPageUiEvent) { when (event) { + TraineeMyPageUiEvent.OnClickModifyMyInfo -> navigateToModifyMyInfo() TraineeMyPageUiEvent.OnClickConnect -> navigateToConnect() is TraineeMyPageUiEvent.OnToggleNotification -> handleToggleNotification( @@ -89,6 +90,10 @@ internal class TraineeMyPageViewModel @Inject constructor( } } + private fun navigateToModifyMyInfo() { + sendEffect(TraineeMyPageEffect.NavigateToModifyMyInfo) + } + private fun navigateToConnect() { sendEffect(TraineeMyPageEffect.NavigateToConnect) } diff --git a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/navigation/TraineeMyPageNavigation.kt b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/navigation/TraineeMyPageNavigation.kt index db318bab..32aa5a3b 100644 --- a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/navigation/TraineeMyPageNavigation.kt +++ b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/navigation/TraineeMyPageNavigation.kt @@ -19,18 +19,22 @@ fun NavController.navigateToTraineeMyPage( fun NavGraphBuilder.traineeMyPageNavGraph( padding: PaddingValues, + navigateToModifyMyInfo: () -> Unit, navigateToTraineeConnect: (ScreenMode) -> Unit, navigateToLogin: () -> Unit, navigateToWebView: (url: String) -> Unit, + myPageDestination: NavGraphBuilder.() -> Unit = { }, ) { navigation(startDestination = Route.TraineeMyPage) { composable { TraineeMyPageRoute( padding = padding, + navigateToModifyMyInfo = navigateToModifyMyInfo, navigateToConnect = navigateToTraineeConnect, navigateToLogin = navigateToLogin, navigateToWebView = navigateToWebView, ) } + myPageDestination() } } From b05cab44c9505b3c9d6ab7945b4e23d0f48f95ea Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Sat, 9 Aug 2025 15:57:34 +0900 Subject: [PATCH 03/26] =?UTF-8?q?[TNT-261]=20refactor:=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=EB=A6=AC=EC=86=8C?= =?UTF-8?q?=EC=8A=A4=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/ui/src/main/res/values/strings.xml | 4 ++++ .../modifymyinfo/TraineeModifyMyInfoContract.kt | 3 ++- .../co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt | 11 +++++++---- .../trainee/mypage/src/main/res/values/strings.xml | 3 --- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 072adaac..87fe4331 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -81,6 +81,10 @@ 언제든지 다시 로그인 할 수 있어요! 로그아웃이 완료되었어요 + 계정을 탈퇴할까요? + 계정 탈퇴가 완료되었어요 + 다음에 더 폭발적인 케미로 다시 만나요! 💣 + %d자 미만으로 입력해주세요. 아직 등록된 기록이 없어요 "정보 수정을 종료할까요?" diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt index bf90fcb5..55854b85 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt @@ -3,6 +3,7 @@ package co.kr.tnt.trainee.modifymyinfo import co.kr.tnt.ui.base.UiEvent import co.kr.tnt.ui.base.UiSideEffect import co.kr.tnt.ui.base.UiState +import co.kr.tnt.ui.resource.DisplayText internal class TraineeModifyMyInfoContract { data object TraineeModifyMyInfoUiState : UiState @@ -10,6 +11,6 @@ internal class TraineeModifyMyInfoContract { data object TraineeModifyMyInfoUiEvent : UiEvent sealed interface TraineeModifyMyInfoEffect : UiSideEffect { - data class ShowToast(val message: String) : TraineeModifyMyInfoEffect + data class ShowToast(val message: DisplayText) : TraineeModifyMyInfoEffect } } diff --git a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt index 1957ca78..0cb494f8 100644 --- a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt +++ b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt @@ -31,10 +31,13 @@ import co.kr.tnt.core.ui.R.string.core_app_push_notification import co.kr.tnt.core.ui.R.string.core_app_version import co.kr.tnt.core.ui.R.string.core_cancel import co.kr.tnt.core.ui.R.string.core_delete_account +import co.kr.tnt.core.ui.R.string.core_delete_account_complete_content +import co.kr.tnt.core.ui.R.string.core_delete_account_title import co.kr.tnt.core.ui.R.string.core_logout import co.kr.tnt.core.ui.R.string.core_logout_complete_title import co.kr.tnt.core.ui.R.string.core_logout_content import co.kr.tnt.core.ui.R.string.core_logout_title +import co.kr.tnt.core.ui.R.string.core_modifying_personal_info import co.kr.tnt.core.ui.R.string.core_ok import co.kr.tnt.core.ui.R.string.core_open_source_license import co.kr.tnt.core.ui.R.string.core_privacy_policy @@ -187,7 +190,7 @@ private fun TraineeMyPageScreen( ) Spacer(Modifier.height(8.dp)) TnTTextButton( - text = stringResource(co.kr.tnt.core.ui.R.string.modifying_personal_info), + text = stringResource(core_modifying_personal_info), size = ButtonSize.Small, type = ButtonType.Gray, onClick = onClickModifyMyInfo, @@ -305,7 +308,7 @@ private fun Dialog( DialogState.DELETE_ACCOUNT_CONFIRM -> { TnTIconPopupDialog( - title = stringResource(R.string.delete_account_title), + title = stringResource(core_delete_account_title), content = stringResource(R.string.delete_account_content), leftButtonText = stringResource(core_cancel), rightButtonText = stringResource(core_ok), @@ -317,8 +320,8 @@ private fun Dialog( DialogState.DELETE_ACCOUNT -> { TnTSingleButtonPopupDialog( - title = stringResource(R.string.delete_account_complete_title), - content = stringResource(R.string.delete_account_complete_content), + title = stringResource(core_delete_account_title), + content = stringResource(core_delete_account_complete_content), buttonText = stringResource(core_ok), cancelable = false, onButtonClick = onClickConfirm, diff --git a/feature/trainee/mypage/src/main/res/values/strings.xml b/feature/trainee/mypage/src/main/res/values/strings.xml index a5221b2e..55652c2c 100644 --- a/feature/trainee/mypage/src/main/res/values/strings.xml +++ b/feature/trainee/mypage/src/main/res/values/strings.xml @@ -8,10 +8,7 @@ %s 트레이너와 연결이 해제되었어요 더 폭발적인 케미로 다시 만나요! - 계정을 탈퇴할까요? 운동 및 식단 기록에 대한 데이터가 사라져요! - 계정 탈퇴가 완료되었어요 - 다음에 더 폭발적인 케미로 다시 만나요! 💣 로그아웃에 실패하였습니다. 탈퇴에 실패하였습니다. From bfd662e4966411c8b9efc554b2e50a94ae26317d Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Sat, 9 Aug 2025 17:22:02 +0900 Subject: [PATCH 04/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=EA=B0=9C=EC=9D=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=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 --- .../kr/tnt/trainee/main/TraineeMainScreen.kt | 4 +- .../TraineeModifyMyInfoContract.kt | 59 +++- .../modifymyinfo/TraineeModifyMyInfoScreen.kt | 332 +++++++++++++++++- .../TraineeModifyMyInfoViewModel.kt | 62 +++- .../TraineeModifyMyInfoNavigation.kt | 8 +- .../src/main/res/values/strings.xml | 9 + 6 files changed, 464 insertions(+), 10 deletions(-) create mode 100644 feature/trainee/modifymyinfo/src/main/res/values/strings.xml diff --git a/feature/trainee/main/src/main/java/co/kr/tnt/trainee/main/TraineeMainScreen.kt b/feature/trainee/main/src/main/java/co/kr/tnt/trainee/main/TraineeMainScreen.kt index a6f149ad..de771310 100644 --- a/feature/trainee/main/src/main/java/co/kr/tnt/trainee/main/TraineeMainScreen.kt +++ b/feature/trainee/main/src/main/java/co/kr/tnt/trainee/main/TraineeMainScreen.kt @@ -92,7 +92,9 @@ private fun TraineeMainScreen( navigateToModifyMyInfo = navController::navigateToTraineeModifyMyInfo, navigateToTraineeConnect = navigateToConnect, ) { - traineeModifyMyInfo() + traineeModifyMyInfo( + navigateToPrevious = navController::safePopBackStack, + ) } } } diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt index 55854b85..ae9a2a08 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt @@ -1,16 +1,71 @@ package co.kr.tnt.trainee.modifymyinfo +import co.kr.tnt.domain.UserProfilePolicy import co.kr.tnt.ui.base.UiEvent import co.kr.tnt.ui.base.UiSideEffect import co.kr.tnt.ui.base.UiState import co.kr.tnt.ui.resource.DisplayText +import java.io.File +import java.time.LocalDate + +private const val MAX_HEIGHT_LENGTH = 3 +private const val MAX_WEIGHT_LENGTH = 5 internal class TraineeModifyMyInfoContract { - data object TraineeModifyMyInfoUiState : UiState + data class TraineeModifyMyInfoUiState( + val profileImage: String? = null, + val name: String = "", + val birthday: LocalDate? = null, + val height: String? = null, + val weight: String? = null, + val ptPurpose: List? = emptyList(), + val caution: String? = "", + ) : UiState { + val isNameValid + get() = name.isBlank() || + name.matches(UserProfilePolicy.USER_NAME_REGEX) && + name.length <= UserProfilePolicy.USER_NAME_MAX_LENGTH + + /** + * 키가 유효한 입력값인지 검사 + * 형식: 정수 3자 + */ + val isHeightValid + get() = height.isNullOrBlank() || ( + height.toIntOrNull() != null && + !height.startsWith("0") && + height.length <= MAX_HEIGHT_LENGTH + ) + + /** + * 몸무게가 유효한 입력값인지 검사 + * 형식: 5자 이하의 실수 (000, 00, 00.0, 000.0) + */ + private val weightRegex = Regex("^(\\d{1,3}(\\.\\d)?)?\$") + val isWeightValid + get() = weight.isNullOrBlank() || ( + weight.matches(weightRegex) && + !weight.startsWith("0") && + weight.length <= MAX_WEIGHT_LENGTH + ) - data object TraineeModifyMyInfoUiEvent : UiEvent + // TODO 완료 버튼 활성화 조건 만들기 + } + + sealed interface TraineeModifyMyInfoUiEvent : UiEvent { + data class OnProfileImageSelect(val image: File) : TraineeModifyMyInfoUiEvent + data class OnNameChange(val name: String) : TraineeModifyMyInfoUiEvent + data class OnHeightChange(val height: String) : TraineeModifyMyInfoUiEvent + data class OnWeightChange(val weight: String) : TraineeModifyMyInfoUiEvent + data class OnBirthdayChange(val birthday: LocalDate) : TraineeModifyMyInfoUiEvent + data class OnPurposeSelected(val purpose: String) : TraineeModifyMyInfoUiEvent + data class OnCautionChange(val text: String) : TraineeModifyMyInfoUiEvent + data object OnBackClick : TraineeModifyMyInfoUiEvent + data object OnNextClick : TraineeModifyMyInfoUiEvent + } sealed interface TraineeModifyMyInfoEffect : UiSideEffect { data class ShowToast(val message: DisplayText) : TraineeModifyMyInfoEffect + data object NavigateToBack : TraineeModifyMyInfoEffect } } diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt index 4672f6c2..9cec7eb9 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -1,34 +1,360 @@ package co.kr.tnt.trainee.modifymyinfo +import android.app.DatePickerDialog +import android.content.Context +import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.kr.tnt.core.ui.R.string.core_entered_wrong_text +import co.kr.tnt.core.ui.R.string.core_height_label +import co.kr.tnt.core.ui.R.string.core_height_unit +import co.kr.tnt.core.ui.R.string.core_name +import co.kr.tnt.core.ui.R.string.core_name_placeholder +import co.kr.tnt.core.ui.R.string.core_text_length_and_format_warning +import co.kr.tnt.core.ui.R.string.core_weight_label +import co.kr.tnt.core.ui.R.string.core_weight_unit +import co.kr.tnt.designsystem.component.TnTLabeledTextField +import co.kr.tnt.designsystem.component.TnTLabeledTextFieldWithCounter +import co.kr.tnt.designsystem.component.TnTOutlinedTextField +import co.kr.tnt.designsystem.component.TnTProfileImage +import co.kr.tnt.designsystem.component.TnTTopBarWithBackButton +import co.kr.tnt.designsystem.snackbar.LocalSnackbar +import co.kr.tnt.designsystem.theme.TnTTheme +import co.kr.tnt.feature.trainee.modifymyinfo.R +import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoEffect +import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiEvent import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState +import co.kr.tnt.ui.model.DefaultUserProfile +import co.kr.tnt.ui.utils.convertToAllowedImageFormat +import coil.compose.rememberAsyncImagePainter +import coil.request.ImageRequest +import java.time.LocalDate +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +private const val MAX_NAME_LENGTH = 15 +private const val MAX_CAUTION_LENGTH = 100 @Composable internal fun TraineeModifyMyInfoRoute( viewModel: TraineeModifyMyInfoViewModel = hiltViewModel(), + navigateToPrevious: () -> Unit, ) { + val context = LocalContext.current val state by viewModel.uiState.collectAsStateWithLifecycle() + val snackbar = LocalSnackbar.current TraineeModifyMyInfoScreen( state = state, + onProfileImageSelect = { uri -> + val profileImageFile = uri.convertToAllowedImageFormat(context) + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnProfileImageSelect(profileImageFile)) + }, + onNameChange = { name -> viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnNameChange(name)) }, + onBirthdayChange = { birthday -> + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnBirthdayChange(birthday)) + }, + onHeightChange = { height -> + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnHeightChange(height)) + }, + onWeightChange = { weight -> + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnWeightChange(weight)) + }, + onCautionChange = { caution -> + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnCautionChange(caution)) + }, + onPurposeSelected = { TODO() }, + onBackClick = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnBackClick) }, + onNextClick = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnNextClick) }, ) + + LaunchedEffect(viewModel.effect) { + viewModel.effect.collect { effect -> + when (effect) { + TraineeModifyMyInfoEffect.NavigateToBack -> navigateToPrevious() + is TraineeModifyMyInfoEffect.ShowToast -> snackbar.show(effect.message.asString(context)) + } + } + } } @Composable private fun TraineeModifyMyInfoScreen( state: TraineeModifyMyInfoUiState, + onProfileImageSelect: (uri: Uri) -> Unit, + onNameChange: (name: String) -> Unit, + onBirthdayChange: (birthday: LocalDate) -> Unit, + onHeightChange: (height: String) -> Unit, + onWeightChange: (weight: String) -> Unit, + onPurposeSelected: (purpose: String) -> Unit, + onCautionChange: (caution: String) -> Unit, + onBackClick: () -> Unit, + onNextClick: () -> Unit, ) { - Scaffold { padding -> + BackHandler { onBackClick() } + + val context = LocalContext.current + val today = LocalDate.now() + + val pickMediaLauncher = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> + uri?.let(onProfileImageSelect) + } + + val painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(state.profileImage) + .placeholder(DefaultUserProfile.Trainee.image) + .error(DefaultUserProfile.Trainee.image) + .build(), + ) + + Scaffold( + topBar = { + TnTTopBarWithBackButton( + title = stringResource(R.string.modifying_my_info), + onBackClick = onBackClick, + ) + }, + containerColor = TnTTheme.colors.commonColors.Common0, + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .background(TnTTheme.colors.commonColors.Common0) + .verticalScroll(rememberScrollState()), + ) { + TnTProfileImage( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + defaultImage = painterResource(DefaultUserProfile.Trainee.image), + image = painter, + onEditClick = { + pickMediaLauncher.launch( + PickVisualMediaRequest( + mediaType = PickVisualMedia.ImageOnly, + ), + ) + }, + ) + Spacer(Modifier.padding(top = 32.dp)) + Column( + verticalArrangement = Arrangement.spacedBy(48.dp), + modifier = Modifier.fillMaxWidth(), + ) { + TnTLabeledTextFieldWithCounter( + title = stringResource(core_name), + value = state.name, + onValueChange = { newValue -> + onNameChange(newValue) + }, + modifier = Modifier.padding(horizontal = 20.dp), + placeholder = stringResource(core_name_placeholder), + maxLength = MAX_NAME_LENGTH, + isSingleLine = true, + showWarning = !state.isNameValid, + isRequired = true, + warningMessage = stringResource( + core_text_length_and_format_warning, + MAX_NAME_LENGTH, + ), + ) + Column { + Text( + text = stringResource(R.string.birthday_label), + color = TnTTheme.colors.neutralColors.Neutral900, + style = TnTTheme.typography.body1Bold, + modifier = Modifier.padding(start = 20.dp, bottom = 8.dp), + ) + BirthdayPicker( + modifier = Modifier.padding(horizontal = 20.dp), + context = context, + today = today, + selectedDate = state.birthday, + onDateSelected = onBirthdayChange, + ) + HorizontalDivider( + thickness = 1.dp, + color = TnTTheme.colors.neutralColors.Neutral200, + modifier = Modifier.padding(horizontal = 20.dp), + ) + } + Column { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) { + TnTLabeledTextField( + title = stringResource(core_height_label), + value = state.height ?: "", + placeholder = "0", + isSingleLine = true, + showWarning = state.isHeightValid.not(), + warningMessage = stringResource(core_entered_wrong_text), + keyboardType = KeyboardType.Number, + trailingComponent = { + UnitLabel(core_height_unit) + }, + onValueChange = onHeightChange, + modifier = Modifier.weight(1f), + ) + TnTLabeledTextField( + title = stringResource(core_weight_label), + value = state.weight ?: "", + placeholder = "00.0", + isSingleLine = true, + showWarning = state.isWeightValid.not(), + warningMessage = stringResource(core_entered_wrong_text), + keyboardType = KeyboardType.Number, + trailingComponent = { + UnitLabel(core_weight_unit) + }, + onValueChange = onWeightChange, + modifier = Modifier.weight(1f), + ) + } + } + Column(Modifier.padding(horizontal = 20.dp)) { + Text( + text = "PT 목적", + style = TnTTheme.typography.body1Bold, + color = TnTTheme.colors.neutralColors.Neutral900, + ) + Spacer(Modifier.padding(top = 12.dp)) + // TODO PTPurpose 다시 생각 + } + Column { + Text( + text = stringResource(R.string.caution_that_trainer_must_know), + style = TnTTheme.typography.body1Bold, + color = TnTTheme.colors.neutralColors.Neutral900, + modifier = Modifier.padding(horizontal = 20.dp), + ) + Spacer(Modifier.padding(top = 8.dp)) + TnTOutlinedTextField( + value = state.caution ?: "", + onValueChange = { newValue -> + onCautionChange(newValue) + }, + modifier = Modifier.padding(horizontal = 20.dp), + isError = (state.caution?.length ?: 0) >= MAX_CAUTION_LENGTH, + warningMessage = stringResource(R.string.caution_length_overflow), + maxLength = 100, + ) + } + } + } + } +} + +@Composable +private fun BirthdayPicker( + modifier: Modifier = Modifier, + context: Context, + today: LocalDate, + selectedDate: LocalDate?, + onDateSelected: (LocalDate) -> Unit, +) { + val dateFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd") + val date = selectedDate ?: LocalDate.of(2001, 1, 1) + + Box( + modifier = modifier + .fillMaxWidth() + .padding(8.dp) + .clickable { + DatePickerDialog( + context, + { _, selectedYear, selectedMonth, selectedDay -> + val newDate = LocalDate.of(selectedYear, selectedMonth + 1, selectedDay) + onDateSelected(newDate) + }, + date.year, + date.monthValue - 1, + date.dayOfMonth, + ) + .apply { + // 오늘 이후는 선택 불가능 + val todayMillis = today + .atStartOfDay(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli() + + datePicker.maxDate = todayMillis - 1 + } + .show() + }, + ) { Text( - "Trainee modify my info", - modifier = Modifier.padding(padding), + text = selectedDate?.format(dateFormatter) ?: stringResource(R.string.birthday_placeholder), + color = if (selectedDate == null) { + TnTTheme.colors.neutralColors.Neutral400 + } else { + TnTTheme.colors.neutralColors.Neutral600 + }, + style = TnTTheme.typography.body1Medium, + textAlign = TextAlign.Start, + ) + } +} + +@Composable +private fun UnitLabel(stringResId: Int) { + Text( + text = stringResource(stringResId), + style = TnTTheme.typography.body1Medium, + color = TnTTheme.colors.neutralColors.Neutral400, + ) +} + +@Preview +@Composable +private fun TraineeModifyMyScreenPreview() { + TnTTheme { + TraineeModifyMyInfoScreen( + state = TraineeModifyMyInfoUiState(name = "김회원"), + onProfileImageSelect = { }, + onNameChange = { }, + onBirthdayChange = { }, + onHeightChange = { }, + onWeightChange = { }, + onPurposeSelected = { }, + onCautionChange = { }, + onBackClick = { }, + onNextClick = { }, ) } } diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt index 91c91a08..c41c189c 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt @@ -1,18 +1,76 @@ package co.kr.tnt.trainee.modifymyinfo +import co.kr.tnt.domain.model.ProfileImageUpdatePolicy import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoEffect import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiEvent import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState import co.kr.tnt.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import java.io.File +import java.time.LocalDate import javax.inject.Inject @HiltViewModel internal class TraineeModifyMyInfoViewModel @Inject constructor() : BaseViewModel( - TraineeModifyMyInfoUiState, + TraineeModifyMyInfoUiState(), ) { + private var profileImageUpdatePolicy: ProfileImageUpdatePolicy = ProfileImageUpdatePolicy.Keep + override suspend fun handleEvent(event: TraineeModifyMyInfoUiEvent) { - TODO("Not yet implemented") + when (event) { + TraineeModifyMyInfoUiEvent.OnBackClick -> navigateToBack() + is TraineeModifyMyInfoUiEvent.OnProfileImageSelect -> { + profileImageUpdatePolicy = ProfileImageUpdatePolicy.Change(File(event.image.path)) + updateProfileImage(event.image.path) + } + + is TraineeModifyMyInfoUiEvent.OnNameChange -> updateName(event.name) + is TraineeModifyMyInfoUiEvent.OnBirthdayChange -> updateBirthday(event.birthday) + is TraineeModifyMyInfoUiEvent.OnHeightChange -> updateHeight(event.height) + is TraineeModifyMyInfoUiEvent.OnWeightChange -> updateWeight(event.weight) + is TraineeModifyMyInfoUiEvent.OnPurposeSelected -> updateSelectedPurposes(event.purpose) + is TraineeModifyMyInfoUiEvent.OnCautionChange -> updateCaution(event.text) + TraineeModifyMyInfoUiEvent.OnNextClick -> navigateToBack() + } + } + + private fun updateSelectedPurposes(purpose: String) { + val updatedPurposes = currentState.ptPurpose.orEmpty().toMutableList().apply { + if (contains(purpose)) { + remove(purpose) + } else { + add(purpose) + } + } + updateState { copy(ptPurpose = updatedPurposes) } + } + + private fun updateCaution(caution: String) { + updateState { copy(caution = caution) } + } + + private fun updateHeight(height: String) { + updateState { copy(height = height) } + } + + private fun updateWeight(weight: String) { + updateState { copy(weight = weight) } + } + + private fun updateBirthday(birthday: LocalDate) { + updateState { copy(birthday = birthday) } + } + + private fun updateName(name: String) { + updateState { copy(name = name) } + } + + private fun updateProfileImage(image: String) { + updateState { copy(profileImage = image) } + } + + private fun navigateToBack() { + sendEffect(TraineeModifyMyInfoEffect.NavigateToBack) } } diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/navigation/TraineeModifyMyInfoNavigation.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/navigation/TraineeModifyMyInfoNavigation.kt index 363a85f8..f93144fe 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/navigation/TraineeModifyMyInfoNavigation.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/navigation/TraineeModifyMyInfoNavigation.kt @@ -14,8 +14,12 @@ fun NavController.navigateToTraineeModifyMyInfo( builder = navOptions, ) -fun NavGraphBuilder.traineeModifyMyInfo() { +fun NavGraphBuilder.traineeModifyMyInfo( + navigateToPrevious: () -> Unit, +) { composable { - TraineeModifyMyInfoRoute() + TraineeModifyMyInfoRoute( + navigateToPrevious = navigateToPrevious, + ) } } diff --git a/feature/trainee/modifymyinfo/src/main/res/values/strings.xml b/feature/trainee/modifymyinfo/src/main/res/values/strings.xml new file mode 100644 index 00000000..4af4187f --- /dev/null +++ b/feature/trainee/modifymyinfo/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + 내 정보 수정 + + 생년월일 + 2001/01/01 + 트레이너가 알아야 할 주의사항 + 100자 미만으로 입력해주세요 + From 074ed0aba26c794c7fb3ca6a4ed560934fd44162 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Sat, 9 Aug 2025 18:56:33 +0900 Subject: [PATCH 05/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20PT=20=EB=AA=A9=EC=A0=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PtPurpose 도메인 모듈로 이동 --- core/ui/src/main/res/values/strings.xml | 8 +++ .../java/co/kr/tnt/domain/model/PtPurpose.kt | 10 ++++ .../modifymyinfo/TraineeModifyMyInfoScreen.kt | 49 ++++++++++++++++++- .../modifymyinfo/model/TraineePtPurpose.kt | 35 +++++++++++++ .../trainee/signup/TraineePTPurposePage.kt | 4 +- .../kr/tnt/trainee/signup/model/PTPurpose.kt | 15 ------ .../trainee/signup/model/TraineePtPurpose.kt | 35 +++++++++++++ .../signup/src/main/res/values/strings.xml | 6 --- 8 files changed, 137 insertions(+), 25 deletions(-) create mode 100644 domain/src/main/java/co/kr/tnt/domain/model/PtPurpose.kt create mode 100644 feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/model/TraineePtPurpose.kt delete mode 100644 feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/model/PTPurpose.kt create mode 100644 feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/model/TraineePtPurpose.kt diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 87fe4331..df150c2e 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -67,6 +67,14 @@ %d회차 수업 + + 체중 감량 + 근력 향상 + 건강 관리 + 유연성 향상 + 바디프로필 + 자세 교정 + 개인정보 수정 앱 푸시 알림 diff --git a/domain/src/main/java/co/kr/tnt/domain/model/PtPurpose.kt b/domain/src/main/java/co/kr/tnt/domain/model/PtPurpose.kt new file mode 100644 index 00000000..f66e3d2e --- /dev/null +++ b/domain/src/main/java/co/kr/tnt/domain/model/PtPurpose.kt @@ -0,0 +1,10 @@ +package co.kr.tnt.domain.model + +enum class PtPurpose { + LOSS_WEIGHT, + STRENGTH, + HEALTH_CARE, + FLEXIBILITY, + BODY_PROFILE, + POSTURE_CORRECTION, +} diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt index 9cec7eb9..ecc239b8 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -12,6 +12,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -48,12 +50,16 @@ import co.kr.tnt.designsystem.component.TnTLabeledTextFieldWithCounter import co.kr.tnt.designsystem.component.TnTOutlinedTextField import co.kr.tnt.designsystem.component.TnTProfileImage import co.kr.tnt.designsystem.component.TnTTopBarWithBackButton +import co.kr.tnt.designsystem.component.button.TnTTextButton +import co.kr.tnt.designsystem.component.button.model.ButtonSize +import co.kr.tnt.designsystem.component.button.model.ButtonType import co.kr.tnt.designsystem.snackbar.LocalSnackbar import co.kr.tnt.designsystem.theme.TnTTheme import co.kr.tnt.feature.trainee.modifymyinfo.R import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoEffect import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiEvent import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState +import co.kr.tnt.trainee.modifymyinfo.model.TraineePtPurpose import co.kr.tnt.ui.model.DefaultUserProfile import co.kr.tnt.ui.utils.convertToAllowedImageFormat import coil.compose.rememberAsyncImagePainter @@ -64,6 +70,8 @@ import java.time.format.DateTimeFormatter private const val MAX_NAME_LENGTH = 15 private const val MAX_CAUTION_LENGTH = 100 +private const val ROW_NUM = 3 +private const val COLUMNS_NUM = 2 @Composable internal fun TraineeModifyMyInfoRoute( @@ -93,7 +101,9 @@ internal fun TraineeModifyMyInfoRoute( onCautionChange = { caution -> viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnCautionChange(caution)) }, - onPurposeSelected = { TODO() }, + onPurposeSelected = { purpose -> + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnPurposeSelected(purpose)) + }, onBackClick = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnBackClick) }, onNextClick = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnNextClick) }, ) @@ -108,6 +118,7 @@ internal fun TraineeModifyMyInfoRoute( } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun TraineeModifyMyInfoScreen( state: TraineeModifyMyInfoUiState, @@ -254,7 +265,25 @@ private fun TraineeModifyMyInfoScreen( color = TnTTheme.colors.neutralColors.Neutral900, ) Spacer(Modifier.padding(top = 12.dp)) - // TODO PTPurpose 다시 생각 + Column { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + maxItemsInEachRow = COLUMNS_NUM, + maxLines = ROW_NUM, + modifier = Modifier.fillMaxWidth(), + ) { + TraineePtPurpose.entries.forEach { purpose -> + val purposeText = stringResource(purpose.textResId) + PurposeButton( + text = purposeText, + isSelected = state.ptPurpose?.contains(purposeText) == true, + onClick = { onPurposeSelected(purposeText) }, + modifier = Modifier.weight(1f), + ) + } + } + } } Column { Text( @@ -340,6 +369,22 @@ private fun UnitLabel(stringResId: Int) { ) } +@Composable +fun PurposeButton( + text: String, + isSelected: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + TnTTextButton( + text = text, + modifier = modifier, + size = ButtonSize.XLarge, + type = if (isSelected) ButtonType.RedOutline else ButtonType.GrayOutline, + onClick = onClick, + ) +} + @Preview @Composable private fun TraineeModifyMyScreenPreview() { diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/model/TraineePtPurpose.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/model/TraineePtPurpose.kt new file mode 100644 index 00000000..ab10eb49 --- /dev/null +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/model/TraineePtPurpose.kt @@ -0,0 +1,35 @@ +package co.kr.tnt.trainee.modifymyinfo.model + +import androidx.annotation.StringRes +import co.kr.tnt.core.ui.R.string.core_body_profile +import co.kr.tnt.core.ui.R.string.core_flexibility +import co.kr.tnt.core.ui.R.string.core_health_care +import co.kr.tnt.core.ui.R.string.core_loss_weight +import co.kr.tnt.core.ui.R.string.core_posture_correction +import co.kr.tnt.core.ui.R.string.core_strength_improvement +import co.kr.tnt.domain.model.PtPurpose + +enum class TraineePtPurpose( + @StringRes val textResId: Int, +) { + LOSS_WEIGHT(core_loss_weight), + STRENGTH(core_strength_improvement), + HEALTH_CARE(core_health_care), + FLEXIBILITY(core_flexibility), + BODY_PROFILE(core_body_profile), + POSTURE_CORRECTION(core_posture_correction), + ; + + companion object { + fun from(purpose: PtPurpose): TraineePtPurpose { + return when (purpose) { + PtPurpose.LOSS_WEIGHT -> LOSS_WEIGHT + PtPurpose.STRENGTH -> STRENGTH + PtPurpose.HEALTH_CARE -> HEALTH_CARE + PtPurpose.FLEXIBILITY -> FLEXIBILITY + PtPurpose.BODY_PROFILE -> BODY_PROFILE + PtPurpose.POSTURE_CORRECTION -> POSTURE_CORRECTION + } + } + } +} diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineePTPurposePage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineePTPurposePage.kt index d6241011..a41504ad 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineePTPurposePage.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineePTPurposePage.kt @@ -27,7 +27,7 @@ import co.kr.tnt.designsystem.theme.TnTTheme import co.kr.tnt.feature.trainee.signup.R import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState import co.kr.tnt.trainee.signup.component.ProgressSteps -import co.kr.tnt.trainee.signup.model.PTPurpose +import co.kr.tnt.trainee.signup.model.TraineePtPurpose private const val ROW_NUM = 3 private const val COLUMNS_NUM = 2 @@ -63,7 +63,7 @@ internal fun TraineePTPurposePage( maxLines = ROW_NUM, modifier = Modifier.fillMaxWidth(), ) { - PTPurpose.entries.forEach { purpose -> + TraineePtPurpose.entries.forEach { purpose -> val purposeText = stringResource(purpose.textResId) PurposeButton( text = purposeText, diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/model/PTPurpose.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/model/PTPurpose.kt deleted file mode 100644 index 4b992b92..00000000 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/model/PTPurpose.kt +++ /dev/null @@ -1,15 +0,0 @@ -package co.kr.tnt.trainee.signup.model - -import androidx.annotation.StringRes -import co.kr.tnt.feature.trainee.signup.R - -enum class PTPurpose( - @StringRes val textResId: Int, -) { - LOSS_WEIGHT(R.string.loss_weight), - STRENGTH(R.string.strength_improvement), - HEALTH_CARE(R.string.health_care), - FLEXIBILITY(R.string.flexibility), - BODY_PROFILE(R.string.body_profile), - POSTURE_CORRECTION(R.string.posture_correction), -} diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/model/TraineePtPurpose.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/model/TraineePtPurpose.kt new file mode 100644 index 00000000..0fd2b456 --- /dev/null +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/model/TraineePtPurpose.kt @@ -0,0 +1,35 @@ +package co.kr.tnt.trainee.signup.model + +import androidx.annotation.StringRes +import co.kr.tnt.core.ui.R.string.core_body_profile +import co.kr.tnt.core.ui.R.string.core_flexibility +import co.kr.tnt.core.ui.R.string.core_health_care +import co.kr.tnt.core.ui.R.string.core_loss_weight +import co.kr.tnt.core.ui.R.string.core_posture_correction +import co.kr.tnt.core.ui.R.string.core_strength_improvement +import co.kr.tnt.domain.model.PtPurpose + +enum class TraineePtPurpose( + @StringRes val textResId: Int, +) { + LOSS_WEIGHT(core_loss_weight), + STRENGTH(core_strength_improvement), + HEALTH_CARE(core_health_care), + FLEXIBILITY(core_flexibility), + BODY_PROFILE(core_body_profile), + POSTURE_CORRECTION(core_posture_correction), + ; + + companion object { + fun from(purpose: PtPurpose): TraineePtPurpose { + return when (purpose) { + PtPurpose.LOSS_WEIGHT -> LOSS_WEIGHT + PtPurpose.STRENGTH -> STRENGTH + PtPurpose.HEALTH_CARE -> HEALTH_CARE + PtPurpose.FLEXIBILITY -> FLEXIBILITY + PtPurpose.BODY_PROFILE -> BODY_PROFILE + PtPurpose.POSTURE_CORRECTION -> POSTURE_CORRECTION + } + } + } +} diff --git a/feature/trainee/signup/src/main/res/values/strings.xml b/feature/trainee/signup/src/main/res/values/strings.xml index 0955dc7f..4f6108fa 100644 --- a/feature/trainee/signup/src/main/res/values/strings.xml +++ b/feature/trainee/signup/src/main/res/values/strings.xml @@ -11,12 +11,6 @@ PT를 받는 목적에 대해\n알려주세요! 다중 선택이 가능해요. - 체중 감량 - 근력 향상 - 건강 관리 - 유연성 향상 - 바디프로필 - 자세 교정 트레이너가 꼭 알아야 할\n주의사항이 있나요? 트레이너에게 알려드릴게요. From 524d579334e09abd3514b6f2d5c0148995f32f42 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Sat, 13 Sep 2025 15:22:30 +0900 Subject: [PATCH 06/26] =?UTF-8?q?[TNT-261]=20fix:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=EA=B0=9C=EC=9D=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=ED=86=B5=EC=9D=BC=20=EB=B0=8F=20UI=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 키보드가 UI 가리지 않도록 수정 --- .../TraineeModifyMyInfoContract.kt | 17 +- .../modifymyinfo/TraineeModifyMyInfoScreen.kt | 333 +++++++++--------- .../TraineeModifyMyInfoViewModel.kt | 17 +- .../src/main/res/values/strings.xml | 2 +- 4 files changed, 189 insertions(+), 180 deletions(-) diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt index ae9a2a08..6622861c 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt @@ -53,15 +53,16 @@ internal class TraineeModifyMyInfoContract { } sealed interface TraineeModifyMyInfoUiEvent : UiEvent { + data object OnDeleteProfileImage : TraineeModifyMyInfoUiEvent data class OnProfileImageSelect(val image: File) : TraineeModifyMyInfoUiEvent - data class OnNameChange(val name: String) : TraineeModifyMyInfoUiEvent - data class OnHeightChange(val height: String) : TraineeModifyMyInfoUiEvent - data class OnWeightChange(val weight: String) : TraineeModifyMyInfoUiEvent - data class OnBirthdayChange(val birthday: LocalDate) : TraineeModifyMyInfoUiEvent - data class OnPurposeSelected(val purpose: String) : TraineeModifyMyInfoUiEvent - data class OnCautionChange(val text: String) : TraineeModifyMyInfoUiEvent - data object OnBackClick : TraineeModifyMyInfoUiEvent - data object OnNextClick : TraineeModifyMyInfoUiEvent + data class OnChangeName(val name: String) : TraineeModifyMyInfoUiEvent + data class OnChangeHeight(val height: String) : TraineeModifyMyInfoUiEvent + data class OnChangeWeight(val weight: String) : TraineeModifyMyInfoUiEvent + data class OnChangeBirthday(val birthday: LocalDate) : TraineeModifyMyInfoUiEvent + data class OnSelectPurpose(val purpose: String) : TraineeModifyMyInfoUiEvent + data class OnChangeCaution(val text: String) : TraineeModifyMyInfoUiEvent + data object OnClickBack : TraineeModifyMyInfoUiEvent + data object OnClickNext : TraineeModifyMyInfoUiEvent } sealed interface TraineeModifyMyInfoEffect : UiSideEffect { diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt index ecc239b8..c0bdd829 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -16,8 +16,10 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -60,6 +62,7 @@ import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyM import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiEvent import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState import co.kr.tnt.trainee.modifymyinfo.model.TraineePtPurpose +import co.kr.tnt.ui.extensions.clearFocusOnTap import co.kr.tnt.ui.model.DefaultUserProfile import co.kr.tnt.ui.utils.convertToAllowedImageFormat import coil.compose.rememberAsyncImagePainter @@ -88,24 +91,24 @@ internal fun TraineeModifyMyInfoRoute( val profileImageFile = uri.convertToAllowedImageFormat(context) viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnProfileImageSelect(profileImageFile)) }, - onNameChange = { name -> viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnNameChange(name)) }, - onBirthdayChange = { birthday -> - viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnBirthdayChange(birthday)) + onChangeName = { name -> viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnChangeName(name)) }, + onChangeBirthday = { birthday -> + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnChangeBirthday(birthday)) }, - onHeightChange = { height -> - viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnHeightChange(height)) + onChangeHeight = { height -> + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnChangeHeight(height)) }, - onWeightChange = { weight -> - viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnWeightChange(weight)) + onChangeWeight = { weight -> + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnChangeWeight(weight)) }, - onCautionChange = { caution -> - viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnCautionChange(caution)) + onChangeCaution = { caution -> + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnChangeCaution(caution)) }, - onPurposeSelected = { purpose -> - viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnPurposeSelected(purpose)) + onSelectPurpose = { purpose -> + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnSelectPurpose(purpose)) }, - onBackClick = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnBackClick) }, - onNextClick = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnNextClick) }, + onClickBack = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnClickBack) }, + onClickNext = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnClickNext) }, ) LaunchedEffect(viewModel.effect) { @@ -123,16 +126,16 @@ internal fun TraineeModifyMyInfoRoute( private fun TraineeModifyMyInfoScreen( state: TraineeModifyMyInfoUiState, onProfileImageSelect: (uri: Uri) -> Unit, - onNameChange: (name: String) -> Unit, - onBirthdayChange: (birthday: LocalDate) -> Unit, - onHeightChange: (height: String) -> Unit, - onWeightChange: (weight: String) -> Unit, - onPurposeSelected: (purpose: String) -> Unit, - onCautionChange: (caution: String) -> Unit, - onBackClick: () -> Unit, - onNextClick: () -> Unit, + onChangeName: (name: String) -> Unit, + onChangeBirthday: (birthday: LocalDate) -> Unit, + onChangeHeight: (height: String) -> Unit, + onChangeWeight: (weight: String) -> Unit, + onSelectPurpose: (purpose: String) -> Unit, + onChangeCaution: (caution: String) -> Unit, + onClickBack: () -> Unit, + onClickNext: () -> Unit, ) { - BackHandler { onBackClick() } + BackHandler { onClickBack() } val context = LocalContext.current val today = LocalDate.now() @@ -153,157 +156,161 @@ private fun TraineeModifyMyInfoScreen( topBar = { TnTTopBarWithBackButton( title = stringResource(R.string.modifying_my_info), - onBackClick = onBackClick, + onBackClick = onClickBack, ) }, containerColor = TnTTheme.colors.commonColors.Common0, + modifier = Modifier.clearFocusOnTap(), ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .background(TnTTheme.colors.commonColors.Common0) - .verticalScroll(rememberScrollState()), - ) { - TnTProfileImage( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 12.dp), - defaultImage = painterResource(DefaultUserProfile.Trainee.image), - image = painter, - onEditClick = { - pickMediaLauncher.launch( - PickVisualMediaRequest( - mediaType = PickVisualMedia.ImageOnly, - ), - ) - }, - ) - Spacer(Modifier.padding(top = 32.dp)) + Box(modifier = Modifier.padding(padding)) { Column( - verticalArrangement = Arrangement.spacedBy(48.dp), - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxSize() + .consumeWindowInsets(padding) + .imePadding() + .background(TnTTheme.colors.commonColors.Common0) + .verticalScroll(rememberScrollState()), ) { - TnTLabeledTextFieldWithCounter( - title = stringResource(core_name), - value = state.name, - onValueChange = { newValue -> - onNameChange(newValue) + TnTProfileImage( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + defaultImage = painterResource(DefaultUserProfile.Trainee.image), + image = painter, + onEditClick = { + pickMediaLauncher.launch( + PickVisualMediaRequest( + mediaType = PickVisualMedia.ImageOnly, + ), + ) }, - modifier = Modifier.padding(horizontal = 20.dp), - placeholder = stringResource(core_name_placeholder), - maxLength = MAX_NAME_LENGTH, - isSingleLine = true, - showWarning = !state.isNameValid, - isRequired = true, - warningMessage = stringResource( - core_text_length_and_format_warning, - MAX_NAME_LENGTH, - ), ) - Column { - Text( - text = stringResource(R.string.birthday_label), - color = TnTTheme.colors.neutralColors.Neutral900, - style = TnTTheme.typography.body1Bold, - modifier = Modifier.padding(start = 20.dp, bottom = 8.dp), - ) - BirthdayPicker( - modifier = Modifier.padding(horizontal = 20.dp), - context = context, - today = today, - selectedDate = state.birthday, - onDateSelected = onBirthdayChange, - ) - HorizontalDivider( - thickness = 1.dp, - color = TnTTheme.colors.neutralColors.Neutral200, + Spacer(Modifier.padding(top = 32.dp)) + Column( + verticalArrangement = Arrangement.spacedBy(48.dp), + modifier = Modifier.fillMaxWidth(), + ) { + TnTLabeledTextFieldWithCounter( + title = stringResource(core_name), + value = state.name, + onValueChange = { newValue -> + onChangeName(newValue) + }, modifier = Modifier.padding(horizontal = 20.dp), + placeholder = stringResource(core_name_placeholder), + maxLength = MAX_NAME_LENGTH, + isSingleLine = true, + showWarning = !state.isNameValid, + isRequired = true, + warningMessage = stringResource( + core_text_length_and_format_warning, + MAX_NAME_LENGTH, + ), ) - } - Column { - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - ) { - TnTLabeledTextField( - title = stringResource(core_height_label), - value = state.height ?: "", - placeholder = "0", - isSingleLine = true, - showWarning = state.isHeightValid.not(), - warningMessage = stringResource(core_entered_wrong_text), - keyboardType = KeyboardType.Number, - trailingComponent = { - UnitLabel(core_height_unit) - }, - onValueChange = onHeightChange, - modifier = Modifier.weight(1f), + Column { + Text( + text = stringResource(R.string.birthday_label), + color = TnTTheme.colors.neutralColors.Neutral900, + style = TnTTheme.typography.body1Bold, + modifier = Modifier.padding(start = 20.dp, bottom = 8.dp), ) - TnTLabeledTextField( - title = stringResource(core_weight_label), - value = state.weight ?: "", - placeholder = "00.0", - isSingleLine = true, - showWarning = state.isWeightValid.not(), - warningMessage = stringResource(core_entered_wrong_text), - keyboardType = KeyboardType.Number, - trailingComponent = { - UnitLabel(core_weight_unit) - }, - onValueChange = onWeightChange, - modifier = Modifier.weight(1f), + BirthdayPicker( + modifier = Modifier.padding(horizontal = 20.dp), + context = context, + today = today, + selectedDate = state.birthday, + onDateSelected = onChangeBirthday, + ) + HorizontalDivider( + thickness = 1.dp, + color = TnTTheme.colors.neutralColors.Neutral200, + modifier = Modifier.padding(horizontal = 20.dp), ) } - } - Column(Modifier.padding(horizontal = 20.dp)) { - Text( - text = "PT 목적", - style = TnTTheme.typography.body1Bold, - color = TnTTheme.colors.neutralColors.Neutral900, - ) - Spacer(Modifier.padding(top = 12.dp)) Column { - FlowRow( - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - maxItemsInEachRow = COLUMNS_NUM, - maxLines = ROW_NUM, - modifier = Modifier.fillMaxWidth(), + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), ) { - TraineePtPurpose.entries.forEach { purpose -> - val purposeText = stringResource(purpose.textResId) - PurposeButton( - text = purposeText, - isSelected = state.ptPurpose?.contains(purposeText) == true, - onClick = { onPurposeSelected(purposeText) }, - modifier = Modifier.weight(1f), - ) + TnTLabeledTextField( + title = stringResource(core_height_label), + value = state.height ?: "", + placeholder = "0", + isSingleLine = true, + showWarning = state.isHeightValid.not(), + warningMessage = stringResource(core_entered_wrong_text), + keyboardType = KeyboardType.Number, + trailingComponent = { + UnitLabel(core_height_unit) + }, + onValueChange = onChangeHeight, + modifier = Modifier.weight(1f), + ) + TnTLabeledTextField( + title = stringResource(core_weight_label), + value = state.weight ?: "", + placeholder = "00.0", + isSingleLine = true, + showWarning = state.isWeightValid.not(), + warningMessage = stringResource(core_entered_wrong_text), + keyboardType = KeyboardType.Number, + trailingComponent = { + UnitLabel(core_weight_unit) + }, + onValueChange = onChangeWeight, + modifier = Modifier.weight(1f), + ) + } + } + Column(Modifier.padding(horizontal = 20.dp)) { + Text( + text = "PT 목적", + style = TnTTheme.typography.body1Bold, + color = TnTTheme.colors.neutralColors.Neutral900, + ) + Spacer(Modifier.padding(top = 12.dp)) + Column { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + maxItemsInEachRow = COLUMNS_NUM, + maxLines = ROW_NUM, + modifier = Modifier.fillMaxWidth(), + ) { + TraineePtPurpose.entries.forEach { purpose -> + val purposeText = stringResource(purpose.textResId) + PurposeButton( + text = purposeText, + isSelected = state.ptPurpose?.contains(purposeText) == true, + onClick = { onSelectPurpose(purposeText) }, + modifier = Modifier.weight(1f), + ) + } } } } + Column(modifier = Modifier.padding(horizontal = 20.dp)) { + Text( + text = stringResource(R.string.edit_caution_that_trainer_must_know), + modifier = Modifier.fillMaxWidth(), + style = TnTTheme.typography.body1Bold, + color = TnTTheme.colors.neutralColors.Neutral900, + ) + Spacer(Modifier.padding(top = 8.dp)) + TnTOutlinedTextField( + value = state.caution ?: "", + onValueChange = { newValue -> + onChangeCaution(newValue) + }, + isError = (state.caution?.length ?: 0) >= MAX_CAUTION_LENGTH, + warningMessage = stringResource(R.string.caution_length_overflow), + maxLength = 100, + ) + } } - Column { - Text( - text = stringResource(R.string.caution_that_trainer_must_know), - style = TnTTheme.typography.body1Bold, - color = TnTTheme.colors.neutralColors.Neutral900, - modifier = Modifier.padding(horizontal = 20.dp), - ) - Spacer(Modifier.padding(top = 8.dp)) - TnTOutlinedTextField( - value = state.caution ?: "", - onValueChange = { newValue -> - onCautionChange(newValue) - }, - modifier = Modifier.padding(horizontal = 20.dp), - isError = (state.caution?.length ?: 0) >= MAX_CAUTION_LENGTH, - warningMessage = stringResource(R.string.caution_length_overflow), - maxLength = 100, - ) - } + Spacer(Modifier.padding(top = 32.dp)) } } } @@ -385,21 +392,21 @@ fun PurposeButton( ) } -@Preview +@Preview(heightDp = 1000) @Composable private fun TraineeModifyMyScreenPreview() { TnTTheme { TraineeModifyMyInfoScreen( state = TraineeModifyMyInfoUiState(name = "김회원"), onProfileImageSelect = { }, - onNameChange = { }, - onBirthdayChange = { }, - onHeightChange = { }, - onWeightChange = { }, - onPurposeSelected = { }, - onCautionChange = { }, - onBackClick = { }, - onNextClick = { }, + onChangeName = { }, + onChangeBirthday = { }, + onChangeHeight = { }, + onChangeWeight = { }, + onSelectPurpose = { }, + onChangeCaution = { }, + onClickBack = { }, + onClickNext = { }, ) } } diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt index c41c189c..c60c2902 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt @@ -19,19 +19,20 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor() : override suspend fun handleEvent(event: TraineeModifyMyInfoUiEvent) { when (event) { - TraineeModifyMyInfoUiEvent.OnBackClick -> navigateToBack() + TraineeModifyMyInfoUiEvent.OnDeleteProfileImage -> TODO() is TraineeModifyMyInfoUiEvent.OnProfileImageSelect -> { profileImageUpdatePolicy = ProfileImageUpdatePolicy.Change(File(event.image.path)) updateProfileImage(event.image.path) } - is TraineeModifyMyInfoUiEvent.OnNameChange -> updateName(event.name) - is TraineeModifyMyInfoUiEvent.OnBirthdayChange -> updateBirthday(event.birthday) - is TraineeModifyMyInfoUiEvent.OnHeightChange -> updateHeight(event.height) - is TraineeModifyMyInfoUiEvent.OnWeightChange -> updateWeight(event.weight) - is TraineeModifyMyInfoUiEvent.OnPurposeSelected -> updateSelectedPurposes(event.purpose) - is TraineeModifyMyInfoUiEvent.OnCautionChange -> updateCaution(event.text) - TraineeModifyMyInfoUiEvent.OnNextClick -> navigateToBack() + is TraineeModifyMyInfoUiEvent.OnChangeName -> updateName(event.name) + is TraineeModifyMyInfoUiEvent.OnChangeBirthday -> updateBirthday(event.birthday) + is TraineeModifyMyInfoUiEvent.OnChangeHeight -> updateHeight(event.height) + is TraineeModifyMyInfoUiEvent.OnChangeWeight -> updateWeight(event.weight) + is TraineeModifyMyInfoUiEvent.OnSelectPurpose -> updateSelectedPurposes(event.purpose) + is TraineeModifyMyInfoUiEvent.OnChangeCaution -> updateCaution(event.text) + TraineeModifyMyInfoUiEvent.OnClickBack -> navigateToBack() + TraineeModifyMyInfoUiEvent.OnClickNext -> navigateToBack() } } diff --git a/feature/trainee/modifymyinfo/src/main/res/values/strings.xml b/feature/trainee/modifymyinfo/src/main/res/values/strings.xml index 4af4187f..5f18a77f 100644 --- a/feature/trainee/modifymyinfo/src/main/res/values/strings.xml +++ b/feature/trainee/modifymyinfo/src/main/res/values/strings.xml @@ -4,6 +4,6 @@ 생년월일 2001/01/01 - 트레이너가 알아야 할 주의사항 + 트레이너가 알아야 할 주의사항 100자 미만으로 입력해주세요 From 36747d31c8412f988009e66efe78e8e1dd1aeffb Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Sat, 13 Sep 2025 16:23:43 +0900 Subject: [PATCH 07/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=20=EC=88=98=EC=A0=95=20=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modifymyinfo/TraineeModifyMyInfoScreen.kt | 109 +++++++++++++++--- .../TraineeModifyMyInfoViewModel.kt | 6 +- .../src/main/res/values/strings.xml | 3 + 3 files changed, 99 insertions(+), 19 deletions(-) diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt index c0bdd829..d933a8bb 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -19,16 +19,22 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -49,6 +55,7 @@ import co.kr.tnt.core.ui.R.string.core_weight_label import co.kr.tnt.core.ui.R.string.core_weight_unit import co.kr.tnt.designsystem.component.TnTLabeledTextField import co.kr.tnt.designsystem.component.TnTLabeledTextFieldWithCounter +import co.kr.tnt.designsystem.component.TnTModalBottomSheet import co.kr.tnt.designsystem.component.TnTOutlinedTextField import co.kr.tnt.designsystem.component.TnTProfileImage import co.kr.tnt.designsystem.component.TnTTopBarWithBackButton @@ -76,6 +83,7 @@ private const val MAX_CAUTION_LENGTH = 100 private const val ROW_NUM = 3 private const val COLUMNS_NUM = 2 +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun TraineeModifyMyInfoRoute( viewModel: TraineeModifyMyInfoViewModel = hiltViewModel(), @@ -85,12 +93,12 @@ internal fun TraineeModifyMyInfoRoute( val state by viewModel.uiState.collectAsStateWithLifecycle() val snackbar = LocalSnackbar.current + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var showBottomSheet by rememberSaveable { mutableStateOf(false) } + TraineeModifyMyInfoScreen( state = state, - onProfileImageSelect = { uri -> - val profileImageFile = uri.convertToAllowedImageFormat(context) - viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnProfileImageSelect(profileImageFile)) - }, + onClickEditImage = { showBottomSheet = true }, onChangeName = { name -> viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnChangeName(name)) }, onChangeBirthday = { birthday -> viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnChangeBirthday(birthday)) @@ -111,6 +119,28 @@ internal fun TraineeModifyMyInfoRoute( onClickNext = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnClickNext) }, ) + if (showBottomSheet) { + TnTModalBottomSheet( + sheetState = sheetState, + onDismissRequest = { + showBottomSheet = false + }, + content = { + EditImageBottomSheetContent( + onClickDelete = { + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnDeleteProfileImage) + showBottomSheet = false + }, + onClickAlbum = { uri -> + val profileImageFile = uri.convertToAllowedImageFormat(context) + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnProfileImageSelect(profileImageFile)) + showBottomSheet = false + }, + ) + }, + ) + } + LaunchedEffect(viewModel.effect) { viewModel.effect.collect { effect -> when (effect) { @@ -125,7 +155,7 @@ internal fun TraineeModifyMyInfoRoute( @Composable private fun TraineeModifyMyInfoScreen( state: TraineeModifyMyInfoUiState, - onProfileImageSelect: (uri: Uri) -> Unit, + onClickEditImage: () -> Unit, onChangeName: (name: String) -> Unit, onChangeBirthday: (birthday: LocalDate) -> Unit, onChangeHeight: (height: String) -> Unit, @@ -140,10 +170,6 @@ private fun TraineeModifyMyInfoScreen( val context = LocalContext.current val today = LocalDate.now() - val pickMediaLauncher = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> - uri?.let(onProfileImageSelect) - } - val painter = rememberAsyncImagePainter( model = ImageRequest.Builder(LocalContext.current) .data(state.profileImage) @@ -177,13 +203,7 @@ private fun TraineeModifyMyInfoScreen( .padding(vertical = 12.dp), defaultImage = painterResource(DefaultUserProfile.Trainee.image), image = painter, - onEditClick = { - pickMediaLauncher.launch( - PickVisualMediaRequest( - mediaType = PickVisualMedia.ImageOnly, - ), - ) - }, + onEditClick = { onClickEditImage() }, ) Spacer(Modifier.padding(top = 32.dp)) Column( @@ -316,6 +336,47 @@ private fun TraineeModifyMyInfoScreen( } } +@Composable +private fun EditImageBottomSheetContent( + onClickDelete: () -> Unit, + onClickAlbum: (uri: Uri) -> Unit, +) { + val pickMediaLauncher = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> + uri?.let(onClickAlbum) + } + + Column( + modifier = Modifier.padding(horizontal = 20.dp), + ) { + Text( + text = stringResource(R.string.delete_image), + modifier = Modifier + .padding(vertical = 4.dp) + .clickable(onClick = onClickDelete), + style = TnTTheme.typography.h4, + color = TnTTheme.colors.neutralColors.Neutral600, + ) + Spacer(Modifier.height(12.dp)) + Text( + text = stringResource(R.string.select_image_from_album), + modifier = Modifier + .padding(vertical = 4.dp) + .clickable( + onClick = { + pickMediaLauncher.launch( + PickVisualMediaRequest( + mediaType = PickVisualMedia.ImageOnly, + ), + ) + }, + ), + style = TnTTheme.typography.h4, + color = TnTTheme.colors.neutralColors.Neutral600, + ) + Spacer(Modifier.height(54.dp)) + } +} + @Composable private fun BirthdayPicker( modifier: Modifier = Modifier, @@ -355,7 +416,8 @@ private fun BirthdayPicker( }, ) { Text( - text = selectedDate?.format(dateFormatter) ?: stringResource(R.string.birthday_placeholder), + text = selectedDate?.format(dateFormatter) + ?: stringResource(R.string.birthday_placeholder), color = if (selectedDate == null) { TnTTheme.colors.neutralColors.Neutral400 } else { @@ -398,7 +460,7 @@ private fun TraineeModifyMyScreenPreview() { TnTTheme { TraineeModifyMyInfoScreen( state = TraineeModifyMyInfoUiState(name = "김회원"), - onProfileImageSelect = { }, + onClickEditImage = { }, onChangeName = { }, onChangeBirthday = { }, onChangeHeight = { }, @@ -410,3 +472,14 @@ private fun TraineeModifyMyScreenPreview() { ) } } + +@Preview +@Composable +private fun ModifyMyInfoBottomSheetContentPreview() { + TnTTheme { + EditImageBottomSheetContent( + onClickDelete = { }, + onClickAlbum = { }, + ) + } +} diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt index c60c2902..64b937d7 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt @@ -19,7 +19,7 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor() : override suspend fun handleEvent(event: TraineeModifyMyInfoUiEvent) { when (event) { - TraineeModifyMyInfoUiEvent.OnDeleteProfileImage -> TODO() + TraineeModifyMyInfoUiEvent.OnDeleteProfileImage -> deleteProfileImage() is TraineeModifyMyInfoUiEvent.OnProfileImageSelect -> { profileImageUpdatePolicy = ProfileImageUpdatePolicy.Change(File(event.image.path)) updateProfileImage(event.image.path) @@ -71,6 +71,10 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor() : updateState { copy(profileImage = image) } } + private fun deleteProfileImage() { + updateState { copy(profileImage = null) } + } + private fun navigateToBack() { sendEffect(TraineeModifyMyInfoEffect.NavigateToBack) } diff --git a/feature/trainee/modifymyinfo/src/main/res/values/strings.xml b/feature/trainee/modifymyinfo/src/main/res/values/strings.xml index 5f18a77f..af7616f1 100644 --- a/feature/trainee/modifymyinfo/src/main/res/values/strings.xml +++ b/feature/trainee/modifymyinfo/src/main/res/values/strings.xml @@ -6,4 +6,7 @@ 2001/01/01 트레이너가 알아야 할 주의사항 100자 미만으로 입력해주세요 + + 삭제하기 + 앨범에서 사진 선택 From 5ed3f82f7c6f31d201e3dde6e65a1d402199d5f1 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Sat, 13 Sep 2025 16:46:59 +0900 Subject: [PATCH 08/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=EA=B0=9C=EC=9D=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20=EB=B2=84=ED=8A=BC=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 --- .../TraineeModifyMyInfoContract.kt | 3 ++- .../modifymyinfo/TraineeModifyMyInfoScreen.kt | 20 ++++++++++++++++--- .../TraineeModifyMyInfoViewModel.kt | 7 ++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt index 6622861c..ea54fcdb 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt @@ -20,6 +20,7 @@ internal class TraineeModifyMyInfoContract { val weight: String? = null, val ptPurpose: List? = emptyList(), val caution: String? = "", + val isEnableComplete: Boolean = false, ) : UiState { val isNameValid get() = name.isBlank() || @@ -62,7 +63,7 @@ internal class TraineeModifyMyInfoContract { data class OnSelectPurpose(val purpose: String) : TraineeModifyMyInfoUiEvent data class OnChangeCaution(val text: String) : TraineeModifyMyInfoUiEvent data object OnClickBack : TraineeModifyMyInfoUiEvent - data object OnClickNext : TraineeModifyMyInfoUiEvent + data object OnClickComplete : TraineeModifyMyInfoUiEvent } sealed interface TraineeModifyMyInfoEffect : UiSideEffect { diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt index d933a8bb..71adc630 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -45,6 +46,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.kr.tnt.core.ui.R.string.core_complete import co.kr.tnt.core.ui.R.string.core_entered_wrong_text import co.kr.tnt.core.ui.R.string.core_height_label import co.kr.tnt.core.ui.R.string.core_height_unit @@ -59,6 +61,7 @@ import co.kr.tnt.designsystem.component.TnTModalBottomSheet import co.kr.tnt.designsystem.component.TnTOutlinedTextField import co.kr.tnt.designsystem.component.TnTProfileImage import co.kr.tnt.designsystem.component.TnTTopBarWithBackButton +import co.kr.tnt.designsystem.component.button.TnTBottomButton import co.kr.tnt.designsystem.component.button.TnTTextButton import co.kr.tnt.designsystem.component.button.model.ButtonSize import co.kr.tnt.designsystem.component.button.model.ButtonType @@ -72,6 +75,7 @@ import co.kr.tnt.trainee.modifymyinfo.model.TraineePtPurpose import co.kr.tnt.ui.extensions.clearFocusOnTap import co.kr.tnt.ui.model.DefaultUserProfile import co.kr.tnt.ui.utils.convertToAllowedImageFormat +import co.kr.tnt.ui.utils.throttled import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import java.time.LocalDate @@ -116,7 +120,7 @@ internal fun TraineeModifyMyInfoRoute( viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnSelectPurpose(purpose)) }, onClickBack = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnClickBack) }, - onClickNext = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnClickNext) }, + onClickComplete = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnClickComplete) }, ) if (showBottomSheet) { @@ -163,7 +167,7 @@ private fun TraineeModifyMyInfoScreen( onSelectPurpose: (purpose: String) -> Unit, onChangeCaution: (caution: String) -> Unit, onClickBack: () -> Unit, - onClickNext: () -> Unit, + onClickComplete: () -> Unit, ) { BackHandler { onClickBack() } @@ -185,6 +189,16 @@ private fun TraineeModifyMyInfoScreen( onBackClick = onClickBack, ) }, + bottomBar = { + TnTBottomButton( + text = stringResource(core_complete), + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding(), + enabled = state.isEnableComplete, + onClick = throttled { onClickComplete() }, + ) + }, containerColor = TnTTheme.colors.commonColors.Common0, modifier = Modifier.clearFocusOnTap(), ) { padding -> @@ -468,7 +482,7 @@ private fun TraineeModifyMyScreenPreview() { onSelectPurpose = { }, onChangeCaution = { }, onClickBack = { }, - onClickNext = { }, + onClickComplete = { }, ) } } diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt index 64b937d7..a2d11d8c 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt @@ -32,7 +32,7 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor() : is TraineeModifyMyInfoUiEvent.OnSelectPurpose -> updateSelectedPurposes(event.purpose) is TraineeModifyMyInfoUiEvent.OnChangeCaution -> updateCaution(event.text) TraineeModifyMyInfoUiEvent.OnClickBack -> navigateToBack() - TraineeModifyMyInfoUiEvent.OnClickNext -> navigateToBack() + TraineeModifyMyInfoUiEvent.OnClickComplete -> updateUserInfo() } } @@ -75,6 +75,11 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor() : updateState { copy(profileImage = null) } } + private fun updateUserInfo() { + // TODO 수정 api 호출 + sendEffect(TraineeModifyMyInfoEffect.NavigateToBack) + } + private fun navigateToBack() { sendEffect(TraineeModifyMyInfoEffect.NavigateToBack) } From 6336357b60134f1ac102e1daf59e7762c80a6a72 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Sat, 13 Sep 2025 17:15:25 +0900 Subject: [PATCH 09/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=EA=B0=9C=EC=9D=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=20=EB=92=A4=EB=A1=9C?= =?UTF-8?q?=EA=B0=80=EA=B8=B0=20=ED=8C=9D=EC=97=85=20UI=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 --- .../TraineeModifyMyInfoContract.kt | 7 +++++++ .../modifymyinfo/TraineeModifyMyInfoScreen.kt | 19 +++++++++++++++++++ .../TraineeModifyMyInfoViewModel.kt | 10 +++++++++- .../src/main/res/values/strings.xml | 3 +++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt index ea54fcdb..fbe80ea1 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt @@ -20,6 +20,7 @@ internal class TraineeModifyMyInfoContract { val weight: String? = null, val ptPurpose: List? = emptyList(), val caution: String? = "", + val dialogState: DialogState = DialogState.NONE, val isEnableComplete: Boolean = false, ) : UiState { val isNameValid @@ -50,6 +51,10 @@ internal class TraineeModifyMyInfoContract { weight.length <= MAX_WEIGHT_LENGTH ) + enum class DialogState { + NONE, + CONFIRM_EXIT, + } // TODO 완료 버튼 활성화 조건 만들기 } @@ -62,6 +67,8 @@ internal class TraineeModifyMyInfoContract { data class OnChangeBirthday(val birthday: LocalDate) : TraineeModifyMyInfoUiEvent data class OnSelectPurpose(val purpose: String) : TraineeModifyMyInfoUiEvent data class OnChangeCaution(val text: String) : TraineeModifyMyInfoUiEvent + data object OnDismissDialog : TraineeModifyMyInfoUiEvent + data object OnClickDialogConfirm : TraineeModifyMyInfoUiEvent data object OnClickBack : TraineeModifyMyInfoUiEvent data object OnClickComplete : TraineeModifyMyInfoUiEvent } diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt index 71adc630..56452d33 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -47,14 +47,17 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.kr.tnt.core.ui.R.string.core_complete +import co.kr.tnt.core.ui.R.string.core_confirm_modify_info_exit import co.kr.tnt.core.ui.R.string.core_entered_wrong_text import co.kr.tnt.core.ui.R.string.core_height_label import co.kr.tnt.core.ui.R.string.core_height_unit import co.kr.tnt.core.ui.R.string.core_name import co.kr.tnt.core.ui.R.string.core_name_placeholder import co.kr.tnt.core.ui.R.string.core_text_length_and_format_warning +import co.kr.tnt.core.ui.R.string.core_unsaved_changes_warning import co.kr.tnt.core.ui.R.string.core_weight_label import co.kr.tnt.core.ui.R.string.core_weight_unit +import co.kr.tnt.designsystem.component.TnTIconPopupDialog import co.kr.tnt.designsystem.component.TnTLabeledTextField import co.kr.tnt.designsystem.component.TnTLabeledTextFieldWithCounter import co.kr.tnt.designsystem.component.TnTModalBottomSheet @@ -71,6 +74,7 @@ import co.kr.tnt.feature.trainee.modifymyinfo.R import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoEffect import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiEvent import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState +import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState.DialogState import co.kr.tnt.trainee.modifymyinfo.model.TraineePtPurpose import co.kr.tnt.ui.extensions.clearFocusOnTap import co.kr.tnt.ui.model.DefaultUserProfile @@ -145,6 +149,21 @@ internal fun TraineeModifyMyInfoRoute( ) } + when (state.dialogState) { + DialogState.NONE -> Unit + DialogState.CONFIRM_EXIT -> { + TnTIconPopupDialog( + title = stringResource(core_confirm_modify_info_exit), + content = stringResource(core_unsaved_changes_warning), + leftButtonText = stringResource(R.string.end), + rightButtonText = stringResource(R.string.keep_edit), + onLeftButtonClick = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnClickDialogConfirm) }, + onRightButtonClick = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnDismissDialog) }, + onDismiss = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnClickDialogConfirm) }, + ) + } + } + LaunchedEffect(viewModel.effect) { viewModel.effect.collect { effect -> when (effect) { diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt index a2d11d8c..24b53b36 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt @@ -4,6 +4,7 @@ import co.kr.tnt.domain.model.ProfileImageUpdatePolicy import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoEffect import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiEvent import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState +import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState.DialogState import co.kr.tnt.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import java.io.File @@ -31,6 +32,11 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor() : is TraineeModifyMyInfoUiEvent.OnChangeWeight -> updateWeight(event.weight) is TraineeModifyMyInfoUiEvent.OnSelectPurpose -> updateSelectedPurposes(event.purpose) is TraineeModifyMyInfoUiEvent.OnChangeCaution -> updateCaution(event.text) + TraineeModifyMyInfoUiEvent.OnClickDialogConfirm -> { + updateState { copy(dialogState = DialogState.NONE) } + sendEffect(TraineeModifyMyInfoEffect.NavigateToBack) + } + TraineeModifyMyInfoUiEvent.OnDismissDialog -> updateState { copy(dialogState = DialogState.NONE) } TraineeModifyMyInfoUiEvent.OnClickBack -> navigateToBack() TraineeModifyMyInfoUiEvent.OnClickComplete -> updateUserInfo() } @@ -81,6 +87,8 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor() : } private fun navigateToBack() { - sendEffect(TraineeModifyMyInfoEffect.NavigateToBack) + // TODO 수정된 항목 있나 확인 후, 있을 때만 dialog 띄우기 + updateState { copy(dialogState = DialogState.CONFIRM_EXIT) } + return } } diff --git a/feature/trainee/modifymyinfo/src/main/res/values/strings.xml b/feature/trainee/modifymyinfo/src/main/res/values/strings.xml index af7616f1..5b4792d9 100644 --- a/feature/trainee/modifymyinfo/src/main/res/values/strings.xml +++ b/feature/trainee/modifymyinfo/src/main/res/values/strings.xml @@ -9,4 +9,7 @@ 삭제하기 앨범에서 사진 선택 + + 종료 + 계속 수정 From 6c3b33140968a9c077252e6c33baa0445979cbd2 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Sat, 25 Oct 2025 14:53:34 +0900 Subject: [PATCH 10/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=EA=B0=9C=EC=9D=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=20=EC=A7=84=EC=9E=85=20?= =?UTF-8?q?=EC=8B=9C=20=EC=A0=95=EB=B3=B4=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TraineeModifyMyInfoViewModel.kt | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt index 24b53b36..a9ce0920 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt @@ -1,23 +1,34 @@ package co.kr.tnt.trainee.modifymyinfo +import androidx.lifecycle.viewModelScope +import co.kr.tnt.core.ui.R.string.core_failed_to_server_request import co.kr.tnt.domain.model.ProfileImageUpdatePolicy +import co.kr.tnt.domain.repository.TraineeRepository import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoEffect import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiEvent import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState.DialogState import co.kr.tnt.ui.base.BaseViewModel +import co.kr.tnt.ui.resource.DisplayText import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import java.io.File import java.time.LocalDate import javax.inject.Inject @HiltViewModel -internal class TraineeModifyMyInfoViewModel @Inject constructor() : +internal class TraineeModifyMyInfoViewModel @Inject constructor( + private val traineeRepository: TraineeRepository, +) : BaseViewModel( - TraineeModifyMyInfoUiState(), - ) { + TraineeModifyMyInfoUiState(), + ) { private var profileImageUpdatePolicy: ProfileImageUpdatePolicy = ProfileImageUpdatePolicy.Keep + init { + loadUserInfo() + } + override suspend fun handleEvent(event: TraineeModifyMyInfoUiEvent) { when (event) { TraineeModifyMyInfoUiEvent.OnDeleteProfileImage -> deleteProfileImage() @@ -36,12 +47,39 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor() : updateState { copy(dialogState = DialogState.NONE) } sendEffect(TraineeModifyMyInfoEffect.NavigateToBack) } + TraineeModifyMyInfoUiEvent.OnDismissDialog -> updateState { copy(dialogState = DialogState.NONE) } TraineeModifyMyInfoUiEvent.OnClickBack -> navigateToBack() TraineeModifyMyInfoUiEvent.OnClickComplete -> updateUserInfo() } } + private fun loadUserInfo() { + viewModelScope.launch { + runCatching { + traineeRepository.getMyInfo() + }.onSuccess { user -> + updateState { + copy( + profileImage = user.image, + name = user.name, + birthday = user.birthday, + height = user.height?.toString(), + weight = user.weight?.toString(), + ptPurpose = user.ptPurpose, + caution = user.caution, + ) + } + }.onFailure { + sendEffect( + TraineeModifyMyInfoEffect.ShowToast( + DisplayText.Resource(core_failed_to_server_request), + ), + ) + } + } + } + private fun updateSelectedPurposes(purpose: String) { val updatedPurposes = currentState.ptPurpose.orEmpty().toMutableList().apply { if (contains(purpose)) { From 1da663b6c980af286d54ca8bf6c61d97aa9dfd5d Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Sat, 25 Oct 2025 16:04:09 +0900 Subject: [PATCH 11/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=EA=B0=9C=EC=9D=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=82=AC=ED=95=AD=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 수정된 항목이 있을 경우, 뒤로가기 클릭 시 Dialog 띄우기 --- .../modifymyinfo/TraineeModifyMyInfoScreen.kt | 4 +- .../TraineeModifyMyInfoViewModel.kt | 44 +++++++++++++++++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt index 56452d33..6aa6302a 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -159,7 +159,7 @@ internal fun TraineeModifyMyInfoRoute( rightButtonText = stringResource(R.string.keep_edit), onLeftButtonClick = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnClickDialogConfirm) }, onRightButtonClick = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnDismissDialog) }, - onDismiss = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnClickDialogConfirm) }, + onDismiss = { viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnDismissDialog) }, ) } } @@ -253,7 +253,7 @@ private fun TraineeModifyMyInfoScreen( placeholder = stringResource(core_name_placeholder), maxLength = MAX_NAME_LENGTH, isSingleLine = true, - showWarning = !state.isNameValid, + showWarning = state.isNameValid.not(), isRequired = true, warningMessage = stringResource( core_text_length_and_format_warning, diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt index a9ce0920..6d7aeb45 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt @@ -3,6 +3,7 @@ package co.kr.tnt.trainee.modifymyinfo import androidx.lifecycle.viewModelScope import co.kr.tnt.core.ui.R.string.core_failed_to_server_request import co.kr.tnt.domain.model.ProfileImageUpdatePolicy +import co.kr.tnt.domain.model.User import co.kr.tnt.domain.repository.TraineeRepository import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoEffect import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiEvent @@ -23,6 +24,7 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor( BaseViewModel( TraineeModifyMyInfoUiState(), ) { + private var initializedInfo: User.Trainee? = null private var profileImageUpdatePolicy: ProfileImageUpdatePolicy = ProfileImageUpdatePolicy.Keep init { @@ -59,6 +61,8 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor( runCatching { traineeRepository.getMyInfo() }.onSuccess { user -> + initializedInfo = user + updateState { copy( profileImage = user.image, @@ -125,8 +129,42 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor( } private fun navigateToBack() { - // TODO 수정된 항목 있나 확인 후, 있을 때만 dialog 띄우기 - updateState { copy(dialogState = DialogState.CONFIRM_EXIT) } - return + if ( + isUpdateInfo( + initializedInfo = initializedInfo, + name = currentState.name, + image = currentState.profileImage, + birthday = currentState.birthday, + height = currentState.height?.toIntOrNull(), + weight = currentState.weight?.toDoubleOrNull(), + ptPurpose = currentState.ptPurpose, + caution = currentState.caution, + ) + ) { + updateState { copy(dialogState = DialogState.CONFIRM_EXIT) } + return + } + + sendEffect(TraineeModifyMyInfoEffect.NavigateToBack) } + + private fun isUpdateInfo( + initializedInfo: User.Trainee?, + name: String, + image: String?, + birthday: LocalDate?, + height: Int?, + weight: Double?, + ptPurpose: List?, + caution: String?, + ): Boolean = + initializedInfo?.let { + it.name != name || + it.image != image || + it.birthday != birthday || + it.height != height || + it.weight != weight || + it.ptPurpose?.toSet() != ptPurpose?.toSet() || + it.caution != caution + } ?: false } From 74c7d8656042831fb2aecfa3fdbc80c2f946cfc2 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Wed, 29 Oct 2025 22:19:23 +0900 Subject: [PATCH 12/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A0=95?= =?UTF-8?q?=EC=B1=85=20=EB=B0=8F=20=EC=A3=BC=EC=9D=98=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=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/ui/src/main/res/values/strings.xml | 1 + .../src/main/java/co/kr/tnt/domain/Policy.kt | 12 ++++++++ .../TraineeModifyMyInfoContract.kt | 30 +++++++------------ .../modifymyinfo/TraineeModifyMyInfoScreen.kt | 17 ++++++----- .../src/main/res/values/strings.xml | 1 - .../signup/TraineeNoteForTrainerPage.kt | 22 ++++++++------ .../trainee/signup/TraineeSignUpContract.kt | 27 +++++++---------- .../tnt/trainee/signup/TraineeSignUpScreen.kt | 2 +- .../signup/src/main/res/values/strings.xml | 1 - 9 files changed, 58 insertions(+), 55 deletions(-) diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index df150c2e..e8b3858f 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ 잘못된 수치를 입력했어요 %s자 미만의 한글 또는 영문으로 입력해주세요 + %s자 미만으로 입력해주세요 트레이니 트레이너 diff --git a/domain/src/main/java/co/kr/tnt/domain/Policy.kt b/domain/src/main/java/co/kr/tnt/domain/Policy.kt index 13b8bc98..17553b3f 100644 --- a/domain/src/main/java/co/kr/tnt/domain/Policy.kt +++ b/domain/src/main/java/co/kr/tnt/domain/Policy.kt @@ -7,6 +7,18 @@ object UserProfilePolicy { // TnT 에서 사용자가 입력할 수 있는 이름의 최대 길이는 15자이다. const val USER_NAME_MAX_LENGTH = 15 + // TnT 에서 사용자가 입력할 수 있는 키의 최대 길이는 3자이다. + const val USER_HEIGHT_MAX_LENGTH = 3 + + // TnT 에서 사용자가 입력할 수 있는 몸무게의 최대 길이는 5자이다. (000.0) + const val USER_WEIGHT_MAX_LENGTH = 5 + + // TnT 에서 사용자가 입력할 수 있는 주의사항의 최대 길이는 100자이다. + const val USER_CAUTION_MAX_LENGTH = 100 + // TnT 에서 사용자가 입력할 수 있는 이름은 한글, 영어, 공백만 허용한다. val USER_NAME_REGEX = Regex("^[a-zA-Zㄱ-ㅎㅏ-ㅣ가-힣 ]+\$") + + // TnT 에서 사용자가 입력할 수 있는 몸무게는 소수점 이하 한 자리까지만 허용한다. (000, 00, 00.0, 000.0) + val USER_WEIGHT_REGEX = Regex("^(\\d{1,3}(\\.\\d)?)?\$") } diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt index fbe80ea1..dcc0a123 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt @@ -8,9 +8,6 @@ import co.kr.tnt.ui.resource.DisplayText import java.io.File import java.time.LocalDate -private const val MAX_HEIGHT_LENGTH = 3 -private const val MAX_WEIGHT_LENGTH = 5 - internal class TraineeModifyMyInfoContract { data class TraineeModifyMyInfoUiState( val profileImage: String? = null, @@ -24,38 +21,33 @@ internal class TraineeModifyMyInfoContract { val isEnableComplete: Boolean = false, ) : UiState { val isNameValid - get() = name.isBlank() || + get() = name.isNotBlank() && name.matches(UserProfilePolicy.USER_NAME_REGEX) && name.length <= UserProfilePolicy.USER_NAME_MAX_LENGTH - /** - * 키가 유효한 입력값인지 검사 - * 형식: 정수 3자 - */ val isHeightValid get() = height.isNullOrBlank() || ( height.toIntOrNull() != null && - !height.startsWith("0") && - height.length <= MAX_HEIGHT_LENGTH + height.startsWith("0").not() && + height.length <= UserProfilePolicy.USER_HEIGHT_MAX_LENGTH ) - /** - * 몸무게가 유효한 입력값인지 검사 - * 형식: 5자 이하의 실수 (000, 00, 00.0, 000.0) - */ - private val weightRegex = Regex("^(\\d{1,3}(\\.\\d)?)?\$") val isWeightValid get() = weight.isNullOrBlank() || ( - weight.matches(weightRegex) && - !weight.startsWith("0") && - weight.length <= MAX_WEIGHT_LENGTH + weight.matches(UserProfilePolicy.USER_WEIGHT_REGEX) && + weight.startsWith("0").not() && + weight.length <= UserProfilePolicy.USER_WEIGHT_MAX_LENGTH + ) + + val isCautionNoteValid + get() = caution.isNullOrBlank() || ( + caution.length < UserProfilePolicy.USER_CAUTION_MAX_LENGTH ) enum class DialogState { NONE, CONFIRM_EXIT, } - // TODO 완료 버튼 활성화 조건 만들기 } sealed interface TraineeModifyMyInfoUiEvent : UiEvent { diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt index 6aa6302a..e4534e35 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -54,6 +54,7 @@ import co.kr.tnt.core.ui.R.string.core_height_unit import co.kr.tnt.core.ui.R.string.core_name import co.kr.tnt.core.ui.R.string.core_name_placeholder import co.kr.tnt.core.ui.R.string.core_text_length_and_format_warning +import co.kr.tnt.core.ui.R.string.core_text_length_warning import co.kr.tnt.core.ui.R.string.core_unsaved_changes_warning import co.kr.tnt.core.ui.R.string.core_weight_label import co.kr.tnt.core.ui.R.string.core_weight_unit @@ -70,6 +71,7 @@ import co.kr.tnt.designsystem.component.button.model.ButtonSize import co.kr.tnt.designsystem.component.button.model.ButtonType import co.kr.tnt.designsystem.snackbar.LocalSnackbar import co.kr.tnt.designsystem.theme.TnTTheme +import co.kr.tnt.domain.UserProfilePolicy import co.kr.tnt.feature.trainee.modifymyinfo.R import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoEffect import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiEvent @@ -86,8 +88,6 @@ import java.time.LocalDate import java.time.ZoneId import java.time.format.DateTimeFormatter -private const val MAX_NAME_LENGTH = 15 -private const val MAX_CAUTION_LENGTH = 100 private const val ROW_NUM = 3 private const val COLUMNS_NUM = 2 @@ -251,13 +251,13 @@ private fun TraineeModifyMyInfoScreen( }, modifier = Modifier.padding(horizontal = 20.dp), placeholder = stringResource(core_name_placeholder), - maxLength = MAX_NAME_LENGTH, + maxLength = UserProfilePolicy.USER_NAME_MAX_LENGTH, isSingleLine = true, showWarning = state.isNameValid.not(), isRequired = true, warningMessage = stringResource( core_text_length_and_format_warning, - MAX_NAME_LENGTH, + UserProfilePolicy.USER_NAME_MAX_LENGTH, ), ) Column { @@ -357,9 +357,12 @@ private fun TraineeModifyMyInfoScreen( onValueChange = { newValue -> onChangeCaution(newValue) }, - isError = (state.caution?.length ?: 0) >= MAX_CAUTION_LENGTH, - warningMessage = stringResource(R.string.caution_length_overflow), - maxLength = 100, + isError = state.isCautionNoteValid.not(), + warningMessage = stringResource( + core_text_length_warning, + UserProfilePolicy.USER_CAUTION_MAX_LENGTH, + ), + maxLength = UserProfilePolicy.USER_CAUTION_MAX_LENGTH, ) } } diff --git a/feature/trainee/modifymyinfo/src/main/res/values/strings.xml b/feature/trainee/modifymyinfo/src/main/res/values/strings.xml index 5b4792d9..f95a775c 100644 --- a/feature/trainee/modifymyinfo/src/main/res/values/strings.xml +++ b/feature/trainee/modifymyinfo/src/main/res/values/strings.xml @@ -5,7 +5,6 @@ 생년월일 2001/01/01 트레이너가 알아야 할 주의사항 - 100자 미만으로 입력해주세요 삭제하기 앨범에서 사진 선택 diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeNoteForTrainerPage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeNoteForTrainerPage.kt index 329a4978..8028e850 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeNoteForTrainerPage.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeNoteForTrainerPage.kt @@ -18,19 +18,20 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import co.kr.tnt.core.ui.R.string.core_next +import co.kr.tnt.core.ui.R.string.core_text_length_warning import co.kr.tnt.designsystem.component.TnTOutlinedTextField import co.kr.tnt.designsystem.component.TnTTopBarWithBackButton import co.kr.tnt.designsystem.component.button.TnTBottomButton import co.kr.tnt.designsystem.theme.TnTTheme +import co.kr.tnt.domain.UserProfilePolicy import co.kr.tnt.feature.trainee.signup.R +import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState import co.kr.tnt.trainee.signup.component.ProgressSteps import co.kr.tnt.ui.extensions.clearFocusOnTap -private const val MAX_LENGTH = 100 - @Composable internal fun TraineeNoteForTrainerPage( - caution: String?, + state: TraineeSignUpUiState, onChangeCaution: (String) -> Unit, onClickBack: () -> Unit, onClickNext: () -> Unit, @@ -58,20 +59,23 @@ internal fun TraineeNoteForTrainerPage( ) Spacer(Modifier.padding(top = 48.dp)) TnTOutlinedTextField( - value = caution ?: "", + value = state.caution ?: "", onValueChange = { newValue -> onChangeCaution(newValue) }, modifier = Modifier.padding(horizontal = 20.dp), - isError = (caution?.length ?: 0) >= MAX_LENGTH, - warningMessage = stringResource(R.string.text_length_overflow), - maxLength = 100, + isError = state.isCautionNoteValid.not(), + warningMessage = stringResource( + core_text_length_warning, + UserProfilePolicy.USER_CAUTION_MAX_LENGTH, + ), + maxLength = UserProfilePolicy.USER_CAUTION_MAX_LENGTH, ) } TnTBottomButton( text = stringResource(core_next), modifier = Modifier.align(Alignment.BottomCenter), - enabled = (caution?.length ?: 0) < MAX_LENGTH, + enabled = state.isCautionNoteValid, onClick = onClickNext, ) } @@ -83,7 +87,7 @@ internal fun TraineeNoteForTrainerPage( private fun TraineeNoteForTrainerPagePreview() { TnTTheme { TraineeNoteForTrainerPage( - caution = "", + state = TraineeSignUpUiState(), onClickBack = {}, onClickNext = {}, onChangeCaution = {}, diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpContract.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpContract.kt index 0d11656a..c91081d4 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpContract.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpContract.kt @@ -9,9 +9,6 @@ import co.kr.tnt.ui.resource.DisplayText import java.io.File import java.time.LocalDate -private const val MAX_HEIGHT_LENGTH = 3 -private const val MAX_WEIGHT_LENGTH = 5 - internal class TraineeSignUpContract { data class TraineeSignUpUiState( val page: TraineeSignUpPage = TraineeSignUpPage.ProfileSetUp, @@ -29,31 +26,27 @@ internal class TraineeSignUpContract { name.matches(UserProfilePolicy.USER_NAME_REGEX) && name.length <= UserProfilePolicy.USER_NAME_MAX_LENGTH - /** - * 키가 유효한 입력값인지 검사 - * 형식: 정수 3자 - */ val isHeightValid get() = height.isNullOrBlank() || ( height.toIntOrNull() != null && - !height.startsWith("0") && - height.length <= MAX_HEIGHT_LENGTH + height.startsWith("0").not() && + height.length <= UserProfilePolicy.USER_HEIGHT_MAX_LENGTH ) - /** - * 몸무게가 유효한 입력값인지 검사 - * 형식: 5자 이하의 실수 (000, 00, 00.0, 000.0) - */ - private val weightRegex = Regex("^(\\d{1,3}(\\.\\d)?)?\$") val isWeightValid get() = weight.isNullOrBlank() || ( - weight.matches(weightRegex) && - !weight.startsWith("0") && - weight.length <= MAX_WEIGHT_LENGTH + weight.matches(UserProfilePolicy.USER_WEIGHT_REGEX) && + weight.startsWith("0").not() && + weight.length <= UserProfilePolicy.USER_WEIGHT_MAX_LENGTH ) val isBasicInfoValid get() = isWeightValid && isHeightValid + + val isCautionNoteValid + get() = caution.isNullOrBlank() || ( + caution.length < UserProfilePolicy.USER_CAUTION_MAX_LENGTH + ) } sealed interface TraineeSignUpUiEvent : UiEvent { diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpScreen.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpScreen.kt index 702e64d7..cb4b229b 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpScreen.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpScreen.kt @@ -97,7 +97,7 @@ private fun TraineeSignUpScreen( ) TraineeSignUpPage.NoteForTrainer -> TraineeNoteForTrainerPage( - caution = state.caution, + state = state, onChangeCaution = onChangeCaution, onClickBack = onClickBack, onClickNext = onClickNext, diff --git a/feature/trainee/signup/src/main/res/values/strings.xml b/feature/trainee/signup/src/main/res/values/strings.xml index 4f6108fa..d8459589 100644 --- a/feature/trainee/signup/src/main/res/values/strings.xml +++ b/feature/trainee/signup/src/main/res/values/strings.xml @@ -14,7 +14,6 @@ 트레이너가 꼭 알아야 할\n주의사항이 있나요? 트레이너에게 알려드릴게요. - 100자 미만으로 입력해주세요 만나서 반가워요\n%s 트레이니님! 트레이너와 함께\n케미를 터뜨려보세요! 🧨 From 0ba0ff47cdd5f3c1759f1168e294b60bc8d4b24d Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Thu, 30 Oct 2025 17:37:08 +0900 Subject: [PATCH 13/26] =?UTF-8?q?[TNT-000]=20fix:=20SignUpRequest=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - goalContents -> ptGoals --- .../src/main/java/co/kr/data/network/model/SignUpRequest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/network/src/main/java/co/kr/data/network/model/SignUpRequest.kt b/data/network/src/main/java/co/kr/data/network/model/SignUpRequest.kt index cf38ac49..39387e48 100644 --- a/data/network/src/main/java/co/kr/data/network/model/SignUpRequest.kt +++ b/data/network/src/main/java/co/kr/data/network/model/SignUpRequest.kt @@ -17,7 +17,7 @@ data class SignUpRequest( val birthday: String? = null, val height: Double? = null, val weight: Double? = null, - val goalContents: List? = null, + val ptGoals: List? = null, val cautionNote: String? = "", ) @@ -34,7 +34,7 @@ fun User.toSignUpRequest( birthday = null, height = null, weight = null, - goalContents = null, + ptGoals = null, cautionNote = null, socialType = socialType, socialId = socialId, @@ -51,7 +51,7 @@ fun User.toSignUpRequest( birthday = birthday?.toString(), height = height?.toDouble(), weight = weight, - goalContents = ptPurpose, + ptGoals = ptPurpose, cautionNote = caution?.ifBlank { null }, socialType = socialType, socialId = socialId, From 2160965ce76dbe2fceb2af09ceb6aef7530567c3 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Thu, 30 Oct 2025 17:40:20 +0900 Subject: [PATCH 14/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=EA=B0=9C=EC=9D=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20'=EC=99=84=EB=A3=8C'=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20=ED=99=9C=EC=84=B1=ED=99=94=20=EC=A1=B0=EA=B1=B4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TraineeModifyMyInfoViewModel.kt | 72 +++++++++++++------ 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt index 6d7aeb45..d38c8987 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt @@ -84,43 +84,51 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor( } } - private fun updateSelectedPurposes(purpose: String) { - val updatedPurposes = currentState.ptPurpose.orEmpty().toMutableList().apply { - if (contains(purpose)) { - remove(purpose) - } else { - add(purpose) - } - } - updateState { copy(ptPurpose = updatedPurposes) } - } - - private fun updateCaution(caution: String) { - updateState { copy(caution = caution) } + private fun updateProfileImage(image: String) { + updateState { copy(profileImage = image) } + isEnableModifyInfo(initializedInfo) } - private fun updateHeight(height: String) { - updateState { copy(height = height) } + private fun deleteProfileImage() { + updateState { copy(profileImage = null) } + isEnableModifyInfo(initializedInfo) } - private fun updateWeight(weight: String) { - updateState { copy(weight = weight) } + private fun updateName(name: String) { + updateState { copy(name = name) } + isEnableModifyInfo(initializedInfo) } private fun updateBirthday(birthday: LocalDate) { updateState { copy(birthday = birthday) } + isEnableModifyInfo(initializedInfo) } - private fun updateName(name: String) { - updateState { copy(name = name) } + private fun updateHeight(height: String) { + updateState { copy(height = height) } + isEnableModifyInfo(initializedInfo) } - private fun updateProfileImage(image: String) { - updateState { copy(profileImage = image) } + private fun updateWeight(weight: String) { + updateState { copy(weight = weight) } + isEnableModifyInfo(initializedInfo) } - private fun deleteProfileImage() { - updateState { copy(profileImage = null) } + private fun updateSelectedPurposes(purpose: String) { + val updatedPurposes = currentState.ptPurpose.orEmpty().toMutableList().apply { + if (contains(purpose)) { + remove(purpose) + } else { + add(purpose) + } + } + updateState { copy(ptPurpose = updatedPurposes) } + isEnableModifyInfo(initializedInfo) + } + + private fun updateCaution(caution: String) { + updateState { copy(caution = caution) } + isEnableModifyInfo(initializedInfo) } private fun updateUserInfo() { @@ -148,6 +156,24 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor( sendEffect(TraineeModifyMyInfoEffect.NavigateToBack) } + private fun isEnableModifyInfo(initializedInfo: User.Trainee?) { + val isEnable = isUpdateInfo( + initializedInfo, + currentState.name, + currentState.profileImage, + currentState.birthday, + currentState.height?.toIntOrNull(), + currentState.weight?.toDoubleOrNull(), + currentState.ptPurpose, + currentState.caution, + ) && currentState.isNameValid && + currentState.isWeightValid && + currentState.isWeightValid && + currentState.isCautionNoteValid + + updateState { copy(isEnableComplete = isEnable) } + } + private fun isUpdateInfo( initializedInfo: User.Trainee?, name: String, From 0e086d620255667453c0a766841bb4e6741317b5 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Mon, 3 Nov 2025 21:15:33 +0900 Subject: [PATCH 15/26] =?UTF-8?q?[TNT-000]=20fix:=20UpdateUserInfoRequest?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - goalContents -> ptGoals --- .../main/java/co/kr/data/network/model/UpdateUserInfoRequest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/network/src/main/java/co/kr/data/network/model/UpdateUserInfoRequest.kt b/data/network/src/main/java/co/kr/data/network/model/UpdateUserInfoRequest.kt index facd2327..130dcdc5 100644 --- a/data/network/src/main/java/co/kr/data/network/model/UpdateUserInfoRequest.kt +++ b/data/network/src/main/java/co/kr/data/network/model/UpdateUserInfoRequest.kt @@ -12,5 +12,5 @@ data class UpdateUserInfoRequest( val height: Double? = null, val weight: Double? = null, val cautionNote: String? = null, - val goalContents: List? = null, + val ptGoals: List? = null, ) From 3b27e3ddb7bac727fa1963116da1e8a1a1a6dcc6 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Fri, 7 Nov 2025 17:34:22 +0900 Subject: [PATCH 16/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=EA=B0=9C=EC=9D=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20API=20=ED=98=B8=EC=B6=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 --- .../network/source/TraineeRemoteDataSource.kt | 7 ++++ .../data/repository/TraineeRepositoryImpl.kt | 38 +++++++++++++++++++ .../domain/repository/TraineeRepository.kt | 5 +++ 3 files changed, 50 insertions(+) diff --git a/data/network/src/main/java/co/kr/data/network/source/TraineeRemoteDataSource.kt b/data/network/src/main/java/co/kr/data/network/source/TraineeRemoteDataSource.kt index 3f6db23d..05d6662b 100644 --- a/data/network/src/main/java/co/kr/data/network/source/TraineeRemoteDataSource.kt +++ b/data/network/src/main/java/co/kr/data/network/source/TraineeRemoteDataSource.kt @@ -34,4 +34,11 @@ class TraineeRemoteDataSource @Inject constructor( suspend fun getMealRecord(dietId: Long) = networkHandler { apiService.getMealRecord(dietId) } + + suspend fun putUserInfo( + profileImage: MultipartBody.Part?, + request: RequestBody, + ) = networkHandler { + apiService.putMyInfo(profileImage, request) + } } diff --git a/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt b/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt index 87a39421..508895c4 100644 --- a/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt +++ b/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt @@ -1,10 +1,13 @@ package co.kr.data.repository +import co.kr.data.network.model.UpdateUserInfoRequest +import co.kr.data.network.model.enum.MemberType import co.kr.data.network.model.toDomain import co.kr.data.network.model.trainee.MealRecordRequest import co.kr.data.network.model.trainee.toDomain import co.kr.data.network.source.TraineeRemoteDataSource import co.kr.data.network.source.UserRemoteDataSource +import co.kr.tnt.domain.model.ProfileImageUpdatePolicy import co.kr.tnt.domain.model.User import co.kr.tnt.domain.model.trainee.TraineeDailyRecord import co.kr.tnt.domain.model.trainee.TraineeDailyRecordStatus @@ -82,4 +85,39 @@ internal class TraineeRepositoryImpl @Inject constructor( override suspend fun getMealRecord(dietId: Long): TraineeMealRecordDetail = traineeRemoteDataSource.getMealRecord(dietId).toDomain(dateFormatter) + + override suspend fun updateUserInfo( + profileImageUpdatePolicy: ProfileImageUpdatePolicy, + userInfo: User.Trainee, + ) { + val (profileImage, isRemoveProfileImage) = when (profileImageUpdatePolicy) { + is ProfileImageUpdatePolicy.Change -> profileImageUpdatePolicy.newProfileImage to false + ProfileImageUpdatePolicy.Keep -> null to false + ProfileImageUpdatePolicy.Remove -> null to true + } + val imagePart = profileImage?.let { + val requestFile = it.asRequestBody("image/*".toMediaTypeOrNull()) + MultipartBody.Part.createFormData("dietImage", it.name, requestFile) + } + val selectedDate = userInfo.birthday?.let { dateFormatter.format(it, "yyyy-MM-dd") } + + val request = UpdateUserInfoRequest( + removeImage = isRemoveProfileImage, + memberType = MemberType.TRAINEE, + name = userInfo.name, + birthDay = selectedDate, + height = userInfo.height?.toDouble(), + weight = userInfo.weight, + cautionNote = userInfo.caution, + ptGoals = userInfo.ptPurpose, + ) + val requestBody = json + .encodeToString(request) + .toRequestBody("application/json".toMediaTypeOrNull()) + + traineeRemoteDataSource.putUserInfo( + profileImage = imagePart, + request = requestBody, + ) + } } diff --git a/domain/src/main/java/co/kr/tnt/domain/repository/TraineeRepository.kt b/domain/src/main/java/co/kr/tnt/domain/repository/TraineeRepository.kt index 44693fea..75c6867a 100644 --- a/domain/src/main/java/co/kr/tnt/domain/repository/TraineeRepository.kt +++ b/domain/src/main/java/co/kr/tnt/domain/repository/TraineeRepository.kt @@ -1,5 +1,6 @@ package co.kr.tnt.domain.repository +import co.kr.tnt.domain.model.ProfileImageUpdatePolicy import co.kr.tnt.domain.model.User import co.kr.tnt.domain.model.trainee.TraineeDailyRecord import co.kr.tnt.domain.model.trainee.TraineeDailyRecordStatus @@ -23,4 +24,8 @@ interface TraineeRepository { suspend fun getMealRecord( dietId: Long, ): TraineeMealRecordDetail + suspend fun updateUserInfo( + profileImageUpdatePolicy: ProfileImageUpdatePolicy, + userInfo: User.Trainee, + ) } From 11545b37ac1f5682d73af2d3f20b3530a4f3a1d1 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Fri, 7 Nov 2025 17:58:22 +0900 Subject: [PATCH 17/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=EA=B0=9C=EC=9D=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TraineeModifyMyInfoContract.kt | 1 + .../modifymyinfo/TraineeModifyMyInfoScreen.kt | 16 +++++++-- .../TraineeModifyMyInfoViewModel.kt | 36 +++++++++++++++++-- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt index dcc0a123..b8d22a30 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoContract.kt @@ -19,6 +19,7 @@ internal class TraineeModifyMyInfoContract { val caution: String? = "", val dialogState: DialogState = DialogState.NONE, val isEnableComplete: Boolean = false, + val isLoading: Boolean = false, ) : UiState { val isNameValid get() = name.isNotBlank() && diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt index e4534e35..ca8dc0f6 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -78,12 +79,14 @@ import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyM import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyMyInfoUiState.DialogState import co.kr.tnt.trainee.modifymyinfo.model.TraineePtPurpose +import co.kr.tnt.ui.component.TnTLoadingScreen import co.kr.tnt.ui.extensions.clearFocusOnTap import co.kr.tnt.ui.model.DefaultUserProfile import co.kr.tnt.ui.utils.convertToAllowedImageFormat import co.kr.tnt.ui.utils.throttled import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest +import kotlinx.coroutines.launch import java.time.LocalDate import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -99,6 +102,7 @@ internal fun TraineeModifyMyInfoRoute( ) { val context = LocalContext.current val state by viewModel.uiState.collectAsStateWithLifecycle() + val coroutineScope = rememberCoroutineScope() val snackbar = LocalSnackbar.current val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -140,9 +144,11 @@ internal fun TraineeModifyMyInfoRoute( showBottomSheet = false }, onClickAlbum = { uri -> - val profileImageFile = uri.convertToAllowedImageFormat(context) - viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnProfileImageSelect(profileImageFile)) - showBottomSheet = false + coroutineScope.launch { + val profileImageFile = uri.convertToAllowedImageFormat(context) + viewModel.setEvent(TraineeModifyMyInfoUiEvent.OnProfileImageSelect(profileImageFile)) + showBottomSheet = false + } }, ) }, @@ -370,6 +376,10 @@ private fun TraineeModifyMyInfoScreen( } } } + + if (state.isLoading) { + TnTLoadingScreen() + } } @Composable diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt index d38c8987..b508ba95 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt @@ -33,7 +33,11 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor( override suspend fun handleEvent(event: TraineeModifyMyInfoUiEvent) { when (event) { - TraineeModifyMyInfoUiEvent.OnDeleteProfileImage -> deleteProfileImage() + TraineeModifyMyInfoUiEvent.OnDeleteProfileImage -> { + profileImageUpdatePolicy = ProfileImageUpdatePolicy.Remove + deleteProfileImage() + } + is TraineeModifyMyInfoUiEvent.OnProfileImageSelect -> { profileImageUpdatePolicy = ProfileImageUpdatePolicy.Change(File(event.image.path)) updateProfileImage(event.image.path) @@ -132,8 +136,34 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor( } private fun updateUserInfo() { - // TODO 수정 api 호출 - sendEffect(TraineeModifyMyInfoEffect.NavigateToBack) + viewModelScope.launch { + updateState { copy(isLoading = true) } + val userInfo = User.Trainee.EMPTY + runCatching { + traineeRepository.updateUserInfo( + profileImageUpdatePolicy = profileImageUpdatePolicy, + userInfo = userInfo.copy( + name = currentState.name, + image = currentState.profileImage, + birthday = currentState.birthday, + weight = currentState.weight?.toDoubleOrNull(), + height = currentState.height?.toIntOrNull(), + ptPurpose = currentState.ptPurpose, + caution = currentState.caution, + ), + ) + }.onSuccess { + sendEffect(TraineeModifyMyInfoEffect.NavigateToBack) + }.onFailure { + sendEffect( + TraineeModifyMyInfoEffect.ShowToast( + DisplayText.Resource(core_failed_to_server_request), + ), + ) + }.also { + updateState { copy(isLoading = false) } + } + } } private fun navigateToBack() { From f2bfd2a9c8c96c9ca47812891b071437a6ac2076 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Mon, 10 Nov 2025 19:04:52 +0900 Subject: [PATCH 18/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=EC=9C=A0=EC=A0=80=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=BA=90=EC=8B=B1=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 수정된 개인 정보가 바로 마이페이지에 반영되도록 수정 --- .../data/repository/TraineeRepositoryImpl.kt | 46 +++++++++++++------ .../domain/repository/TraineeRepository.kt | 3 +- .../kr/tnt/trainee/home/TraineeHomeScreen.kt | 3 +- .../tnt/trainee/home/TraineeHomeViewModel.kt | 5 +- .../modifymyinfo/TraineeModifyMyInfoScreen.kt | 3 +- .../TraineeModifyMyInfoViewModel.kt | 3 +- .../tnt/trainee/mypage/TraineeMyPageScreen.kt | 10 ++-- .../trainee/mypage/TraineeMyPageViewModel.kt | 25 +++++----- 8 files changed, 62 insertions(+), 36 deletions(-) diff --git a/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt b/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt index 508895c4..8b099738 100644 --- a/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt +++ b/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt @@ -14,11 +14,13 @@ import co.kr.tnt.domain.model.trainee.TraineeDailyRecordStatus import co.kr.tnt.domain.model.trainee.TraineeMealRecordDetail import co.kr.tnt.domain.repository.TraineeRepository import co.kr.tnt.domain.utils.DateFormatter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.onStart import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody -import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import java.io.File @@ -33,10 +35,15 @@ internal class TraineeRepositoryImpl @Inject constructor( private val dateFormatter: DateFormatter, private val json: Json, ) : TraineeRepository { - override suspend fun getMyInfo(): User.Trainee { - val user = userRemoteDataSource.getMyInfo().toDomain(dateFormatter) - require(user is User.Trainee) - return user + private val cacheUserInfo = MutableStateFlow(User.Trainee.EMPTY) + + override suspend fun getMyInfo(): Flow { + return cacheUserInfo + .onStart { + if (cacheUserInfo.value == User.Trainee.EMPTY) { + cacheUserInfo.value = fetchUserInfo() + } + } } override suspend fun getWeeklyRecordedDate( @@ -70,7 +77,9 @@ internal class TraineeRepositoryImpl @Inject constructor( dietType = mealType, memo = memo, ) - val requestBody = mealRecordRequest.toRequestBody() + val requestBody = json + .encodeToString(mealRecordRequest) + .toRequestBody("application/json".toMediaTypeOrNull()) traineeRemoteDataSource.postMealRecord( dietImage = imagePart, @@ -78,11 +87,6 @@ internal class TraineeRepositoryImpl @Inject constructor( ) } - private fun MealRecordRequest.toRequestBody(): RequestBody { - val jsonString = json.encodeToString(this) - return jsonString.toRequestBody("application/json".toMediaTypeOrNull()) - } - override suspend fun getMealRecord(dietId: Long): TraineeMealRecordDetail = traineeRemoteDataSource.getMealRecord(dietId).toDomain(dateFormatter) @@ -115,9 +119,21 @@ internal class TraineeRepositoryImpl @Inject constructor( .encodeToString(request) .toRequestBody("application/json".toMediaTypeOrNull()) - traineeRemoteDataSource.putUserInfo( - profileImage = imagePart, - request = requestBody, - ) + runCatching { + traineeRemoteDataSource.putUserInfo( + profileImage = imagePart, + request = requestBody, + ) + }.onSuccess { + cacheUserInfo.value = fetchUserInfo() + }.onFailure { failure -> + throw failure + } + } + + private suspend fun fetchUserInfo(): User.Trainee { + val user = userRemoteDataSource.getMyInfo().toDomain(dateFormatter) + require(user is User.Trainee) + return user } } diff --git a/domain/src/main/java/co/kr/tnt/domain/repository/TraineeRepository.kt b/domain/src/main/java/co/kr/tnt/domain/repository/TraineeRepository.kt index 75c6867a..176d10e6 100644 --- a/domain/src/main/java/co/kr/tnt/domain/repository/TraineeRepository.kt +++ b/domain/src/main/java/co/kr/tnt/domain/repository/TraineeRepository.kt @@ -5,11 +5,12 @@ import co.kr.tnt.domain.model.User import co.kr.tnt.domain.model.trainee.TraineeDailyRecord import co.kr.tnt.domain.model.trainee.TraineeDailyRecordStatus import co.kr.tnt.domain.model.trainee.TraineeMealRecordDetail +import kotlinx.coroutines.flow.Flow import java.io.File import java.time.LocalDate interface TraineeRepository { - suspend fun getMyInfo(): User.Trainee + suspend fun getMyInfo(): Flow suspend fun postMealRecord( mealImage: File?, date: String, diff --git a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeScreen.kt b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeScreen.kt index 43b24b35..abf20783 100644 --- a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeScreen.kt +++ b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeScreen.kt @@ -73,6 +73,7 @@ import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.time.DayOfWeek import java.time.LocalDate @@ -145,7 +146,7 @@ internal fun TraineeHomeRoute( } LaunchedEffect(viewModel.effect) { - viewModel.effect.collect { effect -> + viewModel.effect.collectLatest { effect -> when (effect) { TraineeHomeEffect.NavigateToExerciseRecord -> { showBottomSheet = false diff --git a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt index 08a082ab..fff5cca9 100644 --- a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt +++ b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt @@ -12,6 +12,7 @@ import co.kr.tnt.ui.base.BaseViewModel import co.kr.tnt.ui.resource.DisplayText import com.kizitonwose.calendar.core.yearMonth import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import java.time.Duration @@ -166,10 +167,10 @@ internal class TraineeHomeViewModel @Inject constructor( private fun showConnectDialog() { val currentDateTime = LocalDateTime.now() - + // TODO 트레이너 연결 반영 viewModelScope.launch { runCatching { - traineeRepository.getMyInfo() + traineeRepository.getMyInfo().first() }.onSuccess { result -> updateState { copy(isConnected = result.isConnected) } if (result.isConnected) { diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt index ca8dc0f6..4e2f74d7 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -86,6 +86,7 @@ import co.kr.tnt.ui.utils.convertToAllowedImageFormat import co.kr.tnt.ui.utils.throttled import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.time.LocalDate import java.time.ZoneId @@ -171,7 +172,7 @@ internal fun TraineeModifyMyInfoRoute( } LaunchedEffect(viewModel.effect) { - viewModel.effect.collect { effect -> + viewModel.effect.collectLatest { effect -> when (effect) { TraineeModifyMyInfoEffect.NavigateToBack -> navigateToPrevious() is TraineeModifyMyInfoEffect.ShowToast -> snackbar.show(effect.message.asString(context)) diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt index b508ba95..c19fd7cc 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoViewModel.kt @@ -12,6 +12,7 @@ import co.kr.tnt.trainee.modifymyinfo.TraineeModifyMyInfoContract.TraineeModifyM import co.kr.tnt.ui.base.BaseViewModel import co.kr.tnt.ui.resource.DisplayText import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.io.File import java.time.LocalDate @@ -63,7 +64,7 @@ internal class TraineeModifyMyInfoViewModel @Inject constructor( private fun loadUserInfo() { viewModelScope.launch { runCatching { - traineeRepository.getMyInfo() + traineeRepository.getMyInfo().first() }.onSuccess { user -> initializedInfo = user diff --git a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt index 0cb494f8..46a3d433 100644 --- a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt +++ b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageScreen.kt @@ -71,6 +71,7 @@ import coil.request.ImageRequest import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.android.gms.oss.licenses.OssLicensesMenuActivity +import kotlinx.coroutines.flow.collectLatest import java.time.LocalDate @OptIn(ExperimentalPermissionsApi::class) @@ -98,7 +99,9 @@ internal fun TraineeMyPageRoute( onTogglePushNotification = { viewModel.setEvent( TraineeMyPageUiEvent.OnToggleNotification( - isGrantedPermission = TnTPermission.NOTIFICATION.isRequireGranted(permissionState), + isGrantedPermission = TnTPermission.NOTIFICATION.isRequireGranted( + permissionState, + ), shouldShowRationale = permissionState.shouldShowRationale, ), ) @@ -121,7 +124,7 @@ internal fun TraineeMyPageRoute( } LaunchedEffect(viewModel.effect) { - viewModel.effect.collect { effect -> + viewModel.effect.collectLatest { effect -> when (effect) { TraineeMyPageEffect.NavigateToModifyMyInfo -> navigateToModifyMyInfo() TraineeMyPageEffect.NavigateToConnect -> navigateToConnect(ScreenMode.BACK) @@ -131,9 +134,8 @@ internal fun TraineeMyPageRoute( is TraineeMyPageEffect.RequestPermission -> { if (effect.isExplicitlyDenied) { context.moveToAppSetting() - return@collect + return@collectLatest } - permissionState.launchMultiplePermissionRequest() } diff --git a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt index 99ff5dfd..153d95b3 100644 --- a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt +++ b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt @@ -15,6 +15,7 @@ import co.kr.tnt.trainee.mypage.TraineeMyPageContract.TraineeMyPageUiState.Dialo import co.kr.tnt.ui.base.BaseViewModel import co.kr.tnt.ui.resource.DisplayText import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -70,17 +71,19 @@ internal class TraineeMyPageViewModel @Inject constructor( private fun loadUserData() { viewModelScope.launch { - runCatching { - traineeRepository.getMyInfo() - }.onSuccess { user -> - updateState { copy(user = user) } - }.onFailure { - sendEffect( - TraineeMyPageEffect.ShowToast( - DisplayText.Resource(core_failed_to_server_request), - ), - ) - } + traineeRepository.getMyInfo() + .onEach { user -> + updateState { copy(user = user) } + }.catch { + sendEffect( + TraineeMyPageEffect.ShowToast( + DisplayText.Resource( + core_failed_to_server_request, + ), + ), + ) + } + .launchIn(viewModelScope) settingRepository.isEnablePushNotification() .onEach { isEnablePushNotification -> From 2267c50b3d2b91189656cfd0b0ecf5d24b553d10 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Wed, 19 Nov 2025 17:48:17 +0900 Subject: [PATCH 19/26] =?UTF-8?q?[TNT-261]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=EC=BA=90=EC=8B=9C=EB=90=9C=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=A0=95=EB=B3=B4=20=EA=B0=B1=EC=8B=A0=20=EB=B0=8F?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=ED=99=94=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 - 트레이너 연결 시 최신 연결 상태를 반영하기 위해 캐시 갱신 - 로그아웃/탈퇴 시 다음 로그인 환경을 위해 캐시 초기화 --- .../co/kr/data/repository/TraineeRepositoryImpl.kt | 10 +++++++++- .../co/kr/tnt/domain/repository/TraineeRepository.kt | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt b/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt index 8b099738..bfd3451b 100644 --- a/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt +++ b/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt @@ -125,7 +125,7 @@ internal class TraineeRepositoryImpl @Inject constructor( request = requestBody, ) }.onSuccess { - cacheUserInfo.value = fetchUserInfo() + refreshCachedUserInfo() }.onFailure { failure -> throw failure } @@ -136,4 +136,12 @@ internal class TraineeRepositoryImpl @Inject constructor( require(user is User.Trainee) return user } + + override suspend fun refreshCachedUserInfo() { + cacheUserInfo.value = fetchUserInfo() + } + + override suspend fun clearCachedUserInfo() { + cacheUserInfo.value = User.Trainee.EMPTY + } } diff --git a/domain/src/main/java/co/kr/tnt/domain/repository/TraineeRepository.kt b/domain/src/main/java/co/kr/tnt/domain/repository/TraineeRepository.kt index 176d10e6..478debb7 100644 --- a/domain/src/main/java/co/kr/tnt/domain/repository/TraineeRepository.kt +++ b/domain/src/main/java/co/kr/tnt/domain/repository/TraineeRepository.kt @@ -29,4 +29,6 @@ interface TraineeRepository { profileImageUpdatePolicy: ProfileImageUpdatePolicy, userInfo: User.Trainee, ) + suspend fun refreshCachedUserInfo() + suspend fun clearCachedUserInfo() } From 23de98bcf7783eae6c1be6b88676da1ec4e5fce7 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Wed, 19 Nov 2025 17:50:02 +0900 Subject: [PATCH 20/26] =?UTF-8?q?[TNT-261]=20refactor:=20=ED=8A=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EB=8B=88=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83/=ED=83=88=ED=87=B4=20=EC=8B=9C=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EC=BA=90=EC=8B=9C=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt index 153d95b3..38c67153 100644 --- a/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt +++ b/feature/trainee/mypage/src/main/java/co/kr/tnt/trainee/mypage/TraineeMyPageViewModel.kt @@ -59,9 +59,7 @@ internal class TraineeMyPageViewModel @Inject constructor( TraineeMyPageUiEvent.OnClickLogout -> updateState { copy(dialogState = DialogState.LOGOUT_CONFIRM) } TraineeMyPageUiEvent.OnClickDeleteAccount -> updateState { - copy( - dialogState = DialogState.DELETE_ACCOUNT_CONFIRM, - ) + copy(dialogState = DialogState.DELETE_ACCOUNT_CONFIRM) } TraineeMyPageUiEvent.OnClickDialogConfirm -> handleDialogConfirm() @@ -134,12 +132,14 @@ internal class TraineeMyPageViewModel @Inject constructor( DialogState.LOGOUT -> { updateState { copy(dialogState = DialogState.NONE) } sendEffect(TraineeMyPageEffect.NavigateToLogin) + clearCachedUserInfo() } DialogState.DELETE_ACCOUNT_CONFIRM -> withdraw() DialogState.DELETE_ACCOUNT -> { updateState { copy(dialogState = DialogState.NONE) } sendEffect(TraineeMyPageEffect.NavigateToLogin) + clearCachedUserInfo() } DialogState.SHOULD_ALLOW_PERMISSION -> { @@ -184,4 +184,10 @@ internal class TraineeMyPageViewModel @Inject constructor( } } } + + private fun clearCachedUserInfo() { + viewModelScope.launch { + traineeRepository.clearCachedUserInfo() + } + } } From 986c8dbf74085adafae407d50b80446e0e39f216 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Wed, 19 Nov 2025 18:08:45 +0900 Subject: [PATCH 21/26] =?UTF-8?q?[TNT-261]=20fix:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=84=88=20=EC=97=B0=EA=B2=B0=20=EC=8B=9C=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=A0=95=EB=B3=B4=20=EC=BA=90=EC=8B=9C=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../connect/TraineeConnectViewModel.kt | 9 ++++ .../tnt/trainee/home/TraineeHomeViewModel.kt | 42 +++++++++++-------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectViewModel.kt b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectViewModel.kt index d09deac5..e3d2db1e 100644 --- a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectViewModel.kt +++ b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectViewModel.kt @@ -3,6 +3,7 @@ package co.kr.tnt.trainee.connect import androidx.lifecycle.viewModelScope import co.kr.tnt.core.ui.R.string.core_failed_to_server_request import co.kr.tnt.domain.repository.ConnectRepository +import co.kr.tnt.domain.repository.TraineeRepository import co.kr.tnt.trainee.connect.TraineeConnectContract.TraineeConnectPage import co.kr.tnt.trainee.connect.TraineeConnectContract.TraineeConnectSideEffect import co.kr.tnt.trainee.connect.TraineeConnectContract.TraineeConnectUiEvent @@ -20,6 +21,7 @@ import javax.inject.Inject @HiltViewModel internal class TraineeConnectViewModel @Inject constructor( private val connectRepository: ConnectRepository, + private val traineeRepository: TraineeRepository, ) : BaseViewModel( TraineeConnectUiState(), @@ -100,6 +102,7 @@ internal class TraineeConnectViewModel @Inject constructor( traineeImage = result.traineeImage, ) } + refreshCachedUserInfo() navigateToNext() }.onFailure { sendEffect( @@ -113,6 +116,12 @@ internal class TraineeConnectViewModel @Inject constructor( } } + private fun refreshCachedUserInfo() { + viewModelScope.launch { + traineeRepository.refreshCachedUserInfo() + } + } + private fun navigateToBack() { if (currentState.page == TraineeConnectPage.firstPage) { handleDialogState() diff --git a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt index fff5cca9..2c7c2458 100644 --- a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt +++ b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt @@ -12,8 +12,10 @@ import co.kr.tnt.ui.base.BaseViewModel import co.kr.tnt.ui.resource.DisplayText import com.kizitonwose.calendar.core.yearMonth import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.time.Duration import java.time.LocalDate @@ -162,28 +164,32 @@ internal class TraineeHomeViewModel @Inject constructor( cachedMonthlyRecordState.clear() handleChangeVisibleMonth(currentState.selectedDay.yearMonth) selectDay(currentState.selectedDay) - showConnectDialog() + getUserInfo() } - private fun showConnectDialog() { - val currentDateTime = LocalDateTime.now() - // TODO 트레이너 연결 반영 + private fun getUserInfo() { viewModelScope.launch { - runCatching { - traineeRepository.getMyInfo().first() - }.onSuccess { result -> - updateState { copy(isConnected = result.isConnected) } - if (result.isConnected) { - return@launch + traineeRepository.getMyInfo() + .onEach { user -> + updateState { copy(isConnected = user.isConnected) } + if (user.isConnected.not()) { + showConnectDialog() + } } - }.onFailure { - sendEffect( - TraineeHomeEffect.ShowToast( - DisplayText.Resource(core_failed_to_server_request), - ), - ) - } + .catch { + sendEffect( + TraineeHomeEffect.ShowToast( + DisplayText.Resource(core_failed_to_server_request), + ), + ) + } + .launchIn(viewModelScope) + } + } + private fun showConnectDialog() { + val currentDateTime = LocalDateTime.now() + viewModelScope.launch { val lastHiddenDate = connectRepository.getExplicitDeniedConnectDate().firstOrNull() val isHidden = lastHiddenDate != null && Duration.between(lastHiddenDate, currentDateTime).toHours() < DIALOG_HIDE_DURATION_HOURS From d8820631277c517cb6c972366bf979adfd207774 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Thu, 20 Nov 2025 17:22:32 +0900 Subject: [PATCH 22/26] =?UTF-8?q?[TNT-261]=20fix:=20request=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/co/kr/data/network/model/UpdateUserInfoRequest.kt | 2 +- .../main/java/co/kr/data/repository/TraineeRepositoryImpl.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/network/src/main/java/co/kr/data/network/model/UpdateUserInfoRequest.kt b/data/network/src/main/java/co/kr/data/network/model/UpdateUserInfoRequest.kt index 130dcdc5..5eaa6c10 100644 --- a/data/network/src/main/java/co/kr/data/network/model/UpdateUserInfoRequest.kt +++ b/data/network/src/main/java/co/kr/data/network/model/UpdateUserInfoRequest.kt @@ -8,7 +8,7 @@ data class UpdateUserInfoRequest( val removeImage: Boolean, val memberType: MemberType, val name: String, - val birthDay: String? = null, + val birthday: String? = null, val height: Double? = null, val weight: Double? = null, val cautionNote: String? = null, diff --git a/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt b/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt index bfd3451b..1eccb7d0 100644 --- a/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt +++ b/data/repository/src/main/java/co/kr/data/repository/TraineeRepositoryImpl.kt @@ -101,7 +101,7 @@ internal class TraineeRepositoryImpl @Inject constructor( } val imagePart = profileImage?.let { val requestFile = it.asRequestBody("image/*".toMediaTypeOrNull()) - MultipartBody.Part.createFormData("dietImage", it.name, requestFile) + MultipartBody.Part.createFormData("profileImage", it.name, requestFile) } val selectedDate = userInfo.birthday?.let { dateFormatter.format(it, "yyyy-MM-dd") } @@ -109,7 +109,7 @@ internal class TraineeRepositoryImpl @Inject constructor( removeImage = isRemoveProfileImage, memberType = MemberType.TRAINEE, name = userInfo.name, - birthDay = selectedDate, + birthday = selectedDate, height = userInfo.height?.toDouble(), weight = userInfo.weight, cautionNote = userInfo.caution, From 5be6e314815bf36de770a001dc5fce3927c6d2ac Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Fri, 21 Nov 2025 14:21:07 +0900 Subject: [PATCH 23/26] =?UTF-8?q?[TNT-261]=20refactor:=20=ED=8A=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EB=8B=88=20=EA=B0=9C=EC=9D=B8=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=20=EC=8B=A0?= =?UTF-8?q?=EA=B7=9C=20TextField=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modifymyinfo/TraineeModifyMyInfoScreen.kt | 230 ++++++++---------- 1 file changed, 99 insertions(+), 131 deletions(-) diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt index 4e2f74d7..36f21752 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -37,16 +36,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable 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.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.kr.tnt.core.designsystem.R.string.placeholder_content_input import co.kr.tnt.core.ui.R.string.core_complete import co.kr.tnt.core.ui.R.string.core_confirm_modify_info_exit import co.kr.tnt.core.ui.R.string.core_entered_wrong_text @@ -60,16 +60,16 @@ import co.kr.tnt.core.ui.R.string.core_unsaved_changes_warning import co.kr.tnt.core.ui.R.string.core_weight_label import co.kr.tnt.core.ui.R.string.core_weight_unit import co.kr.tnt.designsystem.component.TnTIconPopupDialog -import co.kr.tnt.designsystem.component.TnTLabeledTextField -import co.kr.tnt.designsystem.component.TnTLabeledTextFieldWithCounter import co.kr.tnt.designsystem.component.TnTModalBottomSheet -import co.kr.tnt.designsystem.component.TnTOutlinedTextField import co.kr.tnt.designsystem.component.TnTProfileImage import co.kr.tnt.designsystem.component.TnTTopBarWithBackButton import co.kr.tnt.designsystem.component.button.TnTBottomButton import co.kr.tnt.designsystem.component.button.TnTTextButton import co.kr.tnt.designsystem.component.button.model.ButtonSize import co.kr.tnt.designsystem.component.button.model.ButtonType +import co.kr.tnt.designsystem.component.textfield.TnTLabeledTextField +import co.kr.tnt.designsystem.component.textfield.TnTSelectableLabeledTextField +import co.kr.tnt.designsystem.component.textfield.model.TnTTextFieldSize import co.kr.tnt.designsystem.snackbar.LocalSnackbar import co.kr.tnt.designsystem.theme.TnTTheme import co.kr.tnt.domain.UserProfilePolicy @@ -250,80 +250,66 @@ private fun TraineeModifyMyInfoScreen( verticalArrangement = Arrangement.spacedBy(48.dp), modifier = Modifier.fillMaxWidth(), ) { - TnTLabeledTextFieldWithCounter( + TnTLabeledTextField( title = stringResource(core_name), value = state.name, - onValueChange = { newValue -> - onChangeName(newValue) - }, + onValueChange = onChangeName, modifier = Modifier.padding(horizontal = 20.dp), placeholder = stringResource(core_name_placeholder), - maxLength = UserProfilePolicy.USER_NAME_MAX_LENGTH, - isSingleLine = true, - showWarning = state.isNameValid.not(), - isRequired = true, + size = TnTTextFieldSize.SMALL, + isWarning = state.isNameValid.not(), warningMessage = stringResource( core_text_length_and_format_warning, UserProfilePolicy.USER_NAME_MAX_LENGTH, ), + maxLength = UserProfilePolicy.USER_NAME_MAX_LENGTH, + showRequiredTitleBadge = true, ) - Column { - Text( - text = stringResource(R.string.birthday_label), - color = TnTTheme.colors.neutralColors.Neutral900, - style = TnTTheme.typography.body1Bold, - modifier = Modifier.padding(start = 20.dp, bottom = 8.dp), - ) - BirthdayPicker( - modifier = Modifier.padding(horizontal = 20.dp), - context = context, - today = today, - selectedDate = state.birthday, - onDateSelected = onChangeBirthday, + BirthdayPicker( + state = state, + context = context, + today = today, + onChangeBirthday = onChangeBirthday, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) { + TnTLabeledTextField( + title = stringResource(core_height_label), + value = state.height ?: "", + placeholder = "0", + isWarning = state.isHeightValid.not(), + warningMessage = stringResource(core_entered_wrong_text), + keyboardType = KeyboardType.Number, + trailing = { + UnitLabel( + modifier = Modifier.align(Alignment.CenterVertically), + stringResId = core_height_unit, + ) + }, + onValueChange = onChangeHeight, + modifier = Modifier.weight(1f), ) - HorizontalDivider( - thickness = 1.dp, - color = TnTTheme.colors.neutralColors.Neutral200, - modifier = Modifier.padding(horizontal = 20.dp), + TnTLabeledTextField( + title = stringResource(core_weight_label), + value = state.weight ?: "", + placeholder = "00.0", + isWarning = state.isWeightValid.not(), + warningMessage = stringResource(core_entered_wrong_text), + keyboardType = KeyboardType.Number, + trailing = { + UnitLabel( + modifier = Modifier.align(Alignment.CenterVertically), + stringResId = core_weight_unit, + ) + }, + onValueChange = onChangeWeight, + modifier = Modifier.weight(1f), ) } - Column { - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - ) { - TnTLabeledTextField( - title = stringResource(core_height_label), - value = state.height ?: "", - placeholder = "0", - isSingleLine = true, - showWarning = state.isHeightValid.not(), - warningMessage = stringResource(core_entered_wrong_text), - keyboardType = KeyboardType.Number, - trailingComponent = { - UnitLabel(core_height_unit) - }, - onValueChange = onChangeHeight, - modifier = Modifier.weight(1f), - ) - TnTLabeledTextField( - title = stringResource(core_weight_label), - value = state.weight ?: "", - placeholder = "00.0", - isSingleLine = true, - showWarning = state.isWeightValid.not(), - warningMessage = stringResource(core_entered_wrong_text), - keyboardType = KeyboardType.Number, - trailingComponent = { - UnitLabel(core_weight_unit) - }, - onValueChange = onChangeWeight, - modifier = Modifier.weight(1f), - ) - } - } Column(Modifier.padding(horizontal = 20.dp)) { Text( text = "PT 목적", @@ -351,27 +337,20 @@ private fun TraineeModifyMyInfoScreen( } } } - Column(modifier = Modifier.padding(horizontal = 20.dp)) { - Text( - text = stringResource(R.string.edit_caution_that_trainer_must_know), - modifier = Modifier.fillMaxWidth(), - style = TnTTheme.typography.body1Bold, - color = TnTTheme.colors.neutralColors.Neutral900, - ) - Spacer(Modifier.padding(top = 8.dp)) - TnTOutlinedTextField( - value = state.caution ?: "", - onValueChange = { newValue -> - onChangeCaution(newValue) - }, - isError = state.isCautionNoteValid.not(), - warningMessage = stringResource( - core_text_length_warning, - UserProfilePolicy.USER_CAUTION_MAX_LENGTH, - ), - maxLength = UserProfilePolicy.USER_CAUTION_MAX_LENGTH, - ) - } + TnTLabeledTextField( + title = stringResource(R.string.edit_caution_that_trainer_must_know), + value = state.caution ?: "", + onValueChange = onChangeCaution, + modifier = Modifier.padding(horizontal = 20.dp), + size = TnTTextFieldSize.LARGE, + placeholder = stringResource(placeholder_content_input), + isWarning = state.isCautionNoteValid.not(), + warningMessage = stringResource( + core_text_length_warning, + UserProfilePolicy.USER_CAUTION_MAX_LENGTH, + ), + maxLength = UserProfilePolicy.USER_CAUTION_MAX_LENGTH, + ) } Spacer(Modifier.padding(top = 32.dp)) } @@ -426,59 +405,48 @@ private fun EditImageBottomSheetContent( @Composable private fun BirthdayPicker( - modifier: Modifier = Modifier, + state: TraineeModifyMyInfoUiState, context: Context, today: LocalDate, - selectedDate: LocalDate?, - onDateSelected: (LocalDate) -> Unit, + onChangeBirthday: (birthday: LocalDate) -> Unit, ) { - val dateFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd") - val date = selectedDate ?: LocalDate.of(2001, 1, 1) - - Box( - modifier = modifier - .fillMaxWidth() - .padding(8.dp) - .clickable { - DatePickerDialog( - context, - { _, selectedYear, selectedMonth, selectedDay -> - val newDate = LocalDate.of(selectedYear, selectedMonth + 1, selectedDay) - onDateSelected(newDate) - }, - date.year, - date.monthValue - 1, - date.dayOfMonth, - ) - .apply { - // 오늘 이후는 선택 불가능 - val todayMillis = today + TnTSelectableLabeledTextField( + modifier = Modifier.padding(horizontal = 20.dp), + value = state.birthday?.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) ?: "", + placeholder = stringResource(R.string.birthday_placeholder), + onClickTextField = { + DatePickerDialog( + context, + { _, selectedYear, selectedMonth, selectedDay -> + val newDate = LocalDate.of(selectedYear, selectedMonth + 1, selectedDay) + onChangeBirthday(newDate) + }, + state.birthday?.year ?: 2001, + (state.birthday?.monthValue?.minus(1)) ?: 0, + state.birthday?.dayOfMonth ?: 1, + ) + .apply { + // 오늘 이후는 선택 불가능 + datePicker.maxDate = + today .atStartOfDay(ZoneId.systemDefault()) .toInstant() .toEpochMilli() - - datePicker.maxDate = todayMillis - 1 - } - .show() - }, - ) { - Text( - text = selectedDate?.format(dateFormatter) - ?: stringResource(R.string.birthday_placeholder), - color = if (selectedDate == null) { - TnTTheme.colors.neutralColors.Neutral400 - } else { - TnTTheme.colors.neutralColors.Neutral600 - }, - style = TnTTheme.typography.body1Medium, - textAlign = TextAlign.Start, - ) - } + } + .show() + }, + title = stringResource(R.string.birthday_label), + size = TnTTextFieldSize.SMALL, + ) } @Composable -private fun UnitLabel(stringResId: Int) { +private fun UnitLabel( + modifier: Modifier = Modifier, + stringResId: Int, +) { Text( + modifier = modifier.padding(end = 12.dp), text = stringResource(stringResId), style = TnTTheme.typography.body1Medium, color = TnTTheme.colors.neutralColors.Neutral400, From 58d3654c08b944c30f53b665e095aae00670c388 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Fri, 21 Nov 2025 14:46:58 +0900 Subject: [PATCH 24/26] =?UTF-8?q?[TNT-000]=20fix:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=B4=88=EB=8C=80=20=EC=BD=94=EB=93=9C=20=EB=B3=B5=EC=82=AC=20?= =?UTF-8?q?=EC=8A=A4=EB=82=B5=EB=B0=94=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/co/kr/tnt/trainer/invite/TrainerInviteContract.kt | 7 ++++++- .../java/co/kr/tnt/trainer/invite/TrainerInviteScreen.kt | 5 ++++- .../co/kr/tnt/trainer/invite/TrainerInviteViewModel.kt | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/feature/trainer/invite/src/main/java/co/kr/tnt/trainer/invite/TrainerInviteContract.kt b/feature/trainer/invite/src/main/java/co/kr/tnt/trainer/invite/TrainerInviteContract.kt index 46b6793f..c0c72cf9 100644 --- a/feature/trainer/invite/src/main/java/co/kr/tnt/trainer/invite/TrainerInviteContract.kt +++ b/feature/trainer/invite/src/main/java/co/kr/tnt/trainer/invite/TrainerInviteContract.kt @@ -3,6 +3,7 @@ package co.kr.tnt.trainer.invite import co.kr.tnt.ui.base.UiEvent import co.kr.tnt.ui.base.UiSideEffect import co.kr.tnt.ui.base.UiState +import co.kr.tnt.ui.model.SnackbarType import co.kr.tnt.ui.resource.DisplayText internal class TrainerInviteContract { @@ -20,7 +21,11 @@ internal class TrainerInviteContract { sealed interface TrainerInviteSideEffect : UiSideEffect { data object NavigateToBack : TrainerInviteSideEffect data object NavigateToHome : TrainerInviteSideEffect - data class ShowToast(val message: DisplayText) : TrainerInviteSideEffect + data class ShowToast( + val message: DisplayText, + val type: SnackbarType = SnackbarType.WARNING, + ) : TrainerInviteSideEffect + data class CopyToClipBoard(val value: String) : TrainerInviteSideEffect } } diff --git a/feature/trainer/invite/src/main/java/co/kr/tnt/trainer/invite/TrainerInviteScreen.kt b/feature/trainer/invite/src/main/java/co/kr/tnt/trainer/invite/TrainerInviteScreen.kt index e9b4df90..1a102d91 100644 --- a/feature/trainer/invite/src/main/java/co/kr/tnt/trainer/invite/TrainerInviteScreen.kt +++ b/feature/trainer/invite/src/main/java/co/kr/tnt/trainer/invite/TrainerInviteScreen.kt @@ -66,7 +66,10 @@ internal fun TrainerInviteRoute( when (effect) { TrainerInviteSideEffect.NavigateToBack -> navigateToPrevious() TrainerInviteSideEffect.NavigateToHome -> navigateToHome(true) - is TrainerInviteSideEffect.ShowToast -> snackbar.show(effect.message.asString(context)) + is TrainerInviteSideEffect.ShowToast -> snackbar.show( + message = effect.message.asString(context), + icon = effect.type.iconRes, + ) is TrainerInviteSideEffect.CopyToClipBoard -> clipboardManager.setText(AnnotatedString(effect.value)) } diff --git a/feature/trainer/invite/src/main/java/co/kr/tnt/trainer/invite/TrainerInviteViewModel.kt b/feature/trainer/invite/src/main/java/co/kr/tnt/trainer/invite/TrainerInviteViewModel.kt index 635f5806..e354306b 100644 --- a/feature/trainer/invite/src/main/java/co/kr/tnt/trainer/invite/TrainerInviteViewModel.kt +++ b/feature/trainer/invite/src/main/java/co/kr/tnt/trainer/invite/TrainerInviteViewModel.kt @@ -8,6 +8,7 @@ import co.kr.tnt.trainer.invite.TrainerInviteContract.TrainerInviteSideEffect import co.kr.tnt.trainer.invite.TrainerInviteContract.TrainerInviteUiEvent import co.kr.tnt.trainer.invite.TrainerInviteContract.TrainerInviteUiState import co.kr.tnt.ui.base.BaseViewModel +import co.kr.tnt.ui.model.SnackbarType import co.kr.tnt.ui.resource.DisplayText import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -28,7 +29,8 @@ internal class TrainerInviteViewModel @Inject constructor( sendEffect(TrainerInviteSideEffect.CopyToClipBoard(event.code)) sendEffect( TrainerInviteSideEffect.ShowToast( - DisplayText.Resource(R.string.code_is_copied), + message = DisplayText.Resource(R.string.code_is_copied), + type = SnackbarType.SUCCESS, ), ) } From a1e6e8a8d744b0c69f8b335173aa15ea49d9d778 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Fri, 21 Nov 2025 17:04:23 +0900 Subject: [PATCH 25/26] =?UTF-8?q?[TNT-000]=20refactor:=20=ED=8A=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EB=84=88=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B3=B5=ED=86=B5=20=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EC=97=B4=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/co/kr/tnt/trainer/mypage/TrainerMyPageScreen.kt | 9 ++++++--- feature/trainer/mypage/src/main/res/values/strings.xml | 3 --- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/feature/trainer/mypage/src/main/java/co/kr/tnt/trainer/mypage/TrainerMyPageScreen.kt b/feature/trainer/mypage/src/main/java/co/kr/tnt/trainer/mypage/TrainerMyPageScreen.kt index b0ac7ba4..17c3f25d 100644 --- a/feature/trainer/mypage/src/main/java/co/kr/tnt/trainer/mypage/TrainerMyPageScreen.kt +++ b/feature/trainer/mypage/src/main/java/co/kr/tnt/trainer/mypage/TrainerMyPageScreen.kt @@ -36,6 +36,9 @@ import co.kr.tnt.core.ui.R.string.core_app_push_notification import co.kr.tnt.core.ui.R.string.core_app_version import co.kr.tnt.core.ui.R.string.core_cancel import co.kr.tnt.core.ui.R.string.core_delete_account +import co.kr.tnt.core.ui.R.string.core_delete_account_complete_content +import co.kr.tnt.core.ui.R.string.core_delete_account_complete_title +import co.kr.tnt.core.ui.R.string.core_delete_account_title import co.kr.tnt.core.ui.R.string.core_logout import co.kr.tnt.core.ui.R.string.core_logout_complete_title import co.kr.tnt.core.ui.R.string.core_logout_content @@ -358,7 +361,7 @@ private fun Dialog( DialogState.DELETE_ACCOUNT_CONFIRM -> { TnTIconPopupDialog( - title = stringResource(R.string.delete_account_title), + title = stringResource(core_delete_account_title), content = stringResource(R.string.delete_account_content), leftButtonText = stringResource(core_cancel), rightButtonText = stringResource(core_ok), @@ -370,8 +373,8 @@ private fun Dialog( DialogState.DELETE_ACCOUNT -> { TnTSingleButtonPopupDialog( - title = stringResource(R.string.delete_account_complete_title), - content = stringResource(R.string.delete_account_complete_content), + title = stringResource(core_delete_account_complete_title), + content = stringResource(core_delete_account_complete_content), buttonText = stringResource(core_ok), cancelable = false, onButtonClick = onClickConfirm, diff --git a/feature/trainer/mypage/src/main/res/values/strings.xml b/feature/trainer/mypage/src/main/res/values/strings.xml index 63e2dff2..6ebc2f6d 100644 --- a/feature/trainer/mypage/src/main/res/values/strings.xml +++ b/feature/trainer/mypage/src/main/res/values/strings.xml @@ -1,10 +1,7 @@ - 계정을 탈퇴할까요? 함께 했던 회원들에 대한 데이터가 사라져요! - 계정 탈퇴가 완료되었어요 - 다음에 더 폭발적인 케미로 다시 만나요! 💣 탈퇴에 실패하였습니다. 로그아웃에 실패하였습니다. 관리 중인 회원 From 45830018d63028ba4ad71881e9afe9f1cb3a5f17 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Fri, 21 Nov 2025 17:32:53 +0900 Subject: [PATCH 26/26] =?UTF-8?q?[TNT-261]=20refactor:=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EB=82=A0=EC=A7=9C=20DEFAULT=5FDATE=EB=A1=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt index 36f21752..16761246 100644 --- a/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt +++ b/feature/trainee/modifymyinfo/src/main/java/co/kr/tnt/trainee/modifymyinfo/TraineeModifyMyInfoScreen.kt @@ -94,6 +94,7 @@ import java.time.format.DateTimeFormatter private const val ROW_NUM = 3 private const val COLUMNS_NUM = 2 +private val DEFAULT_DATE = LocalDate.of(2000, 1, 1) @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -421,9 +422,9 @@ private fun BirthdayPicker( val newDate = LocalDate.of(selectedYear, selectedMonth + 1, selectedDay) onChangeBirthday(newDate) }, - state.birthday?.year ?: 2001, - (state.birthday?.monthValue?.minus(1)) ?: 0, - state.birthday?.dayOfMonth ?: 1, + state.birthday?.year ?: DEFAULT_DATE.year, + (state.birthday?.monthValue?.minus(1)) ?: (DEFAULT_DATE.monthValue - 1), + state.birthday?.dayOfMonth ?: DEFAULT_DATE.dayOfMonth, ) .apply { // 오늘 이후는 선택 불가능