From 7f245f1adec26fcfc95a0152ca7c85af424e5910 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Mon, 27 Jan 2025 16:21:31 +0900 Subject: [PATCH 01/14] =?UTF-8?q?[TNT-114]=20fix:=20RoleState=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 --- .../src/main/java/co/kr/tnt/roleselect/model/RoleState.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/model/RoleState.kt b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/model/RoleState.kt index 3f652a3..6b8ce9b 100644 --- a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/model/RoleState.kt +++ b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/model/RoleState.kt @@ -23,8 +23,8 @@ sealed class RoleState( companion object { fun fromDomain(role: UserType): RoleState { return when (role) { - is UserType.Trainee -> Trainer - is UserType.Trainer -> Trainee + is UserType.Trainer -> Trainer + is UserType.Trainee -> Trainee } } } From 6c0a7e248e9d50aec637874af9ceabd6cdd1b59d Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Mon, 27 Jan 2025 16:52:14 +0900 Subject: [PATCH 02/14] =?UTF-8?q?[TNT-114]=20feat:=20=EC=97=AD=ED=95=A0=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20->=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85?= =?UTF-8?q?=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - authId, authType, email 전달 --- .../java/co/kr/tnt/navigation/RouteModel.kt | 12 ++++- .../main/java/co/kr/tnt/main/ui/TnTNavHost.kt | 19 +++++++- .../tnt/roleselect/RoleSelectionContract.kt | 21 +++++++++ .../tnt/roleselect/RoleSelectionNavigation.kt | 12 ++--- .../kr/tnt/roleselect/RoleSelectionScreen.kt | 44 +++++++++++-------- .../tnt/roleselect/RoleSelectionViewModel.kt | 33 ++++++++++++++ .../tnt/trainee/signup/TraineeSignUpScreen.kt | 3 ++ .../navigation/TraineeSignUpNavigation.kt | 12 ++++- .../tnt/trainer/signup/TrainerSignUpScreen.kt | 3 ++ .../navigation/TrainerSignUpNavigation.kt | 13 +++++- 10 files changed, 143 insertions(+), 29 deletions(-) create mode 100644 feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionContract.kt create mode 100644 feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionViewModel.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 2ca83df..4869e61 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 @@ -13,10 +13,18 @@ sealed interface Route { data object Login : Route @Serializable - data object TrainerSignUp : Route + data class TrainerSignUp( + val authId: String, + val authType: String, + val email: String, + ) : Route @Serializable - data object TraineeSignUp : Route + data class TraineeSignUp( + val authId: String, + val authType: String, + val email: String, + ) : Route @Serializable data class TrainerConnect(val isFromMyPage: Boolean) : Route diff --git a/feature/main/src/main/java/co/kr/tnt/main/ui/TnTNavHost.kt b/feature/main/src/main/java/co/kr/tnt/main/ui/TnTNavHost.kt index 198f551..9dc688a 100644 --- a/feature/main/src/main/java/co/kr/tnt/main/ui/TnTNavHost.kt +++ b/feature/main/src/main/java/co/kr/tnt/main/ui/TnTNavHost.kt @@ -13,9 +13,11 @@ import co.kr.tnt.roleselect.navigateToRoleSelection import co.kr.tnt.roleselect.roleSelectionScreen import co.kr.tnt.trainee.connect.navigation.navigateToTraineeConnect import co.kr.tnt.trainee.connect.navigation.traineeConnectScreen +import co.kr.tnt.trainee.signup.navigation.navigateToTraineeSignUp import co.kr.tnt.trainee.signup.navigation.traineeSignUpScreen import co.kr.tnt.trainer.connect.navigation.navigateToTrainerConnect import co.kr.tnt.trainer.connect.navigation.trainerConnectScreen +import co.kr.tnt.trainer.signup.navigation.navigateToTrainerSignUp import co.kr.tnt.trainer.signup.navigation.trainerSignUpScreen @Composable @@ -50,6 +52,22 @@ fun TnTNavHost( ) }, ) + roleSelectionScreen( + navigateToTraineeSignUp = { authId, authType, email -> + navController.navigateToTraineeSignUp( + authId = authId, + authType = authType, + email = email, + ) + }, + navigateToTrainerSignUp = { authId, authType, email -> + navController.navigateToTrainerSignUp( + authId = authId, + authType = authType, + email = email, + ) + }, + ) trainerSignUpScreen( navigateToPrevious = { navController.popBackStack() }, navigateToConnect = { navController.navigateToTrainerConnect(isFromMyPage = false) }, @@ -70,7 +88,6 @@ fun TnTNavHost( navController.navigateToHome(isTrainer = false, clearBackStack = true) }, ) - roleSelectionScreen() homeNavGraph() } } diff --git a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionContract.kt b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionContract.kt new file mode 100644 index 0000000..329935c --- /dev/null +++ b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionContract.kt @@ -0,0 +1,21 @@ +package co.kr.tnt.roleselect + +import co.kr.tnt.roleselect.model.RoleState +import co.kr.tnt.ui.base.UiEvent +import co.kr.tnt.ui.base.UiSideEffect +import co.kr.tnt.ui.base.UiState + +internal class RoleSelectionContract { + data class RoleSelectionUiState( + val userType: RoleState = RoleState.Trainer, + ) : UiState + + sealed interface RoleSelectionUiEvent : UiEvent { + data class OnNextClick(val role: RoleState) : RoleSelectionUiEvent + } + + sealed interface RoleSelectionEffect : UiSideEffect { + data object NavigateToTrainerSignUp : RoleSelectionEffect + data object NavigateToTraineeSignUp : RoleSelectionEffect + } +} diff --git a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionNavigation.kt b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionNavigation.kt index 15bfeb5..192c586 100644 --- a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionNavigation.kt +++ b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionNavigation.kt @@ -21,13 +21,15 @@ fun NavController.navigateToRoleSelection( builder = navOptions, ) -fun NavGraphBuilder.roleSelectionScreen() { +fun NavGraphBuilder.roleSelectionScreen( + navigateToTraineeSignUp: (authId: String, authType: String, email: String) -> Unit, + navigateToTrainerSignUp: (authId: String, authType: String, email: String) -> Unit, +) { composable { navBackstackEntry -> navBackstackEntry.toRoute().apply { - RoleSelectionScreen( - authId = authId, - authType = authType, - email = email, + RoleSelectionRoute( + navigateToTraineeSignUp = { navigateToTraineeSignUp(authId, authType, email) }, + navigateToTrainerSignUp = { navigateToTrainerSignUp(authId, authType, email) }, ) } } diff --git a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionScreen.kt b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionScreen.kt index 1b09876..8a9dab2 100644 --- a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionScreen.kt +++ b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionScreen.kt @@ -11,6 +11,7 @@ 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.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -21,29 +22,44 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel 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.theme.TnTTheme -import co.kr.tnt.domain.model.AuthType import co.kr.tnt.domain.model.UserType import co.kr.tnt.feature.roleselect.R +import co.kr.tnt.roleselect.RoleSelectionContract.RoleSelectionEffect +import co.kr.tnt.roleselect.RoleSelectionContract.RoleSelectionUiEvent import co.kr.tnt.roleselect.model.RoleState import co.kr.tnt.core.ui.R as uiResource @Composable -@Suppress("UnusedParameter") +internal fun RoleSelectionRoute( + viewModel: RoleSelectionViewModel = hiltViewModel(), + navigateToTraineeSignUp: () -> Unit, + navigateToTrainerSignUp: () -> Unit, +) { + RoleSelectionScreen( + onNextClick = { viewModel.setEvent(RoleSelectionUiEvent.OnNextClick(it)) }, + ) + + LaunchedEffect(viewModel.effect) { + viewModel.effect.collect { effect -> + when (effect) { + RoleSelectionEffect.NavigateToTraineeSignUp -> navigateToTraineeSignUp() + RoleSelectionEffect.NavigateToTrainerSignUp -> navigateToTrainerSignUp() + } + } + } +} + +@Composable fun RoleSelectionScreen( - onRoleSelected: (UserType) -> Unit = {}, - onNextClick: () -> Unit = {}, - authId: String, - authType: String, - email: String, - modifier: Modifier = Modifier, + onNextClick: (RoleState) -> Unit = {}, ) { var selectedRole by remember { mutableStateOf(RoleState.fromDomain(UserType.Trainer())) } - val authType = AuthType.from(authType) Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Column( @@ -71,7 +87,6 @@ fun RoleSelectionScreen( contentDescription = null, modifier = Modifier.align(Alignment.CenterHorizontally), ) - // TODO 선택한 버튼 정보 저장 Row( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier @@ -85,7 +100,6 @@ fun RoleSelectionScreen( type = if (selectedRole == RoleState.Trainer) ButtonType.RedOutline else ButtonType.GrayOutline, onClick = { selectedRole = RoleState.Trainer - onRoleSelected(UserType.Trainer("", "")) }, ) TnTTextButton( @@ -95,14 +109,13 @@ fun RoleSelectionScreen( type = if (selectedRole == RoleState.Trainee) ButtonType.RedOutline else ButtonType.GrayOutline, onClick = { selectedRole = RoleState.Trainee - onRoleSelected(UserType.Trainer("", "")) }, ) } TnTBottomButton( text = stringResource(uiResource.string.next), enabled = true, - onClick = { onNextClick() }, + onClick = { onNextClick(selectedRole) }, ) } } @@ -113,12 +126,7 @@ fun RoleSelectionScreen( private fun RoleScreenPreview() { TnTTheme { RoleSelectionScreen( - onRoleSelected = {}, onNextClick = {}, - authId = "", - authType = "", - email = "", - modifier = Modifier.fillMaxSize(), ) } } diff --git a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionViewModel.kt b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionViewModel.kt new file mode 100644 index 0000000..a105033 --- /dev/null +++ b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionViewModel.kt @@ -0,0 +1,33 @@ +package co.kr.tnt.roleselect + +import co.kr.tnt.roleselect.RoleSelectionContract.RoleSelectionEffect +import co.kr.tnt.roleselect.RoleSelectionContract.RoleSelectionUiEvent +import co.kr.tnt.roleselect.RoleSelectionContract.RoleSelectionUiState +import co.kr.tnt.roleselect.model.RoleState +import co.kr.tnt.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +internal class RoleSelectionViewModel @Inject constructor() : + BaseViewModel( + RoleSelectionUiState(), + ) { + override suspend fun handleEvent(event: RoleSelectionUiEvent) { + when (event) { + is RoleSelectionUiEvent.OnNextClick -> navigateToSignUp(event.role) + } + } + + private fun navigateToSignUp(role: RoleState) { + when (role) { + is RoleState.Trainer -> { + sendEffect(RoleSelectionEffect.NavigateToTrainerSignUp) + } + + is RoleState.Trainee -> { + sendEffect(RoleSelectionEffect.NavigateToTraineeSignUp) + } + } + } + } 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 c1a584b..415dbb0 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 @@ -12,6 +12,9 @@ import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState @Composable internal fun TraineeSignUpRoute( + authId: String, + authType: String, + email: String, navigateToPrevious: () -> Unit, navigateToConnect: () -> Unit, viewModel: TraineeSignUpViewModel = hiltViewModel(), diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/navigation/TraineeSignUpNavigation.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/navigation/TraineeSignUpNavigation.kt index c5fde06..a30ef28 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/navigation/TraineeSignUpNavigation.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/navigation/TraineeSignUpNavigation.kt @@ -9,9 +9,16 @@ import co.kr.tnt.navigation.Route import co.kr.tnt.trainee.signup.TraineeSignUpRoute fun NavController.navigateToTraineeSignUp( + authId: String, + authType: String, + email: String, navOptions: NavOptionsBuilder.() -> Unit = {}, ) = navigate( - route = Route.TraineeSignUp, + route = Route.TraineeSignUp( + authId = authId, + authType = authType, + email = email, + ), builder = navOptions, ) @@ -22,6 +29,9 @@ fun NavGraphBuilder.traineeSignUpScreen( composable { backstackEntry -> backstackEntry.toRoute().apply { TraineeSignUpRoute( + authId = authId, + authType = authType, + email = email, navigateToPrevious = navigateToPrevious, navigateToConnect = { navigateToConnect() }, ) diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt index d0a6d44..b92167c 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt @@ -10,6 +10,9 @@ import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiState @Composable internal fun TrainerSignUpRoute( + authId: String, + authType: String, + email: String, navigateToPrevious: () -> Unit, navigateToConnect: () -> Unit, viewModel: TrainerSignUpViewModel = hiltViewModel(), diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/navigation/TrainerSignUpNavigation.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/navigation/TrainerSignUpNavigation.kt index c17b09b..06811d9 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/navigation/TrainerSignUpNavigation.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/navigation/TrainerSignUpNavigation.kt @@ -9,9 +9,16 @@ import co.kr.tnt.navigation.Route import co.kr.tnt.trainer.signup.TrainerSignUpRoute fun NavController.navigateToTrainerSignUp( + authId: String, + authType: String, + email: String, navOptions: NavOptionsBuilder.() -> Unit = {}, ) = navigate( - route = Route.TrainerSignUp, + route = Route.TrainerSignUp( + authId = authId, + authType = authType, + email = email, + ), builder = navOptions, ) @@ -21,8 +28,10 @@ fun NavGraphBuilder.trainerSignUpScreen( ) { composable { backstackEntry -> backstackEntry.toRoute().apply { - // TODO 115 머지되면 connect로 이동 TrainerSignUpRoute( + authId = authId, + authType = authType, + email = email, navigateToPrevious = navigateToPrevious, navigateToConnect = { navigateToConnect() }, ) From 071a69f3a865d8f8ec93006b67998c2a99c325fe Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Mon, 27 Jan 2025 17:35:04 +0900 Subject: [PATCH 03/14] =?UTF-8?q?[TNT-114]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=84=88=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84,=20=EC=9D=B4=EB=A6=84=20state=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 --- .../trainer/signup/TrainerProfileSetupPage.kt | 37 +++++++++++-------- .../signup/TrainerSignUpCompletePage.kt | 13 +++---- .../trainer/signup/TrainerSignUpContract.kt | 4 ++ .../tnt/trainer/signup/TrainerSignUpScreen.kt | 9 +++++ .../trainer/signup/TrainerSignUpViewModel.kt | 11 ++++++ 5 files changed, 51 insertions(+), 23 deletions(-) diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt index 0cfce9d..3e0b97f 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt @@ -19,9 +19,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -36,14 +34,20 @@ import co.kr.tnt.designsystem.component.image.TnTProfileImage import co.kr.tnt.designsystem.theme.TnTTheme import co.kr.tnt.domain.IMAGE_MAX_SIZE import co.kr.tnt.feature.trainer.signup.R +import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiState import co.kr.tnt.ui.coil.ResizeTransformation import co.kr.tnt.ui.model.DefaultUserProfile import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import co.kr.tnt.core.ui.R as uiResource +private const val MAX_LENGTH = 15 + @Composable -fun TrainerProfileSetupPage( +internal fun TrainerProfileSetupPage( + state: TrainerSignUpUiState, + onProfileImageSelected: (Uri) -> Unit, + onNameChanged: (String) -> Unit, onBackClick: () -> Unit, onNextClick: () -> Unit, ) { @@ -51,20 +55,18 @@ fun TrainerProfileSetupPage( val context = LocalContext.current - // TODO 상태 관리 따로 빼기 - val maxLength = 15 - var text by remember { mutableStateOf("") } - val isWarning by remember { derivedStateOf { text.length > maxLength } } - var profileImage by remember { mutableStateOf(null) } + val isWarning by remember(state.name) { + derivedStateOf { state.name.length > MAX_LENGTH } + } val pickMediaLauncher = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> if (uri != null) { - profileImage = uri + onProfileImageSelected(uri) } } val painter = rememberAsyncImagePainter( model = ImageRequest.Builder(context) - .data(profileImage) + .data(state.profileImage) .transformations(ResizeTransformation(IMAGE_MAX_SIZE)) .build(), ) @@ -92,7 +94,7 @@ fun TrainerProfileSetupPage( .fillMaxWidth() .padding(vertical = 12.dp), defaultImage = painterResource(DefaultUserProfile.Trainer.image), - image = profileImage?.let { painter }, + image = state.profileImage?.let { painter }, onEditClick = { pickMediaLauncher.launch( PickVisualMediaRequest( @@ -104,24 +106,24 @@ fun TrainerProfileSetupPage( Spacer(Modifier.padding(top = 60.dp)) TnTLabeledTextFieldWithCounter( title = stringResource(uiResource.string.name), - value = text, + value = state.name, onValueChange = { newValue -> val filteredText = validateInput(newValue) - text = filteredText + onNameChanged(filteredText) }, modifier = Modifier.padding(horizontal = 20.dp), placeholder = stringResource(R.string.name_placeholder), - maxLength = maxLength, + maxLength = MAX_LENGTH, isSingleLine = true, showWarning = isWarning, isRequired = true, - warningMessage = stringResource(R.string.text_length_warning, maxLength), + warningMessage = stringResource(R.string.text_length_warning, MAX_LENGTH), ) } TnTBottomButton( text = stringResource(uiResource.string.next), modifier = Modifier.align(Alignment.BottomCenter), - enabled = text.isNotBlank() && !isWarning, + enabled = state.name.isNotBlank() && !isWarning, onClick = onNextClick, ) } @@ -140,6 +142,9 @@ private fun validateInput(input: String): String { private fun TrainerProfileSetupPagePreview() { TnTTheme { TrainerProfileSetupPage( + state = TODO(), + onNameChanged = {}, + onProfileImageSelected = {}, onBackClick = {}, onNextClick = {}, ) diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt index c5c0840..1579cff 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt @@ -22,21 +22,19 @@ import co.kr.tnt.designsystem.component.button.TnTBottomButton import co.kr.tnt.designsystem.component.image.TnTProfileImage import co.kr.tnt.designsystem.theme.TnTTheme import co.kr.tnt.feature.trainer.signup.R +import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiState import co.kr.tnt.ui.model.DefaultUserProfile import coil.compose.rememberAsyncImagePainter import co.kr.tnt.core.ui.R as uiResource @Composable -fun TrainerSignUpCompletePage( +internal fun TrainerSignUpCompletePage( + state: TrainerSignUpUiState, onBackClick: () -> Unit, onNextClick: () -> Unit, ) { BackHandler { onBackClick() } - // TODO 이름, 프로필 이미지 불러오기 - val name = "김헬짱" - val profileImage = "https://buly.kr/7FQeS5M" - Scaffold( containerColor = TnTTheme.colors.commonColors.Common0, ) { innerPadding -> @@ -54,7 +52,7 @@ fun TrainerSignUpCompletePage( .padding(bottom = 66.dp), ) { Text( - text = stringResource(R.string.nice_to_meet_you_trainer, name), + text = stringResource(R.string.nice_to_meet_you_trainer, state.name), color = TnTTheme.colors.neutralColors.Neutral950, style = TnTTheme.typography.h1, textAlign = Center, @@ -70,7 +68,7 @@ fun TrainerSignUpCompletePage( Spacer(Modifier.padding(top = 28.dp)) TnTProfileImage( defaultImage = painterResource(DefaultUserProfile.Trainer.image), - image = rememberAsyncImagePainter(profileImage), + image = rememberAsyncImagePainter(state.profileImage), imageSize = 200.dp, showEditButton = false, ) @@ -91,6 +89,7 @@ private fun TrainerSignUpCompletePagePreview() { TrainerSignUpCompletePage( onBackClick = {}, onNextClick = {}, + state = TODO(), ) } } diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt index a77a505..9a0e615 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt @@ -1,5 +1,6 @@ package co.kr.tnt.trainer.signup +import android.net.Uri import co.kr.tnt.ui.base.UiEvent import co.kr.tnt.ui.base.UiSideEffect import co.kr.tnt.ui.base.UiState @@ -8,9 +9,12 @@ internal class TrainerSignUpContract { data class TrainerSignUpUiState( val page: TrainerSignUpPage = TrainerSignUpPage.ProfileSetUp, val name: String = "", + val profileImage: Uri? = null, ) : UiState sealed interface TrainerSignUpUiEvent : UiEvent { + data class OnImagePicked(val imageUri: Uri) : TrainerSignUpUiEvent + data class OnNameChanged(val name: String) : TrainerSignUpUiEvent data object OnNextClick : TrainerSignUpUiEvent data object OnBackClick : TrainerSignUpUiEvent } diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt index b92167c..9d075dd 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt @@ -1,5 +1,6 @@ package co.kr.tnt.trainer.signup +import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -21,6 +22,8 @@ internal fun TrainerSignUpRoute( TrainerSignUpScreen( state = uiState, + onNameChanged = { viewModel.setEvent(TrainerSignUpUiEvent.OnNameChanged(it)) }, + onProfileImageSelected = { viewModel.setEvent(TrainerSignUpUiEvent.OnImagePicked(it)) }, onNextClick = { viewModel.setEvent(TrainerSignUpUiEvent.OnNextClick) }, onBackClick = { viewModel.setEvent(TrainerSignUpUiEvent.OnBackClick) }, ) @@ -38,15 +41,21 @@ internal fun TrainerSignUpRoute( @Composable private fun TrainerSignUpScreen( state: TrainerSignUpUiState, + onProfileImageSelected: (Uri) -> Unit, + onNameChanged: (String) -> Unit, onNextClick: () -> Unit, onBackClick: () -> Unit, ) { when (state.page) { TrainerSignUpContract.TrainerSignUpPage.ProfileSetUp -> TrainerProfileSetupPage( + state = state, + onProfileImageSelected = onProfileImageSelected, + onNameChanged = onNameChanged, onNextClick = onNextClick, onBackClick = onBackClick, ) TrainerSignUpContract.TrainerSignUpPage.SignUpComplete -> TrainerSignUpCompletePage( + state = state, onNextClick = onNextClick, onBackClick = onBackClick, ) diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt index 0ec565c..4b8d1f8 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt @@ -1,5 +1,6 @@ package co.kr.tnt.trainer.signup +import android.net.Uri import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpEffect import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpPage import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiEvent @@ -15,11 +16,21 @@ internal class TrainerSignUpViewModel @Inject constructor() : ) { override suspend fun handleEvent(event: TrainerSignUpUiEvent) { when (event) { + is TrainerSignUpUiEvent.OnImagePicked -> setProfileImage(event.imageUri) + is TrainerSignUpUiEvent.OnNameChanged -> setName(event.name) TrainerSignUpUiEvent.OnNextClick -> navigateToNext() TrainerSignUpUiEvent.OnBackClick -> navigateToBack() } } + private fun setProfileImage(imageUri: Uri) { + updateState { copy(profileImage = imageUri) } + } + + private fun setName(name: String) { + updateState { copy(name = name) } + } + private fun navigateToNext() { val nextPage = when (currentState.page) { TrainerSignUpPage.SignUpComplete -> { From 9c3737894e6e3060837d9c9a98a99ade3e22a96d Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Mon, 27 Jan 2025 17:51:37 +0900 Subject: [PATCH 04/14] =?UTF-8?q?[TNT-114]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=84=88=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84,=20=EC=9D=B4=EB=A6=84=20state=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 --- .../trainer/signup/TrainerProfileSetupPage.kt | 12 ++++++------ .../trainer/signup/TrainerSignUpCompletePage.kt | 4 ++-- .../tnt/trainer/signup/TrainerSignUpContract.kt | 4 ++-- .../tnt/trainer/signup/TrainerSignUpViewModel.kt | 16 ++++++++++++++-- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt index 3e0b97f..0c75519 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt @@ -55,8 +55,8 @@ internal fun TrainerProfileSetupPage( val context = LocalContext.current - val isWarning by remember(state.name) { - derivedStateOf { state.name.length > MAX_LENGTH } + val isWarning by remember(state.trainerState.name) { + derivedStateOf { state.trainerState.name.length > MAX_LENGTH } } val pickMediaLauncher = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> @@ -66,7 +66,7 @@ internal fun TrainerProfileSetupPage( } val painter = rememberAsyncImagePainter( model = ImageRequest.Builder(context) - .data(state.profileImage) + .data(state.trainerState.image) .transformations(ResizeTransformation(IMAGE_MAX_SIZE)) .build(), ) @@ -94,7 +94,7 @@ internal fun TrainerProfileSetupPage( .fillMaxWidth() .padding(vertical = 12.dp), defaultImage = painterResource(DefaultUserProfile.Trainer.image), - image = state.profileImage?.let { painter }, + image = state.trainerState.image?.let { painter }, onEditClick = { pickMediaLauncher.launch( PickVisualMediaRequest( @@ -106,7 +106,7 @@ internal fun TrainerProfileSetupPage( Spacer(Modifier.padding(top = 60.dp)) TnTLabeledTextFieldWithCounter( title = stringResource(uiResource.string.name), - value = state.name, + value = state.trainerState.name, onValueChange = { newValue -> val filteredText = validateInput(newValue) onNameChanged(filteredText) @@ -123,7 +123,7 @@ internal fun TrainerProfileSetupPage( TnTBottomButton( text = stringResource(uiResource.string.next), modifier = Modifier.align(Alignment.BottomCenter), - enabled = state.name.isNotBlank() && !isWarning, + enabled = state.trainerState.name.isNotBlank() && !isWarning, onClick = onNextClick, ) } diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt index 1579cff..85eafc0 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt @@ -52,7 +52,7 @@ internal fun TrainerSignUpCompletePage( .padding(bottom = 66.dp), ) { Text( - text = stringResource(R.string.nice_to_meet_you_trainer, state.name), + text = stringResource(R.string.nice_to_meet_you_trainer, state.trainerState.name), color = TnTTheme.colors.neutralColors.Neutral950, style = TnTTheme.typography.h1, textAlign = Center, @@ -68,7 +68,7 @@ internal fun TrainerSignUpCompletePage( Spacer(Modifier.padding(top = 28.dp)) TnTProfileImage( defaultImage = painterResource(DefaultUserProfile.Trainer.image), - image = rememberAsyncImagePainter(state.profileImage), + image = rememberAsyncImagePainter(state.trainerState.image), imageSize = 200.dp, showEditButton = false, ) diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt index 9a0e615..12663e6 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt @@ -1,6 +1,7 @@ package co.kr.tnt.trainer.signup import android.net.Uri +import co.kr.tnt.domain.model.UserType import co.kr.tnt.ui.base.UiEvent import co.kr.tnt.ui.base.UiSideEffect import co.kr.tnt.ui.base.UiState @@ -8,8 +9,7 @@ import co.kr.tnt.ui.base.UiState internal class TrainerSignUpContract { data class TrainerSignUpUiState( val page: TrainerSignUpPage = TrainerSignUpPage.ProfileSetUp, - val name: String = "", - val profileImage: Uri? = null, + val trainerState: UserType.Trainer = UserType.Trainer(), ) : UiState sealed interface TrainerSignUpUiEvent : UiEvent { diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt index 4b8d1f8..8cc5a5c 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt @@ -24,11 +24,23 @@ internal class TrainerSignUpViewModel @Inject constructor() : } private fun setProfileImage(imageUri: Uri) { - updateState { copy(profileImage = imageUri) } + updateState { + copy( + trainerState = trainerState.copy( + image = imageUri.toString(), + ), + ) + } } private fun setName(name: String) { - updateState { copy(name = name) } + updateState { + copy( + trainerState = trainerState.copy( + name = name, + ), + ) + } } private fun navigateToNext() { From 8ff993a6029e33f213caf033f2b78d1fda7659bd Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Mon, 27 Jan 2025 17:52:20 +0900 Subject: [PATCH 05/14] =?UTF-8?q?[TNT-114]=20fix:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A0=95=EB=A0=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt | 1 - .../co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt | 8 ++------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt index 200c723..e52c8bb 100644 --- a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt +++ b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt @@ -25,7 +25,6 @@ internal class TrainerConnectViewModel @Inject constructor() : TrainerConnectUiEvent.OnNextClick -> navigateToNext() TrainerConnectUiEvent.OnBackClick -> navigateToBack() TrainerConnectUiEvent.OnSkipClick -> navigateToHome() - else -> {} } } diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt index 8cc5a5c..40e4a30 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt @@ -26,9 +26,7 @@ internal class TrainerSignUpViewModel @Inject constructor() : private fun setProfileImage(imageUri: Uri) { updateState { copy( - trainerState = trainerState.copy( - image = imageUri.toString(), - ), + trainerState = trainerState.copy(image = imageUri.toString()), ) } } @@ -36,9 +34,7 @@ internal class TrainerSignUpViewModel @Inject constructor() : private fun setName(name: String) { updateState { copy( - trainerState = trainerState.copy( - name = name, - ), + trainerState = trainerState.copy(name = name), ) } } From c2d814b81f43e2f260d06b655bb996df870e96e5 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Mon, 27 Jan 2025 18:23:44 +0900 Subject: [PATCH 06/14] =?UTF-8?q?[TNT-114]=20refactor:=20=ED=8A=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EB=84=88=20=EC=9D=B4=EB=A6=84,=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=20=EC=84=A4=EC=A0=95=20=EA=B4=80=EB=A0=A8=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/tnt/trainer/signup/TrainerProfileSetupPage.kt | 12 ++++++------ .../kr/tnt/trainer/signup/TrainerSignUpContract.kt | 4 ++-- .../co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt | 12 ++++++------ .../kr/tnt/trainer/signup/TrainerSignUpViewModel.kt | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt index 0c75519..75d8aa9 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt @@ -46,8 +46,8 @@ private const val MAX_LENGTH = 15 @Composable internal fun TrainerProfileSetupPage( state: TrainerSignUpUiState, - onProfileImageSelected: (Uri) -> Unit, - onNameChanged: (String) -> Unit, + onProfileImageSelect: (Uri) -> Unit, + onNameChange: (String) -> Unit, onBackClick: () -> Unit, onNextClick: () -> Unit, ) { @@ -61,7 +61,7 @@ internal fun TrainerProfileSetupPage( val pickMediaLauncher = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> if (uri != null) { - onProfileImageSelected(uri) + onProfileImageSelect(uri) } } val painter = rememberAsyncImagePainter( @@ -109,7 +109,7 @@ internal fun TrainerProfileSetupPage( value = state.trainerState.name, onValueChange = { newValue -> val filteredText = validateInput(newValue) - onNameChanged(filteredText) + onNameChange(filteredText) }, modifier = Modifier.padding(horizontal = 20.dp), placeholder = stringResource(R.string.name_placeholder), @@ -143,8 +143,8 @@ private fun TrainerProfileSetupPagePreview() { TnTTheme { TrainerProfileSetupPage( state = TODO(), - onNameChanged = {}, - onProfileImageSelected = {}, + onNameChange = {}, + onProfileImageSelect = {}, onBackClick = {}, onNextClick = {}, ) diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt index 12663e6..34dfb81 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt @@ -13,8 +13,8 @@ internal class TrainerSignUpContract { ) : UiState sealed interface TrainerSignUpUiEvent : UiEvent { - data class OnImagePicked(val imageUri: Uri) : TrainerSignUpUiEvent - data class OnNameChanged(val name: String) : TrainerSignUpUiEvent + data class OnImageChange(val imageUri: Uri) : TrainerSignUpUiEvent + data class OnNameChange(val name: String) : TrainerSignUpUiEvent data object OnNextClick : TrainerSignUpUiEvent data object OnBackClick : TrainerSignUpUiEvent } diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt index 9d075dd..5c1b5c6 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt @@ -22,8 +22,8 @@ internal fun TrainerSignUpRoute( TrainerSignUpScreen( state = uiState, - onNameChanged = { viewModel.setEvent(TrainerSignUpUiEvent.OnNameChanged(it)) }, - onProfileImageSelected = { viewModel.setEvent(TrainerSignUpUiEvent.OnImagePicked(it)) }, + onNameChange = { viewModel.setEvent(TrainerSignUpUiEvent.OnNameChange(it)) }, + onProfileImageSelect = { viewModel.setEvent(TrainerSignUpUiEvent.OnImageChange(it)) }, onNextClick = { viewModel.setEvent(TrainerSignUpUiEvent.OnNextClick) }, onBackClick = { viewModel.setEvent(TrainerSignUpUiEvent.OnBackClick) }, ) @@ -41,16 +41,16 @@ internal fun TrainerSignUpRoute( @Composable private fun TrainerSignUpScreen( state: TrainerSignUpUiState, - onProfileImageSelected: (Uri) -> Unit, - onNameChanged: (String) -> Unit, + onProfileImageSelect: (Uri) -> Unit, + onNameChange: (String) -> Unit, onNextClick: () -> Unit, onBackClick: () -> Unit, ) { when (state.page) { TrainerSignUpContract.TrainerSignUpPage.ProfileSetUp -> TrainerProfileSetupPage( state = state, - onProfileImageSelected = onProfileImageSelected, - onNameChanged = onNameChanged, + onProfileImageSelect = onProfileImageSelect, + onNameChange = onNameChange, onNextClick = onNextClick, onBackClick = onBackClick, ) diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt index 40e4a30..620234e 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt @@ -16,8 +16,8 @@ internal class TrainerSignUpViewModel @Inject constructor() : ) { override suspend fun handleEvent(event: TrainerSignUpUiEvent) { when (event) { - is TrainerSignUpUiEvent.OnImagePicked -> setProfileImage(event.imageUri) - is TrainerSignUpUiEvent.OnNameChanged -> setName(event.name) + is TrainerSignUpUiEvent.OnImageChange -> setProfileImage(event.imageUri) + is TrainerSignUpUiEvent.OnNameChange -> setName(event.name) TrainerSignUpUiEvent.OnNextClick -> navigateToNext() TrainerSignUpUiEvent.OnBackClick -> navigateToBack() } From eb79acaf28a6cfc92ed81f25bae9ebd48610c088 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Mon, 27 Jan 2025 20:58:42 +0900 Subject: [PATCH 07/14] =?UTF-8?q?[TNT-114]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=8B=88=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20state=EB=A1=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/co/kr/tnt/domain/model/UserType.kt | 7 +- .../trainee/signup/TraineeBasicInfoPage.kt | 62 +++------ .../signup/TraineeNoteForTrainerPage.kt | 16 +-- .../trainee/signup/TraineePTPurposePage.kt | 37 ++---- .../trainee/signup/TraineeProfileSetupPage.kt | 37 +++--- .../signup/TraineeSignUpCompletePage.kt | 14 ++- .../trainee/signup/TraineeSignUpContract.kt | 18 ++- .../tnt/trainee/signup/TraineeSignUpScreen.kt | 32 +++++ .../trainee/signup/TraineeSignUpViewModel.kt | 119 ++++++++++++++++++ 9 files changed, 240 insertions(+), 102 deletions(-) diff --git a/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt b/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt index c4a2731..60c2267 100644 --- a/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt +++ b/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt @@ -1,5 +1,7 @@ package co.kr.tnt.domain.model +import java.time.LocalDate + sealed class UserType { abstract val id: String abstract val name: String @@ -15,10 +17,11 @@ sealed class UserType { override val id: String = "", override val name: String = "", override val image: String? = null, - val age: Int = 0, + val birthday: LocalDate? = null, + val age: Int? = 0, val weight: Float = 0f, val height: Int = 0, val ptPurpose: List = emptyList(), - val caution: String? = null, + val caution: String? = "", ) : UserType() } diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeBasicInfoPage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeBasicInfoPage.kt index 0144da7..736f5dc 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeBasicInfoPage.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeBasicInfoPage.kt @@ -18,11 +18,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -36,32 +32,25 @@ 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.feature.trainee.signup.R +import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState import co.kr.tnt.trainee.signup.component.ProgressSteps import java.time.LocalDate import java.time.ZoneId import java.time.format.DateTimeFormatter import co.kr.tnt.core.ui.R as uiResource -private const val MAX_HEIGHT_LENGTH = 3 -private const val MAX_WEIGHT_LENGTH = 5 - @Composable -fun TraineeBasicInfoPage( +internal fun TraineeBasicInfoPage( + state: TraineeSignUpUiState, + onHeightChange: (String) -> Unit, + onWeightChange: (String) -> Unit, + onBirthdayChange: (LocalDate) -> Unit, onBackClick: () -> Unit, onNextClick: () -> Unit, ) { BackHandler { onBackClick() } - // TODO 상태 관리 따로 빼기 val today = LocalDate.now() - var height by remember { mutableStateOf("") } - var weight by remember { mutableStateOf("") } - var birthday by remember { mutableStateOf(null) } - - val isHeightValid by remember { derivedStateOf { height.isNotEmpty() && validateHeight(height) } } - val isWeightValid by remember { derivedStateOf { weight.isNotEmpty() && validateWeight(weight) } } - - val isFormValid by remember { derivedStateOf { isHeightValid && isWeightValid } } Scaffold( topBar = { TnTTopBarWithBackButton(onBackClick = onBackClick) }, @@ -90,8 +79,8 @@ fun TraineeBasicInfoPage( BirthdayPicker( modifier = Modifier.padding(horizontal = 20.dp), today = today, - selectedDate = birthday, - onDateSelected = { birthday = it }, + selectedDate = state.traineeState.birthday, + onDateSelected = { onBirthdayChange(it) }, ) HorizontalDivider( thickness = 1.dp, @@ -107,32 +96,32 @@ fun TraineeBasicInfoPage( ) { TnTLabeledTextField( title = stringResource(uiResource.string.height_label), - value = height, + value = state.height, placeholder = "0", isSingleLine = true, - showWarning = !validateHeight(height), + showWarning = state.height.isNotEmpty() && !state.isHeightValid, warningMessage = stringResource(R.string.entered_wrong_number), isRequired = true, keyboardType = KeyboardType.Number, trailingComponent = { UnitLabel(uiResource.string.height_unit) }, - onValueChange = { height = it }, + onValueChange = { onHeightChange(it) }, modifier = Modifier.weight(1f), ) TnTLabeledTextField( title = stringResource(uiResource.string.weight_label), - value = weight, + value = state.weight, placeholder = "00.0", isSingleLine = true, - showWarning = !validateWeight(weight), + showWarning = state.weight.isNotEmpty() && !state.isWeightValid, warningMessage = stringResource(R.string.entered_wrong_number), isRequired = true, keyboardType = KeyboardType.Number, trailingComponent = { UnitLabel(uiResource.string.weight_unit) }, - onValueChange = { weight = it }, + onValueChange = { onWeightChange(it) }, modifier = Modifier.weight(1f), ) } @@ -140,7 +129,7 @@ fun TraineeBasicInfoPage( TnTBottomButton( text = stringResource(uiResource.string.next), modifier = Modifier.align(Alignment.BottomCenter), - enabled = isFormValid, + enabled = state.isBasicInfoValid, onClick = onNextClick, ) } @@ -207,23 +196,6 @@ private fun UnitLabel(stringResId: Int) { ) } -/** - * 키가 유효한 입력값인지 검사 - * 형식: 정수 3자 - */ -private fun validateHeight(input: String): Boolean { - return input.isEmpty() || input.toIntOrNull() != null && !input.startsWith("0") && input.length <= MAX_HEIGHT_LENGTH -} - -/** - * 몸무게가 유효한 입력값인지 검사 - * 형식: 5자 이하의 실수 (000, 00, 00.0, 000.0) - */ -private fun validateWeight(input: String): Boolean { - val weightRegex = Regex("^(\\d{1,3}(\\.\\d)?)?\$") - return input.isEmpty() || input.matches(weightRegex) && !input.startsWith("0") && input.length <= MAX_WEIGHT_LENGTH -} - @Preview(showBackground = true) @Composable private fun TraineeBasicInfoPagePreview() { @@ -231,6 +203,10 @@ private fun TraineeBasicInfoPagePreview() { TraineeBasicInfoPage( onBackClick = {}, onNextClick = {}, + state = TODO(), + onHeightChange = {}, + onWeightChange = {}, + onBirthdayChange = {}, ) } } 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 9ae56d2..d8efcc4 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 @@ -12,8 +12,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -25,21 +23,21 @@ 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.feature.trainee.signup.R +import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState import co.kr.tnt.trainee.signup.component.ProgressSteps import co.kr.tnt.core.ui.R as uiResource private const val MAX_LENGTH = 100 @Composable -fun TraineeNoteForTrainerPage( +internal fun TraineeNoteForTrainerPage( + state: TraineeSignUpUiState, + onCautionChange: (String) -> Unit, onBackClick: () -> Unit, onNextClick: () -> Unit, ) { BackHandler { onBackClick() } - // TODO 상태 관리 따로 빼기 - var text by remember { mutableStateOf("") } - Scaffold( topBar = { TnTTopBarWithBackButton(onBackClick = onBackClick) }, containerColor = TnTTheme.colors.commonColors.Common0, @@ -59,10 +57,10 @@ fun TraineeNoteForTrainerPage( ) Spacer(Modifier.padding(top = 48.dp)) TnTOutlinedTextField( - value = text, + value = state.traineeState.caution.toString(), onValueChange = { newValue -> if (newValue.length <= MAX_LENGTH) { - text = newValue + onCautionChange(newValue) } }, modifier = Modifier.padding(horizontal = 20.dp), @@ -85,6 +83,8 @@ private fun TraineeNoteForTrainerPagePreview() { TraineeNoteForTrainerPage( onBackClick = {}, onNextClick = {}, + state = TODO(), + onCautionChange = {}, ) } } 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 2c4be6c..3fb0ddf 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 @@ -13,8 +13,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -28,6 +26,7 @@ import co.kr.tnt.designsystem.component.button.model.ButtonSize import co.kr.tnt.designsystem.component.button.model.ButtonType 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.core.ui.R as uiResource @@ -37,15 +36,14 @@ private const val COLUMNS_NUM = 2 @OptIn(ExperimentalLayoutApi::class) @Composable -fun TraineePTPurposePage( +internal fun TraineePTPurposePage( + state: TraineeSignUpUiState, + onPurposeSelected: (String) -> Unit, onBackClick: () -> Unit, onNextClick: () -> Unit, ) { BackHandler { onBackClick() } - // TODO 리소스 id값 텍스트로 전환해 넘겨주기 - var selectedPurposes by remember { mutableStateOf(setOf()) } - Scaffold( topBar = { TnTTopBarWithBackButton(onBackClick = onBackClick) }, containerColor = TnTTheme.colors.commonColors.Common0, @@ -68,12 +66,11 @@ fun TraineePTPurposePage( modifier = Modifier.fillMaxWidth(), ) { PTPurpose.entries.forEach { purpose -> + val purposeText = stringResource(purpose.textResId) PurposeButton( - text = stringResource(purpose.textResId), - isSelected = purpose in selectedPurposes, - onClick = { - selectedPurposes = toggleSelection(selectedPurposes, purpose) - }, + text = purposeText, + isSelected = purposeText in state.traineeState.ptPurpose, + onClick = { onPurposeSelected(purposeText) }, modifier = Modifier.weight(1f), ) } @@ -83,7 +80,7 @@ fun TraineePTPurposePage( TnTBottomButton( text = stringResource(uiResource.string.next), onClick = onNextClick, - enabled = selectedPurposes.isNotEmpty(), + enabled = state.traineeState.ptPurpose.isNotEmpty(), modifier = Modifier.align(Alignment.BottomCenter), ) } @@ -106,27 +103,15 @@ fun PurposeButton( ) } -// 선택된 값 업데이트 -private fun toggleSelection( - selectedPurposes: Set, - purpose: PTPurpose, -): Set { - return selectedPurposes.toMutableSet().apply { - if (contains(purpose)) { - remove(purpose) - } else { - add(purpose) - } - }.toSet() -} - @Preview(showBackground = true) @Composable private fun TraineePTPurposePagePreview() { TnTTheme { TraineePTPurposePage( + state = TODO(), onBackClick = {}, onNextClick = {}, + onPurposeSelected = {}, ) } } diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeProfileSetupPage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeProfileSetupPage.kt index 5e79bf5..72f85a2 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeProfileSetupPage.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeProfileSetupPage.kt @@ -18,9 +18,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -35,6 +33,7 @@ import co.kr.tnt.designsystem.component.image.TnTProfileImage import co.kr.tnt.designsystem.theme.TnTTheme import co.kr.tnt.domain.IMAGE_MAX_SIZE 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.coil.ResizeTransformation import co.kr.tnt.ui.model.DefaultUserProfile @@ -42,8 +41,13 @@ import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import co.kr.tnt.core.ui.R as uiResource +private const val MAX_LENGTH = 15 + @Composable -fun TraineeProfileSetupPage( +internal fun TraineeProfileSetupPage( + state: TraineeSignUpUiState, + onProfileImageSelect: (Uri) -> Unit, + onNameChange: (String) -> Unit, onBackClick: () -> Unit, onNextClick: () -> Unit, ) { @@ -51,20 +55,18 @@ fun TraineeProfileSetupPage( val context = LocalContext.current - // TODO 상태 관리 따로 빼기 - val maxLength = 15 - var text by remember { mutableStateOf("") } - val isWarning by remember { derivedStateOf { text.length > maxLength } } - var profileImage by remember { mutableStateOf(null) } + val isWarning by remember(state.traineeState.name) { + derivedStateOf { state.traineeState.name.length > MAX_LENGTH } + } val pickMediaLauncher = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> if (uri != null) { - profileImage = uri + onProfileImageSelect(uri) } } val painter = rememberAsyncImagePainter( model = ImageRequest.Builder(context) - .data(profileImage) + .data(state.traineeState.image) .transformations(ResizeTransformation(IMAGE_MAX_SIZE)) .build(), ) @@ -91,7 +93,7 @@ fun TraineeProfileSetupPage( .fillMaxWidth() .padding(vertical = 12.dp), defaultImage = painterResource(DefaultUserProfile.Trainee.image), - image = profileImage?.let { painter }, + image = state.traineeState.image?.let { painter }, onEditClick = { pickMediaLauncher.launch( PickVisualMediaRequest( @@ -103,23 +105,23 @@ fun TraineeProfileSetupPage( Spacer(Modifier.padding(top = 60.dp)) TnTLabeledTextFieldWithCounter( title = stringResource(uiResource.string.name), - value = text, + value = state.traineeState.name, onValueChange = { newValue -> val filteredText = validateInput(newValue) - text = filteredText + onNameChange(filteredText) }, modifier = Modifier.padding(horizontal = 20.dp), placeholder = stringResource(R.string.enter_your_name), - maxLength = maxLength, + maxLength = MAX_LENGTH, isSingleLine = true, showWarning = isWarning, isRequired = true, - warningMessage = stringResource(R.string.text_length_warning, maxLength), + warningMessage = stringResource(R.string.text_length_warning, MAX_LENGTH), ) } TnTBottomButton( text = stringResource(uiResource.string.next), - enabled = text.isNotBlank() && !isWarning, + enabled = state.traineeState.name.isNotBlank() && !isWarning, onClick = onNextClick, modifier = Modifier.align(Alignment.BottomCenter), ) @@ -141,6 +143,9 @@ private fun TraineeProfileSetupPagePreview() { TraineeProfileSetupPage( onBackClick = {}, onNextClick = {}, + state = TODO(), + onProfileImageSelect = {}, + onNameChange = {}, ) } } diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt index dd60f9c..4262dda 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt @@ -1,5 +1,6 @@ package co.kr.tnt.trainee.signup +import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -22,20 +23,20 @@ import co.kr.tnt.designsystem.component.button.TnTBottomButton import co.kr.tnt.designsystem.component.image.TnTProfileImage 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.ui.model.DefaultUserProfile import coil.compose.rememberAsyncImagePainter import co.kr.tnt.core.ui.R as uiResource @Composable -fun TraineeSignUpCompletePage( +internal fun TraineeSignUpCompletePage( + state: TraineeSignUpUiState, onBackClick: () -> Unit, onNextClick: () -> Unit, ) { BackHandler { onBackClick() } - // TODO 이름, 프로필 사진 불러오기 - val name = "김헬짱" - val profileImage = "https://buly.kr/7FQeS5M" + Log.d("test", "${state.traineeState}") Scaffold( containerColor = TnTTheme.colors.commonColors.Common0, @@ -54,7 +55,7 @@ fun TraineeSignUpCompletePage( .padding(bottom = 66.dp), ) { Text( - text = stringResource(R.string.nice_to_meet_you_trainee, name), + text = stringResource(R.string.nice_to_meet_you_trainee, state.traineeState.name), color = TnTTheme.colors.neutralColors.Neutral950, style = TnTTheme.typography.h1, textAlign = Center, @@ -70,7 +71,7 @@ fun TraineeSignUpCompletePage( Spacer(Modifier.padding(top = 28.dp)) TnTProfileImage( defaultImage = painterResource(DefaultUserProfile.Trainee.image), - image = rememberAsyncImagePainter(profileImage), + image = rememberAsyncImagePainter(state.traineeState.image), imageSize = 200.dp, showEditButton = false, ) @@ -91,6 +92,7 @@ private fun TraineeSignUpCompletePagePreview() { TraineeSignUpCompletePage( onBackClick = {}, onNextClick = {}, + state = TODO(), ) } } 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 d1b3925..229d39e 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 @@ -1,16 +1,32 @@ package co.kr.tnt.trainee.signup +import android.net.Uri +import co.kr.tnt.domain.model.UserType import co.kr.tnt.ui.base.UiEvent import co.kr.tnt.ui.base.UiSideEffect import co.kr.tnt.ui.base.UiState +import java.time.LocalDate internal class TraineeSignUpContract { data class TraineeSignUpUiState( val page: TraineeSignUpPage = TraineeSignUpPage.ProfileSetUp, - val name: String = "", + val height: String = "", + val weight: String = "", + val isHeightValid: Boolean = false, + val isWeightValid: Boolean = false, + val isBasicInfoValid: Boolean = false, + val traineeState: UserType.Trainee = UserType.Trainee(), ) : UiState sealed interface TraineeSignUpUiEvent : UiEvent { + data class OnImageChange(val imageUri: Uri) : TraineeSignUpUiEvent + data class OnNameChange(val name: String) : TraineeSignUpUiEvent + data class OnHeightChange(val height: String) : TraineeSignUpUiEvent + data class OnWeightChange(val weight: String) : TraineeSignUpUiEvent + data class OnBirthdayChange(val birthday: LocalDate) : TraineeSignUpUiEvent + data object OnBasicFormUpdate : TraineeSignUpUiEvent + data class OnPurposeSelected(val purpose: String) : TraineeSignUpUiEvent + data class OnCautionChange(val text: String) : TraineeSignUpUiEvent data object OnNextClick : TraineeSignUpUiEvent data object OnBackClick : TraineeSignUpUiEvent } 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 415dbb0..55d91b7 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 @@ -1,5 +1,6 @@ package co.kr.tnt.trainee.signup +import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -9,6 +10,7 @@ import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpEffect import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpPage import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiEvent import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState +import java.time.LocalDate @Composable internal fun TraineeSignUpRoute( @@ -23,6 +25,13 @@ internal fun TraineeSignUpRoute( TraineeSignUpScreen( state = uiState, + onNameChange = { viewModel.setEvent(TraineeSignUpUiEvent.OnNameChange(it)) }, + onProfileImageSelect = { viewModel.setEvent(TraineeSignUpUiEvent.OnImageChange(it)) }, + onHeightChange = { viewModel.setEvent(TraineeSignUpUiEvent.OnHeightChange(it)) }, + onWeightChange = { viewModel.setEvent(TraineeSignUpUiEvent.OnWeightChange(it)) }, + onBirthdayChange = { viewModel.setEvent(TraineeSignUpUiEvent.OnBirthdayChange(it)) }, + onPurposeSelected = { viewModel.setEvent(TraineeSignUpUiEvent.OnPurposeSelected(it)) }, + onCautionChange = { viewModel.setEvent(TraineeSignUpUiEvent.OnCautionChange(it)) }, onBackClick = { viewModel.setEvent(TraineeSignUpUiEvent.OnBackClick) }, onNextClick = { viewModel.setEvent(TraineeSignUpUiEvent.OnNextClick) }, ) @@ -40,27 +49,50 @@ internal fun TraineeSignUpRoute( @Composable private fun TraineeSignUpScreen( state: TraineeSignUpUiState, + onProfileImageSelect: (Uri) -> Unit, + onNameChange: (String) -> Unit, + onHeightChange: (String) -> Unit, + onWeightChange: (String) -> Unit, + onCautionChange: (String) -> Unit, + onBirthdayChange: (LocalDate) -> Unit, + onPurposeSelected: (String) -> Unit, onNextClick: () -> Unit, onBackClick: () -> Unit, ) { when (state.page) { TraineeSignUpPage.ProfileSetUp -> TraineeProfileSetupPage( + state = state, + onProfileImageSelect = onProfileImageSelect, + onNameChange = onNameChange, onBackClick = onBackClick, onNextClick = onNextClick, ) + TraineeSignUpPage.BasicInfo -> TraineeBasicInfoPage( + state = state, + onHeightChange = onHeightChange, + onWeightChange = onWeightChange, + onBirthdayChange = onBirthdayChange, onBackClick = onBackClick, onNextClick = onNextClick, ) + TraineeSignUpPage.NoteForTrainer -> TraineeNoteForTrainerPage( + state = state, + onCautionChange = onCautionChange, onBackClick = onBackClick, onNextClick = onNextClick, ) + TraineeSignUpPage.PTPurpose -> TraineePTPurposePage( + state = state, + onPurposeSelected = onPurposeSelected, onBackClick = onBackClick, onNextClick = onNextClick, ) + TraineeSignUpPage.SignUpComplete -> TraineeSignUpCompletePage( + state = state, onBackClick = onBackClick, onNextClick = onNextClick, ) diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt index 8c2d38d..70298d7 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt @@ -1,13 +1,18 @@ package co.kr.tnt.trainee.signup +import android.net.Uri import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpEffect import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpPage import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiEvent import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState import co.kr.tnt.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import java.time.LocalDate import javax.inject.Inject +private const val MAX_HEIGHT_LENGTH = 3 +private const val MAX_WEIGHT_LENGTH = 5 + @HiltViewModel internal class TraineeSignUpViewModel @Inject constructor() : BaseViewModel( @@ -15,11 +20,120 @@ internal class TraineeSignUpViewModel @Inject constructor() : ) { override suspend fun handleEvent(event: TraineeSignUpUiEvent) { when (event) { + is TraineeSignUpUiEvent.OnImageChange -> updateProfileImage(event.imageUri) + is TraineeSignUpUiEvent.OnNameChange -> updateName(event.name) + is TraineeSignUpUiEvent.OnHeightChange -> updateHeight(event.height) + is TraineeSignUpUiEvent.OnWeightChange -> updateWeight(event.weight) + is TraineeSignUpUiEvent.OnBirthdayChange -> updateBirthday(event.birthday) + TraineeSignUpUiEvent.OnBasicFormUpdate -> setBasicFormDate() + is TraineeSignUpUiEvent.OnPurposeSelected -> updateSelectedPurposes(event.purpose) + is TraineeSignUpUiEvent.OnCautionChange -> updateCaution(event.text) TraineeSignUpUiEvent.OnBackClick -> navigateToBack() TraineeSignUpUiEvent.OnNextClick -> navigateToNext() } } + private fun updateProfileImage(imageUri: Uri) { + updateState { + copy( + traineeState = traineeState.copy(image = imageUri.toString()), + ) + } + } + + private fun updateName(name: String) { + updateState { + copy( + traineeState = traineeState.copy(name = name), + ) + } + } + + private fun updateHeight(height: String) { + val isValid = validateHeight(height) + updateState { + val isFormValid = isValid && isWeightValid + copy( + height = height, + isHeightValid = isValid, + isBasicInfoValid = isFormValid, + ) + } + } + + private fun updateWeight(weight: String) { + val isValid = validateWeight(weight) + updateState { + val isFormValid = isHeightValid && isValid + copy( + weight = weight, + isWeightValid = isValid, + isBasicInfoValid = isFormValid, + ) + } + } + + private fun updateBirthday(birthday: LocalDate) { + updateState { + copy( + traineeState = traineeState.copy(birthday = birthday), + ) + } + } + + private fun setBasicFormDate() { + updateState { + copy( + traineeState = traineeState.copy( + weight = weight.toFloat(), + height = height.toInt(), + ), + ) + } + } + + /** + * 키가 유효한 입력값인지 검사 + * 형식: 정수 3자 + */ + private fun validateHeight(input: String): Boolean { + return input.isNotEmpty() && input.toIntOrNull() != null && + !input.startsWith("0") && input.length <= MAX_HEIGHT_LENGTH + } + + /** + * 몸무게가 유효한 입력값인지 검사 + * 형식: 5자 이하의 실수 (000, 00, 00.0, 000.0) + */ + private fun validateWeight(input: String): Boolean { + val weightRegex = Regex("^(\\d{1,3}(\\.\\d)?)?\$") + return input.isNotEmpty() && input.matches(weightRegex) && + !input.startsWith("0") && input.length <= MAX_WEIGHT_LENGTH + } + + private fun updateSelectedPurposes(purpose: String) { + val updatedPurposes = currentState.traineeState.ptPurpose.toMutableList().apply { + if (contains(purpose)) { + remove(purpose) + } else { + add(purpose) + } + } + updateState { + copy( + traineeState = traineeState.copy(ptPurpose = updatedPurposes), + ) + } + } + + private fun updateCaution(text: String) { + updateState { + copy( + traineeState = traineeState.copy(caution = text), + ) + } + } + private fun navigateToNext() { val nextPage = when (currentState.page) { TraineeSignUpPage.SignUpComplete -> { @@ -27,6 +141,11 @@ internal class TraineeSignUpViewModel @Inject constructor() : return } + TraineeSignUpPage.BasicInfo -> { + setBasicFormDate() + TraineeSignUpPage.getNextPage(currentState.page) + } + else -> TraineeSignUpPage.getNextPage(currentState.page) } updateState { copy(page = nextPage) } From bb6d29e987d03777944f15903c996cf246245280 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Tue, 28 Jan 2025 13:37:50 +0900 Subject: [PATCH 08/14] =?UTF-8?q?[TNT-114]=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 몸무게 Float에서 Double로 수정 - FileUtils 이용해 이미지 파일 변환 - SignUpRequestMapper를 통해 trainer, trainee 데이터 다르게 전달 --- .../main/java/co/kr/tnt/ui/util/FileUtils.kt | 34 +++++++++ .../co/kr/data/network/model/SignUpRequest.kt | 71 +++++++++++++++++++ .../kr/data/network/model/SignUpResponse.kt | 21 ++++++ .../co/kr/data/network/service/ApiService.kt | 12 ++++ .../network/source/SignUpRemoteDataSource.kt | 21 ++++++ data/repository/build.gradle.kts | 2 + .../data/repository/SignUpRepositoryImpl.kt | 56 +++++++++++++++ .../kr/data/repository/di/RepositoryModule.kt | 7 ++ domain/build.gradle.kts | 1 + .../co/kr/tnt/domain/model/SignUpResult.kt | 8 +++ .../java/co/kr/tnt/domain/model/UserType.kt | 2 +- .../tnt/domain/repository/SignUpRepository.kt | 15 ++++ .../connect/TraineeConnectViewModel.kt | 2 +- .../connect/TrainerConnectViewModel.kt | 2 +- 14 files changed, 251 insertions(+), 3 deletions(-) create mode 100644 core/ui/src/main/java/co/kr/tnt/ui/util/FileUtils.kt create mode 100644 data/network/src/main/java/co/kr/data/network/model/SignUpRequest.kt create mode 100644 data/network/src/main/java/co/kr/data/network/model/SignUpResponse.kt create mode 100644 data/network/src/main/java/co/kr/data/network/source/SignUpRemoteDataSource.kt create mode 100644 data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt create mode 100644 domain/src/main/java/co/kr/tnt/domain/model/SignUpResult.kt create mode 100644 domain/src/main/java/co/kr/tnt/domain/repository/SignUpRepository.kt diff --git a/core/ui/src/main/java/co/kr/tnt/ui/util/FileUtils.kt b/core/ui/src/main/java/co/kr/tnt/ui/util/FileUtils.kt new file mode 100644 index 0000000..9682700 --- /dev/null +++ b/core/ui/src/main/java/co/kr/tnt/ui/util/FileUtils.kt @@ -0,0 +1,34 @@ +package co.kr.tnt.ui.util + +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.provider.MediaStore +import android.util.Log +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File + +fun Uri.toMultiPart(context: Context, keyName: String = "image"): MultipartBody.Part? { + return getRealPathFromUri(this, context)?.let { filePath -> + val file = File(filePath) + val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) + MultipartBody.Part.createFormData(keyName, file.name, requestFile) + } ?: run { + Log.e("toMultipartImagePart", "Error creating multipart image for URI: $this") + null + } +} + +fun getRealPathFromUri(uri: Uri, context: Context): String? { + val projection = arrayOf(MediaStore.Images.Media.DATA) + val cursor: Cursor? = context.contentResolver.query(uri, projection, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) + return it.getString(columnIndex) + } + } + return null +} 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 new file mode 100644 index 0000000..f630b05 --- /dev/null +++ b/data/network/src/main/java/co/kr/data/network/model/SignUpRequest.kt @@ -0,0 +1,71 @@ +package co.kr.data.network.model + +import co.kr.tnt.domain.model.UserType +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import java.time.LocalDate + +@Serializable +data class SignUpRequest( + val socialType: String, + val socialId: String, + val fcmToken: String, + val serviceAgreement: Boolean, + val collectionAgreement: Boolean, + val advertisementAgreement: Boolean, + val socialEmail: String, + val memberType: String, + val name: String, + @Contextual + val birthday: LocalDate? = null, + val height: Double? = null, + val weight: Double? = null, + val goalContents: List? = null, + val cautionNote: String? = "", +) + +object SignUpRequestMapper { + fun fromUserType( + userType: UserType, + socialId: String, + socialType: String, + email: String, + fcmToken: String, + ): SignUpRequest { + return when (userType) { + is UserType.Trainer -> SignUpRequest( + memberType = "trainer", + name = userType.name, + birthday = null, + height = null, + weight = null, + goalContents = null, + cautionNote = null, + socialType = socialType, + socialId = socialId, + socialEmail = email, + fcmToken = fcmToken, + serviceAgreement = true, + collectionAgreement = true, + advertisementAgreement = true, + ) + + is UserType.Trainee -> SignUpRequest( + memberType = "trainee", + name = userType.name, + birthday = userType.birthday, + height = userType.height.toDouble(), + weight = userType.weight, + goalContents = userType.ptPurpose, + cautionNote = userType.caution, + socialType = socialType, + socialId = socialId, + socialEmail = email, + fcmToken = fcmToken, + serviceAgreement = true, + collectionAgreement = true, + advertisementAgreement = true, + ) + } + } +} diff --git a/data/network/src/main/java/co/kr/data/network/model/SignUpResponse.kt b/data/network/src/main/java/co/kr/data/network/model/SignUpResponse.kt new file mode 100644 index 0000000..77de8c8 --- /dev/null +++ b/data/network/src/main/java/co/kr/data/network/model/SignUpResponse.kt @@ -0,0 +1,21 @@ +package co.kr.data.network.model + +import co.kr.tnt.domain.model.SignUpResult +import kotlinx.serialization.Serializable + +@Serializable +data class SignUpResponse( + val memberType: String, + val sessionId: String, + val name: String, + val profileImageUrl: String, +) + +fun SignUpResponse.toDomain(): SignUpResult { + return SignUpResult( + memberType = memberType, + sessionId = sessionId, + name = name, + profileImageUrl = profileImageUrl, + ) +} diff --git a/data/network/src/main/java/co/kr/data/network/service/ApiService.kt b/data/network/src/main/java/co/kr/data/network/service/ApiService.kt index a3be07a..bb5412c 100644 --- a/data/network/src/main/java/co/kr/data/network/service/ApiService.kt +++ b/data/network/src/main/java/co/kr/data/network/service/ApiService.kt @@ -2,12 +2,24 @@ package co.kr.data.network.service import co.kr.data.network.model.LoginRequest import co.kr.data.network.model.LoginResponse +import co.kr.data.network.model.SignUpResponse +import okhttp3.MultipartBody +import okhttp3.RequestBody import retrofit2.http.Body +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part interface ApiService { @POST("/login") suspend fun postLogin( @Body request: LoginRequest, ): LoginResponse + + @Multipart + @POST("/members/sign-up") + suspend fun postSignUp( + @Part profileImage: MultipartBody.Part?, + @Part("request") request: RequestBody, + ): SignUpResponse } diff --git a/data/network/src/main/java/co/kr/data/network/source/SignUpRemoteDataSource.kt b/data/network/src/main/java/co/kr/data/network/source/SignUpRemoteDataSource.kt new file mode 100644 index 0000000..cb59d42 --- /dev/null +++ b/data/network/src/main/java/co/kr/data/network/source/SignUpRemoteDataSource.kt @@ -0,0 +1,21 @@ +package co.kr.data.network.source + +import co.kr.data.network.model.SignUpResponse +import co.kr.data.network.service.ApiService +import co.kr.data.network.util.networkHandler +import okhttp3.MultipartBody +import okhttp3.RequestBody +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SignUpRemoteDataSource @Inject constructor( + private val apiService: ApiService, +) { + suspend fun postSignUp( + profileImage: MultipartBody.Part?, + request: RequestBody, + ): SignUpResponse = networkHandler { + apiService.postSignUp(profileImage, request) + } +} diff --git a/data/repository/build.gradle.kts b/data/repository/build.gradle.kts index de82399..af8f0eb 100644 --- a/data/repository/build.gradle.kts +++ b/data/repository/build.gradle.kts @@ -13,4 +13,6 @@ dependencies { implementation(projects.domain) implementation(projects.data.network) implementation(projects.data.storage) + implementation(libs.okhttp.logging) + implementation(libs.kotlinx.serialization.json) } diff --git a/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt b/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt new file mode 100644 index 0000000..8785783 --- /dev/null +++ b/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt @@ -0,0 +1,56 @@ +package co.kr.data.repository + +import co.kr.data.network.model.SignUpRequest +import co.kr.data.network.model.SignUpRequestMapper +import co.kr.data.network.model.toDomain +import co.kr.data.network.source.SignUpRemoteDataSource +import co.kr.data.storage.source.SessionLocalDataSource +import co.kr.tnt.domain.model.SignUpResult +import co.kr.tnt.domain.model.UserType +import co.kr.tnt.domain.repository.SignUpRepository +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import javax.inject.Inject + +class SignUpRepositoryImpl @Inject constructor( + private val signupRemoteDataSource: SignUpRemoteDataSource, + private val sessionLocalDataSource: SessionLocalDataSource, +) : SignUpRepository { + override suspend fun signUp( + profileImage: MultipartBody.Part?, + userType: UserType, + socialId: String, + socialType: String, + email: String, + ): SignUpResult { + // TODO FCM token + val signUpRequest = SignUpRequestMapper.fromUserType( + userType = userType, + socialId = socialId, + socialType = socialType, + email = email, + fcmToken = "EMPTY", + ) + val requestBody = prepareJsonRequestBody(signUpRequest) + + val response = signupRemoteDataSource.postSignUp( + profileImage = profileImage, + request = requestBody, + ) + + response.sessionId.let { sessionId -> + sessionLocalDataSource.updateSessionId(sessionId) + } + + return response.toDomain() + } + + private fun prepareJsonRequestBody(signUpRequest: SignUpRequest): RequestBody { + val jsonString = Json.encodeToString(signUpRequest) + return jsonString.toRequestBody("application/json".toMediaTypeOrNull()) + } +} diff --git a/data/repository/src/main/java/co/kr/data/repository/di/RepositoryModule.kt b/data/repository/src/main/java/co/kr/data/repository/di/RepositoryModule.kt index a41cb99..d510ece 100644 --- a/data/repository/src/main/java/co/kr/data/repository/di/RepositoryModule.kt +++ b/data/repository/src/main/java/co/kr/data/repository/di/RepositoryModule.kt @@ -1,7 +1,9 @@ package co.kr.data.repository.di import co.kr.data.repository.LoginRepositoryImpl +import co.kr.data.repository.SignUpRepositoryImpl import co.kr.tnt.domain.repository.LoginRepository +import co.kr.tnt.domain.repository.SignUpRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -14,4 +16,9 @@ internal abstract class RepositoryModule { abstract fun bindsLoginRepository( repository: LoginRepositoryImpl, ): LoginRepository + + @Binds + abstract fun bindsSignUpRepository( + repository: SignUpRepositoryImpl, + ): SignUpRepository } diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 98cc3bf..bd6cea8 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -5,5 +5,6 @@ plugins { dependencies { implementation(libs.inject) + implementation(libs.okhttp.logging) implementation(libs.coroutines.core) } diff --git a/domain/src/main/java/co/kr/tnt/domain/model/SignUpResult.kt b/domain/src/main/java/co/kr/tnt/domain/model/SignUpResult.kt new file mode 100644 index 0000000..4019e29 --- /dev/null +++ b/domain/src/main/java/co/kr/tnt/domain/model/SignUpResult.kt @@ -0,0 +1,8 @@ +package co.kr.tnt.domain.model + +data class SignUpResult( + val memberType: String, + val sessionId: String, + val name: String, + val profileImageUrl: String, +) diff --git a/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt b/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt index 60c2267..f29d9fb 100644 --- a/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt +++ b/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt @@ -19,7 +19,7 @@ sealed class UserType { override val image: String? = null, val birthday: LocalDate? = null, val age: Int? = 0, - val weight: Float = 0f, + val weight: Double = 0.0, val height: Int = 0, val ptPurpose: List = emptyList(), val caution: String? = "", diff --git a/domain/src/main/java/co/kr/tnt/domain/repository/SignUpRepository.kt b/domain/src/main/java/co/kr/tnt/domain/repository/SignUpRepository.kt new file mode 100644 index 0000000..2081c78 --- /dev/null +++ b/domain/src/main/java/co/kr/tnt/domain/repository/SignUpRepository.kt @@ -0,0 +1,15 @@ +package co.kr.tnt.domain.repository + +import co.kr.tnt.domain.model.SignUpResult +import co.kr.tnt.domain.model.UserType +import okhttp3.MultipartBody + +interface SignUpRepository { + suspend fun signUp( + profileImage: MultipartBody.Part?, + userType: UserType, + socialId: String, + socialType: String, + email: String, + ): SignUpResult +} 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 9ac73af..758b821 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 @@ -48,7 +48,7 @@ internal class TraineeConnectViewModel @Inject constructor() : name = "김회원", image = "https://buly.kr/3j7VVqN", age = 25, - weight = 100F, + weight = 100.0, height = 165, ptPurpose = listOf("체중 감량", "자세 교정"), caution = "발목이 안좋아서 발목에 무리가는 행동을 하면 안돼요. 잘 부탁드려요!", diff --git a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt index e52c8bb..3d9f4a9 100644 --- a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt +++ b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt @@ -42,7 +42,7 @@ internal class TrainerConnectViewModel @Inject constructor() : name = "김회원", image = "https://buly.kr/3j7VVqN", age = 25, - weight = 100F, + weight = 100.0, height = 165, ptPurpose = listOf("체중 감량", "자세 교정"), caution = "발목이 안좋아서 발목에 무리가는 행동을 하면 안돼요. 잘 부탁드려요!", From f942102e87009738be4afc99f0016848bbcc033d Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Tue, 28 Jan 2025 13:39:23 +0900 Subject: [PATCH 09/14] =?UTF-8?q?[TNT-114]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=84=88=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20API?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/ui/src/main/res/values/strings.xml | 2 + .../signup/TrainerSignUpCompletePage.kt | 6 +- .../trainer/signup/TrainerSignUpContract.kt | 9 ++ .../tnt/trainer/signup/TrainerSignUpScreen.kt | 21 +++- .../trainer/signup/TrainerSignUpViewModel.kt | 112 ++++++++++++------ 5 files changed, 110 insertions(+), 40 deletions(-) diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 2211686..318e8d3 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -16,6 +16,8 @@ 닫기 확인 + 서버 요청에 실패했어요 + 트레이니 트레이너 diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt index 85eafc0..b595623 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt @@ -1,5 +1,6 @@ package co.kr.tnt.trainer.signup +import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -18,6 +19,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign.Companion.Center import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import co.kr.tnt.designsystem.component.button.TnTBottomButton import co.kr.tnt.designsystem.component.image.TnTProfileImage import co.kr.tnt.designsystem.theme.TnTTheme @@ -31,7 +33,7 @@ import co.kr.tnt.core.ui.R as uiResource internal fun TrainerSignUpCompletePage( state: TrainerSignUpUiState, onBackClick: () -> Unit, - onNextClick: () -> Unit, + onNextClick: (Uri?) -> Unit, ) { BackHandler { onBackClick() } @@ -75,7 +77,7 @@ internal fun TrainerSignUpCompletePage( } TnTBottomButton( text = stringResource(uiResource.string.start), - onClick = onNextClick, + onClick = { onNextClick(state.trainerState.image?.toUri()) }, modifier = Modifier.align(Alignment.BottomCenter), ) } diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt index 34dfb81..fbde199 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt @@ -1,5 +1,6 @@ package co.kr.tnt.trainer.signup +import android.content.Context import android.net.Uri import co.kr.tnt.domain.model.UserType import co.kr.tnt.ui.base.UiEvent @@ -17,9 +18,17 @@ internal class TrainerSignUpContract { data class OnNameChange(val name: String) : TrainerSignUpUiEvent data object OnNextClick : TrainerSignUpUiEvent data object OnBackClick : TrainerSignUpUiEvent + data class RequestSignUp( + val context: Context, + val imageUri: Uri?, + val id: String, + val email: String, + val authType: String, + ) : TrainerSignUpUiEvent } sealed interface TrainerSignUpEffect : UiSideEffect { + data class ShowToast(val message: String) : TrainerSignUpEffect data object NavigateToBack : TrainerSignUpEffect data object NavigateToConnect : TrainerSignUpEffect } diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt index 5c1b5c6..0aecbd5 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpScreen.kt @@ -1,9 +1,11 @@ package co.kr.tnt.trainer.signup import android.net.Uri +import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiEvent @@ -18,6 +20,7 @@ internal fun TrainerSignUpRoute( navigateToConnect: () -> Unit, viewModel: TrainerSignUpViewModel = hiltViewModel(), ) { + val context = LocalContext.current val uiState by viewModel.uiState.collectAsStateWithLifecycle() TrainerSignUpScreen( @@ -26,6 +29,17 @@ internal fun TrainerSignUpRoute( onProfileImageSelect = { viewModel.setEvent(TrainerSignUpUiEvent.OnImageChange(it)) }, onNextClick = { viewModel.setEvent(TrainerSignUpUiEvent.OnNextClick) }, onBackClick = { viewModel.setEvent(TrainerSignUpUiEvent.OnBackClick) }, + onSubmitSignUp = { uri -> + viewModel.setEvent( + TrainerSignUpUiEvent.RequestSignUp( + context = context, + imageUri = uri, + id = authId, + email = email, + authType = authType, + ), + ) + }, ) LaunchedEffect(viewModel.effect) { @@ -33,6 +47,9 @@ internal fun TrainerSignUpRoute( when (effect) { TrainerSignUpContract.TrainerSignUpEffect.NavigateToBack -> navigateToPrevious() TrainerSignUpContract.TrainerSignUpEffect.NavigateToConnect -> navigateToConnect() + is TrainerSignUpContract.TrainerSignUpEffect.ShowToast -> { + Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() + } } } } @@ -43,6 +60,7 @@ private fun TrainerSignUpScreen( state: TrainerSignUpUiState, onProfileImageSelect: (Uri) -> Unit, onNameChange: (String) -> Unit, + onSubmitSignUp: (Uri?) -> Unit, onNextClick: () -> Unit, onBackClick: () -> Unit, ) { @@ -54,9 +72,10 @@ private fun TrainerSignUpScreen( onNextClick = onNextClick, onBackClick = onBackClick, ) + TrainerSignUpContract.TrainerSignUpPage.SignUpComplete -> TrainerSignUpCompletePage( state = state, - onNextClick = onNextClick, + onNextClick = onSubmitSignUp, onBackClick = onBackClick, ) } diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt index 620234e..5ff096e 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt @@ -1,65 +1,103 @@ package co.kr.tnt.trainer.signup +import android.content.Context import android.net.Uri +import androidx.lifecycle.viewModelScope +import co.kr.tnt.domain.repository.SignUpRepository import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpEffect import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpPage import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiEvent import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiState import co.kr.tnt.ui.base.BaseViewModel +import co.kr.tnt.ui.util.toMultiPart import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import javax.inject.Inject +import co.kr.tnt.core.ui.R as uiResource @HiltViewModel -internal class TrainerSignUpViewModel @Inject constructor() : - BaseViewModel( +internal class TrainerSignUpViewModel @Inject constructor( + private val signUpRepository: SignUpRepository, +) : BaseViewModel( TrainerSignUpUiState(), ) { - override suspend fun handleEvent(event: TrainerSignUpUiEvent) { - when (event) { - is TrainerSignUpUiEvent.OnImageChange -> setProfileImage(event.imageUri) - is TrainerSignUpUiEvent.OnNameChange -> setName(event.name) - TrainerSignUpUiEvent.OnNextClick -> navigateToNext() - TrainerSignUpUiEvent.OnBackClick -> navigateToBack() - } - } + private var signupResult: Boolean? = null - private fun setProfileImage(imageUri: Uri) { - updateState { - copy( - trainerState = trainerState.copy(image = imageUri.toString()), - ) - } + override suspend fun handleEvent(event: TrainerSignUpUiEvent) { + when (event) { + is TrainerSignUpUiEvent.OnImageChange -> setProfileImage(event.imageUri) + is TrainerSignUpUiEvent.OnNameChange -> setName(event.name) + TrainerSignUpUiEvent.OnNextClick -> navigateToNext() + TrainerSignUpUiEvent.OnBackClick -> navigateToBack() + is TrainerSignUpUiEvent.RequestSignUp -> signUp( + context = event.context, + imageUri = event.imageUri, + id = event.id, + email = event.email, + authType = event.authType, + ) } + } + + private fun signUp( + context: Context, + imageUri: Uri?, + id: String, + email: String, + authType: String, + ) { + viewModelScope.launch { + val profileImagePart = imageUri?.toMultiPart(context) - private fun setName(name: String) { - updateState { - copy( - trainerState = trainerState.copy(name = name), + runCatching { + signUpRepository.signUp( + profileImage = profileImagePart, + userType = currentState.trainerState, + socialId = id, + socialType = authType, + email = email, ) + }.onSuccess { + signupResult = true + sendEffect(TrainerSignUpEffect.NavigateToConnect) + }.onFailure { + // TODO 디자인 시스템 Toast 적용 + val message = context.getString(uiResource.string.error_server_request_failed) + sendEffect(TrainerSignUpEffect.ShowToast(message)) } } + } - private fun navigateToNext() { - val nextPage = when (currentState.page) { - TrainerSignUpPage.SignUpComplete -> { - sendEffect(TrainerSignUpEffect.NavigateToConnect) - return - } + private fun setProfileImage(imageUri: Uri) { + updateState { + copy( + trainerState = trainerState.copy(image = imageUri.toString()), + ) + } + } - else -> TrainerSignUpPage.getNextPage(currentState.page) - } - updateState { copy(page = nextPage) } + private fun setName(name: String) { + updateState { + copy( + trainerState = trainerState.copy(name = name), + ) } + } - private fun navigateToBack() { - val previousPage = when (currentState.page) { - TrainerSignUpPage.ProfileSetUp -> { - sendEffect(TrainerSignUpEffect.NavigateToBack) - return - } + private fun navigateToNext() { + val nextPage = TrainerSignUpPage.getNextPage(currentState.page) + updateState { copy(page = nextPage) } + } - else -> TrainerSignUpPage.getPreviousPage(currentState.page) + private fun navigateToBack() { + val previousPage = when (currentState.page) { + TrainerSignUpPage.ProfileSetUp -> { + sendEffect(TrainerSignUpEffect.NavigateToBack) + return } - updateState { copy(page = previousPage) } + + else -> TrainerSignUpPage.getPreviousPage(currentState.page) } + updateState { copy(page = previousPage) } } +} From e6a14b9e2c7aa672835a1c7b12e092a0bdbfee6b Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Tue, 28 Jan 2025 15:17:38 +0900 Subject: [PATCH 10/14] =?UTF-8?q?[TNT-114]=20refactor:=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=A3=BC=EC=9D=98=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=9E=85=EB=A0=A5=20=EC=97=90=EB=9F=AC=20=EB=AC=B8?= =?UTF-8?q?=EA=B5=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../co/kr/tnt/trainee/signup/TraineeNoteForTrainerPage.kt | 4 ++-- feature/trainee/signup/src/main/res/values/strings.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 d8efcc4..58d70aa 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 @@ -11,8 +11,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -64,6 +62,8 @@ internal fun TraineeNoteForTrainerPage( } }, modifier = Modifier.padding(horizontal = 20.dp), + isError = (state.traineeState.caution?.length ?: 0) == MAX_LENGTH, + warningMessage = stringResource(R.string.text_length_overflow), maxLength = 100, ) } diff --git a/feature/trainee/signup/src/main/res/values/strings.xml b/feature/trainee/signup/src/main/res/values/strings.xml index dc06f29..c81f3af 100644 --- a/feature/trainee/signup/src/main/res/values/strings.xml +++ b/feature/trainee/signup/src/main/res/values/strings.xml @@ -22,7 +22,7 @@ 트레이너가 꼭 알아야 할\n주의사항이 있나요? 트레이너에게 알려드릴게요. - 글자 수를 초과했어요 + 100자 미만으로 입력해주세요 만나서 반가워요\n%s 트레이니님! 트레이너와 함께\n케미를 터뜨려보세요! 🧨 From b9d0ba979184a59d70f283c2771217b56ce51992 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Tue, 28 Jan 2025 15:26:09 +0900 Subject: [PATCH 11/14] =?UTF-8?q?[TNT-114]=20feat:=20=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EB=84=88=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20API?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 생일 String으로 변경 - 주의사항 ""이면 null로 바꿔 전송 --- .../co/kr/data/network/model/SignUpRequest.kt | 9 +- .../signup/TraineeSignUpCompletePage.kt | 6 +- .../trainee/signup/TraineeSignUpContract.kt | 9 + .../tnt/trainee/signup/TraineeSignUpScreen.kt | 20 +- .../trainee/signup/TraineeSignUpViewModel.kt | 271 ++++++++++-------- .../trainer/signup/TrainerSignUpViewModel.kt | 5 +- 6 files changed, 190 insertions(+), 130 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 f630b05..efe0070 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 @@ -1,9 +1,7 @@ package co.kr.data.network.model import co.kr.tnt.domain.model.UserType -import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable -import java.time.LocalDate @Serializable data class SignUpRequest( @@ -16,8 +14,7 @@ data class SignUpRequest( val socialEmail: String, val memberType: String, val name: String, - @Contextual - val birthday: LocalDate? = null, + val birthday: String? = null, val height: Double? = null, val weight: Double? = null, val goalContents: List? = null, @@ -53,11 +50,11 @@ object SignUpRequestMapper { is UserType.Trainee -> SignUpRequest( memberType = "trainee", name = userType.name, - birthday = userType.birthday, + birthday = userType.birthday?.toString(), height = userType.height.toDouble(), weight = userType.weight, goalContents = userType.ptPurpose, - cautionNote = userType.caution, + cautionNote = userType.caution?.ifBlank { null }, socialType = socialType, socialId = socialId, socialEmail = email, diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt index 4262dda..8269027 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt @@ -1,5 +1,6 @@ package co.kr.tnt.trainee.signup +import android.net.Uri import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.foundation.background @@ -19,6 +20,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign.Companion.Center import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import co.kr.tnt.designsystem.component.button.TnTBottomButton import co.kr.tnt.designsystem.component.image.TnTProfileImage import co.kr.tnt.designsystem.theme.TnTTheme @@ -32,7 +34,7 @@ import co.kr.tnt.core.ui.R as uiResource internal fun TraineeSignUpCompletePage( state: TraineeSignUpUiState, onBackClick: () -> Unit, - onNextClick: () -> Unit, + onNextClick: (Uri?) -> Unit, ) { BackHandler { onBackClick() } @@ -78,7 +80,7 @@ internal fun TraineeSignUpCompletePage( } TnTBottomButton( text = stringResource(uiResource.string.start), - onClick = onNextClick, + onClick = { onNextClick(state.traineeState.image?.toUri()) }, modifier = Modifier.align(Alignment.BottomCenter), ) } 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 229d39e..4e8328d 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 @@ -1,5 +1,6 @@ package co.kr.tnt.trainee.signup +import android.content.Context import android.net.Uri import co.kr.tnt.domain.model.UserType import co.kr.tnt.ui.base.UiEvent @@ -29,9 +30,17 @@ internal class TraineeSignUpContract { data class OnCautionChange(val text: String) : TraineeSignUpUiEvent data object OnNextClick : TraineeSignUpUiEvent data object OnBackClick : TraineeSignUpUiEvent + data class RequestSignUp( + val context: Context, + val imageUri: Uri?, + val id: String, + val email: String, + val authType: String, + ) : TraineeSignUpUiEvent } sealed interface TraineeSignUpEffect : UiSideEffect { + data class ShowToast(val message: String) : TraineeSignUpEffect data object NavigateToBack : TraineeSignUpEffect data object NavigateToConnect : TraineeSignUpEffect } 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 55d91b7..acaf1d3 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 @@ -1,9 +1,11 @@ package co.kr.tnt.trainee.signup import android.net.Uri +import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpEffect @@ -21,6 +23,7 @@ internal fun TraineeSignUpRoute( navigateToConnect: () -> Unit, viewModel: TraineeSignUpViewModel = hiltViewModel(), ) { + val context = LocalContext.current val uiState by viewModel.uiState.collectAsStateWithLifecycle() TraineeSignUpScreen( @@ -34,6 +37,17 @@ internal fun TraineeSignUpRoute( onCautionChange = { viewModel.setEvent(TraineeSignUpUiEvent.OnCautionChange(it)) }, onBackClick = { viewModel.setEvent(TraineeSignUpUiEvent.OnBackClick) }, onNextClick = { viewModel.setEvent(TraineeSignUpUiEvent.OnNextClick) }, + onSubmitSignUp = { uri -> + viewModel.setEvent( + TraineeSignUpUiEvent.RequestSignUp( + context = context, + imageUri = uri, + id = authId, + email = email, + authType = authType, + ), + ) + }, ) LaunchedEffect(viewModel.effect) { @@ -41,6 +55,9 @@ internal fun TraineeSignUpRoute( when (effect) { TraineeSignUpEffect.NavigateToBack -> navigateToPrevious() TraineeSignUpEffect.NavigateToConnect -> navigateToConnect() + is TraineeSignUpEffect.ShowToast -> { + Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() + } } } } @@ -56,6 +73,7 @@ private fun TraineeSignUpScreen( onCautionChange: (String) -> Unit, onBirthdayChange: (LocalDate) -> Unit, onPurposeSelected: (String) -> Unit, + onSubmitSignUp: (Uri?) -> Unit, onNextClick: () -> Unit, onBackClick: () -> Unit, ) { @@ -94,7 +112,7 @@ private fun TraineeSignUpScreen( TraineeSignUpPage.SignUpComplete -> TraineeSignUpCompletePage( state = state, onBackClick = onBackClick, - onNextClick = onNextClick, + onNextClick = onSubmitSignUp, ) } } diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt index 70298d7..4f7ac4f 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt @@ -1,12 +1,18 @@ package co.kr.tnt.trainee.signup +import android.content.Context import android.net.Uri +import androidx.lifecycle.viewModelScope +import co.kr.tnt.core.ui.R +import co.kr.tnt.domain.repository.SignUpRepository import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpEffect import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpPage import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiEvent import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState import co.kr.tnt.ui.base.BaseViewModel +import co.kr.tnt.ui.util.toMultiPart import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import java.time.LocalDate import javax.inject.Inject @@ -14,152 +20,183 @@ private const val MAX_HEIGHT_LENGTH = 3 private const val MAX_WEIGHT_LENGTH = 5 @HiltViewModel -internal class TraineeSignUpViewModel @Inject constructor() : - BaseViewModel( +internal class TraineeSignUpViewModel @Inject constructor( + private val signUpRepository: SignUpRepository, +) : BaseViewModel( TraineeSignUpUiState(), ) { - override suspend fun handleEvent(event: TraineeSignUpUiEvent) { - when (event) { - is TraineeSignUpUiEvent.OnImageChange -> updateProfileImage(event.imageUri) - is TraineeSignUpUiEvent.OnNameChange -> updateName(event.name) - is TraineeSignUpUiEvent.OnHeightChange -> updateHeight(event.height) - is TraineeSignUpUiEvent.OnWeightChange -> updateWeight(event.weight) - is TraineeSignUpUiEvent.OnBirthdayChange -> updateBirthday(event.birthday) - TraineeSignUpUiEvent.OnBasicFormUpdate -> setBasicFormDate() - is TraineeSignUpUiEvent.OnPurposeSelected -> updateSelectedPurposes(event.purpose) - is TraineeSignUpUiEvent.OnCautionChange -> updateCaution(event.text) - TraineeSignUpUiEvent.OnBackClick -> navigateToBack() - TraineeSignUpUiEvent.OnNextClick -> navigateToNext() - } + override suspend fun handleEvent(event: TraineeSignUpUiEvent) { + when (event) { + is TraineeSignUpUiEvent.OnImageChange -> updateProfileImage(event.imageUri) + is TraineeSignUpUiEvent.OnNameChange -> updateName(event.name) + is TraineeSignUpUiEvent.OnHeightChange -> updateHeight(event.height) + is TraineeSignUpUiEvent.OnWeightChange -> updateWeight(event.weight) + is TraineeSignUpUiEvent.OnBirthdayChange -> updateBirthday(event.birthday) + TraineeSignUpUiEvent.OnBasicFormUpdate -> setBasicFormDate() + is TraineeSignUpUiEvent.OnPurposeSelected -> updateSelectedPurposes(event.purpose) + is TraineeSignUpUiEvent.OnCautionChange -> updateCaution(event.text) + TraineeSignUpUiEvent.OnBackClick -> navigateToBack() + TraineeSignUpUiEvent.OnNextClick -> navigateToNext() + is TraineeSignUpUiEvent.RequestSignUp -> signUp( + context = event.context, + imageUri = event.imageUri, + id = event.id, + email = event.email, + authType = event.authType, + ) } + } - private fun updateProfileImage(imageUri: Uri) { - updateState { - copy( - traineeState = traineeState.copy(image = imageUri.toString()), + private fun signUp( + context: Context, + imageUri: Uri?, + id: String, + email: String, + authType: String, + ) { + viewModelScope.launch { + val profileImagePart = imageUri?.toMultiPart(context, "profileImage") + + runCatching { + signUpRepository.signUp( + profileImage = profileImagePart, + userType = currentState.traineeState, + socialId = id, + socialType = authType, + email = email, ) + }.onSuccess { + sendEffect(TraineeSignUpEffect.NavigateToConnect) + }.onFailure { + // TODO 디자인 시스템 Toast 적용 + val message = context.getString(R.string.error_server_request_failed) + sendEffect(TraineeSignUpEffect.ShowToast(message)) } } + } - private fun updateName(name: String) { - updateState { - copy( - traineeState = traineeState.copy(name = name), - ) - } + private fun updateProfileImage(imageUri: Uri) { + updateState { + copy( + traineeState = traineeState.copy(image = imageUri.toString()), + ) } + } - private fun updateHeight(height: String) { - val isValid = validateHeight(height) - updateState { - val isFormValid = isValid && isWeightValid - copy( - height = height, - isHeightValid = isValid, - isBasicInfoValid = isFormValid, - ) - } + private fun updateName(name: String) { + updateState { + copy( + traineeState = traineeState.copy(name = name), + ) } + } - private fun updateWeight(weight: String) { - val isValid = validateWeight(weight) - updateState { - val isFormValid = isHeightValid && isValid - copy( - weight = weight, - isWeightValid = isValid, - isBasicInfoValid = isFormValid, - ) - } + private fun updateHeight(height: String) { + val isValid = validateHeight(height) + updateState { + val isFormValid = isValid && isWeightValid + copy( + height = height, + isHeightValid = isValid, + isBasicInfoValid = isFormValid, + ) } + } - private fun updateBirthday(birthday: LocalDate) { - updateState { - copy( - traineeState = traineeState.copy(birthday = birthday), - ) - } + private fun updateWeight(weight: String) { + val isValid = validateWeight(weight) + updateState { + val isFormValid = isHeightValid && isValid + copy( + weight = weight, + isWeightValid = isValid, + isBasicInfoValid = isFormValid, + ) } + } - private fun setBasicFormDate() { - updateState { - copy( - traineeState = traineeState.copy( - weight = weight.toFloat(), - height = height.toInt(), - ), - ) - } + private fun updateBirthday(birthday: LocalDate) { + updateState { + copy( + traineeState = traineeState.copy(birthday = birthday), + ) } + } - /** - * 키가 유효한 입력값인지 검사 - * 형식: 정수 3자 - */ - private fun validateHeight(input: String): Boolean { - return input.isNotEmpty() && input.toIntOrNull() != null && - !input.startsWith("0") && input.length <= MAX_HEIGHT_LENGTH + private fun setBasicFormDate() { + updateState { + copy( + traineeState = traineeState.copy( + weight = weight.toDouble(), + height = height.toInt(), + ), + ) } + } - /** - * 몸무게가 유효한 입력값인지 검사 - * 형식: 5자 이하의 실수 (000, 00, 00.0, 000.0) - */ - private fun validateWeight(input: String): Boolean { - val weightRegex = Regex("^(\\d{1,3}(\\.\\d)?)?\$") - return input.isNotEmpty() && input.matches(weightRegex) && - !input.startsWith("0") && input.length <= MAX_WEIGHT_LENGTH - } + /** + * 키가 유효한 입력값인지 검사 + * 형식: 정수 3자 + */ + private fun validateHeight(input: String): Boolean { + return input.isNotEmpty() && input.toIntOrNull() != null && + !input.startsWith("0") && input.length <= MAX_HEIGHT_LENGTH + } - private fun updateSelectedPurposes(purpose: String) { - val updatedPurposes = currentState.traineeState.ptPurpose.toMutableList().apply { - if (contains(purpose)) { - remove(purpose) - } else { - add(purpose) - } - } - updateState { - copy( - traineeState = traineeState.copy(ptPurpose = updatedPurposes), - ) - } - } + /** + * 몸무게가 유효한 입력값인지 검사 + * 형식: 5자 이하의 실수 (000, 00, 00.0, 000.0) + */ + private fun validateWeight(input: String): Boolean { + val weightRegex = Regex("^(\\d{1,3}(\\.\\d)?)?\$") + return input.isNotEmpty() && input.matches(weightRegex) && + !input.startsWith("0") && input.length <= MAX_WEIGHT_LENGTH + } - private fun updateCaution(text: String) { - updateState { - copy( - traineeState = traineeState.copy(caution = text), - ) + private fun updateSelectedPurposes(purpose: String) { + val updatedPurposes = currentState.traineeState.ptPurpose.toMutableList().apply { + if (contains(purpose)) { + remove(purpose) + } else { + add(purpose) } } + updateState { + copy( + traineeState = traineeState.copy(ptPurpose = updatedPurposes), + ) + } + } - private fun navigateToNext() { - val nextPage = when (currentState.page) { - TraineeSignUpPage.SignUpComplete -> { - sendEffect(TraineeSignUpEffect.NavigateToConnect) - return - } - - TraineeSignUpPage.BasicInfo -> { - setBasicFormDate() - TraineeSignUpPage.getNextPage(currentState.page) - } + private fun updateCaution(text: String) { + updateState { + copy( + traineeState = traineeState.copy(caution = text), + ) + } + } - else -> TraineeSignUpPage.getNextPage(currentState.page) + private fun navigateToNext() { + val nextPage = when (currentState.page) { + TraineeSignUpPage.BasicInfo -> { + setBasicFormDate() + TraineeSignUpPage.getNextPage(currentState.page) } - updateState { copy(page = nextPage) } - } - private fun navigateToBack() { - val previousPage = when (currentState.page) { - TraineeSignUpPage.ProfileSetUp -> { - sendEffect(TraineeSignUpEffect.NavigateToBack) - return - } + else -> TraineeSignUpPage.getNextPage(currentState.page) + } + updateState { copy(page = nextPage) } + } - else -> TraineeSignUpPage.getPreviousPage(currentState.page) + private fun navigateToBack() { + val previousPage = when (currentState.page) { + TraineeSignUpPage.ProfileSetUp -> { + sendEffect(TraineeSignUpEffect.NavigateToBack) + return } - updateState { copy(page = previousPage) } + + else -> TraineeSignUpPage.getPreviousPage(currentState.page) } + updateState { copy(page = previousPage) } } +} diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt index 5ff096e..ba19d4f 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt @@ -21,8 +21,6 @@ internal class TrainerSignUpViewModel @Inject constructor( ) : BaseViewModel( TrainerSignUpUiState(), ) { - private var signupResult: Boolean? = null - override suspend fun handleEvent(event: TrainerSignUpUiEvent) { when (event) { is TrainerSignUpUiEvent.OnImageChange -> setProfileImage(event.imageUri) @@ -47,7 +45,7 @@ internal class TrainerSignUpViewModel @Inject constructor( authType: String, ) { viewModelScope.launch { - val profileImagePart = imageUri?.toMultiPart(context) + val profileImagePart = imageUri?.toMultiPart(context, "profileImage") runCatching { signUpRepository.signUp( @@ -58,7 +56,6 @@ internal class TrainerSignUpViewModel @Inject constructor( email = email, ) }.onSuccess { - signupResult = true sendEffect(TrainerSignUpEffect.NavigateToConnect) }.onFailure { // TODO 디자인 시스템 Toast 적용 From f3263f6e5fb4b8c6f077b3031ec02e2a1e55842e Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Wed, 29 Jan 2025 02:06:07 +0900 Subject: [PATCH 12/14] =?UTF-8?q?[TNT-114]=20fix:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20File=EC=9D=84=20=EB=84=98=EA=B2=A8=EC=A3=BC?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20state=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 - 도메인이 네트워크 무관해야 하기 때문 - UserType을 UI 데이터 저장 용도로 쓰지 않게 수정 --- .../main/java/co/kr/tnt/ui/util/FileUtils.kt | 11 +- .../data/repository/SignUpRepositoryImpl.kt | 11 +- domain/build.gradle.kts | 1 - .../java/co/kr/tnt/domain/model/UserType.kt | 24 ++-- .../tnt/domain/repository/SignUpRepository.kt | 4 +- .../kr/tnt/roleselect/RoleSelectionScreen.kt | 12 +- .../trainee/connect/TraineeConnectContract.kt | 18 ++- .../connect/TraineeConnectViewModel.kt | 1 + .../trainee/signup/TraineeBasicInfoPage.kt | 11 +- .../signup/TraineeNoteForTrainerPage.kt | 9 +- .../trainee/signup/TraineePTPurposePage.kt | 8 +- .../trainee/signup/TraineeProfileSetupPage.kt | 20 +--- .../signup/TraineeSignUpCompletePage.kt | 12 +- .../trainee/signup/TraineeSignUpContract.kt | 39 ++++-- .../tnt/trainee/signup/TraineeSignUpScreen.kt | 2 +- .../trainee/signup/TraineeSignUpViewModel.kt | 113 ++++-------------- .../trainer/connect/TrainerConnectContract.kt | 18 ++- .../connect/TrainerConnectViewModel.kt | 1 + .../trainer/signup/TrainerProfileSetupPage.kt | 14 +-- .../signup/TrainerSignUpCompletePage.kt | 9 +- .../trainer/signup/TrainerSignUpContract.kt | 4 +- .../trainer/signup/TrainerSignUpViewModel.kt | 23 ++-- 22 files changed, 173 insertions(+), 192 deletions(-) diff --git a/core/ui/src/main/java/co/kr/tnt/ui/util/FileUtils.kt b/core/ui/src/main/java/co/kr/tnt/ui/util/FileUtils.kt index 9682700..3fdab8b 100644 --- a/core/ui/src/main/java/co/kr/tnt/ui/util/FileUtils.kt +++ b/core/ui/src/main/java/co/kr/tnt/ui/util/FileUtils.kt @@ -5,18 +5,13 @@ import android.database.Cursor import android.net.Uri import android.provider.MediaStore import android.util.Log -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.MultipartBody -import okhttp3.RequestBody.Companion.asRequestBody import java.io.File -fun Uri.toMultiPart(context: Context, keyName: String = "image"): MultipartBody.Part? { +fun Uri.toFile(context: Context): File? { return getRealPathFromUri(this, context)?.let { filePath -> - val file = File(filePath) - val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) - MultipartBody.Part.createFormData(keyName, file.name, requestFile) + File(filePath) } ?: run { - Log.e("toMultipartImagePart", "Error creating multipart image for URI: $this") + Log.e("toFile", "Error creating file for URI: $this") null } } diff --git a/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt b/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt index 8785783..d5e07b9 100644 --- a/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt +++ b/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt @@ -13,7 +13,9 @@ 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 import javax.inject.Inject class SignUpRepositoryImpl @Inject constructor( @@ -21,12 +23,17 @@ class SignUpRepositoryImpl @Inject constructor( private val sessionLocalDataSource: SessionLocalDataSource, ) : SignUpRepository { override suspend fun signUp( - profileImage: MultipartBody.Part?, + profileImage: File?, userType: UserType, socialId: String, socialType: String, email: String, ): SignUpResult { + val profileImagePart = profileImage?.let { + val requestFile = it.asRequestBody("image/*".toMediaTypeOrNull()) + MultipartBody.Part.createFormData("profileImage", it.name, requestFile) + } + // TODO FCM token val signUpRequest = SignUpRequestMapper.fromUserType( userType = userType, @@ -38,7 +45,7 @@ class SignUpRepositoryImpl @Inject constructor( val requestBody = prepareJsonRequestBody(signUpRequest) val response = signupRemoteDataSource.postSignUp( - profileImage = profileImage, + profileImage = profileImagePart, request = requestBody, ) diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index bd6cea8..98cc3bf 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -5,6 +5,5 @@ plugins { dependencies { implementation(libs.inject) - implementation(libs.okhttp.logging) implementation(libs.coroutines.core) } diff --git a/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt b/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt index f29d9fb..989a556 100644 --- a/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt +++ b/domain/src/main/java/co/kr/tnt/domain/model/UserType.kt @@ -8,20 +8,20 @@ sealed class UserType { abstract val image: String? data class Trainer( - override val id: String = "", - override val name: String = "", - override val image: String? = null, + override val id: String, + override val name: String, + override val image: String?, ) : UserType() data class Trainee( - override val id: String = "", - override val name: String = "", - override val image: String? = null, - val birthday: LocalDate? = null, - val age: Int? = 0, - val weight: Double = 0.0, - val height: Int = 0, - val ptPurpose: List = emptyList(), - val caution: String? = "", + override val id: String, + override val name: String, + override val image: String?, + val birthday: LocalDate?, + val age: Int?, + val weight: Double, + val height: Int, + val ptPurpose: List, + val caution: String?, ) : UserType() } diff --git a/domain/src/main/java/co/kr/tnt/domain/repository/SignUpRepository.kt b/domain/src/main/java/co/kr/tnt/domain/repository/SignUpRepository.kt index 2081c78..07494d9 100644 --- a/domain/src/main/java/co/kr/tnt/domain/repository/SignUpRepository.kt +++ b/domain/src/main/java/co/kr/tnt/domain/repository/SignUpRepository.kt @@ -2,11 +2,11 @@ package co.kr.tnt.domain.repository import co.kr.tnt.domain.model.SignUpResult import co.kr.tnt.domain.model.UserType -import okhttp3.MultipartBody +import java.io.File interface SignUpRepository { suspend fun signUp( - profileImage: MultipartBody.Part?, + profileImage: File?, userType: UserType, socialId: String, socialType: String, diff --git a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionScreen.kt b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionScreen.kt index 8a9dab2..1225319 100644 --- a/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionScreen.kt +++ b/feature/roleselect/src/main/java/co/kr/tnt/roleselect/RoleSelectionScreen.kt @@ -59,7 +59,17 @@ internal fun RoleSelectionRoute( fun RoleSelectionScreen( onNextClick: (RoleState) -> Unit = {}, ) { - var selectedRole by remember { mutableStateOf(RoleState.fromDomain(UserType.Trainer())) } + var selectedRole by remember { + mutableStateOf( + RoleState.fromDomain( + UserType.Trainer( + id = "", + name = "", + image = "TODO()", + ), + ), + ) + } Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Column( diff --git a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectContract.kt b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectContract.kt index f22ea23..6e0e724 100644 --- a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectContract.kt +++ b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectContract.kt @@ -16,8 +16,22 @@ internal class TraineeConnectContract { val completedSession: Int = 0, val totalSession: Int = 0, val selectedStartDate: LocalDate = LocalDate.now(), - val trainerState: UserType.Trainer = UserType.Trainer(), - val traineeState: UserType.Trainee = UserType.Trainee(), + val trainerState: UserType.Trainer = UserType.Trainer( + id = "", + name = "", + image = null, + ), + val traineeState: UserType.Trainee = UserType.Trainee( + id = "", + name = "", + image = null, + birthday = null, + age = 0, + weight = 0.0, + height = 0, + ptPurpose = emptyList(), + caution = null, + ), ) : UiState sealed interface TraineeConnectUiEvent : UiEvent { 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 758b821..5cfba12 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 @@ -47,6 +47,7 @@ internal class TraineeConnectViewModel @Inject constructor() : id = "trainee", name = "김회원", image = "https://buly.kr/3j7VVqN", + birthday = null, age = 25, weight = 100.0, height = 165, diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeBasicInfoPage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeBasicInfoPage.kt index 736f5dc..c536da0 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeBasicInfoPage.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeBasicInfoPage.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -79,8 +78,8 @@ internal fun TraineeBasicInfoPage( BirthdayPicker( modifier = Modifier.padding(horizontal = 20.dp), today = today, - selectedDate = state.traineeState.birthday, - onDateSelected = { onBirthdayChange(it) }, + selectedDate = state.birthday, + onDateSelected = onBirthdayChange, ) HorizontalDivider( thickness = 1.dp, @@ -106,7 +105,7 @@ internal fun TraineeBasicInfoPage( trailingComponent = { UnitLabel(uiResource.string.height_unit) }, - onValueChange = { onHeightChange(it) }, + onValueChange = onHeightChange, modifier = Modifier.weight(1f), ) TnTLabeledTextField( @@ -121,7 +120,7 @@ internal fun TraineeBasicInfoPage( trailingComponent = { UnitLabel(uiResource.string.weight_unit) }, - onValueChange = { onWeightChange(it) }, + onValueChange = onWeightChange, modifier = Modifier.weight(1f), ) } @@ -201,9 +200,9 @@ private fun UnitLabel(stringResId: Int) { private fun TraineeBasicInfoPagePreview() { TnTTheme { TraineeBasicInfoPage( + state = TraineeSignUpUiState(), onBackClick = {}, onNextClick = {}, - state = TODO(), onHeightChange = {}, onWeightChange = {}, onBirthdayChange = {}, 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 58d70aa..72cfa3e 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 @@ -21,7 +21,6 @@ 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.feature.trainee.signup.R -import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState import co.kr.tnt.trainee.signup.component.ProgressSteps import co.kr.tnt.core.ui.R as uiResource @@ -29,7 +28,7 @@ private const val MAX_LENGTH = 100 @Composable internal fun TraineeNoteForTrainerPage( - state: TraineeSignUpUiState, + caution: String?, onCautionChange: (String) -> Unit, onBackClick: () -> Unit, onNextClick: () -> Unit, @@ -55,14 +54,14 @@ internal fun TraineeNoteForTrainerPage( ) Spacer(Modifier.padding(top = 48.dp)) TnTOutlinedTextField( - value = state.traineeState.caution.toString(), + value = caution ?: "", onValueChange = { newValue -> if (newValue.length <= MAX_LENGTH) { onCautionChange(newValue) } }, modifier = Modifier.padding(horizontal = 20.dp), - isError = (state.traineeState.caution?.length ?: 0) == MAX_LENGTH, + isError = (caution?.length ?: 0) == MAX_LENGTH, warningMessage = stringResource(R.string.text_length_overflow), maxLength = 100, ) @@ -81,9 +80,9 @@ internal fun TraineeNoteForTrainerPage( private fun TraineeNoteForTrainerPagePreview() { TnTTheme { TraineeNoteForTrainerPage( + caution = "", onBackClick = {}, onNextClick = {}, - state = TODO(), onCautionChange = {}, ) } 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 3fb0ddf..e9612cc 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 @@ -12,8 +12,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -69,7 +67,7 @@ internal fun TraineePTPurposePage( val purposeText = stringResource(purpose.textResId) PurposeButton( text = purposeText, - isSelected = purposeText in state.traineeState.ptPurpose, + isSelected = purposeText in state.ptPurpose, onClick = { onPurposeSelected(purposeText) }, modifier = Modifier.weight(1f), ) @@ -80,7 +78,7 @@ internal fun TraineePTPurposePage( TnTBottomButton( text = stringResource(uiResource.string.next), onClick = onNextClick, - enabled = state.traineeState.ptPurpose.isNotEmpty(), + enabled = state.ptPurpose.isNotEmpty(), modifier = Modifier.align(Alignment.BottomCenter), ) } @@ -108,7 +106,7 @@ fun PurposeButton( private fun TraineePTPurposePagePreview() { TnTTheme { TraineePTPurposePage( - state = TODO(), + state = TraineeSignUpUiState(), onBackClick = {}, onNextClick = {}, onPurposeSelected = {}, diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeProfileSetupPage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeProfileSetupPage.kt index 72f85a2..4d4c209 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeProfileSetupPage.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeProfileSetupPage.kt @@ -16,9 +16,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -54,11 +51,6 @@ internal fun TraineeProfileSetupPage( BackHandler { onBackClick() } val context = LocalContext.current - - val isWarning by remember(state.traineeState.name) { - derivedStateOf { state.traineeState.name.length > MAX_LENGTH } - } - val pickMediaLauncher = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> if (uri != null) { onProfileImageSelect(uri) @@ -66,7 +58,7 @@ internal fun TraineeProfileSetupPage( } val painter = rememberAsyncImagePainter( model = ImageRequest.Builder(context) - .data(state.traineeState.image) + .data(state.image) .transformations(ResizeTransformation(IMAGE_MAX_SIZE)) .build(), ) @@ -93,7 +85,7 @@ internal fun TraineeProfileSetupPage( .fillMaxWidth() .padding(vertical = 12.dp), defaultImage = painterResource(DefaultUserProfile.Trainee.image), - image = state.traineeState.image?.let { painter }, + image = state.image?.let { painter }, onEditClick = { pickMediaLauncher.launch( PickVisualMediaRequest( @@ -105,7 +97,7 @@ internal fun TraineeProfileSetupPage( Spacer(Modifier.padding(top = 60.dp)) TnTLabeledTextFieldWithCounter( title = stringResource(uiResource.string.name), - value = state.traineeState.name, + value = state.name, onValueChange = { newValue -> val filteredText = validateInput(newValue) onNameChange(filteredText) @@ -114,14 +106,14 @@ internal fun TraineeProfileSetupPage( placeholder = stringResource(R.string.enter_your_name), maxLength = MAX_LENGTH, isSingleLine = true, - showWarning = isWarning, + showWarning = !state.isNameValid, isRequired = true, warningMessage = stringResource(R.string.text_length_warning, MAX_LENGTH), ) } TnTBottomButton( text = stringResource(uiResource.string.next), - enabled = state.traineeState.name.isNotBlank() && !isWarning, + enabled = state.name.isNotBlank() && state.isNameValid, onClick = onNextClick, modifier = Modifier.align(Alignment.BottomCenter), ) @@ -141,9 +133,9 @@ private fun validateInput(input: String): String { private fun TraineeProfileSetupPagePreview() { TnTTheme { TraineeProfileSetupPage( + state = TraineeSignUpUiState(), onBackClick = {}, onNextClick = {}, - state = TODO(), onProfileImageSelect = {}, onNameChange = {}, ) diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt index 8269027..a753361 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpCompletePage.kt @@ -1,7 +1,6 @@ package co.kr.tnt.trainee.signup import android.net.Uri -import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -20,7 +19,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign.Companion.Center import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import co.kr.tnt.designsystem.component.button.TnTBottomButton import co.kr.tnt.designsystem.component.image.TnTProfileImage import co.kr.tnt.designsystem.theme.TnTTheme @@ -38,8 +36,6 @@ internal fun TraineeSignUpCompletePage( ) { BackHandler { onBackClick() } - Log.d("test", "${state.traineeState}") - Scaffold( containerColor = TnTTheme.colors.commonColors.Common0, ) { innerPadding -> @@ -57,7 +53,7 @@ internal fun TraineeSignUpCompletePage( .padding(bottom = 66.dp), ) { Text( - text = stringResource(R.string.nice_to_meet_you_trainee, state.traineeState.name), + text = stringResource(R.string.nice_to_meet_you_trainee, state.name), color = TnTTheme.colors.neutralColors.Neutral950, style = TnTTheme.typography.h1, textAlign = Center, @@ -73,14 +69,14 @@ internal fun TraineeSignUpCompletePage( Spacer(Modifier.padding(top = 28.dp)) TnTProfileImage( defaultImage = painterResource(DefaultUserProfile.Trainee.image), - image = rememberAsyncImagePainter(state.traineeState.image), + image = rememberAsyncImagePainter(state.image), imageSize = 200.dp, showEditButton = false, ) } TnTBottomButton( text = stringResource(uiResource.string.start), - onClick = { onNextClick(state.traineeState.image?.toUri()) }, + onClick = { onNextClick(state.image) }, modifier = Modifier.align(Alignment.BottomCenter), ) } @@ -94,7 +90,7 @@ private fun TraineeSignUpCompletePagePreview() { TraineeSignUpCompletePage( onBackClick = {}, onNextClick = {}, - state = TODO(), + state = TraineeSignUpUiState(name = "김회원"), ) } } 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 4e8328d..0f15a58 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 @@ -2,22 +2,48 @@ package co.kr.tnt.trainee.signup import android.content.Context import android.net.Uri -import co.kr.tnt.domain.model.UserType import co.kr.tnt.ui.base.UiEvent import co.kr.tnt.ui.base.UiSideEffect import co.kr.tnt.ui.base.UiState import java.time.LocalDate +private const val MAX_NAME_LENGTH = 15 +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, + val name: String = "", + val image: Uri? = null, + val birthday: LocalDate? = null, val height: String = "", val weight: String = "", - val isHeightValid: Boolean = false, - val isWeightValid: Boolean = false, - val isBasicInfoValid: Boolean = false, - val traineeState: UserType.Trainee = UserType.Trainee(), - ) : UiState + val ptPurpose: List = emptyList(), + val caution: String? = "", + ) : UiState { + val isNameValid get() = name.length <= MAX_NAME_LENGTH + + /** + * 키가 유효한 입력값인지 검사 + * 형식: 정수 3자 + */ + val isHeightValid + get() = height.isNotEmpty() && 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.isNotEmpty() && weight.matches(weightRegex) && + !weight.startsWith("0") && weight.length <= MAX_WEIGHT_LENGTH + + val isBasicInfoValid + get() = isWeightValid && isHeightValid + } sealed interface TraineeSignUpUiEvent : UiEvent { data class OnImageChange(val imageUri: Uri) : TraineeSignUpUiEvent @@ -25,7 +51,6 @@ internal class TraineeSignUpContract { data class OnHeightChange(val height: String) : TraineeSignUpUiEvent data class OnWeightChange(val weight: String) : TraineeSignUpUiEvent data class OnBirthdayChange(val birthday: LocalDate) : TraineeSignUpUiEvent - data object OnBasicFormUpdate : TraineeSignUpUiEvent data class OnPurposeSelected(val purpose: String) : TraineeSignUpUiEvent data class OnCautionChange(val text: String) : TraineeSignUpUiEvent data object OnNextClick : TraineeSignUpUiEvent 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 acaf1d3..5c6ed56 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 @@ -96,7 +96,7 @@ private fun TraineeSignUpScreen( ) TraineeSignUpPage.NoteForTrainer -> TraineeNoteForTrainerPage( - state = state, + caution = state.caution, onCautionChange = onCautionChange, onBackClick = onBackClick, onNextClick = onNextClick, diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt index 4f7ac4f..137ba1a 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt @@ -4,21 +4,19 @@ import android.content.Context import android.net.Uri import androidx.lifecycle.viewModelScope import co.kr.tnt.core.ui.R +import co.kr.tnt.domain.model.UserType import co.kr.tnt.domain.repository.SignUpRepository import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpEffect import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpPage import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiEvent import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState import co.kr.tnt.ui.base.BaseViewModel -import co.kr.tnt.ui.util.toMultiPart +import co.kr.tnt.ui.util.toFile import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.time.LocalDate import javax.inject.Inject -private const val MAX_HEIGHT_LENGTH = 3 -private const val MAX_WEIGHT_LENGTH = 5 - @HiltViewModel internal class TraineeSignUpViewModel @Inject constructor( private val signUpRepository: SignUpRepository, @@ -32,7 +30,6 @@ internal class TraineeSignUpViewModel @Inject constructor( is TraineeSignUpUiEvent.OnHeightChange -> updateHeight(event.height) is TraineeSignUpUiEvent.OnWeightChange -> updateWeight(event.weight) is TraineeSignUpUiEvent.OnBirthdayChange -> updateBirthday(event.birthday) - TraineeSignUpUiEvent.OnBasicFormUpdate -> setBasicFormDate() is TraineeSignUpUiEvent.OnPurposeSelected -> updateSelectedPurposes(event.purpose) is TraineeSignUpUiEvent.OnCautionChange -> updateCaution(event.text) TraineeSignUpUiEvent.OnBackClick -> navigateToBack() @@ -55,12 +52,23 @@ internal class TraineeSignUpViewModel @Inject constructor( authType: String, ) { viewModelScope.launch { - val profileImagePart = imageUri?.toMultiPart(context, "profileImage") + val state = currentState + val profileImagePart = imageUri?.toFile(context) runCatching { signUpRepository.signUp( profileImage = profileImagePart, - userType = currentState.traineeState, + userType = UserType.Trainee( + id = id, + name = state.name, + image = state.image.toString(), + birthday = state.birthday, + age = null, + weight = state.weight.toDouble(), + height = state.height.toInt(), + ptPurpose = state.ptPurpose, + caution = state.caution, + ), socialId = id, socialType = authType, email = email, @@ -76,115 +84,42 @@ internal class TraineeSignUpViewModel @Inject constructor( } private fun updateProfileImage(imageUri: Uri) { - updateState { - copy( - traineeState = traineeState.copy(image = imageUri.toString()), - ) - } + updateState { copy(image = imageUri) } } private fun updateName(name: String) { - updateState { - copy( - traineeState = traineeState.copy(name = name), - ) - } + updateState { copy(name = name) } } private fun updateHeight(height: String) { - val isValid = validateHeight(height) - updateState { - val isFormValid = isValid && isWeightValid - copy( - height = height, - isHeightValid = isValid, - isBasicInfoValid = isFormValid, - ) - } + updateState { copy(height = height) } } private fun updateWeight(weight: String) { - val isValid = validateWeight(weight) - updateState { - val isFormValid = isHeightValid && isValid - copy( - weight = weight, - isWeightValid = isValid, - isBasicInfoValid = isFormValid, - ) - } + updateState { copy(weight = weight) } } private fun updateBirthday(birthday: LocalDate) { - updateState { - copy( - traineeState = traineeState.copy(birthday = birthday), - ) - } - } - - private fun setBasicFormDate() { - updateState { - copy( - traineeState = traineeState.copy( - weight = weight.toDouble(), - height = height.toInt(), - ), - ) - } - } - - /** - * 키가 유효한 입력값인지 검사 - * 형식: 정수 3자 - */ - private fun validateHeight(input: String): Boolean { - return input.isNotEmpty() && input.toIntOrNull() != null && - !input.startsWith("0") && input.length <= MAX_HEIGHT_LENGTH - } - - /** - * 몸무게가 유효한 입력값인지 검사 - * 형식: 5자 이하의 실수 (000, 00, 00.0, 000.0) - */ - private fun validateWeight(input: String): Boolean { - val weightRegex = Regex("^(\\d{1,3}(\\.\\d)?)?\$") - return input.isNotEmpty() && input.matches(weightRegex) && - !input.startsWith("0") && input.length <= MAX_WEIGHT_LENGTH + updateState { copy(birthday = birthday) } } private fun updateSelectedPurposes(purpose: String) { - val updatedPurposes = currentState.traineeState.ptPurpose.toMutableList().apply { + val updatedPurposes = currentState.ptPurpose.toMutableList().apply { if (contains(purpose)) { remove(purpose) } else { add(purpose) } } - updateState { - copy( - traineeState = traineeState.copy(ptPurpose = updatedPurposes), - ) - } + updateState { copy(ptPurpose = updatedPurposes) } } private fun updateCaution(text: String) { - updateState { - copy( - traineeState = traineeState.copy(caution = text), - ) - } + updateState { copy(caution = text) } } private fun navigateToNext() { - val nextPage = when (currentState.page) { - TraineeSignUpPage.BasicInfo -> { - setBasicFormDate() - TraineeSignUpPage.getNextPage(currentState.page) - } - - else -> TraineeSignUpPage.getNextPage(currentState.page) - } + val nextPage = TraineeSignUpPage.getNextPage(currentState.page) updateState { copy(page = nextPage) } } diff --git a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectContract.kt b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectContract.kt index 2ccf95a..87ed5be 100644 --- a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectContract.kt +++ b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectContract.kt @@ -9,8 +9,22 @@ internal class TrainerConnectContract { data class TrainerConnectUiState( val page: TrainerConnectPage = TrainerConnectPage.CodeGeneration, val inviteCode: String = "", - val trainerState: UserType.Trainer = UserType.Trainer(), - val traineeState: UserType.Trainee = UserType.Trainee(), + val trainerState: UserType.Trainer = UserType.Trainer( + id = "", + name = "", + image = null, + ), + val traineeState: UserType.Trainee = UserType.Trainee( + id = "", + name = "", + image = null, + birthday = null, + age = 0, + weight = 0.0, + height = 0, + ptPurpose = emptyList(), + caution = null, + ), ) : UiState sealed interface TrainerConnectUiEvent : UiEvent { diff --git a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt index 3d9f4a9..93f32b0 100644 --- a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt +++ b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectViewModel.kt @@ -41,6 +41,7 @@ internal class TrainerConnectViewModel @Inject constructor() : id = "trainee", name = "김회원", image = "https://buly.kr/3j7VVqN", + birthday = null, age = 25, weight = 100.0, height = 165, diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt index 75d8aa9..4f2901a 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerProfileSetupPage.kt @@ -55,8 +55,8 @@ internal fun TrainerProfileSetupPage( val context = LocalContext.current - val isWarning by remember(state.trainerState.name) { - derivedStateOf { state.trainerState.name.length > MAX_LENGTH } + val isWarning by remember(state.name) { + derivedStateOf { state.name.length > MAX_LENGTH } } val pickMediaLauncher = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> @@ -66,7 +66,7 @@ internal fun TrainerProfileSetupPage( } val painter = rememberAsyncImagePainter( model = ImageRequest.Builder(context) - .data(state.trainerState.image) + .data(state.image) .transformations(ResizeTransformation(IMAGE_MAX_SIZE)) .build(), ) @@ -94,7 +94,7 @@ internal fun TrainerProfileSetupPage( .fillMaxWidth() .padding(vertical = 12.dp), defaultImage = painterResource(DefaultUserProfile.Trainer.image), - image = state.trainerState.image?.let { painter }, + image = state.image?.let { painter }, onEditClick = { pickMediaLauncher.launch( PickVisualMediaRequest( @@ -106,7 +106,7 @@ internal fun TrainerProfileSetupPage( Spacer(Modifier.padding(top = 60.dp)) TnTLabeledTextFieldWithCounter( title = stringResource(uiResource.string.name), - value = state.trainerState.name, + value = state.name, onValueChange = { newValue -> val filteredText = validateInput(newValue) onNameChange(filteredText) @@ -123,7 +123,7 @@ internal fun TrainerProfileSetupPage( TnTBottomButton( text = stringResource(uiResource.string.next), modifier = Modifier.align(Alignment.BottomCenter), - enabled = state.trainerState.name.isNotBlank() && !isWarning, + enabled = state.name.isNotBlank() && !isWarning, onClick = onNextClick, ) } @@ -142,7 +142,7 @@ private fun validateInput(input: String): String { private fun TrainerProfileSetupPagePreview() { TnTTheme { TrainerProfileSetupPage( - state = TODO(), + state = TrainerSignUpUiState(), onNameChange = {}, onProfileImageSelect = {}, onBackClick = {}, diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt index b595623..7ebd3b9 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpCompletePage.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign.Companion.Center import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import co.kr.tnt.designsystem.component.button.TnTBottomButton import co.kr.tnt.designsystem.component.image.TnTProfileImage import co.kr.tnt.designsystem.theme.TnTTheme @@ -54,7 +53,7 @@ internal fun TrainerSignUpCompletePage( .padding(bottom = 66.dp), ) { Text( - text = stringResource(R.string.nice_to_meet_you_trainer, state.trainerState.name), + text = stringResource(R.string.nice_to_meet_you_trainer, state.name), color = TnTTheme.colors.neutralColors.Neutral950, style = TnTTheme.typography.h1, textAlign = Center, @@ -70,14 +69,14 @@ internal fun TrainerSignUpCompletePage( Spacer(Modifier.padding(top = 28.dp)) TnTProfileImage( defaultImage = painterResource(DefaultUserProfile.Trainer.image), - image = rememberAsyncImagePainter(state.trainerState.image), + image = rememberAsyncImagePainter(state.image), imageSize = 200.dp, showEditButton = false, ) } TnTBottomButton( text = stringResource(uiResource.string.start), - onClick = { onNextClick(state.trainerState.image?.toUri()) }, + onClick = { onNextClick(state.image) }, modifier = Modifier.align(Alignment.BottomCenter), ) } @@ -91,7 +90,7 @@ private fun TrainerSignUpCompletePagePreview() { TrainerSignUpCompletePage( onBackClick = {}, onNextClick = {}, - state = TODO(), + state = TrainerSignUpUiState(), ) } } diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt index fbde199..4b35640 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpContract.kt @@ -2,7 +2,6 @@ package co.kr.tnt.trainer.signup import android.content.Context import android.net.Uri -import co.kr.tnt.domain.model.UserType import co.kr.tnt.ui.base.UiEvent import co.kr.tnt.ui.base.UiSideEffect import co.kr.tnt.ui.base.UiState @@ -10,7 +9,8 @@ import co.kr.tnt.ui.base.UiState internal class TrainerSignUpContract { data class TrainerSignUpUiState( val page: TrainerSignUpPage = TrainerSignUpPage.ProfileSetUp, - val trainerState: UserType.Trainer = UserType.Trainer(), + val name: String = "", + val image: Uri? = null, ) : UiState sealed interface TrainerSignUpUiEvent : UiEvent { diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt index ba19d4f..886876f 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt @@ -3,13 +3,14 @@ package co.kr.tnt.trainer.signup import android.content.Context import android.net.Uri import androidx.lifecycle.viewModelScope +import co.kr.tnt.domain.model.UserType import co.kr.tnt.domain.repository.SignUpRepository import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpEffect import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpPage import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiEvent import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiState import co.kr.tnt.ui.base.BaseViewModel -import co.kr.tnt.ui.util.toMultiPart +import co.kr.tnt.ui.util.toFile import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @@ -45,12 +46,16 @@ internal class TrainerSignUpViewModel @Inject constructor( authType: String, ) { viewModelScope.launch { - val profileImagePart = imageUri?.toMultiPart(context, "profileImage") + val profileImagePart = imageUri?.toFile(context) runCatching { signUpRepository.signUp( profileImage = profileImagePart, - userType = currentState.trainerState, + userType = UserType.Trainer( + id = id, + name = currentState.name, + image = currentState.image.toString(), + ), socialId = id, socialType = authType, email = email, @@ -66,19 +71,11 @@ internal class TrainerSignUpViewModel @Inject constructor( } private fun setProfileImage(imageUri: Uri) { - updateState { - copy( - trainerState = trainerState.copy(image = imageUri.toString()), - ) - } + updateState { copy(image = imageUri) } } private fun setName(name: String) { - updateState { - copy( - trainerState = trainerState.copy(name = name), - ) - } + updateState { copy(name = name) } } private fun navigateToNext() { From a3274b64eaa1531bd0c43ed76a2f997224a08ee8 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Wed, 29 Jan 2025 02:10:43 +0900 Subject: [PATCH 13/14] =?UTF-8?q?[TNT-114]=20fix:=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EB=93=A4=20preview=20=EC=98=A4=EB=A5=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 --- .../kr/tnt/trainee/connect/CodeEntryPage.kt | 4 +-- .../tnt/trainee/connect/PTSessionFormPage.kt | 2 +- .../connect/TraineeConnectCompletePage.kt | 2 +- .../tnt/trainer/connect/CodeGenerationPage.kt | 2 +- .../tnt/trainer/connect/TraineeProfilePage.kt | 25 +++++++++++++++---- .../connect/TrainerConnectCompletePage.kt | 2 +- 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/CodeEntryPage.kt b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/CodeEntryPage.kt index d0d0067..d2b3277 100644 --- a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/CodeEntryPage.kt +++ b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/CodeEntryPage.kt @@ -113,9 +113,9 @@ private fun CodeEntryPagePreview() { isFromMyPage = false, onSkipClick = {}, onNextClick = {}, - state = TODO(), + state = TraineeConnectUiState(), onValidateClick = {}, - onCodeChanged = TODO(), + onCodeChanged = {}, onBackClick = {}, ) } diff --git a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/PTSessionFormPage.kt b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/PTSessionFormPage.kt index c34426c..70efa05 100644 --- a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/PTSessionFormPage.kt +++ b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/PTSessionFormPage.kt @@ -267,7 +267,7 @@ private fun PTSessionFormPagePreview() { PTSessionFormPage( onNextClick = {}, onBackClick = {}, - state = TODO(), + state = TraineeConnectUiState(), ) } } diff --git a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectCompletePage.kt b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectCompletePage.kt index 636a54d..a196055 100644 --- a/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectCompletePage.kt +++ b/feature/trainee/connect/src/main/java/co/kr/tnt/trainee/connect/TraineeConnectCompletePage.kt @@ -132,7 +132,7 @@ private fun ProfileSection( private fun TraineeConnectCompletePagePreview() { TnTTheme { TraineeConnectCompletePage( - state = TODO(), + state = TraineeConnectUiState(), onNextClick = {}, onBackClick = {}, ) diff --git a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/CodeGenerationPage.kt b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/CodeGenerationPage.kt index 7e3bd00..e11c649 100644 --- a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/CodeGenerationPage.kt +++ b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/CodeGenerationPage.kt @@ -177,7 +177,7 @@ private fun copyToClipboard(context: Context, text: String) { private fun CodeGenerationPagePreview() { TnTTheme { CodeGenerationPage( - state = TODO(), + state = TrainerConnectUiState(), onBackClick = {}, onSkipClick = {}, onRegenerateClick = {}, diff --git a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TraineeProfilePage.kt b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TraineeProfilePage.kt index 59b5805..5dfea4a 100644 --- a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TraineeProfilePage.kt +++ b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TraineeProfilePage.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp import co.kr.tnt.designsystem.component.button.TnTBottomButton import co.kr.tnt.designsystem.component.image.TnTProfileImage import co.kr.tnt.designsystem.theme.TnTTheme +import co.kr.tnt.domain.model.UserType import co.kr.tnt.feature.trainer.connect.R import co.kr.tnt.trainer.connect.TrainerConnectContract.TrainerConnectUiState import co.kr.tnt.ui.model.DefaultUserProfile @@ -106,10 +107,12 @@ internal fun TraineeProfilePage( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth(), ) { - TextWithLabel( - label = stringResource(uiResource.string.age_label), - text = trainee.age.toString() + stringResource(uiResource.string.age_unit), - ) + if (trainee.age.toString() != "null") { + TextWithLabel( + label = stringResource(uiResource.string.age_label), + text = trainee.age.toString() + stringResource(uiResource.string.age_unit), + ) + } TextWithLabel( label = stringResource(uiResource.string.height_label), text = trainee.height.toString() + stringResource(uiResource.string.height_unit), @@ -198,7 +201,19 @@ private fun TextWithBackground( private fun TraineeProfilePagePreview() { TnTTheme { TraineeProfilePage( - state = TODO(), + state = TrainerConnectUiState( + traineeState = UserType.Trainee( + id = "", + name = "김회원", + image = null, + birthday = null, + age = null, + weight = 55.0, + height = 150, + ptPurpose = listOf("체중 감량", "자세 교정"), + caution = null, + ), + ), onNextClick = {}, onBackClick = {}, ) diff --git a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectCompletePage.kt b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectCompletePage.kt index d2c6e33..eaa9e33 100644 --- a/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectCompletePage.kt +++ b/feature/trainer/connect/src/main/java/co/kr/tnt/trainer/connect/TrainerConnectCompletePage.kt @@ -129,7 +129,7 @@ private fun ProfileSection( private fun TrainerConnectCompletePagePreview() { TnTTheme { TrainerConnectCompletePage( - state = TODO(), + state = TrainerConnectUiState(), onNextClick = {}, onBackClick = {}, ) From 6201215ef4e6fb81c20af9bd8aaa18cdf5005b91 Mon Sep 17 00:00:00 2001 From: SeonJeongk Date: Wed, 29 Jan 2025 21:08:59 +0900 Subject: [PATCH 14/14] =?UTF-8?q?[TNT-114]=20fix:=20SignUpRequest=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../co/kr/data/network/model/SignUpRequest.kt | 81 +++++++++---------- .../data/repository/SignUpRepositoryImpl.kt | 16 ++-- 2 files changed, 46 insertions(+), 51 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 efe0070..030a9f1 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 @@ -21,48 +21,45 @@ data class SignUpRequest( val cautionNote: String? = "", ) -object SignUpRequestMapper { - fun fromUserType( - userType: UserType, - socialId: String, - socialType: String, - email: String, - fcmToken: String, - ): SignUpRequest { - return when (userType) { - is UserType.Trainer -> SignUpRequest( - memberType = "trainer", - name = userType.name, - birthday = null, - height = null, - weight = null, - goalContents = null, - cautionNote = null, - socialType = socialType, - socialId = socialId, - socialEmail = email, - fcmToken = fcmToken, - serviceAgreement = true, - collectionAgreement = true, - advertisementAgreement = true, - ) +fun UserType.toSignUpRequest( + socialId: String, + socialType: String, + email: String, + fcmToken: String, +): SignUpRequest { + return when (this) { + is UserType.Trainer -> SignUpRequest( + memberType = "trainer", + name = name, + birthday = null, + height = null, + weight = null, + goalContents = null, + cautionNote = null, + socialType = socialType, + socialId = socialId, + socialEmail = email, + fcmToken = fcmToken, + serviceAgreement = true, + collectionAgreement = true, + advertisementAgreement = true, + ) - is UserType.Trainee -> SignUpRequest( - memberType = "trainee", - name = userType.name, - birthday = userType.birthday?.toString(), - height = userType.height.toDouble(), - weight = userType.weight, - goalContents = userType.ptPurpose, - cautionNote = userType.caution?.ifBlank { null }, - socialType = socialType, - socialId = socialId, - socialEmail = email, - fcmToken = fcmToken, - serviceAgreement = true, - collectionAgreement = true, - advertisementAgreement = true, - ) - } + is UserType.Trainee -> SignUpRequest( + memberType = "trainee", + name = name, + birthday = birthday?.toString(), + height = height.toDouble(), + weight = weight, + goalContents = ptPurpose, + cautionNote = caution?.ifBlank { null }, + socialType = socialType, + socialId = socialId, + socialEmail = email, + fcmToken = fcmToken, + serviceAgreement = true, + collectionAgreement = true, + advertisementAgreement = true, + ) } } diff --git a/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt b/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt index d5e07b9..f026958 100644 --- a/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt +++ b/data/repository/src/main/java/co/kr/data/repository/SignUpRepositoryImpl.kt @@ -1,8 +1,8 @@ package co.kr.data.repository import co.kr.data.network.model.SignUpRequest -import co.kr.data.network.model.SignUpRequestMapper import co.kr.data.network.model.toDomain +import co.kr.data.network.model.toSignUpRequest import co.kr.data.network.source.SignUpRemoteDataSource import co.kr.data.storage.source.SessionLocalDataSource import co.kr.tnt.domain.model.SignUpResult @@ -21,6 +21,7 @@ import javax.inject.Inject class SignUpRepositoryImpl @Inject constructor( private val signupRemoteDataSource: SignUpRemoteDataSource, private val sessionLocalDataSource: SessionLocalDataSource, + private val json: Json, ) : SignUpRepository { override suspend fun signUp( profileImage: File?, @@ -35,29 +36,26 @@ class SignUpRepositoryImpl @Inject constructor( } // TODO FCM token - val signUpRequest = SignUpRequestMapper.fromUserType( - userType = userType, + val signUpRequest = userType.toSignUpRequest( socialId = socialId, socialType = socialType, email = email, fcmToken = "EMPTY", ) - val requestBody = prepareJsonRequestBody(signUpRequest) + val requestBody = signUpRequest.toRequestBody(Json) val response = signupRemoteDataSource.postSignUp( profileImage = profileImagePart, request = requestBody, ) - response.sessionId.let { sessionId -> - sessionLocalDataSource.updateSessionId(sessionId) - } + sessionLocalDataSource.updateSessionId(response.sessionId) return response.toDomain() } - private fun prepareJsonRequestBody(signUpRequest: SignUpRequest): RequestBody { - val jsonString = Json.encodeToString(signUpRequest) + private fun SignUpRequest.toRequestBody(json: Json): RequestBody { + val jsonString = json.encodeToString(this) return jsonString.toRequestBody("application/json".toMediaTypeOrNull()) } }