diff --git a/app/build.gradle b/app/build.gradle index 045c25b..1e7e43a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,8 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'com.google.devtools.ksp' + id 'dagger.hilt.android.plugin' } android { @@ -50,24 +52,36 @@ dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.activity:activity-compose:1.8.2' + + // Compose BOM implementation platform('androidx.compose:compose-bom:2024.03.00') implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material3:material3' - implementation 'androidx.compose.ui:ui-graphics-android:1.6.8' - implementation 'androidx.compose.ui:ui-test-android:1.6.8' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation platform('androidx.compose:compose-bom:2024.03.00') - androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + + // Debugging and Testing debugImplementation 'androidx.compose.ui:ui-tooling' + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' debugImplementation 'androidx.compose.ui:ui-test-manifest' + androidTestImplementation platform('androidx.compose:compose-bom:2024.03.00') // Navigation implementation 'androidx.navigation:navigation-compose:2.7.7' // Serialization - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' + + // Hilt (Dependency Injection) + implementation 'com.google.dagger:hilt-android:2.49' + ksp 'com.google.dagger:hilt-compiler:2.49' + implementation 'androidx.hilt:hilt-navigation-compose:1.2.0' + + // Testing + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.appcompat:appcompat:1.6.1' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4523ccb..7e580ca 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> diff --git a/app/src/main/java/com/sopt/now/compose/NOWSoptApp.kt b/app/src/main/java/com/sopt/now/compose/NOWSoptApp.kt new file mode 100644 index 0000000..b8d6548 --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/NOWSoptApp.kt @@ -0,0 +1,20 @@ +package com.sopt.now.compose + +import android.app.Application +import android.content.Context +import androidx.appcompat.app.AppCompatDelegate +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class NOWSoptApp : Application() { + override fun onCreate() { + super.onCreate() + appContext = applicationContext + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + } + + companion object { + lateinit var appContext: Context + private set + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/component/AuthTextField.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/component/AuthTextField.kt index ef16c93..a7dd629 100644 --- a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/component/AuthTextField.kt +++ b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/component/AuthTextField.kt @@ -3,6 +3,7 @@ package com.sopt.now.compose.presentation.ui.auth.component import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -11,15 +12,19 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.painterResource 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 @@ -28,14 +33,17 @@ import com.sopt.now.compose.ui.theme.CustomTheme @Composable fun AuthTextField( - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, + value: String, + onValueChange: (String) -> Unit, modifier: Modifier = Modifier, isFocused: Boolean, onFocusChanged: (Boolean) -> Unit, onRemove: () -> Unit, hint: String, - visualTransformation: VisualTransformation = VisualTransformation.None + visualTransformation: VisualTransformation = VisualTransformation.None, + focusRequester: FocusRequester = FocusRequester(), + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, ) { Box(modifier = modifier) { BasicTextField( @@ -43,10 +51,13 @@ fun AuthTextField( onValueChange = onValueChange, modifier = Modifier .fillMaxWidth() + .focusRequester(focusRequester) .onFocusChanged { focusState -> onFocusChanged(focusState.isFocused) }, textStyle = TextStyle(color = CustomTheme.colors.gray01), + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, singleLine = true, visualTransformation = visualTransformation, cursorBrush = SolidColor(CustomTheme.colors.gray01), @@ -60,8 +71,10 @@ fun AuthTextField( .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Box { - if (value.text.isEmpty()) { + Box( + modifier = Modifier.weight(1f) + ) { + if (value.isEmpty()) { Text( text = hint, style = CustomTheme.typography.body1Medium, @@ -70,15 +83,17 @@ fun AuthTextField( } innerTextField() } - Spacer(modifier = Modifier.weight(1f)) Image( painter = painterResource(id = R.drawable.ic_textfield_clear), contentDescription = null, modifier = Modifier .padding(end = 6.dp) - .clickable { onRemove() } + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onRemove + ) ) - } Spacer(modifier = Modifier.height(4.dp)) Box( diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/component/SignInButton.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/component/SignInButton.kt deleted file mode 100644 index 1cceee7..0000000 --- a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/component/SignInButton.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.sopt.now.compose.presentation.ui.auth.component - -import android.content.Context -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.sopt.now.compose.R -import com.sopt.now.compose.presentation.utils.showToast -import com.sopt.now.compose.ui.theme.CustomTheme - -@Composable -fun SignInButton( - context: Context, - inputId: String, - inputPassword: String, - id: String, - password: String, - nickname: String, - phoneNumber: String, - onClickSignIn: (String, String, String, String) -> Unit -) { - Button( - onClick = { - when { - inputId.isEmpty() || inputPassword.isEmpty() -> showToast( - context, - context.getString(R.string.signin_signin_failure_toast) - ) - - inputId != id -> showToast( - context, - context.getString(R.string.signin_signin_id_incorrect) - ) - - inputPassword != password -> showToast( - context, - context.getString(R.string.signin_signin_password_incorrect) - ) - - else -> { - showToast(context, context.getString(R.string.signin_signin_success_toast)) - onClickSignIn(id, password, nickname, phoneNumber) - } - } - }, - modifier = Modifier - .fillMaxWidth(), - colors = ButtonDefaults.buttonColors(CustomTheme.colors.mainYellow), - shape = RoundedCornerShape(10.dp) - ) { - Text( - text = stringResource(R.string.signin_signin_button), - color = CustomTheme.colors.gray01 - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/component/SignUpTextButton.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/component/SignUpTextButton.kt deleted file mode 100644 index 16bce3d..0000000 --- a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/component/SignUpTextButton.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.sopt.now.compose.presentation.ui.auth.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.sopt.now.compose.R -import com.sopt.now.compose.ui.theme.CustomTheme - -@Composable -fun SignUpTextButton( - modifier: Modifier -) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource(R.string.signin_signup_button), - color = CustomTheme.colors.gray04, - style = CustomTheme.typography.body2Medium - ) - Box( - modifier = Modifier - .height(1.dp) - .width(60.dp) - .background(CustomTheme.colors.gray04) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/navigation/AuthNavGraph.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/navigation/AuthNavGraph.kt index 112207b..bc0529f 100644 --- a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/navigation/AuthNavGraph.kt +++ b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/navigation/AuthNavGraph.kt @@ -1,51 +1,26 @@ package com.sopt.now.compose.presentation.ui.auth.navigation import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType import androidx.navigation.compose.composable -import androidx.navigation.navArgument +import com.sopt.now.compose.presentation.ui.auth.screen.AuthViewModel import com.sopt.now.compose.presentation.ui.auth.screen.SignInRoute import com.sopt.now.compose.presentation.ui.auth.screen.SignUpRoute -import com.sopt.now.compose.presentation.ui.home.navigation.HomeNavigator -import com.sopt.now.compose.presentation.ui.home.screen.HomeRoute fun NavGraphBuilder.authNavGraph( authNavigator: AuthNavigator, - homeNavigator: HomeNavigator + authViewModel: AuthViewModel ) { - composable( - route = "signIn/{id}/{password}/{nickname}/{phoneNumber}", - arguments = listOf( - navArgument("id") { type = NavType.StringType }, - navArgument("password") { type = NavType.StringType }, - navArgument("nickname") { type = NavType.StringType }, - navArgument("phoneNumber") { type = NavType.StringType } + composable(route = "signIn") { + SignInRoute( + authNavigator = authNavigator, + authViewModel = authViewModel ) - ) { backStackEntry -> - val id = backStackEntry.arguments?.getString("id") ?: "" - val password = backStackEntry.arguments?.getString("password") ?: "" - val nickname = backStackEntry.arguments?.getString("nickname") ?: "" - val phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "" - SignInRoute(authNavigator, id, password, nickname, phoneNumber) } composable(route = "signUp") { - SignUpRoute(authNavigator) - } - - composable( - route = "home/{id}/{password}/{nickname}/{phoneNumber}", - arguments = listOf( - navArgument("id") { type = NavType.StringType }, - navArgument("password") { type = NavType.StringType }, - navArgument("nickname") { type = NavType.StringType }, - navArgument("phoneNumber") { type = NavType.StringType } + SignUpRoute( + authNavigator = authNavigator, + authViewModel = authViewModel ) - ) { backStackEntry -> - val id = backStackEntry.arguments?.getString("id") ?: "" - val password = backStackEntry.arguments?.getString("password") ?: "" - val nickname = backStackEntry.arguments?.getString("nickname") ?: "" - val phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "" - HomeRoute(homeNavigator, id, password, nickname, phoneNumber) } } \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/navigation/AuthNavigator.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/navigation/AuthNavigator.kt index d690c23..2c44a9a 100644 --- a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/navigation/AuthNavigator.kt +++ b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/navigation/AuthNavigator.kt @@ -5,15 +5,15 @@ import androidx.navigation.NavController class AuthNavigator( private val navController: NavController ) { - fun navigateToSignIn(id: String, password: String, nickname: String, phoneNumber: String) { - navController.navigate("signIn/$id/$password/$nickname/$phoneNumber") + fun navigateToSignIn() { + navController.navigate("signIn") } fun navigateToSignUp() { navController.navigate("signUp") } - fun navigateToHome(id: String, password: String, nickname: String, phoneNumber: String) { + fun navigateToHome(id: String?, password: String?, nickname: String?, phoneNumber: String?) { navController.navigate("home/$id/$password/$nickname/$phoneNumber") } diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/AuthViewModel.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/AuthViewModel.kt new file mode 100644 index 0000000..3355d5c --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/AuthViewModel.kt @@ -0,0 +1,174 @@ +package com.sopt.now.compose.presentation.ui.auth.screen + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import com.sopt.now.compose.data.User +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import java.util.regex.Pattern +import javax.inject.Inject + +@HiltViewModel +class AuthViewModel @Inject constructor() : ViewModel() { + var signInId by mutableStateOf("") + private set + var signInPassword by mutableStateOf("") + private set + var signUpId by mutableStateOf("") + private set + var signUpPassword by mutableStateOf("") + private set + var signUpNickname by mutableStateOf("") + private set + var signUpPhoneNumber by mutableStateOf("") + private set + + var isSignInIdTextFieldFocused by mutableStateOf(false) + private set + var isSignInPasswordTextFieldFocused by mutableStateOf(false) + private set + var isSignUpIdTextFieldFocused by mutableStateOf(false) + private set + var isSignUpPasswordTextFieldFocused by mutableStateOf(false) + private set + var isSignUpNicknameTextFieldFocused by mutableStateOf(false) + private set + var isSignUpPhoneNumberTextFieldFocused by mutableStateOf(false) + private set + + private val _user = MutableStateFlow(null) + val user: StateFlow = _user + + private val _signInState = MutableStateFlow(SignInState.Idle) + val signInState: StateFlow = _signInState + + private val _signUpState = MutableStateFlow(SignUpState.Idle) + val signUpState: StateFlow = _signUpState + + fun onSignInIdChange(newId: String) { + signInId = newId + } + + fun onSignInPasswordChange(newPassword: String) { + signInPassword = newPassword + } + + fun onSignInIdFocusChange(isFocused: Boolean) { + isSignInIdTextFieldFocused = isFocused + } + + fun onSignInPasswordFocusChange(isFocused: Boolean) { + isSignInPasswordTextFieldFocused = isFocused + } + + private fun signInIdValidation(): Boolean { + return signInId.isNotEmpty() && signInId == _user.value?.id + } + + private fun signInPasswordValidation(): Boolean { + return signInPassword.isNotEmpty() && signInPassword == _user.value?.password + } + + fun signInValidation() { + _signInState.value = + when { + (!signInIdValidation()) -> SignInState.IdInvalid + (!signInPasswordValidation()) -> SignInState.PasswordInvalid + else -> SignInState.Success + } + } + + fun onSignUpIdChange(newId: String) { + signUpId = newId + } + + fun onSignUpPasswordChange(newPassword: String) { + signUpPassword = newPassword + } + + fun onSignUpNicknameChange(newNickname: String) { + signUpNickname = newNickname + } + + fun onSignUpPhoneNumberChange(newPhoneNumber: String) { + signUpPhoneNumber = newPhoneNumber + } + + fun onSignUpIdFocusChange(isFocused: Boolean) { + isSignUpIdTextFieldFocused = isFocused + } + + fun onSignUpPasswordFocusChange(isFocused: Boolean) { + isSignUpPasswordTextFieldFocused = isFocused + } + + fun onSignUpNicknameFocusChange(isFocused: Boolean) { + isSignUpNicknameTextFieldFocused = isFocused + } + + fun onSignUpPhoneNumberFocusChange(isFocused: Boolean) { + isSignUpPhoneNumberTextFieldFocused = isFocused + } + + private fun setUser() { + _user.value = User( + id = signUpId, + password = signUpPassword, + nickname = signUpNickname, + phoneNumber = signUpPhoneNumber + ) + } + + private fun signUpIdValidation(): Boolean { + return ID_VALIDATION_PATTERN.matcher(_user.value?.id.toString()).matches() + } + + private fun signUpPasswordValidation(): Boolean { + return PW_VALIDATION_PATTERN.matcher(_user.value?.password.toString()).matches() + } + + private fun signUpNicknameValidation(): Boolean { + return NICKNAME_VALIDATION_PATTERN.matcher(_user.value?.nickname.toString()).matches() + } + + private fun signUpPhoneNumberValidation(): Boolean { + return PHONE_NUMBER_VALIDATION_PATTERN.matcher(_user.value?.phoneNumber.toString()) + .matches() + } + + fun signUpValidation() { + setUser() + _signUpState.value = + when { + (!signUpIdValidation()) -> SignUpState.IdInvalid + (!signUpPasswordValidation()) -> SignUpState.PasswordInvalid + (!signUpNicknameValidation()) -> SignUpState.NicknameInvalid + (!signUpPhoneNumberValidation()) -> SignUpState.PhoneNumberInvalid + else -> SignUpState.Success + } + } + + companion object { + private const val ID_MIN_LENGTH = 6 + private const val ID_MAX_LENGTH = 10 + private const val PW_MIN_LENGTH = 8 + private const val PW_MAX_LENGTH = 12 + + private const val ID_VALIDATION_REGEX = "^[a-zA-Z0-9]{$ID_MIN_LENGTH,$ID_MAX_LENGTH}$" + private val ID_VALIDATION_PATTERN: Pattern = Pattern.compile(ID_VALIDATION_REGEX) + + private const val PW_VALIDATION_REGEX = "^[a-zA-Z0-9]{$PW_MIN_LENGTH,$PW_MAX_LENGTH}$" + private val PW_VALIDATION_PATTERN: Pattern = Pattern.compile(PW_VALIDATION_REGEX) + + private const val NICKNAME_VALIDATION_REGEX = "^\\S+$" + private val NICKNAME_VALIDATION_PATTERN: Pattern = + Pattern.compile(NICKNAME_VALIDATION_REGEX) + + private const val PHONE_NUMBER_VALIDATION_REGEX = "^010-\\d{4}-\\d{4}$" + private val PHONE_NUMBER_VALIDATION_PATTERN: Pattern = + Pattern.compile(PHONE_NUMBER_VALIDATION_REGEX) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignInScreen.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignInScreen.kt index 3ca5c4c..b75ab07 100644 --- a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignInScreen.kt +++ b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignInScreen.kt @@ -1,39 +1,35 @@ package com.sopt.now.compose.presentation.ui.auth.screen -import android.content.Context import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.interaction.MutableInteractionSource 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.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sopt.now.compose.R import com.sopt.now.compose.presentation.ui.auth.component.AuthTextField -import com.sopt.now.compose.presentation.ui.auth.component.SignInButton -import com.sopt.now.compose.presentation.ui.auth.component.SignUpTextButton import com.sopt.now.compose.presentation.ui.auth.navigation.AuthNavigator import com.sopt.now.compose.presentation.utils.showToast import com.sopt.now.compose.ui.theme.CustomTheme @@ -41,102 +37,131 @@ import com.sopt.now.compose.ui.theme.CustomTheme @Composable fun SignInRoute( authNavigator: AuthNavigator, - id: String, - password: String, - nickname: String, - phoneNumber: String + authViewModel: AuthViewModel ) { + val context = LocalContext.current + + fun onClickSignIn() { + authViewModel.signInValidation() + when (authViewModel.signInState.value) { + is SignInState.IdInvalid -> { + showToast(context, context.getString(R.string.signin_signin_id_incorrect)) + } + + is SignInState.PasswordInvalid -> { + showToast(context, context.getString(R.string.signin_signin_password_incorrect)) + } + + is SignInState.Success -> { + showToast(context, context.getString(R.string.signin_signin_success_toast)) + authNavigator.navigateToHome( + authViewModel.user.value?.id, + authViewModel.user.value?.password, + authViewModel.user.value?.nickname, + authViewModel.user.value?.phoneNumber, + ) + } + + else -> {} + } + } + SignInScreen( - onClickSignIn = { id, password, nickname, phoneNumber -> - authNavigator.navigateToHome( - id, - password, - nickname, - phoneNumber - ) - }, + authViewModel = authViewModel, + onClickSignIn = { onClickSignIn() }, onClickSignUp = { authNavigator.navigateToSignUp() }, - id = id, - password = password, - nickname = nickname, - phoneNumber = phoneNumber ) } @Composable fun SignInScreen( - onClickSignIn: (String, String, String, String) -> Unit, + authViewModel: AuthViewModel, + onClickSignIn: () -> Unit, onClickSignUp: () -> Unit, - id: String, - password: String, - nickname: String, - phoneNumber: String ) { - val context = LocalContext.current - var inputId by remember { mutableStateOf(TextFieldValue("")) } - var inputPassword by remember { mutableStateOf(TextFieldValue("")) } - var isIdTextFieldFocused by remember { mutableStateOf(false) } - var isPasswordTextFieldFocused by remember { mutableStateOf(false) } + val idFocusRequester = remember { FocusRequester() } + val passwordFocusRequester = remember { FocusRequester() } Column( modifier = Modifier .fillMaxSize() + .background(color = CustomTheme.colors.white) .padding(30.dp), horizontalAlignment = Alignment.CenterHorizontally ) { + Spacer(modifier = Modifier.height(30.dp)) Text( text = stringResource(R.string.signin_title), - modifier = Modifier - .wrapContentWidth() - .padding(top = 30.dp), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, style = CustomTheme.typography.head1 ) Spacer(modifier = Modifier.height(80.dp)) AuthTextField( - value = inputId, - onValueChange = { inputId = it }, - modifier = Modifier - .fillMaxWidth(), - isFocused = isIdTextFieldFocused, - onFocusChanged = { isIdTextFieldFocused = it }, - onRemove = { inputId = TextFieldValue("") }, - hint = stringResource(R.string.signin_id_hint) + value = authViewModel.signInId, + onValueChange = { authViewModel.onSignInIdChange(it) }, + modifier = Modifier.fillMaxWidth(), + isFocused = authViewModel.isSignInIdTextFieldFocused, + onFocusChanged = { authViewModel.onSignInIdFocusChange(it) }, + onRemove = { authViewModel.onSignInIdChange("") }, + hint = stringResource(R.string.signin_id_hint), + focusRequester = idFocusRequester, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { passwordFocusRequester.requestFocus() }) ) Spacer(modifier = Modifier.height(40.dp)) AuthTextField( - value = inputPassword, - onValueChange = { inputPassword = it }, - modifier = Modifier - .fillMaxWidth(), - isFocused = isPasswordTextFieldFocused, - onFocusChanged = { isPasswordTextFieldFocused = it }, - onRemove = { inputPassword = TextFieldValue("") }, + value = authViewModel.signInPassword, + onValueChange = { authViewModel.onSignInPasswordChange(it) }, + modifier = Modifier.fillMaxWidth(), + isFocused = authViewModel.isSignInPasswordTextFieldFocused, + onFocusChanged = { authViewModel.onSignInPasswordFocusChange(it) }, + onRemove = { authViewModel.onSignInPasswordChange("") }, hint = stringResource(R.string.signin_password_hint), - visualTransformation = PasswordVisualTransformation() + visualTransformation = PasswordVisualTransformation(), + focusRequester = passwordFocusRequester, ) Spacer(modifier = Modifier.height(40.dp)) - SignUpTextButton( + Text( + text = stringResource(R.string.signin_signup_button), modifier = Modifier - .wrapContentSize() .align(Alignment.End) - .clickable { onClickSignUp() } + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onClickSignUp + ), + color = CustomTheme.colors.gray04, + textDecoration = TextDecoration.Underline, + style = CustomTheme.typography.body2Medium ) Spacer(modifier = Modifier.weight(1f)) SignInButton( - context = context, - inputId = inputId.text, - inputPassword = inputPassword.text, - id = id, - password = password, - nickname = nickname, - phoneNumber = phoneNumber, + modifier = Modifier.fillMaxWidth(), onClickSignIn = onClickSignIn ) } } +@Composable +fun SignInButton( + modifier: Modifier = Modifier, + onClickSignIn: () -> Unit +) { + Button( + onClick = onClickSignIn, + modifier = modifier, + colors = ButtonDefaults.buttonColors(CustomTheme.colors.mainYellow), + shape = RoundedCornerShape(10.dp) + ) { + Text( + text = stringResource(R.string.signin_signin_button), color = CustomTheme.colors.gray01 + ) + } +} + @Preview(showBackground = true) @Composable -fun show() { +fun ShowSignInScreen() { } \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignInState.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignInState.kt new file mode 100644 index 0000000..9590e34 --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignInState.kt @@ -0,0 +1,8 @@ +package com.sopt.now.compose.presentation.ui.auth.screen + +sealed class SignInState { + data object Idle: SignInState() + data object IdInvalid: SignInState() + data object PasswordInvalid: SignInState() + data object Success: SignInState() +} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignUpScreen.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignUpScreen.kt index 20c483a..8d98e1a 100644 --- a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignUpScreen.kt +++ b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignUpScreen.kt @@ -1,31 +1,33 @@ package com.sopt.now.compose.presentation.ui.auth.screen -import androidx.compose.foundation.layout.Column +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box 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.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sopt.now.compose.R -import com.sopt.now.compose.data.User import com.sopt.now.compose.presentation.ui.auth.component.AuthTextField import com.sopt.now.compose.presentation.ui.auth.navigation.AuthNavigator import com.sopt.now.compose.presentation.utils.showToast @@ -33,158 +35,154 @@ import com.sopt.now.compose.ui.theme.CustomTheme @Composable fun SignUpRoute( - authNavigator: AuthNavigator + authNavigator: AuthNavigator, + authViewModel: AuthViewModel ) { - SignUpScreen( - onClickSignUp = { id, password, nickname, phoneNumber -> - authNavigator.navigateToSignIn( - id, - password, - nickname, - phoneNumber - ) + val context = LocalContext.current + + fun onClickSignUp() { + authViewModel.signUpValidation() + when (authViewModel.signUpState.value) { + is SignUpState.IdInvalid -> { + showToast(context, context.getString(R.string.signup_id_invalid)) + } + + is SignUpState.PasswordInvalid -> { + showToast(context, context.getString(R.string.signup_password_invalid)) + } + + is SignUpState.NicknameInvalid -> { + showToast(context, context.getString(R.string.signup_nickname_invalid)) + } + + is SignUpState.PhoneNumberInvalid -> { + showToast(context, context.getString(R.string.signup_phone_number_invalid)) + } + + is SignUpState.Success -> { + showToast(context, context.getString(R.string.signup_signup_success)) + authNavigator.navigateToSignIn() + Log.d("user_info", authViewModel.user.value.toString()) + } + + else -> {} } + } + + SignUpScreen( + authViewModel = authViewModel, + onClickSignUp = { onClickSignUp() } ) } @Composable fun SignUpScreen( - onClickSignUp: (String, String, String, String) -> Unit + authViewModel: AuthViewModel, + onClickSignUp: () -> Unit ) { - val context = LocalContext.current - - var inputId by remember { mutableStateOf(TextFieldValue("")) } - var inputPassword by remember { mutableStateOf(TextFieldValue("")) } - var inputNickname by remember { mutableStateOf(TextFieldValue("")) } - var inputPhoneNumber by remember { mutableStateOf(TextFieldValue("")) } - - var isIdTextFieldFocused by remember { mutableStateOf(false) } - var isPasswordTextFieldFocused by remember { mutableStateOf(false) } - var isNicknameTextFieldFocused by remember { mutableStateOf(false) } - var isPhoneNumberTextFieldFocused by remember { mutableStateOf(false) } + val idFocusRequester = remember { FocusRequester() } + val passwordFocusRequester = remember { FocusRequester() } + val nicknameFocusRequester = remember { FocusRequester() } + val phoneNumberFocusRequester = remember { FocusRequester() } - Column( + Box( modifier = Modifier .fillMaxSize() + .background(color = CustomTheme.colors.white) .padding(30.dp), - horizontalAlignment = Alignment.CenterHorizontally + contentAlignment = Alignment.TopCenter ) { - Spacer(modifier = Modifier.height(30.dp)) - - Text( - text = stringResource(R.string.signup_title), - modifier = Modifier - .wrapContentSize(), - style = CustomTheme.typography.head1 - ) - - Spacer(modifier = Modifier.height(30.dp)) - - Text( - text = stringResource(R.string.signup_id_title), - modifier = Modifier - .wrapContentSize() - .align(Alignment.Start), - style = CustomTheme.typography.head2 - ) - Spacer(modifier = Modifier.height(18.dp)) - AuthTextField( - value = inputId, - onValueChange = { inputId = it }, - modifier = Modifier - .fillMaxWidth(), - isFocused = isIdTextFieldFocused, - onFocusChanged = { isIdTextFieldFocused = it }, - onRemove = { inputId = TextFieldValue("") }, - hint = stringResource(R.string.signup_id_hint) - ) - - Spacer(modifier = Modifier.height(30.dp)) - - Text( - text = stringResource(R.string.signup_password_title), - modifier = Modifier - .wrapContentSize() - .align(Alignment.Start), - style = CustomTheme.typography.head2 - ) - Spacer(modifier = Modifier.height(18.dp)) - AuthTextField( - value = inputPassword, - onValueChange = { inputPassword = it }, - modifier = Modifier - .fillMaxWidth(), - isFocused = isPasswordTextFieldFocused, - onFocusChanged = { isPasswordTextFieldFocused = it }, - onRemove = { inputPassword = TextFieldValue("") }, - hint = stringResource(R.string.signup_password_hint), - visualTransformation = PasswordVisualTransformation() - ) - - Spacer(modifier = Modifier.height(30.dp)) - - Text( - text = stringResource(R.string.signup_nickname_title), - modifier = Modifier - .wrapContentSize() - .align(Alignment.Start), - style = CustomTheme.typography.head2 - ) - Spacer(modifier = Modifier.height(18.dp)) - AuthTextField( - value = inputNickname, - onValueChange = { inputNickname = it }, - modifier = Modifier - .fillMaxWidth(), - isFocused = isNicknameTextFieldFocused, - onFocusChanged = { isNicknameTextFieldFocused = it }, - onRemove = { inputNickname = TextFieldValue("") }, - hint = stringResource(R.string.signup_nickname_hint) - ) - - Spacer(modifier = Modifier.height(30.dp)) - - Text( - text = stringResource(R.string.signup_phone_number_title), - modifier = Modifier - .wrapContentSize() - .align(Alignment.Start), - style = CustomTheme.typography.head2 - ) - Spacer(modifier = Modifier.height(18.dp)) - AuthTextField( - value = inputPhoneNumber, - onValueChange = { inputPhoneNumber = it }, - modifier = Modifier - .fillMaxWidth(), - isFocused = isPhoneNumberTextFieldFocused, - onFocusChanged = { isPhoneNumberTextFieldFocused = it }, - onRemove = { inputPhoneNumber = TextFieldValue("") }, - hint = stringResource(R.string.signup_phone_number_hint) - ) - - Spacer(modifier = Modifier.weight(1f)) - - Button( - onClick = { - val user = setUser( - inputId.text, - inputPassword.text, - inputPassword.text, - inputPhoneNumber.text + LazyColumn( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + item { + Spacer(modifier = Modifier.height(30.dp)) + Text( + text = stringResource(R.string.signup_title), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = CustomTheme.typography.head1 + ) + Spacer(modifier = Modifier.height(30.dp)) + Text( + text = stringResource(R.string.signup_id_title), + style = CustomTheme.typography.head2 + ) + Spacer(modifier = Modifier.height(18.dp)) + AuthTextField( + value = authViewModel.signUpId, + onValueChange = { authViewModel.onSignUpIdChange(it) }, + modifier = Modifier.fillMaxWidth(), + isFocused = authViewModel.isSignUpIdTextFieldFocused, + onFocusChanged = { authViewModel.onSignUpIdFocusChange(it) }, + onRemove = { authViewModel.onSignUpIdChange("") }, + hint = stringResource(R.string.signup_id_hint), + focusRequester = idFocusRequester, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { passwordFocusRequester.requestFocus() }) + ) + Spacer(modifier = Modifier.height(30.dp)) + Text( + text = stringResource(R.string.signup_password_title), + style = CustomTheme.typography.head2 + ) + Spacer(modifier = Modifier.height(18.dp)) + AuthTextField( + value = authViewModel.signUpPassword, + onValueChange = { authViewModel.onSignUpPasswordChange(it) }, + modifier = Modifier.fillMaxWidth(), + isFocused = authViewModel.isSignUpPasswordTextFieldFocused, + onFocusChanged = { authViewModel.onSignUpPasswordFocusChange(it) }, + onRemove = { authViewModel.onSignUpPasswordChange("") }, + hint = stringResource(R.string.signup_password_hint), + visualTransformation = PasswordVisualTransformation(), + focusRequester = passwordFocusRequester, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { nicknameFocusRequester.requestFocus() }) + ) + Spacer(modifier = Modifier.height(30.dp)) + Text( + text = stringResource(R.string.signup_nickname_title), + style = CustomTheme.typography.head2 ) - if (SignUpValidation.isSignUpValid(user)) { - showToast(context, context.getString(R.string.signup_signup_success)) - onClickSignUp( - inputId.text, - inputPassword.text, - inputNickname.text, - inputPhoneNumber.text - ) - } else showToast(context, context.getString(R.string.signup_signup_failure)) - }, + Spacer(modifier = Modifier.height(18.dp)) + AuthTextField( + value = authViewModel.signUpNickname, + onValueChange = { authViewModel.onSignUpNicknameChange(it) }, + modifier = Modifier.fillMaxWidth(), + isFocused = authViewModel.isSignUpNicknameTextFieldFocused, + onFocusChanged = { authViewModel.onSignUpNicknameFocusChange(it) }, + onRemove = { authViewModel.onSignUpNicknameChange("") }, + hint = stringResource(R.string.signup_nickname_hint), + focusRequester = nicknameFocusRequester, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { phoneNumberFocusRequester.requestFocus() }) + ) + Spacer(modifier = Modifier.height(30.dp)) + Text( + text = stringResource(R.string.signup_phone_number_title), + style = CustomTheme.typography.head2 + ) + Spacer(modifier = Modifier.height(18.dp)) + AuthTextField( + value = authViewModel.signUpPhoneNumber, + onValueChange = { authViewModel.onSignUpPhoneNumberChange(it) }, + modifier = Modifier.fillMaxWidth(), + isFocused = authViewModel.isSignUpPhoneNumberTextFieldFocused, + onFocusChanged = { authViewModel.onSignUpPhoneNumberFocusChange(it) }, + onRemove = { authViewModel.onSignUpPhoneNumberChange("") }, + hint = stringResource(R.string.signup_phone_number_hint), + focusRequester = phoneNumberFocusRequester + ) + Spacer(modifier = Modifier.height(80.dp)) + } + } + Button( + onClick = onClickSignUp, modifier = Modifier - .fillMaxWidth(), + .fillMaxWidth() + .align(Alignment.BottomCenter), colors = ButtonDefaults.buttonColors(CustomTheme.colors.mainYellow), shape = RoundedCornerShape(10.dp) ) { @@ -196,21 +194,9 @@ fun SignUpScreen( } } -fun setUser( - inputId: String, - inputPassword: String, - inputNickname: String, - inputPhoneNumber: String -): User = User( - id = inputId, - password = inputPassword, - nickname = inputNickname, - phoneNumber = inputPhoneNumber -) - @Preview(showBackground = true) @Composable -fun showSignUp() { +fun ShowSignUp() { } \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignUpState.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignUpState.kt new file mode 100644 index 0000000..4264a15 --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignUpState.kt @@ -0,0 +1,10 @@ +package com.sopt.now.compose.presentation.ui.auth.screen + +sealed class SignUpState { + data object Idle: SignUpState() + data object IdInvalid: SignUpState() + data object PasswordInvalid: SignUpState() + data object NicknameInvalid: SignUpState() + data object PhoneNumberInvalid: SignUpState() + data object Success: SignUpState() +} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignUpValidation.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignUpValidation.kt deleted file mode 100644 index 6c68a76..0000000 --- a/app/src/main/java/com/sopt/now/compose/presentation/ui/auth/screen/SignUpValidation.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.sopt.now.compose.presentation.ui.auth.screen - -import com.sopt.now.compose.data.User -import java.util.regex.Pattern - -object SignUpValidation { - - private const val ID_MIN_LENGTH = 6 - private const val ID_MAX_LENGTH = 10 - private const val PW_MIN_LENGTH = 8 - private const val PW_MAX_LENGTH = 12 - - private const val ID_VALIDATION_REGEX = "^[a-zA-Z0-9]{$ID_MIN_LENGTH,$ID_MAX_LENGTH}$" - private val ID_VALIDATION_PATTERN: Pattern = Pattern.compile(ID_VALIDATION_REGEX) - - private const val PW_VALIDATION_REGEX = "^[a-zA-Z0-9]{$PW_MIN_LENGTH,$PW_MAX_LENGTH}$" - private val PW_VALIDATION_PATTERN: Pattern = Pattern.compile(PW_VALIDATION_REGEX) - - private const val NICKNAME_VALIDATION_REGEX = "^\\S+$" - private val NICKNAME_VALIDATION_PATTERN: Pattern = Pattern.compile(NICKNAME_VALIDATION_REGEX) - - private const val PHONE_NUMBER_VALIDATION_REGEX = "^010-\\d{4}-\\d{4}$" - private val PHONE_NUMBER_VALIDATION_PATTERN: Pattern = Pattern.compile(PHONE_NUMBER_VALIDATION_REGEX) - - private fun isIdValid(inputId: String): Boolean { - return ID_VALIDATION_PATTERN.matcher(inputId).matches() - } - - private fun isPasswordValid(inputPassword: String): Boolean { - return PW_VALIDATION_PATTERN.matcher(inputPassword).matches() - } - - private fun isNicknameValid(inputNickname: String): Boolean { - return NICKNAME_VALIDATION_PATTERN.matcher(inputNickname).matches() - } - - private fun isPhoneNumberValid(inputPhoneNumber: String): Boolean { - return PHONE_NUMBER_VALIDATION_PATTERN.matcher(inputPhoneNumber).matches() - } - - fun isSignUpValid(user: User): Boolean { - return isIdValid(user.id) && - isPasswordValid(user.password) && - isNicknameValid(user.nickname) && - isPhoneNumberValid(user.phoneNumber) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/home/navigation/HomeNavGraph.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/home/navigation/HomeNavGraph.kt index a50b78e..01504da 100644 --- a/app/src/main/java/com/sopt/now/compose/presentation/ui/home/navigation/HomeNavGraph.kt +++ b/app/src/main/java/com/sopt/now/compose/presentation/ui/home/navigation/HomeNavGraph.kt @@ -6,9 +6,8 @@ import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.sopt.now.compose.presentation.ui.home.screen.HomeRoute -fun NavGraphBuilder.homeNavGraph( - homeNavigator: HomeNavigator -) { +fun NavGraphBuilder.homeNavGraph(homeNavigator: HomeNavigator) { + composable( route = "home/{id}/{password}/{nickname}/{phoneNumber}", arguments = listOf( diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/home/screen/HomeScreen.kt index 6aad6ea..8d3edcb 100644 --- a/app/src/main/java/com/sopt/now/compose/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/now/compose/presentation/ui/home/screen/HomeScreen.kt @@ -1,6 +1,9 @@ package com.sopt.now.compose.presentation.ui.home.screen +import android.app.Activity +import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -10,8 +13,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +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 import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -19,6 +27,7 @@ import androidx.compose.ui.unit.dp import com.sopt.now.compose.R import com.sopt.now.compose.presentation.ui.home.component.UserInfoText import com.sopt.now.compose.presentation.ui.home.navigation.HomeNavigator +import com.sopt.now.compose.presentation.utils.showToast import com.sopt.now.compose.ui.theme.CustomTheme @Composable @@ -29,6 +38,23 @@ fun HomeRoute( nickname: String, phoneNumber: String, ) { + var backPressedTime by remember { mutableLongStateOf(0L) } + val backPressThreshold = 2000 + val context = LocalContext.current + + BackHandler { + val currentTime = System.currentTimeMillis() + if (currentTime - backPressedTime <= backPressThreshold) { + (context as? Activity)?.finish() + } else { + backPressedTime = currentTime + showToast( + context = context, + message = context.getString(R.string.home_backhandler_caution) + ) + } + } + HomeScreen( id = id, password = password, @@ -47,6 +73,7 @@ fun HomeScreen( Column( modifier = Modifier .fillMaxSize() + .background(color = CustomTheme.colors.white) .padding(40.dp), horizontalAlignment = Alignment.Start ) { diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/main/MainActivity.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/main/MainActivity.kt index eb00ea5..ddd21c4 100644 --- a/app/src/main/java/com/sopt/now/compose/presentation/ui/main/MainActivity.kt +++ b/app/src/main/java/com/sopt/now/compose/presentation/ui/main/MainActivity.kt @@ -8,12 +8,16 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.rememberNavController import com.sopt.now.compose.presentation.ui.auth.navigation.AuthNavigator +import com.sopt.now.compose.presentation.ui.auth.screen.AuthViewModel import com.sopt.now.compose.presentation.ui.home.navigation.HomeNavigator import com.sopt.now.compose.presentation.ui.navigator.MainNavHost import com.sopt.now.compose.ui.theme.CUSTOMTheme +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -21,6 +25,7 @@ class MainActivity : ComponentActivity() { CUSTOMTheme { val navController = rememberNavController() val authNavigator = remember(navController) { AuthNavigator(navController) } + val authViewModel: AuthViewModel = hiltViewModel() val homeNavigator = remember(navController) { HomeNavigator(navController) } Scaffold( @@ -30,6 +35,7 @@ class MainActivity : ComponentActivity() { navHostController = navController, modifier = Modifier.padding(paddingValues), authNavigator = authNavigator, + authViewModel = authViewModel, homeNavigator = homeNavigator ) } diff --git a/app/src/main/java/com/sopt/now/compose/presentation/ui/navigator/MainNavHost.kt b/app/src/main/java/com/sopt/now/compose/presentation/ui/navigator/MainNavHost.kt index 7b01057..93fd58f 100644 --- a/app/src/main/java/com/sopt/now/compose/presentation/ui/navigator/MainNavHost.kt +++ b/app/src/main/java/com/sopt/now/compose/presentation/ui/navigator/MainNavHost.kt @@ -10,6 +10,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import com.sopt.now.compose.presentation.ui.auth.navigation.AuthNavigator import com.sopt.now.compose.presentation.ui.auth.navigation.authNavGraph +import com.sopt.now.compose.presentation.ui.auth.screen.AuthViewModel import com.sopt.now.compose.presentation.ui.home.navigation.HomeNavigator import com.sopt.now.compose.presentation.ui.home.navigation.homeNavGraph @@ -18,6 +19,7 @@ fun MainNavHost( navHostController: NavHostController, modifier: Modifier = Modifier, authNavigator: AuthNavigator, + authViewModel: AuthViewModel, homeNavigator: HomeNavigator ) { Box( @@ -27,9 +29,9 @@ fun MainNavHost( ) { NavHost( navController = navHostController, - startDestination = "signIn/{id}/{password}/{nickname}/{phoneNumber}" + startDestination = "signIn" ) { - authNavGraph(authNavigator, homeNavigator) + authNavGraph(authNavigator,authViewModel) homeNavGraph(homeNavigator) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 93951c9..80512fd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,7 +7,6 @@ 비밀번호를 입력하세요 (8~12자) 회원가입 로그인 하기 - 입력하지 않은 정보가 있습니다 일치하지 않는 아이디입니다 일치하지 않는 비밀번호입니다 로그인에 성공했습니다 @@ -22,6 +21,10 @@ 닉네임을 입력하세요 전화번호 010-XXXX-XXXX + 아이디 형식이 잘못되었습니다 + 비밀번호 형식이 잘못되었습니다 + 닉네임 형식이 잘못되었습니다 + 전화번호 형식이 잘못되었습니다 회원가입에 성공했습니다 회원가입에 실패했습니다 회원가입 하기 @@ -30,4 +33,5 @@ 아이디 비밀번호 전화번호 + 뒤로 버튼을 한 번 더 누르시면 종료됩니다. \ No newline at end of file diff --git a/build.gradle b/build.gradle index be6601c..ae9fedd 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,8 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. +// Top-level build file plugins { id 'com.android.application' version '8.3.1' apply false id 'com.android.library' version '8.3.1' apply false id 'org.jetbrains.kotlin.android' version '1.9.0' apply false + id 'com.google.dagger.hilt.android' version '2.49' apply false + id 'com.google.devtools.ksp' version '1.9.0-1.0.13' apply false } \ No newline at end of file