diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/component/EmailTextField.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/component/EmailTextField.kt new file mode 100644 index 00000000..4b834f7d --- /dev/null +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/component/EmailTextField.kt @@ -0,0 +1,95 @@ +package kr.co.lion.modigm.ui.login.component + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +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.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import kr.co.lion.modigm.R +import kr.co.lion.modigm.ui.login.util.dpToSp + +@Composable +fun EmailTextField( + modifier: Modifier = Modifier, + userEmail: String, + onValueChange: (String) -> Unit, + placeholder: @Composable () -> Unit, + errorMessage: String = "" +) { + val isEmailInputError = errorMessage != "" + val pointColor = Color(ContextCompat.getColor(LocalContext.current, R.color.pointColor)) + + OutlinedTextField( + modifier = modifier, + value = userEmail, + onValueChange = { onValueChange(it) }, + textStyle = LocalTextStyle.current.copy(fontSize = dpToSp(16.dp)), + colors = TextFieldDefaults.colors( + focusedTextColor = Color.Black, + unfocusedTextColor = Color.Black, + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + focusedIndicatorColor = pointColor, + unfocusedIndicatorColor = Color.Black, + errorContainerColor = Color.White, + ), + placeholder = placeholder, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.icon_mail_24px), + contentDescription = "이메일 리딩 아이콘" + ) + }, + trailingIcon = { + if (userEmail.isNotEmpty()) { + IconButton( + onClick = { onValueChange("") } + ) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = "이메일 초기화 아이콘" + ) + } + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next, + ), + singleLine = true, + isError = isEmailInputError, + ) + if (isEmailInputError) { + Text( + text = errorMessage, + color = Color.Red, + fontSize = dpToSp(12.dp), + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +fun EmailTextFieldPreview() { + EmailTextField( + userEmail = "", + onValueChange = {}, + placeholder = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/component/ErrorAlertDialog.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/component/ErrorAlertDialog.kt new file mode 100644 index 00000000..a3c66f52 --- /dev/null +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/component/ErrorAlertDialog.kt @@ -0,0 +1,35 @@ +package kr.co.lion.modigm.ui.login.component + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable + +@Composable +fun ErrorAlertDialog( + title: String, + message: String, + confirmButtonText: String = "확인", + onConfirmClick: () -> Unit, + onDismissRequest: () -> Unit = {}, + dismissOnBackPress: Boolean = false, + dismissOnClickOutside: Boolean = false, +) { + AlertDialog( + onDismissRequest = { + if (dismissOnBackPress || dismissOnClickOutside) { + onDismissRequest() + } + }, + title = { Text(text = title) }, + text = { Text(text = message) }, + confirmButton = { + TextButton(onClick = { + onConfirmClick() + onDismissRequest() + }) { + Text(text = confirmButtonText) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/social/component/SocialLoginLoading.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/component/LoginLoading.kt similarity index 76% rename from app/src/main/java/kr/co/lion/modigm/ui/login/social/component/SocialLoginLoading.kt rename to app/src/main/java/kr/co/lion/modigm/ui/login/component/LoginLoading.kt index dc3862b5..81342f7d 100644 --- a/app/src/main/java/kr/co/lion/modigm/ui/login/social/component/SocialLoginLoading.kt +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/component/LoginLoading.kt @@ -1,24 +1,23 @@ -package kr.co.lion.modigm.ui.login.social.component +package kr.co.lion.modigm.ui.login.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.tooling.preview.Preview @Composable -fun SocialLoginLoading( +fun LoginLoading( modifier: Modifier = Modifier, isLoading: Boolean ) { if (isLoading) { Box( modifier = modifier - .fillMaxSize() .background(Color.Black.copy(alpha = 0.5f)) .pointerInput(Unit) { }, contentAlignment = Alignment.Center @@ -26,4 +25,10 @@ fun SocialLoginLoading( CircularProgressIndicator(color = Color.White) } } +} + +@Preview(showBackground = true) +@Composable +fun LoginLoadingPreview() { + LoginLoading(isLoading = true) } \ No newline at end of file diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/component/PasswordTextFeild.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/component/PasswordTextFeild.kt new file mode 100644 index 00000000..e7ffd043 --- /dev/null +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/component/PasswordTextFeild.kt @@ -0,0 +1,93 @@ +package kr.co.lion.modigm.ui.login.component + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +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.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import kr.co.lion.modigm.R +import kr.co.lion.modigm.ui.login.util.dpToSp + +@Composable +fun PasswordTextField( + modifier: Modifier = Modifier, + userPassword: String, + onValueChange: (String) -> Unit, + placeholder: @Composable () -> Unit, + errorMessage: String = "" +) { + var isPasswordVisible by remember { mutableStateOf(false) } + val pointColor = Color(ContextCompat.getColor(LocalContext.current, R.color.pointColor)) + val isPasswordInputError = errorMessage != "" + + OutlinedTextField( + modifier = modifier, + value = userPassword, + onValueChange = { + onValueChange(it) + isPasswordVisible = it.isEmpty() + }, + textStyle = LocalTextStyle.current.copy(fontSize = dpToSp(16.dp)), + colors = TextFieldDefaults.colors( + focusedTextColor = Color.Black, + unfocusedTextColor = Color.Black, + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + focusedIndicatorColor = pointColor, + unfocusedIndicatorColor = Color.Black, + errorContainerColor = Color.White, + ), + placeholder = placeholder, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.icon_key_24px), + contentDescription = "비밀번호" + ) + }, + trailingIcon = { + IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) { + Icon( + imageVector = if (isPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = if (isPasswordVisible) "비밀번호 숨김" else "비밀번호 보임" + ) + } + }, + visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + singleLine = true, + isError = isPasswordInputError, + ) + if (isPasswordInputError) { + Text( + text = errorMessage, + color = Color.Red, + fontSize = dpToSp(12.dp), + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/component/ScrollArrow.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/component/ScrollArrow.kt new file mode 100644 index 00000000..dbc0fd74 --- /dev/null +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/component/ScrollArrow.kt @@ -0,0 +1,65 @@ +package kr.co.lion.modigm.ui.login.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import kr.co.lion.modigm.R +import kr.co.lion.modigm.ui.login.util.animateEnterExit + +@Composable +fun ScrollArrow( + scrollState: ScrollState, + modifier: Modifier = Modifier +) { + val isVisible = remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(scrollState) { + snapshotFlow { scrollState.canScrollForward } + .collect { canScrollForward -> + isVisible.value = canScrollForward + } + } + + Box( + modifier = modifier + .fillMaxWidth() + .height(50.dp) + .background(Color.Transparent), + contentAlignment = Alignment.Center + ) { + if (isVisible.value) { + Image( + painter = painterResource(id = R.drawable.arrow_down_24px), + contentDescription = "Scroll Arrow", + modifier = Modifier + .size(24.dp) + .clickable { + coroutineScope.launch { + scrollState.animateScrollTo(scrollState.maxValue) + } + } + .animateEnterExit(), + colorFilter = ColorFilter.tint(Color.White) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/component/SocialLoginButton.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/component/SocialLoginButton.kt new file mode 100644 index 00000000..22557f51 --- /dev/null +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/component/SocialLoginButton.kt @@ -0,0 +1,38 @@ +package kr.co.lion.modigm.ui.login.component + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun SocialLoginButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + colors: ButtonColors, + content: @Composable () -> Unit +) { + Button( + onClick = onClick, + modifier = modifier, + colors = colors, + contentPadding = PaddingValues(0.dp), + shape = RoundedCornerShape(8.dp), + content = { content() } + ) +} + +@Preview(showBackground = true) +@Composable +fun SocialLoginButtonPreview() { + SocialLoginButton( + onClick = {}, + colors = ButtonDefaults.buttonColors(), + content = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/email/EmailLoginFragment.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/email/EmailLoginFragment.kt index 4ea8fb9b..909a2a21 100644 --- a/app/src/main/java/kr/co/lion/modigm/ui/login/email/EmailLoginFragment.kt +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/email/EmailLoginFragment.kt @@ -1,194 +1,71 @@ package kr.co.lion.modigm.ui.login.email import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher -import android.util.Log +import android.view.LayoutInflater import android.view.View -import android.view.inputmethod.EditorInfo +import android.view.ViewGroup +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment import androidx.fragment.app.commit -import androidx.fragment.app.replace -import androidx.fragment.app.viewModels import kr.co.lion.modigm.R -import kr.co.lion.modigm.databinding.FragmentEmailLoginBinding -import kr.co.lion.modigm.ui.VBBaseFragment import kr.co.lion.modigm.ui.join.JoinFragment -import kr.co.lion.modigm.ui.login.CustomLoginErrorDialog import kr.co.lion.modigm.ui.login.FindEmailFragment import kr.co.lion.modigm.ui.login.FindPasswordFragment -import kr.co.lion.modigm.ui.login.email.viewmodel.EmailLoginViewModel import kr.co.lion.modigm.ui.study.BottomNaviFragment import kr.co.lion.modigm.util.FragmentName import kr.co.lion.modigm.util.JoinType -import kr.co.lion.modigm.util.hideSoftInput -import kr.co.lion.modigm.util.shake -import kr.co.lion.modigm.util.showSoftInput -class EmailLoginFragment : VBBaseFragment(FragmentEmailLoginBinding::inflate) { - - // 뷰모델 - private val viewModel: EmailLoginViewModel by viewModels() // LoginViewModel 인스턴스 생성 - - // 태그 - private val logTag by lazy { EmailLoginFragment::class.simpleName } - - // --------------------------------- LC START --------------------------------- - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - initView() // 초기 UI 설정 - observeViewModel() // ViewModel의 데이터 변경 관찰 - } - - override fun onResume() { - super.onResume() - // 이메일 텍스트 필드 포커싱 및 소프트키보드 보여주기 - with(binding) { - requireActivity().showSoftInput(textInputEditOtherEmail) - } - } - - override fun onDestroyView() { - super.onDestroyView() - - viewModel.clearViewModelData() // ViewModel 데이터 초기화 - } - - // --------------------------------- LC END --------------------------------- - - private fun initView() { - with(binding){ - - // 실시간 텍스트 변경 감지 설정 - textInputEditOtherEmail.apply{ - // 텍스트 변경 시 - addTextChangedListener(inputWatcher) - - // 포커스 변경 시 - setOnFocusChangeListener { _, hasFocus -> - if (hasFocus) { - // 이메일 필드가 포커스를 받을 때 해당 필드로 스크롤 - otherLoginScrollView.smoothScrollTo(0, textViewOtherTitle.top) - requireActivity().showSoftInput(this) - } else { - // 이메일 필드가 포커스를 잃으면 키보드 숨기기 - requireActivity().hideSoftInput() - } +class EmailLoginFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + setContent { + + Scaffold( + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> + EmailLoginScreen( + navigateToBottomNavi = { navigateToBottomNaviFragment() }, + navigateToFindEmail = { navigateToFindEmailFragment() }, + navigateToFindPassword = { navigateToFindPasswordFragment() }, + navigateToSocialLogin = { navigateToSocialLoginFragment() }, + navigateToJoin = {navigateToJoinFragment() }, + modifier = Modifier.padding(innerPadding), + ) } } - // 비밀번호 입력 - textInputEditOtherPassword.apply { - // 텍스트 변경 시 - addTextChangedListener(inputWatcher) - // 에디터 액션 설정 - setOnEditorActionListener { _, actionId, _ -> - // 엔터키 입력 시 로그인 시도 - if (actionId == EditorInfo.IME_ACTION_DONE) { - buttonOtherLogin.performClick() // 로그인 버튼 클릭 - true - } else { - false - } - } - // 포커스 변경 시 - setOnFocusChangeListener { _, hasFocus -> - if (hasFocus) { - // 비밀번호 필드가 포커스를 받을 때 해당 필드로 스크롤 - otherLoginScrollView.smoothScrollTo(0, textViewOtherSecondTitle.top) - requireActivity().showSoftInput(this) - } - if(!hasFocus){ - // 비밀번호 필드가 포커스를 잃으면 키보드 숨기기 - requireActivity().hideSoftInput() - } - } - } - - // 로그인 버튼 초기값을 비활성화 상태로 설정 - buttonOtherLogin.isEnabled = false - - // 로그인 버튼 클릭 시 로그인 시도 - buttonOtherLogin.setOnClickListener { - if(!checkAllInput()) { - return@setOnClickListener - } - requireActivity().hideSoftInput() - - showLoginLoading() - val email = textInputEditOtherEmail.text.toString() - val password = textInputEditOtherPassword.text.toString() - val autoLogin = checkBoxOtherAutoLogin.isChecked - viewModel.emailLogin(email, password, autoLogin) - } - - // 회원가입 버튼 클릭 시 회원가입 화면으로 이동 - buttonOtherJoin.setOnClickListener { - val joinType = JoinType.EMAIL - goToJoinFragment(joinType) - } - - // 이메일 찾기 버튼 클릭 시 - buttonOtherFindEmail.setOnClickListener { - parentFragmentManager.commit { - replace(R.id.containerMain) - addToBackStack(FragmentName.FIND_EMAIL.str) - } - } - - // 비밀번호 찾기 버튼 클릭 시 - buttonOtherFindPassword.setOnClickListener { - parentFragmentManager.commit { - replace(R.id.containerMain) - addToBackStack(FragmentName.FIND_PW.str) - } - } - // 돌아가기 버튼 클릭 시 - buttonOtherBack.setOnClickListener { - parentFragmentManager.popBackStack() - } } } - // ViewModel의 데이터 변경을 관찰하여 UI 업데이트 - private fun observeViewModel() { - // 이메일 로그인 데이터 관찰 - viewModel.emailLoginResult.observe(viewLifecycleOwner) { result -> - if (result) { - hideLoginLoading() - Log.i(logTag, "이메일 로그인 성공") - val joinType = JoinType.EMAIL - goToBottomNaviFragment(joinType) - } - } - // 이메일 로그인 실패 시 에러 처리 - viewModel.emailLoginError.observe(viewLifecycleOwner) { error -> - if (error != null) { - hideLoginLoading() - showErrorDialog(error) - } + private fun navigateToFindEmailFragment() { + parentFragmentManager.commit { + replace(R.id.containerMain, FindEmailFragment()) + addToBackStack(FragmentName.FIND_EMAIL.str) } } - /** - * 로그인 오류 처리 메서드 - * @param e 발생한 오류 - */ - private fun showErrorDialog(e: Throwable) { - val message = if (e.message != null) { - e.message.toString() - } else { - "알 수 없는 오류!\n코드번호: 9999" + private fun navigateToFindPasswordFragment() { + parentFragmentManager.commit { + replace(R.id.containerMain, FindPasswordFragment()) + addToBackStack(FragmentName.FIND_PASSWORD.str) } - showLoginErrorDialog(message) } - // 회원가입 화면으로 이동하는 메소드 - private fun goToJoinFragment(joinType: JoinType) { - Log.d(logTag, "navigateToJoinFragment - joinType: ${joinType.provider}") + private fun navigateToSocialLoginFragment() { + parentFragmentManager.popBackStack() + } + private fun navigateToJoinFragment() { val bundle = Bundle().apply { - putString("joinType", joinType.provider) + putString("joinType", JoinType.EMAIL.provider) } parentFragmentManager.commit { replace(R.id.containerMain, JoinFragment().apply { arguments = bundle }) @@ -196,129 +73,13 @@ class EmailLoginFragment : VBBaseFragment(FragmentEma } } - private fun goToBottomNaviFragment(joinType: JoinType) { - + private fun navigateToBottomNaviFragment() { val bundle = Bundle().apply { - putString("joinType", joinType.provider) + putString("joinType", JoinType.EMAIL.provider) } parentFragmentManager.commit { replace(R.id.containerMain, BottomNaviFragment().apply { arguments = bundle }) addToBackStack(FragmentName.BOTTOM_NAVI.str) } } - - // 오류 다이얼로그 표시 - private fun showLoginErrorDialog(message: String) { - // 다이얼로그 생성 - val dialog = CustomLoginErrorDialog(requireContext()) - with(dialog){ - // 다이얼로그 제목 - setTitle("오류") - // 다이얼로그 메시지 - setMessage(message) - // 확인 버튼 - setPositiveButton("확인") { - // 확인 버튼 클릭 시 다이얼로그 닫기 - dismiss() - } - // 다이얼로그 표시 - show() - } - - } - - // 유효성 검사 - private fun checkAllInput(): Boolean { - return checkEmail() && checkPassword() - } - - private fun checkEmail(): Boolean { - with(binding){ - // 에러 메시지를 설정하고 포커스와 흔들기 동작을 수행하는 함수 - fun showError(message: String) { - textInputLayoutOtherEmail.error = message - textInputEditOtherEmail.requestFocus() - textInputEditOtherEmail.shake() - } - return when { - textInputEditOtherEmail.text.toString().isEmpty() -> { - showError("이메일을 입력해주세요.") - false - } - !isEmailValid(textInputEditOtherEmail.text.toString()) -> { - showError("올바른 이메일을 입력해주세요.") - false - } - else -> { - textInputLayoutOtherEmail.error = null - true - } - } - } - } - - private fun checkPassword(): Boolean { - with(binding) { - - // 에러 메시지를 설정하고 포커스와 흔들기 동작을 수행하는 함수 - fun showError(message: String) { - textInputLayoutOtherPassword.error = message - textInputEditOtherPassword.requestFocus() - textInputEditOtherPassword.shake() - } - return when { - textInputEditOtherPassword.text.toString().isEmpty() -> { - showError("비밀번호를 입력해주세요.") - false - } - !isPasswordValid(textInputEditOtherPassword.text.toString()) -> { - showError("올바른 비밀번호를 입력해주세요.") - false - } - else -> { - textInputLayoutOtherPassword.error = null - true - } - } - } - } - - // 이메일 유효성을 검사하는 함수 - private fun isEmailValid(email: String): Boolean { - return android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() - } - - - // 비밀번호 유효성을 검사하는 함수 - private fun isPasswordValid(password: String): Boolean { - return password.length >= 6 - } - - // 유효성 검사 및 버튼 활성화/비활성화 업데이트 - private val inputWatcher = object : TextWatcher { - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - with(binding){ - buttonOtherLogin.isEnabled = - !textInputEditOtherEmail.text.isNullOrEmpty() && !textInputEditOtherPassword.text.isNullOrEmpty() - } - } - override fun afterTextChanged(p0: Editable?) { - } - } - - // 로딩 화면 표시 - private fun showLoginLoading() { - with(binding){ - layoutLoginLoadingBackground.visibility = View.VISIBLE - } - } - // 로딩 화면 숨김 - private fun hideLoginLoading() { - with(binding){ - layoutLoginLoadingBackground.visibility = View.GONE - } - } - } diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/email/EmailLoginScreen.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/email/EmailLoginScreen.kt new file mode 100644 index 00000000..8f1670b9 --- /dev/null +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/email/EmailLoginScreen.kt @@ -0,0 +1,393 @@ +package kr.co.lion.modigm.ui.login.email + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults.buttonColors +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +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.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.viewmodel.compose.viewModel +import kr.co.lion.modigm.R +import kr.co.lion.modigm.ui.login.component.EmailTextField +import kr.co.lion.modigm.ui.login.component.ErrorAlertDialog +import kr.co.lion.modigm.ui.login.component.LoginLoading +import kr.co.lion.modigm.ui.login.component.PasswordTextField +import kr.co.lion.modigm.ui.login.component.ScrollArrow +import kr.co.lion.modigm.ui.login.util.dpToSp + +@Composable +fun EmailLoginScreen( + navigateToBottomNavi: () -> Unit, + navigateToFindEmail: () -> Unit, + navigateToFindPassword: () -> Unit, + navigateToSocialLogin: () -> Unit, + navigateToJoin: () -> Unit, + modifier: Modifier = Modifier, + viewModel: EmailLoginViewModel = viewModel() +) { + val userEmail = viewModel.userEmail.collectAsState() + val userPassword = viewModel.userPassword.collectAsState() + val isChecked = viewModel.isChecked.collectAsState() + val isLoginButtonEnabled = viewModel.isLoginEnabled.collectAsState() + val isLoading = viewModel.isLoading.collectAsState() + + val emailLoginResult = viewModel.emailLoginResult.collectAsState() + + LaunchedEffect(emailLoginResult) { + if (emailLoginResult.value) { + navigateToBottomNavi() + } + } + + val emailLoginError = viewModel.emailLoginError.collectAsState() + val showDialog = remember { mutableStateOf(false) } + val dialogMessage = remember { mutableStateOf("") } + + LaunchedEffect(emailLoginError.value) { + emailLoginError.value?.let { error -> + dialogMessage.value = error.message ?: "알 수 없는 오류입니다." + showDialog.value = true + } + } + if (showDialog.value) { + ErrorAlertDialog( + title = "오류", + message = dialogMessage.value, + confirmButtonText = "확인", + onConfirmClick = { + showDialog.value = false + viewModel.clearLoginError() + }, + onDismissRequest = { + showDialog.value = false + viewModel.clearLoginError() + } + ) + } + + val lifecycleOwner = LocalLifecycleOwner.current + val keyboardController = LocalSoftwareKeyboardController.current + val emailFocusRequester = remember { FocusRequester() } + val passwordFocusRequester = remember { FocusRequester() } + + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + emailFocusRequester.requestFocus() + keyboardController?.show() + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + viewModel.clearViewModelData() + } + } + val emailInputError = viewModel.emailInputError.collectAsState() + val passwordInputError = viewModel.passwordInputError.collectAsState() + + LaunchedEffect(emailInputError.value, passwordInputError.value) { + when { + emailInputError.value != "" -> { + emailFocusRequester.requestFocus() + keyboardController?.show() + } + passwordInputError.value != "" -> { + passwordFocusRequester.requestFocus() + keyboardController?.show() + } + } + } + val focusField = viewModel.focusField.collectAsState() + + LaunchedEffect(focusField.value) { + when (focusField.value) { + FocusTarget.EMAIL_INPUT -> emailFocusRequester.requestFocus() + FocusTarget.PASSWORD_INPUT -> passwordFocusRequester.requestFocus() + null -> Unit + } + } + + val scrollState = rememberScrollState() + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White), + ) { + Column( + modifier = modifier + .verticalScroll(scrollState) + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + Text( + text = "모우다임", + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(top = 100.dp), + fontFamily = FontFamily(Font(R.font.one_mobile_pop_otf)), + fontSize = dpToSp(70.dp), + color = Color.Black, + fontWeight = FontWeight.Normal + ) + Text( + text = "개발자 스터디의 새로운 패러다임", + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(top = 10.dp), + fontFamily = FontFamily(Font(R.font.one_mobile_pop_otf)), + fontSize = dpToSp(22.dp), + color = Color.Black, + fontWeight = FontWeight.Normal + ) + + EmailTextField( + modifier = Modifier + .fillMaxWidth() + .padding(top = 30.dp) + .focusRequester(emailFocusRequester) + .focusable(), + userEmail = userEmail.value, + onValueChange = { + viewModel.onUserEmailChange(it) + viewModel.clearAllInputError() + }, + placeholder = { Text(text = "이메일") }, + errorMessage = emailInputError.value, + ) + + PasswordTextField( + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp) + .focusRequester(passwordFocusRequester) + .focusable(), + userPassword = userPassword.value, + onValueChange = { + viewModel.onUserPasswordChange(it) + viewModel.clearAllInputError() + }, + placeholder = { Text(text = "비밀번호") }, + errorMessage = passwordInputError.value, + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp, end = 0.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = isChecked.value, + onCheckedChange = { viewModel.onCheckedChange(it) }, + modifier = Modifier + .size(20.dp) + .padding(top = 0.dp, bottom = 0.dp, start = 0.dp, end = 0.dp) + ) + Spacer( + modifier = Modifier + .size(8.dp) + .clickable { viewModel.onCheckedChange(!isChecked.value) } + ) + Text( + text = "자동 로그인", + fontSize = dpToSp(16.dp), + modifier = Modifier.clickable { viewModel.onCheckedChange(!isChecked.value) } + ) + } + + Row( + modifier = Modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton( + modifier = Modifier.padding(start = 0.dp, end = 15.dp), + onClick = { navigateToFindEmail() }, + contentPadding = PaddingValues(0.dp), + ) { + Text( + text = "이메일 찾기", + color = Color.Black, + fontSize = dpToSp(16.dp) + ) + } + VerticalDivider( + modifier = Modifier + .height(15.dp), + thickness = 1.dp, + color = Color.Black, + ) + TextButton( + modifier = Modifier + .padding(start = 15.dp, end = 0.dp), + onClick = { navigateToFindPassword() }, + contentPadding = PaddingValues(0.dp) + ) { + Text( + text = "비밀번호 찾기", + color = Color.Black, + fontSize = 16.sp, + ) + } + } + } + + Button( + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp), + onClick = { + keyboardController?.hide() + + if(viewModel.checkAllInputAndSetError()) { + viewModel.emailLogin( + userEmail = userEmail.value, + userPassword = userPassword.value, + autoLoginValue = isChecked.value + ) + } + }, + colors = buttonColors(Color(ContextCompat.getColor(LocalContext.current, R.color.pointColor))), + enabled = isLoginButtonEnabled.value + ) { + Text( + text = "로그인", + fontSize = dpToSp(16.dp) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 0.dp, end = 0.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton( + modifier = Modifier, + onClick = { navigateToSocialLogin() }, + contentPadding = PaddingValues(0.dp) + ) { + Icon( + painter = painterResource(R.drawable.icon_back_24px), + contentDescription = "돌아가기", + tint = Color.Black + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = "돌아가기", + fontSize = dpToSp(16.dp), + color = Color.Black + ) + } + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier + .padding(end = 4.dp), + text = "계정이 없으신가요?", + fontSize = dpToSp(16.dp), + color = Color.Black + ) + TextButton( + modifier = Modifier, + onClick = { navigateToJoin() }, + contentPadding = PaddingValues(0.dp) + ) { + Icon( + modifier = Modifier.padding(end = 4.dp), + painter = painterResource(R.drawable.icon_person_add_24px), + contentDescription = "회원가입", + tint = Color.Black + ) + Text( + modifier = Modifier, + text = "회원가입", + fontSize = dpToSp(16.dp), + color = Color.Black + ) + } + } + } + } + + ScrollArrow( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + scrollState = scrollState, + ) + + LoginLoading( + modifier = Modifier + .fillMaxSize(), + isLoading = isLoading.value + ) + } +} + +@Preview(showBackground = true) +@Composable +fun EmailLoginScreenPreview() { + EmailLoginScreen( + navigateToBottomNavi = {}, + navigateToFindEmail = {}, + navigateToFindPassword = {}, + navigateToSocialLogin = {}, + navigateToJoin = {}, + ) +} + diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/email/viewmodel/EmailLoginViewModel.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/email/EmailLoginViewModel.kt similarity index 50% rename from app/src/main/java/kr/co/lion/modigm/ui/login/email/viewmodel/EmailLoginViewModel.kt rename to app/src/main/java/kr/co/lion/modigm/ui/login/email/EmailLoginViewModel.kt index f7880e3d..31b0d489 100644 --- a/app/src/main/java/kr/co/lion/modigm/ui/login/email/viewmodel/EmailLoginViewModel.kt +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/email/EmailLoginViewModel.kt @@ -1,8 +1,7 @@ -package kr.co.lion.modigm.ui.login.email.viewmodel +package kr.co.lion.modigm.ui.login.email import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import android.util.Patterns import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.firebase.FirebaseNetworkException @@ -10,6 +9,11 @@ import com.google.firebase.FirebaseTooManyRequestsException import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException import com.google.firebase.auth.FirebaseAuthInvalidUserException import com.google.firebase.messaging.FirebaseMessaging +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kr.co.lion.modigm.repository.LoginRepository import kr.co.lion.modigm.util.JoinType @@ -19,23 +23,83 @@ class EmailLoginViewModel: ViewModel() { private val loginRepository by lazy { LoginRepository() } - private val _isLoading = MutableLiveData(false) - val isLoading : LiveData = _isLoading + private val _userEmail = MutableStateFlow("") + val userEmail: StateFlow = _userEmail - // 이메일 로그인 - private val _emailLoginResult = MutableLiveData() - val emailLoginResult: LiveData = _emailLoginResult + fun onUserEmailChange(newEmail: String) { + _userEmail.value = newEmail + } + + private val _userPassword = MutableStateFlow("") + val userPassword: StateFlow = _userPassword + + fun onUserPasswordChange(newPassword: String) { + _userPassword.value = newPassword + } + + private val _isChecked = MutableStateFlow(false) + val isChecked: StateFlow = _isChecked + + fun onCheckedChange(isChecked: Boolean) { + _isChecked.value = isChecked + } + + val isLoginEnabled: StateFlow = combine(_userEmail, _userPassword) { email, password -> + email.isNotBlank() && password.isNotBlank() + }.stateIn(viewModelScope, SharingStarted.Eagerly, false) + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading + + private val _emailLoginResult = MutableStateFlow(false) + val emailLoginResult: StateFlow = _emailLoginResult + + private val _emailLoginError = MutableStateFlow(null) + val emailLoginError: StateFlow = _emailLoginError + + fun clearLoginError() { + _emailLoginError.value = null + } + + private val _emailInputError = MutableStateFlow("") + val emailInputError: StateFlow = _emailInputError - private val _emailLoginError = MutableLiveData() - val emailLoginError: LiveData = _emailLoginError + private val _passwordInputError = MutableStateFlow("") + val passwordInputError: StateFlow = _passwordInputError - fun emailLogin(email: String, password: String, autoLoginValue: Boolean) { + fun clearAllInputError() { + _emailInputError.value = "" + _passwordInputError.value = "" + } + fun checkAllInputAndSetError(): Boolean { + _emailInputError.value = if (!isEmailValid()) "올바른 이메일 형식이 아닙니다." else "" + _passwordInputError.value = if (!isPasswordValid()) "비밀번호는 6자 이상이어야 합니다." else "" + + _focusField.value = when { + !isEmailValid() -> FocusTarget.EMAIL_INPUT + !isPasswordValid() -> FocusTarget.PASSWORD_INPUT + else -> null + } + + return isEmailValid() && isPasswordValid() + } + private fun isEmailValid(): Boolean { + return Patterns.EMAIL_ADDRESS.matcher(_userEmail.value).matches() + } + private fun isPasswordValid(): Boolean { + return _userPassword.value.length >= 6 + } + + private val _focusField = MutableStateFlow(null) + val focusField: StateFlow = _focusField + + fun emailLogin(userEmail: String, userPassword: String, autoLoginValue: Boolean) { _isLoading.value = true - _emailLoginResult.postValue(false) + _emailLoginResult.value = false viewModelScope.launch { - val result = loginRepository.emailLogin(email, password) + val result = loginRepository.emailLogin(userEmail, userPassword) result.onSuccess { userIdx -> - _isLoading.postValue(false) + _isLoading.value = false prefs.setBoolean( key = "autoLogin", value = autoLoginValue @@ -48,14 +112,13 @@ class EmailLoginViewModel: ViewModel() { key = "currentUserIdx", value = userIdx ) - _emailLoginResult.postValue(true) - - // 로그인 성공 후 즉시 FCM 토큰 등록 registerFcmTokenToServer(userIdx) + _emailLoginResult.value = true + }.onFailure { e -> - _isLoading.postValue(false) + _isLoading.value = false prefs.clearAllPrefs() - _emailLoginResult.postValue(false) + _emailLoginResult.value = false // 예외 처리 및 사용자에게 전달할 메시지 설정 val errorMessage = when (e) { is FirebaseAuthInvalidCredentialsException -> { @@ -78,16 +141,18 @@ class EmailLoginViewModel: ViewModel() { "로그인 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요." } } - _emailLoginError.postValue(Exception(errorMessage, e)) + _emailLoginError.value = Throwable(errorMessage, e) } } } // 뷰모델 데이터 초기화 fun clearViewModelData() { - _isLoading.postValue(false) - _emailLoginResult.postValue(false) - _emailLoginError.postValue(null) + _isLoading.value = false + _emailLoginResult.value = false + _emailLoginError.value = null + _emailInputError.value = "" + _passwordInputError.value = "" } private fun registerFcmTokenToServer(userIdx: Int) { @@ -114,4 +179,7 @@ class EmailLoginViewModel: ViewModel() { } } } -} \ No newline at end of file +} + + +enum class FocusTarget { EMAIL_INPUT, PASSWORD_INPUT } \ No newline at end of file diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/social/SocialLoginFragment.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/social/SocialLoginFragment.kt index 9fda18ec..a26608f2 100644 --- a/app/src/main/java/kr/co/lion/modigm/ui/login/social/SocialLoginFragment.kt +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/social/SocialLoginFragment.kt @@ -5,6 +5,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedCallback +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.commit @@ -37,15 +40,69 @@ class SocialLoginFragment : Fragment() { ): View { return ComposeView(requireContext()).apply { setContent { + val isLoading by viewModel.isLoading.observeAsState(false) + val kakaoLoginResult by viewModel.kakaoLoginResult.observeAsState(false) + val githubLoginResult by viewModel.githubLoginResult.observeAsState(false) + val kakaoJoinResult by viewModel.kakaoJoinResult.observeAsState(false) + val githubJoinResult by viewModel.githubJoinResult.observeAsState(false) + val emailLoginResult by viewModel.emailAutoLoginResult.observeAsState(false) + val kakaoLoginError by viewModel.kakaoLoginError.observeAsState() + val githubLoginError by viewModel.githubLoginError.observeAsState() + val autoLoginError by viewModel.autoLoginError.observeAsState() + + LaunchedEffect(kakaoLoginResult) { + if (kakaoLoginResult) { + navigateToBottomNaviFragment(JoinType.KAKAO) + } + } + + LaunchedEffect(githubLoginResult) { + if (githubLoginResult) { + navigateToBottomNaviFragment(JoinType.GITHUB) + } + } + + LaunchedEffect(kakaoJoinResult) { + if (kakaoJoinResult) { + navigateToJoinFragment(JoinType.KAKAO) + } + } + + LaunchedEffect(githubJoinResult) { + if (githubJoinResult) { + navigateToJoinFragment(JoinType.GITHUB) + } + } + + LaunchedEffect(emailLoginResult) { + if (emailLoginResult) { + navigateToBottomNaviFragment(JoinType.EMAIL) + } + } + + LaunchedEffect(kakaoLoginError) { + kakaoLoginError?.let { error -> + showLoginErrorDialog(error) + } + } + + LaunchedEffect(githubLoginError) { + githubLoginError?.let { error -> + showLoginErrorDialog(error) + } + } + + LaunchedEffect(autoLoginError) { + autoLoginError?.let { error -> + requireActivity().showLoginSnackBar(error.message.toString(), null) + } + } + SocialLoginScreen( - viewModel = viewModel, + isLoading = isLoading, onKakaoLoginClick = { viewModel.kakaoLogin(requireContext()) }, onGithubLoginClick = { viewModel.githubLogin(requireActivity()) }, - onEmailLoginClick = { navigateToEmailLoginFragment() }, - navigateToJoinFragment = { joinType -> navigateToJoinFragment(joinType) }, - navigateToBottomNaviFragment = { joinType -> navigateToBottomNaviFragment(joinType) }, - showLoginErrorDialog = { message -> showLoginErrorDialog(message) }, - showSnackBar = { message -> requireActivity().showLoginSnackBar(message,null) } + onNavigateEmailLoginClick = { navigateToEmailLoginFragment() }, ) } } @@ -125,6 +182,7 @@ class SocialLoginFragment : Fragment() { } private fun navigateToBottomNaviFragment(joinType: JoinType) { + viewModel.registerFcmTokenToServer() val bundle = Bundle().apply { putString("joinType", joinType.provider) } diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/social/SocialLoginScreen.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/social/SocialLoginScreen.kt index b7acb5c9..597feb97 100644 --- a/app/src/main/java/kr/co/lion/modigm/ui/login/social/SocialLoginScreen.kt +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/social/SocialLoginScreen.kt @@ -1,15 +1,7 @@ package kr.co.lion.modigm.ui.login.social -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween import androidx.compose.foundation.Image -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -21,250 +13,196 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import kotlinx.coroutines.launch import kr.co.lion.modigm.R -import kr.co.lion.modigm.ui.login.social.component.EmailLoginButton -import kr.co.lion.modigm.ui.login.social.component.GithubLoginButton -import kr.co.lion.modigm.ui.login.social.component.KakaoLoginButton -import kr.co.lion.modigm.ui.login.social.component.SocialLoginLoading -import kr.co.lion.modigm.util.JoinType +import kr.co.lion.modigm.ui.login.component.LoginLoading +import kr.co.lion.modigm.ui.login.component.ScrollArrow +import kr.co.lion.modigm.ui.login.component.SocialLoginButton +import kr.co.lion.modigm.ui.login.util.dpToSp @Composable fun SocialLoginScreen( - viewModel: SocialLoginViewModel, + isLoading: Boolean, onKakaoLoginClick: () -> Unit, onGithubLoginClick: () -> Unit, - onEmailLoginClick: () -> Unit, - navigateToJoinFragment: (JoinType) -> Unit, - navigateToBottomNaviFragment: (JoinType) -> Unit, - showLoginErrorDialog: (Throwable) -> Unit, - showSnackBar: (String) -> Unit -) { - val scrollState = rememberScrollState() - val isLoading by viewModel.isLoading.observeAsState(false) - val kakaoLoginResult by viewModel.kakaoLoginResult.observeAsState(false) - val githubLoginResult by viewModel.githubLoginResult.observeAsState(false) - val kakaoJoinResult by viewModel.kakaoJoinResult.observeAsState(false) - val githubJoinResult by viewModel.githubJoinResult.observeAsState(false) - val emailLoginResult by viewModel.emailAutoLoginResult.observeAsState(false) - val kakaoLoginError by viewModel.kakaoLoginError.observeAsState() - val githubLoginError by viewModel.githubLoginError.observeAsState() - val autoLoginError by viewModel.autoLoginError.observeAsState() - - LaunchedEffect(kakaoLoginResult) { - if (kakaoLoginResult) { - val joinType = JoinType.KAKAO - viewModel.registerFcmTokenToServer() - navigateToBottomNaviFragment(joinType) - } - } + onNavigateEmailLoginClick: () -> Unit, - LaunchedEffect(githubLoginResult) { - if (githubLoginResult) { - val joinType = JoinType.GITHUB - viewModel.registerFcmTokenToServer() - navigateToBottomNaviFragment(joinType) - } - } - - LaunchedEffect(kakaoJoinResult) { - if (kakaoJoinResult) { - val joinType = JoinType.KAKAO - navigateToJoinFragment(joinType) - } - } - - LaunchedEffect(githubJoinResult) { - if (githubJoinResult) { - val joinType = JoinType.GITHUB - navigateToJoinFragment(joinType) - } - } - - LaunchedEffect(emailLoginResult) { - if (emailLoginResult) { - viewModel.registerFcmTokenToServer() - val joinType = JoinType.EMAIL - navigateToBottomNaviFragment(joinType) - } - } - - LaunchedEffect(kakaoLoginError) { - kakaoLoginError?.let { e -> - showLoginErrorDialog(e) - } - } - - LaunchedEffect(githubLoginError) { - githubLoginError?.let { e -> - showLoginErrorDialog(e) - } - } - - LaunchedEffect(autoLoginError) { - autoLoginError?.let { e -> - showSnackBar(e.message.toString()) - } - } + ) { + val scrollState = rememberScrollState() Box( modifier = Modifier.fillMaxSize() ) { - BackgroundImage() + Image( + painter = painterResource(id = R.drawable.background_login2), + contentDescription = "Social Login Background", + modifier = Modifier + .fillMaxSize() + .blur(5.dp), + contentScale = ContentScale.Crop + ) + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.4f)) + ) Column( modifier = Modifier .verticalScroll(scrollState) .fillMaxSize() .padding(30.dp) ) { - LogoImage() - LoginTitle() - LoginSubTitleText() - KakaoLoginButton(onClick = onKakaoLoginClick) - GithubLoginButton(onClick = onGithubLoginClick) - EmailLoginButton(onClick = onEmailLoginClick) - } - SocialLoginScrollArrow( - scrollState = scrollState, - modifier = Modifier.align(Alignment.BottomCenter) - ) - SocialLoginLoading(isLoading = isLoading) - } -} - -@Composable -fun BackgroundImage() { - Image( - painter = painterResource(id = R.drawable.background_login2), - contentDescription = "Social Login Background", - modifier = Modifier - .fillMaxSize() - .blur(5.dp), - contentScale = ContentScale.Crop - ) - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.4f)) - ) -} - -@Composable -fun LogoImage() { - Image( - painter = painterResource(id = R.drawable.logo_modigm), - contentDescription = "Login Logo", - modifier = Modifier - .fillMaxSize() - .padding(top = 100.dp) - .size(150.dp, 150.dp), - alignment = Alignment.Center - ) -} - -@Composable -fun LoginTitle() { - Text( - text = "모우다임", - modifier = Modifier - .wrapContentWidth() - .wrapContentHeight() - .padding(top = 60.dp), - fontFamily = FontFamily(Font(R.font.one_mobile_pop_otf)), - fontSize = 70.sp, - color = Color.White, - fontWeight = FontWeight.Normal - ) -} - -@Composable -fun LoginSubTitleText() { - Text( - text = "개발자 스터디의 새로운 패러다임", - modifier = Modifier - .wrapContentWidth() - .wrapContentHeight() - .padding(top = 10.dp), - fontFamily = FontFamily(Font(R.font.one_mobile_pop_otf)), - fontSize = 22.sp, - color = Color.White, - fontWeight = FontWeight.Normal - ) -} - -@Composable -fun SocialLoginScrollArrow( - scrollState: ScrollState, - modifier: Modifier = Modifier -) { - val isVisible = remember { mutableStateOf(false) } - val coroutineScope = rememberCoroutineScope() - - LaunchedEffect(scrollState) { - snapshotFlow { scrollState.canScrollForward } - .collect { canScrollForward -> - isVisible.value = canScrollForward - } - } - - Box( - modifier = modifier - .fillMaxWidth() - .height(50.dp) - .background(Color.Transparent), - contentAlignment = Alignment.Center - ) { - if (isVisible.value) { Image( - painter = painterResource(id = R.drawable.arrow_down_24px), - contentDescription = "Scroll Arrow", + painter = painterResource(id = R.drawable.logo_modigm), + contentDescription = "로그인 로고 이미지", + modifier = Modifier + .fillMaxSize() + .padding(top = 100.dp) + .size(150.dp, 150.dp), + alignment = Alignment.Center + ) + Text( + text = "모우다임", + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(top = 60.dp), + fontFamily = FontFamily(Font(R.font.one_mobile_pop_otf)), + fontSize = dpToSp(70.dp), + color = Color.White, + fontWeight = FontWeight.Normal + ) + Text( + text = "개발자 스터디의 새로운 패러다임", + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(top = 10.dp), + fontFamily = FontFamily(Font(R.font.one_mobile_pop_otf)), + fontSize = dpToSp(22.dp), + color = Color.White, + fontWeight = FontWeight.Normal + ) + + SocialLoginButton( + modifier = Modifier + .fillMaxWidth() + .padding(top = 40.dp), + onClick = onKakaoLoginClick, + content = { + Image( + painter = painterResource(id = R.drawable.kakao_login_large_wide), + contentDescription = "카카오 로그인", + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + contentScale = ContentScale.FillBounds + ) + }, + colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent) + ) + SocialLoginButton( modifier = Modifier - .size(24.dp) - .clickable { - coroutineScope.launch { - scrollState.animateScrollTo(scrollState.maxValue) - } + .fillMaxWidth() + .padding(top = 30.dp) + .height(48.dp), + onClick = onGithubLoginClick, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Black, + contentColor = Color.White + ), + content = { + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + Icon( + modifier = Modifier + .size(24.dp) + .align(Alignment.CenterStart), + painter = painterResource(id = R.drawable.icon_github_logo), + contentDescription = "깃허브 로고 아이콘", + tint = Color.White + ) + Text( + modifier = Modifier + .align(Alignment.Center) + .offset(x = 12.dp), + text = "깃허브 로그인", + fontSize = dpToSp(16.dp), + color = Color.White + ) } - .animateEnterExit(), - colorFilter = ColorFilter.tint(Color.White) + } ) + + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TextButton( + onClick = onNavigateEmailLoginClick, + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + .padding(top = 20.dp), + colors = ButtonDefaults.textButtonColors( + contentColor = Color.White + ), + shape = RoundedCornerShape(0.dp), + ) { + Text( + text = "다른 방법으로 로그인", + fontSize = dpToSp(16.dp), + fontFamily = FontFamily(Font(R.font.one_mobile_pop_otf)), + fontWeight = FontWeight.Normal, + color = Color.White + ) + } + } } + + ScrollArrow( + scrollState = scrollState, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + ) + + LoginLoading( + modifier = Modifier + .fillMaxSize(), + isLoading = isLoading + ) } } +@Preview(showBackground = true) @Composable -fun Modifier.animateEnterExit(): Modifier { - val infiniteTransition = rememberInfiniteTransition(label = "") - val offsetY by infiniteTransition.animateFloat( - initialValue = 0f, - targetValue = 10f, - animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 800, easing = LinearEasing), - repeatMode = RepeatMode.Reverse - ), - label = "" +fun SocialLoginScreenPreview() { + SocialLoginScreen( + isLoading = false, + onKakaoLoginClick = {}, + onGithubLoginClick = {}, + onNavigateEmailLoginClick = {}, ) - return this.offset(y = offsetY.dp) } \ No newline at end of file diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/util/AnimateEnterExit.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/util/AnimateEnterExit.kt new file mode 100644 index 00000000..b71c06af --- /dev/null +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/util/AnimateEnterExit.kt @@ -0,0 +1,29 @@ +package kr.co.lion.modigm.ui.login.util + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun Modifier.animateEnterExit(): Modifier { + val infiniteTransition = rememberInfiniteTransition(label = "") + val offsetY by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 10f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 800, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "" + ) + return this.offset(y = offsetY.dp) +} + diff --git a/app/src/main/java/kr/co/lion/modigm/ui/login/util/DpToSp.kt b/app/src/main/java/kr/co/lion/modigm/ui/login/util/DpToSp.kt new file mode 100644 index 00000000..95eddbbf --- /dev/null +++ b/app/src/main/java/kr/co/lion/modigm/ui/login/util/DpToSp.kt @@ -0,0 +1,8 @@ +package kr.co.lion.modigm.ui.login.util + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp + +@Composable +fun dpToSp(dp: Dp) = with(LocalDensity.current) { dp.toSp() } \ No newline at end of file diff --git a/app/src/main/java/kr/co/lion/modigm/util/FragmentName.kt b/app/src/main/java/kr/co/lion/modigm/util/FragmentName.kt index 745c904c..16adb6b6 100644 --- a/app/src/main/java/kr/co/lion/modigm/util/FragmentName.kt +++ b/app/src/main/java/kr/co/lion/modigm/util/FragmentName.kt @@ -23,8 +23,8 @@ enum class FragmentName (var str: String){ EMAIL_LOGIN("EmailLoginFragment"), FIND_EMAIL("FindEmailFragment"), FIND_EMAIL_AUTH("FindEmailAuthFragment"), - FIND_PW("FindPwFragment"), - FIND_PW_AUTH("FindPwAuthFragment"), + FIND_PASSWORD("FindPasswordFragment"), + FIND_PW_AUTH("FindPasswordAuthFragment"), RESET_PW("ResetPwFragment"), // 프로필