diff --git a/feature/main/src/main/java/org/sopt/main/login/LoginContract.kt b/feature/main/src/main/java/org/sopt/main/login/LoginContract.kt new file mode 100644 index 0000000..22d09f2 --- /dev/null +++ b/feature/main/src/main/java/org/sopt/main/login/LoginContract.kt @@ -0,0 +1,19 @@ +package org.sopt.main.login + +import org.sopt.main.model.User + +data class LoginState( + val id: String = "", + val password: String = "", + val registeredId: String = "", + val registeredPassword: String = "", + val name: String = "", + val hobby: String = "" +) + +sealed interface LoginSideEffect { + data class showSnackbar(val message: String) : LoginSideEffect + data class LoginSuccess(val user: User) : LoginSideEffect + data object NavigateToSignUp : LoginSideEffect + data object SignupSuccess : LoginSideEffect +} \ No newline at end of file diff --git a/feature/main/src/main/java/org/sopt/main/login/LoginScreen.kt b/feature/main/src/main/java/org/sopt/main/login/LoginScreen.kt new file mode 100644 index 0000000..0a8a3c2 --- /dev/null +++ b/feature/main/src/main/java/org/sopt/main/login/LoginScreen.kt @@ -0,0 +1,164 @@ +package org.sopt.main.login + +import android.os.Bundle +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect +import org.sopt.designsystem.component.button.RegularButton +import org.sopt.designsystem.component.textfield.RegularTextField +import org.sopt.designsystem.ui.theme.NOWSOPTAndroidTheme +import org.sopt.main.model.User +import org.sopt.ui.intent.getParcelableSafe + +@Composable +fun LoginRoute( + navController: NavController, + viewModel: LoginViewModel = hiltViewModel(), + popBackStack: () -> Unit, + navigateHome: (User) -> Unit, + navigateSignup: () -> Unit, +){ + val state = viewModel.collectAsState().value + val snackBarHostState = remember { SnackbarHostState() } + + viewModel.collectSideEffect { sideEffect -> + when (sideEffect) { + is LoginSideEffect.NavigateToSignUp -> { + navigateSignup() + } + + is LoginSideEffect.LoginSuccess -> { + navigateHome(sideEffect.user) + } + LoginSideEffect.SignupSuccess -> { + snackBarHostState.currentSnackbarData?.dismiss() + snackBarHostState.showSnackbar("회원가입 성공") + } + is LoginSideEffect.showSnackbar -> { + snackBarHostState.currentSnackbarData?.dismiss() + snackBarHostState.showSnackbar(sideEffect.message) + } + } + } + + LaunchedEffect(true) { + navController.previousBackStackEntry?.savedStateHandle?.run { + val bundle = get("user") + val user = bundle?.getParcelableSafe("user", User::class.java) + viewModel.signupSuccess(user) + } + } + + LoginScreen( + snackBarHostState = snackBarHostState, + state = state, + onClickLoginBtn = viewModel::login, + onClickSignupBtn = viewModel::signup, + onValueChangeId = viewModel::updateId, + onValueChangePw = viewModel::updatePw + ) +} + +@Composable +fun LoginScreen( + snackBarHostState: SnackbarHostState, + state: LoginState = LoginState(), + onClickLoginBtn: () -> Unit = {}, + onClickSignupBtn: () -> Unit = {}, + onValueChangeId: (String) -> Unit = {}, + onValueChangePw: (String) -> Unit = {} +) { + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackBarHostState) } + ){ it -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 50.dp + it.calculateTopPadding(), bottom = 20.dp + it.calculateBottomPadding(), start = 20.dp, end = 20.dp) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box ( + modifier = Modifier + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Welcome To Sopt", + style = MaterialTheme.typography.headlineMedium, + color = Color.Black, + ) + } + + Spacer(modifier = Modifier.height(70.dp)) + + RegularTextField( + modifier = Modifier.padding(end = 20.dp), + title = "ID", + textStyle = MaterialTheme.typography.headlineSmall, + value = state.id, + placeholder = "아이디를 입력하세요", + onValueChange = onValueChangeId + ) + + Spacer(modifier = Modifier.height(70.dp)) + + RegularTextField( + modifier = Modifier.padding(end = 20.dp), + title = "PW", + textStyle = MaterialTheme.typography.headlineSmall, + value = state.password, + placeholder = "비밀번호를 입력하세요", + onValueChange = onValueChangePw + ) + + Spacer(modifier = Modifier.weight(2f)) + + RegularButton( + text = "로그인", + clickable = true, + onClick = onClickLoginBtn + ) + + Spacer(modifier = Modifier.height(30.dp)) + + RegularButton( + text = "회원가입", + clickable = true, + onClick = onClickSignupBtn + ) + + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun LoginScreenPreview() { + NOWSOPTAndroidTheme { + //LoginScreen() + } +} \ No newline at end of file diff --git a/feature/main/src/main/java/org/sopt/main/login/LoginViewModel.kt b/feature/main/src/main/java/org/sopt/main/login/LoginViewModel.kt new file mode 100644 index 0000000..cd85cc9 --- /dev/null +++ b/feature/main/src/main/java/org/sopt/main/login/LoginViewModel.kt @@ -0,0 +1,57 @@ +package org.sopt.main.login + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import org.sopt.main.model.User +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( +) : ContainerHost, ViewModel() { + override val container: Container = container(LoginState()) + + fun signup() = intent { + postSideEffect(LoginSideEffect.NavigateToSignUp) + } + + fun login() = intent { + with(state) { + when { + checkRegister() -> postSideEffect(LoginSideEffect.showSnackbar("회원가입 먼저하셈")) + matchesUserInfo(state.id, state.password) -> postSideEffect(LoginSideEffect.LoginSuccess(createUser())) + else -> postSideEffect(LoginSideEffect.showSnackbar("로그인 실패ㅁㄴ")) + } + } + } + private fun LoginState.checkRegister() = registeredId.isBlank() || registeredPassword.isBlank() + + private fun LoginState.matchesUserInfo(id: String, password: String) = registeredId == id && registeredPassword == password + + private fun LoginState.createUser() = User(registeredId, registeredPassword, name, hobby) + + fun updateId(id: String) = updateState { copy(id = id) } + fun updatePw(pw: String) = updateState { copy(password = pw) } + + private fun updateState(reducer: LoginState.() -> LoginState) = intent { + reduce { state.reducer() } + } + + fun signupSuccess(user: User?) = intent { + postSideEffect(LoginSideEffect.SignupSuccess) + + reduce { + state.copy( + registeredId = user?.id.orEmpty(), + registeredPassword = user?.pw.orEmpty(), + name = user?.name.orEmpty(), + hobby = user?.hobby.orEmpty() + ) + } + } +} \ No newline at end of file diff --git a/feature/main/src/main/java/org/sopt/main/login/navigation/LoginNavigation.kt b/feature/main/src/main/java/org/sopt/main/login/navigation/LoginNavigation.kt new file mode 100644 index 0000000..17dc939 --- /dev/null +++ b/feature/main/src/main/java/org/sopt/main/login/navigation/LoginNavigation.kt @@ -0,0 +1,37 @@ +package org.sopt.main.login.navigation + +import android.os.Bundle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import org.sopt.main.login.LoginRoute +import org.sopt.main.model.User + +fun NavController.navigateLogin(user: User) { + val bundle = Bundle().apply { + putParcelable("user", user) + } + navigate(LoginRoute.route){ + currentBackStackEntry?.savedStateHandle?.set("user", bundle) + } +} + +fun NavGraphBuilder.loginNavGraph( + navController: NavController, + popBackStack: () -> Unit = {}, + navigateSignup: () -> Unit = {}, + navigateHome: (User) -> Unit = {}, +){ + composable(route = LoginRoute.route) { navBackStackEntry -> + LoginRoute( + navController = navController, + popBackStack = popBackStack, + navigateHome = navigateHome, + navigateSignup = navigateSignup + ) + } +} + +object LoginRoute { + const val route = "login" +} \ No newline at end of file