From 2b4fdc021292ca263cec4ce6a8a03d80a4f07c11 Mon Sep 17 00:00:00 2001 From: TaejungKim <92737123+imtaejugkim@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:44:25 +0900 Subject: [PATCH 1/7] =?UTF-8?q?#13=20[Chore]=20:=20=EC=88=98=EC=A0=95=20hi?= =?UTF-8?q?lt=20=EC=A3=BC=EC=9E=85=20=EC=98=A4=EB=A5=98=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 --- app/build.gradle.kts | 4 ---- build.gradle.kts | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cc3aec2..c771d90 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -56,10 +56,6 @@ android { } } -hilt { - enableAggregatingTask = false -} - dependencies { diff --git a/build.gradle.kts b/build.gradle.kts index 952b930..9c16d0c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,5 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.hilt.android) apply false } \ No newline at end of file From 1c519779c9f7b7848c0ea1573efd24a07094ed5a Mon Sep 17 00:00:00 2001 From: TaejungKim <92737123+imtaejugkim@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:45:12 +0900 Subject: [PATCH 2/7] =?UTF-8?q?#13=20[Feat]=20:=20=EC=B6=94=EA=B0=80=20Sta?= =?UTF-8?q?te,=20Event,=20SideEffect=20=EA=B3=B5=ED=86=B5=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20BaseViewModel=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../and/presentation/core/BaseViewModel.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/src/main/java/org/sopt/and/presentation/core/BaseViewModel.kt diff --git a/app/src/main/java/org/sopt/and/presentation/core/BaseViewModel.kt b/app/src/main/java/org/sopt/and/presentation/core/BaseViewModel.kt new file mode 100644 index 0000000..7021cc8 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/core/BaseViewModel.kt @@ -0,0 +1,38 @@ +package org.sopt.and.presentation.core + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +abstract class BaseViewModel : + ViewModel() { + + private val initialState: State by lazy { createInitialState() } + abstract fun createInitialState(): State + + private val _uiState = MutableStateFlow(initialState) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _sideEffect = MutableSharedFlow() + val sideEffect: SharedFlow = _sideEffect.asSharedFlow() + + fun setState(reduce: State.() -> State) { + _uiState.value = _uiState.value.reduce() + } + + fun setSideEffect(builder: () -> SideEffect) { + viewModelScope.launch { _sideEffect.emit(builder()) } + } + + fun setEvent(event: Event) { + viewModelScope.launch { handleEvent(event) } + } + + protected abstract suspend fun handleEvent(event: Event) +} \ No newline at end of file From 637a82953654d8d6caad4552c1c57eded208fcf3 Mon Sep 17 00:00:00 2001 From: TaejungKim <92737123+imtaejugkim@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:45:47 +0900 Subject: [PATCH 3/7] =?UTF-8?q?#13=20[Feat]=20:=20State,=20SideEffect,=20E?= =?UTF-8?q?vent=EC=97=90=20=EB=94=B0=EB=A5=B8=20SignUp=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 --- .../org/sopt/and/presentation/core/UiEvent.kt | 3 + .../and/presentation/core/UiSideEffect.kt | 3 + .../org/sopt/and/presentation/core/UiState.kt | 3 + .../and/presentation/signup/SignUpContract.kt | 27 +++ .../and/presentation/signup/SignUpScreen.kt | 220 +++++++++--------- .../and/presentation/signup/UserViewModel.kt | 47 +++- .../signup/components/IdHobbyTextField.kt | 12 +- .../signup/components/PasswordTextField.kt | 9 +- .../main/java/org/sopt/and/util/ShowToast.kt | 4 +- 9 files changed, 197 insertions(+), 131 deletions(-) create mode 100644 app/src/main/java/org/sopt/and/presentation/core/UiEvent.kt create mode 100644 app/src/main/java/org/sopt/and/presentation/core/UiSideEffect.kt create mode 100644 app/src/main/java/org/sopt/and/presentation/core/UiState.kt create mode 100644 app/src/main/java/org/sopt/and/presentation/signup/SignUpContract.kt diff --git a/app/src/main/java/org/sopt/and/presentation/core/UiEvent.kt b/app/src/main/java/org/sopt/and/presentation/core/UiEvent.kt new file mode 100644 index 0000000..e33567d --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/core/UiEvent.kt @@ -0,0 +1,3 @@ +package org.sopt.and.presentation.core + +interface UiEvent \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/core/UiSideEffect.kt b/app/src/main/java/org/sopt/and/presentation/core/UiSideEffect.kt new file mode 100644 index 0000000..350619e --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/core/UiSideEffect.kt @@ -0,0 +1,3 @@ +package org.sopt.and.presentation.core + +interface UiSideEffect \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/core/UiState.kt b/app/src/main/java/org/sopt/and/presentation/core/UiState.kt new file mode 100644 index 0000000..6319c2b --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/core/UiState.kt @@ -0,0 +1,3 @@ +package org.sopt.and.presentation.core + +interface UiState \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/signup/SignUpContract.kt b/app/src/main/java/org/sopt/and/presentation/signup/SignUpContract.kt new file mode 100644 index 0000000..6ad4b61 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/signup/SignUpContract.kt @@ -0,0 +1,27 @@ +package org.sopt.and.presentation.signup + +import org.sopt.and.presentation.core.UiEvent +import org.sopt.and.presentation.core.UiSideEffect +import org.sopt.and.presentation.core.UiState + +// State 정의 +data class SignUpState( + val username: String = "", + val password: String = "", + val hobby: String = "", + val isLoading: Boolean = false +) : UiState + +// Event 정의 +sealed class SignUpEvent : UiEvent { + data class UsernameChanged(val username: String) : SignUpEvent() + data class PasswordChanged(val password: String) : SignUpEvent() + data class HobbyChanged(val hobby: String) : SignUpEvent() + object SignUpClicked : SignUpEvent() +} + +// SideEffect 정의 +sealed class SignUpSideEffect : UiSideEffect { + data class ShowToast(val message: String) : SignUpSideEffect() + object NavigateToLogin : SignUpSideEffect() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt b/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt index 21ac4df..a53cb7b 100644 --- a/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt @@ -14,16 +14,12 @@ 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 -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -42,34 +38,21 @@ fun SignUpScreen( navController: NavController, viewModel: UserViewModel = hiltViewModel() ) { - // textStyle 변경을 위한 textFieldValue 추적 - val idState = remember { mutableStateOf(TextFieldValue()) } - val passwordState = remember { mutableStateOf(TextFieldValue()) } - val hobbyState = remember { mutableStateOf(TextFieldValue()) } + val state by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current - // 모든 textFiled가 채워졌는지 판단하는 변수 - val allFieldFilled = idState.value.text.isNotEmpty() && passwordState.value.text.isNotEmpty() && hobbyState.value.text.isNotEmpty() - val registerState = viewModel.userRegisterState.collectAsStateWithLifecycle().value - - LaunchedEffect(registerState) { - when (registerState) { - is RegisterState.Loading -> { } - is RegisterState.Success -> { - navController.navigate("login") { - popUpTo(0) { inclusive = true } + LaunchedEffect(Unit) { + viewModel.sideEffect.collect { effect -> + when (effect) { + is SignUpSideEffect.ShowToast -> { + context.showToast(effect.message) } - } - - is RegisterState.Failure -> { - when (registerState.code) { - "00" -> context.showToast(R.string.sign_up_error) - "01" -> context.showToast(R.string.sign_up_eight) - else -> {} + is SignUpSideEffect.NavigateToLogin -> { + navController.navigate("login") { + popUpTo(0) { inclusive = true } + } } } - - else -> {} } } @@ -85,95 +68,112 @@ fun SignUpScreen( .padding(16.dp), ) { Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.TopEnd + modifier = Modifier.fillMaxSize() ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 44.dp) + .background(Color.Black) + .padding(16.dp), + ) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.TopEnd + ) { + Text( + text = "회원가입", + color = Color.White, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + Image( + painter = painterResource(R.drawable.ic_exit), + contentDescription = "X 버튼", + modifier = Modifier.size(24.dp) + ) + } + + Spacer(modifier = Modifier.weight(3f)) + SignUpTitle( + firstText = stringResource(R.string.sign_up_title_top_start), + firstColor = Color.White, + secondText = stringResource(R.string.sign_up_title_top_end), + secondColor = Color.Gray + ) + + SignUpTitle( + firstText = stringResource(R.string.sign_up_title_bottom_start), + firstColor = Color.White, + secondText = stringResource(R.string.sign_up_title_bottom_end), + secondColor = Color.Gray + ) + Spacer(modifier = Modifier.weight(2f)) + + IdHobbyTextField( + valueState = state.username, + onValueChange = { + viewModel.setEvent(SignUpEvent.UsernameChanged(it)) + }, + holderText = R.string.log_in_id, + modifier = Modifier.padding(bottom = 8.dp) + ) + + Spacer(modifier = Modifier.weight(0.5f)) + SignUpInfoRow( + iconResId = R.drawable.ic_info, + text = stringResource(R.string.sign_up_id) + ) + + Spacer(modifier = Modifier.weight(1f)) + PasswordField( + passwordState = state.password, + onValueChange = { + viewModel.setEvent(SignUpEvent.PasswordChanged(it)) + }, + modifier = Modifier.padding(top = 8.dp) + ) + + Spacer(modifier = Modifier.weight(0.5f)) + SignUpInfoRow( + iconResId = R.drawable.ic_info, + text = stringResource(R.string.sign_up_passwd) + ) + + Spacer(modifier = Modifier.weight(1f)) + IdHobbyTextField( + valueState = state.hobby, + onValueChange = { + viewModel.setEvent(SignUpEvent.HobbyChanged(it)) + }, + holderText = R.string.sign_up_hobby, + modifier = Modifier.padding(bottom = 8.dp) + ) + + Spacer(modifier = Modifier.weight(2f)) + SocialServiceLogIn() + Spacer(modifier = Modifier.weight(8f)) + } + Text( - text = "회원가입", + text = "Wavve 회원가입", color = Color.White, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .background( + if (state.username.isNotEmpty() && state.password.isNotEmpty() && state.hobby.isNotEmpty()) Color.Blue else Color.Gray + ) + .padding(10.dp) + .align(Alignment.BottomCenter) + .fillMaxWidth() + .clickable( + enabled = state.username.isNotEmpty() && state.password.isNotEmpty() && state.hobby.isNotEmpty(), + onClick = { + viewModel.setEvent(SignUpEvent.SignUpClicked) + } + ), textAlign = TextAlign.Center ) - Image( - painter = painterResource(R.drawable.ic_exit), - contentDescription = "X 버튼", - modifier = Modifier.size(24.dp) - ) } - - Spacer(modifier = Modifier.weight(3f)) - SignUpTitle( - firstText = stringResource(R.string.sign_up_title_top_start), - firstColor = Color.White, - secondText = stringResource(R.string.sign_up_title_top_end), - secondColor = Color.Gray - ) - - SignUpTitle( - firstText = stringResource(R.string.sign_up_title_bottom_start), - firstColor = Color.White, - secondText = stringResource(R.string.sign_up_title_bottom_end), - secondColor = Color.Gray - ) - Spacer(modifier = Modifier.weight(2f)) - IdHobbyTextField( - valueState = idState, - holderText = R.string.log_in_id, - modifier = Modifier.padding(bottom = 8.dp) - ) - - Spacer(modifier = Modifier.weight(0.5f)) - SignUpInfoRow( - iconResId = R.drawable.ic_info, - text = stringResource(R.string.sign_up_id) - ) - - Spacer(modifier = Modifier.weight(1f)) - PasswordField( - passwordState = passwordState, - modifier = Modifier.padding(top = 8.dp) - ) - - Spacer(modifier = Modifier.weight(0.5f)) - SignUpInfoRow( - iconResId = R.drawable.ic_info, - text = stringResource(R.string.sign_up_passwd) - ) - - Spacer(modifier = Modifier.weight(1f)) - IdHobbyTextField( - valueState = hobbyState, - holderText = R.string.sign_up_hobby, - modifier = Modifier.padding(bottom = 8.dp) - ) - - Spacer(modifier = Modifier.weight(2f)) - SocialServiceLogIn() - Spacer(modifier = Modifier.weight(8f)) } - - Text( - text = "Wavve 회원가입", - color = Color.White, - modifier = Modifier - .background( - if (allFieldFilled) Color.Blue else Color.Gray, - ) - .padding(10.dp) - .align(Alignment.BottomCenter) - .fillMaxWidth() - .clickable( - enabled = allFieldFilled, - onClick = { - val textId = idState.value.text - val textPasswd = passwordState.value.text - val textHobby = hobbyState.value.text - // viewModel의 signUp을 통해 success boolean 판단 - viewModel.signUp(textId, textPasswd, textHobby) - } - ), - textAlign = TextAlign.Center - ) - } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/signup/UserViewModel.kt b/app/src/main/java/org/sopt/and/presentation/signup/UserViewModel.kt index 6125c5d..96d635a 100644 --- a/app/src/main/java/org/sopt/and/presentation/signup/UserViewModel.kt +++ b/app/src/main/java/org/sopt/and/presentation/signup/UserViewModel.kt @@ -9,31 +9,64 @@ import kotlinx.coroutines.launch import org.sopt.and.domain.model.UserLoginRequest import org.sopt.and.domain.model.UserRegisterRequest import org.sopt.and.domain.repository.UserRepository +import org.sopt.and.presentation.core.BaseViewModel import org.sopt.and.presentation.login.LoginState import javax.inject.Inject @HiltViewModel class UserViewModel @Inject constructor( private val userRepository: UserRepository -) : ViewModel() { +) : BaseViewModel() { private val _userRegisterState = MutableStateFlow(RegisterState.Idle) val userRegisterState: StateFlow = _userRegisterState private val _userLoginState = MutableStateFlow(LoginState.Idle) val userLoginState: StateFlow = _userLoginState + override fun createInitialState(): SignUpState = SignUpState() + + override suspend fun handleEvent(event: SignUpEvent) { + when (event) { + is SignUpEvent.UsernameChanged -> { + setState { copy(username = event.username) } + } + + is SignUpEvent.PasswordChanged -> { + setState { copy(password = event.password) } + } + + is SignUpEvent.HobbyChanged -> { + setState { copy(hobby = event.hobby) } + } + + is SignUpEvent.SignUpClicked -> { + signUp() + } + } + } + // 회원가입 로직 - fun signUp(username: String, password: String, hobby: String) { - _userRegisterState.value = RegisterState.Loading + fun signUp() { + setState { copy(isLoading = true) } viewModelScope.launch { + val state = uiState.value val result = userRepository.postUserRegistering( UserRegisterRequest( - username = username, password = password, hobby = hobby + username = state.username, + password = state.password, + hobby = state.hobby ) ) - _userRegisterState.value = - result.fold(onSuccess = { RegisterState.Success(it.no) }, - onFailure = { RegisterState.Failure(it.message ?: "") }) + result.fold( + onSuccess = { + setState { copy(isLoading = false) } + setSideEffect { SignUpSideEffect.NavigateToLogin } + }, + onFailure = { + setState { copy(isLoading = false) } + setSideEffect { SignUpSideEffect.ShowToast(it.message ?: "회원가입 실패") } + } + ) } } diff --git a/app/src/main/java/org/sopt/and/presentation/signup/components/IdHobbyTextField.kt b/app/src/main/java/org/sopt/and/presentation/signup/components/IdHobbyTextField.kt index ea74d55..9990ab5 100644 --- a/app/src/main/java/org/sopt/and/presentation/signup/components/IdHobbyTextField.kt +++ b/app/src/main/java/org/sopt/and/presentation/signup/components/IdHobbyTextField.kt @@ -5,29 +5,27 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.TextFieldValue -import org.sopt.and.R @Composable fun IdHobbyTextField( - valueState: MutableState, + valueState: String, + onValueChange: (String) -> Unit, modifier: Modifier = Modifier, holderText: Int, isSingleLine: Boolean = true, keyboardOptions: KeyboardOptions = KeyboardOptions.Default ) { OutlinedTextField( - value = valueState.value, - onValueChange = { valueState.value = it }, + value = valueState, + onValueChange = onValueChange, singleLine = isSingleLine, placeholder = { Text(stringResource(holderText)) }, textStyle = TextStyle(color = Color.White), modifier = modifier.fillMaxWidth(), keyboardOptions = keyboardOptions ) -} \ No newline at end of file +} diff --git a/app/src/main/java/org/sopt/and/presentation/signup/components/PasswordTextField.kt b/app/src/main/java/org/sopt/and/presentation/signup/components/PasswordTextField.kt index 8eb7a21..1e55561 100644 --- a/app/src/main/java/org/sopt/and/presentation/signup/components/PasswordTextField.kt +++ b/app/src/main/java/org/sopt/and/presentation/signup/components/PasswordTextField.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -14,21 +13,21 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import org.sopt.and.R @Composable fun PasswordField( - passwordState: MutableState, + passwordState: String, + onValueChange: (String) -> Unit, modifier: Modifier = Modifier ) { val showPassword = remember { mutableStateOf(false) } OutlinedTextField( - value = passwordState.value, - onValueChange = { passwordState.value = it }, + value = passwordState, + onValueChange = onValueChange, singleLine = true, textStyle = TextStyle(color = Color.White), placeholder = { Text(stringResource(R.string.log_in_passwd)) }, diff --git a/app/src/main/java/org/sopt/and/util/ShowToast.kt b/app/src/main/java/org/sopt/and/util/ShowToast.kt index 20f94e5..7318cef 100644 --- a/app/src/main/java/org/sopt/and/util/ShowToast.kt +++ b/app/src/main/java/org/sopt/and/util/ShowToast.kt @@ -4,6 +4,6 @@ import android.content.Context import android.widget.Toast import androidx.annotation.StringRes -fun Context.showToast(@StringRes messageResId: Int) { - Toast.makeText(this, this.getString(messageResId), Toast.LENGTH_SHORT).show() +fun Context.showToast(messageResId: String) { + Toast.makeText(this, messageResId, Toast.LENGTH_SHORT).show() } \ No newline at end of file From 0f3f9bb8e910ce27d56a63b54cd670173293798c Mon Sep 17 00:00:00 2001 From: TaejungKim <92737123+imtaejugkim@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:31:41 +0900 Subject: [PATCH 4/7] =?UTF-8?q?#13=20[Feat]=20:=20State,=20SideEffect,=20E?= =?UTF-8?q?vent=EC=97=90=20=EB=94=B0=EB=A5=B8=20LogIn=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../and/presentation/login/LogInContract.kt | 26 +++++++ .../and/presentation/login/LogInScreen.kt | 73 +++++++++---------- .../and/presentation/login/LogInViewModel.kt | 72 ++++++++++++++++++ .../sopt/and/presentation/login/LoginState.kt | 8 -- .../login/components/AuthManagement.kt | 1 - .../and/presentation/signup/RegisterState.kt | 8 -- .../and/presentation/signup/SignUpScreen.kt | 2 +- .../{UserViewModel.kt => SignUpViewModel.kt} | 23 +----- .../java/org/sopt/and/util/ShowSnackBar.kt | 17 +++++ app/src/main/res/values/strings.xml | 1 + 10 files changed, 152 insertions(+), 79 deletions(-) create mode 100644 app/src/main/java/org/sopt/and/presentation/login/LogInContract.kt create mode 100644 app/src/main/java/org/sopt/and/presentation/login/LogInViewModel.kt delete mode 100644 app/src/main/java/org/sopt/and/presentation/login/LoginState.kt delete mode 100644 app/src/main/java/org/sopt/and/presentation/signup/RegisterState.kt rename app/src/main/java/org/sopt/and/presentation/signup/{UserViewModel.kt => SignUpViewModel.kt} (68%) create mode 100644 app/src/main/java/org/sopt/and/util/ShowSnackBar.kt diff --git a/app/src/main/java/org/sopt/and/presentation/login/LogInContract.kt b/app/src/main/java/org/sopt/and/presentation/login/LogInContract.kt new file mode 100644 index 0000000..e4b0cfe --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/login/LogInContract.kt @@ -0,0 +1,26 @@ +package org.sopt.and.presentation.login + +import org.sopt.and.presentation.core.UiEvent +import org.sopt.and.presentation.core.UiSideEffect +import org.sopt.and.presentation.core.UiState + +// State 정의 +data class LogInState( + val username: String = "", + val password: String = "", + val isLoading: Boolean = false, + val errorMessage: Int? = null +) : UiState + +// Event 정의 +sealed class LogInEvent : UiEvent { + data class UsernameChanged(val username: String) : LogInEvent() + data class PasswordChanged(val password: String) : LogInEvent() + object LogInClicked : LogInEvent() +} + +// SideEffect 정의 +sealed class LogInEffect : UiSideEffect { + data class ShowSnackBar(val message: Int) : LogInEffect() + object NavigateToMain : LogInEffect() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/login/LogInScreen.kt b/app/src/main/java/org/sopt/and/presentation/login/LogInScreen.kt index 004fe42..4269fe5 100644 --- a/app/src/main/java/org/sopt/and/presentation/login/LogInScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/login/LogInScreen.kt @@ -1,6 +1,5 @@ package org.sopt.and.presentation.login -import android.app.Activity import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -20,10 +19,10 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -31,55 +30,52 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import org.sopt.and.R -import org.sopt.and.data.local.SharedPreferenceManager.saveToken import org.sopt.and.presentation.login.components.AuthManagement +import org.sopt.and.presentation.signup.SignUpEvent import org.sopt.and.presentation.signup.components.PasswordField -import org.sopt.and.presentation.signup.UserViewModel import org.sopt.and.presentation.signup.components.IdHobbyTextField import org.sopt.and.presentation.signup.components.SocialServiceLogIn +import org.sopt.and.util.showSnackBar @Composable fun LogInScreen( navController: NavController, - viewModel: UserViewModel = hiltViewModel() + viewModel: LogInViewModel = hiltViewModel() ) { - // textStyle 변경을 위한 textFieldValue 추적 - val idState = remember { mutableStateOf(TextFieldValue()) } - val passwordState = remember { mutableStateOf(TextFieldValue()) } - val loginState = viewModel.userLoginState.collectAsState().value - val context = LocalContext.current as Activity - val snackbarHostState = remember { SnackbarHostState() } + val state by viewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current + val snackBarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() - LaunchedEffect(loginState) { - when (loginState) { - is LoginState.Loading -> {} - is LoginState.Success -> { - saveToken(loginState.data) - navController.popBackStack("login", inclusive = true) - navController.navigate("mainScreen") + LaunchedEffect(Unit) { + viewModel.sideEffect.collect { effect -> + when (effect) { + is LogInEffect.ShowSnackBar -> { + snackBarHostState.showSnackBar( + context = context, + scope = coroutineScope, + message = effect.message + ) + } + is LogInEffect.NavigateToMain -> { + navController.popBackStack("login", inclusive = true) + navController.navigate("mainScreen") + } } - is LoginState.Failure -> { - snackbarHostState.showSnackbar( - message = context.getString(R.string.log_in_method), - actionLabel = context.getString(R.string.log_in_ok) - ) - } - else -> {} } - } // SnackBar 구현을 위해 Scaffold 안에 정의 Scaffold( // 스낵바의 표시 상태 관리 - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }) { padding -> + snackbarHost = { SnackbarHost(hostState = snackBarHostState) }) { padding -> Column( modifier = Modifier @@ -103,32 +99,31 @@ fun LogInScreen( modifier = Modifier.size(28.dp) ) } - - // id text remeber를 통한 변수 변경 - var textId by remember { mutableStateOf("") } - Spacer(modifier = Modifier.weight(2f)) // 윤곽선의 색상 및 두께를 커스텀 가능한 OutLinedTextField IdHobbyTextField( - valueState = idState, + valueState = state.username, + onValueChange = { + viewModel.setEvent(LogInEvent.UsernameChanged(it)) + }, holderText = R.string.log_in_id, modifier = Modifier.padding(bottom = 8.dp) ) Spacer(modifier = Modifier.height(10.dp)) - // password text remeber를 통한 변수 변경 - var textPasswd by remember { mutableStateOf("") } PasswordField( - passwordState = passwordState, modifier = Modifier.padding(top = 8.dp) + passwordState = state.password, + onValueChange = { + viewModel.setEvent(LogInEvent.PasswordChanged(it)) + }, + modifier = Modifier.padding(top = 8.dp) ) Spacer(modifier = Modifier.weight(1f)) Button( onClick = { - textId = idState.value.text - textPasswd = passwordState.value.text - viewModel.logIn(textId, textPasswd) + viewModel.setEvent(LogInEvent.LogInClicked) }, modifier = Modifier.fillMaxWidth(), contentPadding = PaddingValues(16.dp), // ButtonDefaults 의 4가지 형태 diff --git a/app/src/main/java/org/sopt/and/presentation/login/LogInViewModel.kt b/app/src/main/java/org/sopt/and/presentation/login/LogInViewModel.kt new file mode 100644 index 0000000..254c5e7 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/login/LogInViewModel.kt @@ -0,0 +1,72 @@ +package org.sopt.and.presentation.login + +import android.util.Log +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.sopt.and.domain.model.UserLoginRequest +import org.sopt.and.domain.repository.UserRepository +import org.sopt.and.presentation.core.BaseViewModel +import javax.inject.Inject +import org.sopt.and.R +import retrofit2.HttpException + +@HiltViewModel +class LogInViewModel @Inject constructor( + private val userRepository: UserRepository +) : BaseViewModel() { + + override fun createInitialState(): LogInState = LogInState() + + override suspend fun handleEvent(event: LogInEvent) { + when (event) { + is LogInEvent.UsernameChanged -> { + setState { copy(username = event.username) } + } + + is LogInEvent.PasswordChanged -> { + setState { copy(password = event.password) } + } + + is LogInEvent.LogInClicked -> { + logIn() + } + } + } + + + // 로그인 로직 + fun logIn() { + setState { copy(isLoading = true) } + viewModelScope.launch { + val state = uiState.value + val result = userRepository.postUserLogin( + UserLoginRequest( + username = state.username, + password = state.password + ) + ) + result.fold( + onSuccess = { + setState { copy(isLoading = false) } + setSideEffect { LogInEffect.NavigateToMain } + }, + onFailure = { error -> + val message = if (error is HttpException) { + Log.d("error", error.code().toString()) + when (error.code()) { + 400 -> R.string.log_in_method + 403 -> R.string.sign_up_paswd + else -> R.string.unknown_error + } + } else { + R.string.unknown_error + } + setState { copy(isLoading = false) } + setSideEffect { LogInEffect.ShowSnackBar(message) } + } + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/login/LoginState.kt b/app/src/main/java/org/sopt/and/presentation/login/LoginState.kt deleted file mode 100644 index c177c3a..0000000 --- a/app/src/main/java/org/sopt/and/presentation/login/LoginState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.sopt.and.presentation.login - -sealed class LoginState { - data object Idle: LoginState() - data object Loading: LoginState() - data class Success(val data: String): LoginState() - data class Failure(val code: String): LoginState() -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/login/components/AuthManagement.kt b/app/src/main/java/org/sopt/and/presentation/login/components/AuthManagement.kt index 880e5fb..a11bac3 100644 --- a/app/src/main/java/org/sopt/and/presentation/login/components/AuthManagement.kt +++ b/app/src/main/java/org/sopt/and/presentation/login/components/AuthManagement.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import org.sopt.and.R -import org.sopt.and.presentation.signup.UserViewModel @Composable fun AuthManagement( diff --git a/app/src/main/java/org/sopt/and/presentation/signup/RegisterState.kt b/app/src/main/java/org/sopt/and/presentation/signup/RegisterState.kt deleted file mode 100644 index 386c05f..0000000 --- a/app/src/main/java/org/sopt/and/presentation/signup/RegisterState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.sopt.and.presentation.signup - -sealed class RegisterState { - data object Idle: RegisterState() - data object Loading: RegisterState() - data class Success(val token: Int): RegisterState() - data class Failure(val code: String): RegisterState() -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt b/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt index a53cb7b..c2ad090 100644 --- a/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt @@ -36,7 +36,7 @@ import org.sopt.and.util.showToast @Composable fun SignUpScreen( navController: NavController, - viewModel: UserViewModel = hiltViewModel() + viewModel: SignUpViewModel = hiltViewModel() ) { val state by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current diff --git a/app/src/main/java/org/sopt/and/presentation/signup/UserViewModel.kt b/app/src/main/java/org/sopt/and/presentation/signup/SignUpViewModel.kt similarity index 68% rename from app/src/main/java/org/sopt/and/presentation/signup/UserViewModel.kt rename to app/src/main/java/org/sopt/and/presentation/signup/SignUpViewModel.kt index 96d635a..027e4f0 100644 --- a/app/src/main/java/org/sopt/and/presentation/signup/UserViewModel.kt +++ b/app/src/main/java/org/sopt/and/presentation/signup/SignUpViewModel.kt @@ -1,6 +1,5 @@ package org.sopt.and.presentation.signup -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -10,18 +9,12 @@ import org.sopt.and.domain.model.UserLoginRequest import org.sopt.and.domain.model.UserRegisterRequest import org.sopt.and.domain.repository.UserRepository import org.sopt.and.presentation.core.BaseViewModel -import org.sopt.and.presentation.login.LoginState import javax.inject.Inject @HiltViewModel -class UserViewModel @Inject constructor( +class SignUpViewModel @Inject constructor( private val userRepository: UserRepository ) : BaseViewModel() { - private val _userRegisterState = MutableStateFlow(RegisterState.Idle) - val userRegisterState: StateFlow = _userRegisterState - - private val _userLoginState = MutableStateFlow(LoginState.Idle) - val userLoginState: StateFlow = _userLoginState override fun createInitialState(): SignUpState = SignUpState() @@ -69,18 +62,4 @@ class UserViewModel @Inject constructor( ) } } - - // 로그인 로직 - fun logIn(username: String, password: String) { - _userLoginState.value = LoginState.Loading - viewModelScope.launch { - val result = userRepository.postUserLogin( - UserLoginRequest( - username = username, password = password - ) - ) - _userLoginState.value = result.fold(onSuccess = { LoginState.Success(it.token) }, - onFailure = { LoginState.Failure(it.message ?: "") }) - } - } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/util/ShowSnackBar.kt b/app/src/main/java/org/sopt/and/util/ShowSnackBar.kt new file mode 100644 index 0000000..8303de5 --- /dev/null +++ b/app/src/main/java/org/sopt/and/util/ShowSnackBar.kt @@ -0,0 +1,17 @@ +package org.sopt.and.util + +import android.content.Context +import androidx.compose.material3.SnackbarHostState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +fun SnackbarHostState.showSnackBar( + context: Context, + scope: CoroutineScope, + message: Int, + actionLabel: String? = "확인" +) { + scope.launch { + this@showSnackBar.showSnackbar(context.getString(message), actionLabel) + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b46b761..410b457 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,6 +32,7 @@ 만으로 Wavve를 즐길 수 있어요! + 알 수 없는 오류가 발생하였습니다. From 2260732575b26d73bace5313d94047d8c3aacbf4 Mon Sep 17 00:00:00 2001 From: TaejungKim <92737123+imtaejugkim@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:32:01 +0900 Subject: [PATCH 5/7] =?UTF-8?q?#13=20[Chore]=20:=20=EC=88=98=EC=A0=95=20Ma?= =?UTF-8?q?inActivity=20viewModel=20=EC=A0=84=EB=8B=AC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/sopt/and/presentation/MainActivity.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/sopt/and/presentation/MainActivity.kt b/app/src/main/java/org/sopt/and/presentation/MainActivity.kt index 8720edf..33942e8 100644 --- a/app/src/main/java/org/sopt/and/presentation/MainActivity.kt +++ b/app/src/main/java/org/sopt/and/presentation/MainActivity.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -17,7 +16,6 @@ import org.sopt.and.presentation.login.LogInScreen import org.sopt.and.presentation.main.MainScreen import org.sopt.and.presentation.navigation.Screen import org.sopt.and.presentation.signup.SignUpScreen -import org.sopt.and.presentation.signup.UserViewModel import org.sopt.and.ui.theme.ANDANDROIDTheme @AndroidEntryPoint @@ -29,17 +27,16 @@ class MainActivity : ComponentActivity() { ANDANDROIDTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> val navController = rememberNavController() - val userViewModel: UserViewModel = viewModel() NavHost( navController = navController, startDestination = Screen.LogIn.route, modifier = Modifier.padding(innerPadding) ) { composable(Screen.LogIn.route) { - LogInScreen(navController, userViewModel) + LogInScreen(navController) } composable(Screen.SignUp.route) { - SignUpScreen(navController, userViewModel) + SignUpScreen(navController) } composable(Screen.MainScreen.route) { MainScreen(navController) From f72b089dcbb25d29e88f9d63f0795271704a9539 Mon Sep 17 00:00:00 2001 From: TaejungKim <92737123+imtaejugkim@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:32:18 +0900 Subject: [PATCH 6/7] =?UTF-8?q?#13=20[Chore]=20:=20=EC=88=98=EC=A0=95=20Lo?= =?UTF-8?q?g=20=EB=AC=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/sopt/and/presentation/login/LogInViewModel.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/org/sopt/and/presentation/login/LogInViewModel.kt b/app/src/main/java/org/sopt/and/presentation/login/LogInViewModel.kt index 254c5e7..8ccbc68 100644 --- a/app/src/main/java/org/sopt/and/presentation/login/LogInViewModel.kt +++ b/app/src/main/java/org/sopt/and/presentation/login/LogInViewModel.kt @@ -1,6 +1,5 @@ package org.sopt.and.presentation.login -import android.util.Log import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -53,7 +52,6 @@ class LogInViewModel @Inject constructor( }, onFailure = { error -> val message = if (error is HttpException) { - Log.d("error", error.code().toString()) when (error.code()) { 400 -> R.string.log_in_method 403 -> R.string.sign_up_paswd From 9b408af4b9febc356551ecfd0aff8c962504b272 Mon Sep 17 00:00:00 2001 From: TaejungKim <92737123+imtaejugkim@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:34:03 +0900 Subject: [PATCH 7/7] =?UTF-8?q?#13=20[Chore]=20:=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EA=B5=AC=EB=AC=B8=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/sopt/and/presentation/login/LogInContract.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/sopt/and/presentation/login/LogInContract.kt b/app/src/main/java/org/sopt/and/presentation/login/LogInContract.kt index e4b0cfe..67fa406 100644 --- a/app/src/main/java/org/sopt/and/presentation/login/LogInContract.kt +++ b/app/src/main/java/org/sopt/and/presentation/login/LogInContract.kt @@ -9,7 +9,6 @@ data class LogInState( val username: String = "", val password: String = "", val isLoading: Boolean = false, - val errorMessage: Int? = null ) : UiState // Event 정의