Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

회원가입 입력 구현 #73

Merged
merged 15 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
android:roundIcon="@mipmap/ic_priceguard_round"
android:supportsRtl="true"
android:theme="@style/Theme.PriceGuard"
tools:targetApi="31">
tools:targetApi="34">
<activity
android:name=".ui.intro.IntroActivity"
android:exported="false" />
Expand Down
177 changes: 175 additions & 2 deletions android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,184 @@
package app.priceguard.ui.signup

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.NestedScrollView
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import app.priceguard.R
import app.priceguard.databinding.ActivitySignupBinding
import app.priceguard.ui.signup.SignupViewModel.SignupEvent
import app.priceguard.ui.signup.SignupViewModel.SignupUIState
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback
import com.google.android.material.button.MaterialButton
import com.google.android.material.progressindicator.CircularProgressIndicatorSpec
import com.google.android.material.progressindicator.IndeterminateDrawable
import kotlinx.coroutines.launch

class SignupActivity : AppCompatActivity() {

private lateinit var binding: ActivitySignupBinding
private val signupViewModel: SignupViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_signup)
binding.vm = signupViewModel
binding.lifecycleOwner = this

setNavigationButton()
disableAppBarScroll()
observeState()
}

private fun disableAppBarScroll() {
val clLayoutParams = binding.ablSignupTopbar.layoutParams as CoordinatorLayout.LayoutParams
val scrollView: NestedScrollView = binding.nsvSignupContent
val viewTreeObserver = scrollView.viewTreeObserver
val disabledAblBehavior = AppBarLayout.Behavior()
disabledAblBehavior.setDragCallback(object : DragCallback() {
override fun canDrag(appBarLayout: AppBarLayout): Boolean {
return false
}
})
val enabledAblBehavior = AppBarLayout.Behavior()
enabledAblBehavior.setDragCallback(object : DragCallback() {
override fun canDrag(appBarLayout: AppBarLayout): Boolean {
return true
}
})
EunhoKang marked this conversation as resolved.
Show resolved Hide resolved

viewTreeObserver.addOnGlobalLayoutListener {
if (scrollView.measuredHeight - scrollView.getChildAt(0).height >= 0) {
clLayoutParams.behavior = disabledAblBehavior
} else {
clLayoutParams.behavior = enabledAblBehavior
}
}
}

private fun handleSignupEvent(event: SignupEvent) {
val spec =
CircularProgressIndicatorSpec(
this,
null,
0,
R.style.Theme_PriceGuard_CircularProgressIndicator
)

val progressIndicatorDrawable =
IndeterminateDrawable.createCircularDrawable(this, spec).apply {
setVisible(true, true)
}

when (event) {
is SignupEvent.SignupStart -> {
(binding.btnSignupSignup as MaterialButton).icon = progressIndicatorDrawable
}

is SignupEvent.SignupFinish -> {
(binding.btnSignupSignup as MaterialButton).icon = null
}

is SignupEvent.SignupError -> {}
}
}

private fun setNavigationButton() {
binding.mtSignupTopbar.setNavigationOnClickListener {
finish()
}
}

private fun observeState() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
signupViewModel.state.collect { state ->
updateNameTextFieldUI(state)
updateEmailTextFieldUI(state)
updatePasswordTextFieldUI(state)
updateRetypePasswordTextFieldUI(state)
}
}
}

lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
signupViewModel.eventFlow.collect { event ->
handleSignupEvent(event)
}
}
}
}

private fun updateNameTextFieldUI(state: SignupUIState) {
when (state.isNameError) {
true -> {
binding.tilSignupName.error = getString(R.string.name_required)
}

else -> {
binding.tilSignupName.error = null
}
}
}

private fun updateEmailTextFieldUI(state: SignupUIState) {
when (state.isEmailError) {
null -> {
binding.tilSignupEmail.error = null
binding.tilSignupEmail.helperText = " "
}

true -> {
binding.tilSignupEmail.error = getString(R.string.invalid_email)
}

false -> {
binding.tilSignupEmail.error = null
binding.tilSignupEmail.helperText = getString(R.string.valid_email)
}
}
}

private fun updatePasswordTextFieldUI(state: SignupUIState) {
when (state.isPasswordError) {
null -> {
binding.tilSignupPassword.error = null
binding.tilSignupPassword.helperText = " "
}

true -> {
binding.tilSignupPassword.error = getString(R.string.invalid_password)
}

false -> {
binding.tilSignupPassword.error = null
binding.tilSignupPassword.helperText = getString(R.string.valid_password)
}
}
}

private fun updateRetypePasswordTextFieldUI(state: SignupUIState) {
when (state.isRetypePasswordError) {
null -> {
binding.tilSignupRetypePassword.error = null
binding.tilSignupRetypePassword.helperText = " "
}

true -> {
binding.tilSignupRetypePassword.error = getString(R.string.password_mismatch)
}

false -> {
binding.tilSignupRetypePassword.error = null
binding.tilSignupRetypePassword.helperText = getString(R.string.password_match)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package app.priceguard.ui.signup

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class SignupViewModel : ViewModel() {

data class SignupUIState(
Taewan-P marked this conversation as resolved.
Show resolved Hide resolved
val name: String = "",
val email: String = "",
val password: String = "",
val retypePassword: String = "",
val isSignupReady: Boolean = false,
val isNameError: Boolean? = null,
val isEmailError: Boolean? = null,
val isPasswordError: Boolean? = null,
val isRetypePasswordError: Boolean? = null,
val isSignupStarted: Boolean = false
)

private val emailPattern =
"""^[\w.+-]+@((?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,6}$""".toRegex()
private val passwordPattern =
"""^(?=[A-Za-z\d!@#$%^&*]*\d)(?=[A-Za-z\d!@#$%^&*]*[a-z])(?=[A-Za-z\d!@#$%^&*]*[A-Z])(?=[A-Za-z\d!@#$%^&*]*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,16}$""".toRegex()

private val _state: MutableStateFlow<SignupUIState> = MutableStateFlow(SignupUIState())
val state: StateFlow<SignupUIState> = _state.asStateFlow()

private val _eventFlow: MutableSharedFlow<SignupEvent> = MutableSharedFlow(replay = 0)
val eventFlow: SharedFlow<SignupEvent> = _eventFlow.asSharedFlow()

fun signup() {
viewModelScope.launch {
if (_state.value.isSignupStarted) {
Log.d("Signup", "Signup already requested. Skipping")
return@launch
}

sendEvent(SignupEvent.SignupStart)
updateSignupStarted(true)
Log.d("ViewModel", "Event Start Sent")
delay(3000L)
// TODO: 제거하고 Signup 네트워크 로직 넣기
// TODO: 회원가입 성공시 로그인 정보 저장하기
// TODO: 회원가입 실패시 메세지 표시하기
sendEvent(SignupEvent.SignupFinish)
updateSignupStarted(false)
Log.d("ViewModel", "Event Finish Sent")
}
}

fun updateName(name: String) {
_state.value = _state.value.copy(name = name)

updateNameError()
updateIsSignupReady()
}

fun updateEmail(email: String) {
_state.value = _state.value.copy(email = email)

updateEmailError()
updateIsSignupReady()
}

fun updatePassword(password: String) {
Taewan-P marked this conversation as resolved.
Show resolved Hide resolved
_state.value = _state.value.copy(password = password)

updatePasswordError()
updateRetypePasswordError()
updateIsSignupReady()
}

fun updateRetypePassword(retypePassword: String) {
_state.value = _state.value.copy(retypePassword = retypePassword)

updateRetypePasswordError()
updateIsSignupReady()
}

private fun isValidName(): Boolean {
return _state.value.name.isNotBlank()
}

private fun isValidEmail(): Boolean {
return emailPattern.matchEntire(_state.value.email) != null
}

private fun isValidPassword(): Boolean {
return passwordPattern.matchEntire(_state.value.password) != null
}

private fun isValidRetypePassword(): Boolean {
return _state.value.retypePassword.isNotBlank() && _state.value.password == _state.value.retypePassword
}

private fun sendEvent(event: SignupEvent) {
viewModelScope.launch {
_eventFlow.emit(event)
}
}

private fun updateIsSignupReady() {
_state.value =
_state.value.copy(isSignupReady = isValidName() && isValidEmail() && isValidPassword() && isValidRetypePassword())
}

private fun updateNameError() {
_state.value.let { uiState ->
if (isValidName()) {
_state.value = uiState.copy(isNameError = false)
} else {
_state.value = uiState.copy(isNameError = true)
}
}
}

private fun updateEmailError() {
_state.value.let { uiState ->
when {
isValidEmail() -> {
_state.value = uiState.copy(isEmailError = false)
}

uiState.email.isEmpty() -> {
_state.value = uiState.copy(isEmailError = null)
}

else -> {
_state.value = uiState.copy(isEmailError = true)
}
}
}
}

private fun updatePasswordError() {
_state.value.let { uiState ->
when {
isValidPassword() -> {
_state.value = uiState.copy(isPasswordError = false)
}

uiState.password.isEmpty() -> {
_state.value = uiState.copy(isPasswordError = null)
}

else -> {
_state.value = uiState.copy(isPasswordError = true)
}
}
}
}

private fun updateRetypePasswordError() {
_state.value.let { uiState ->
when {
isValidRetypePassword() -> {
_state.value = uiState.copy(isRetypePasswordError = false)
}

uiState.retypePassword.isEmpty() -> {
_state.value = uiState.copy(isRetypePasswordError = null)
}

else -> {
_state.value = uiState.copy(isRetypePasswordError = true)
}
}
}
}

private fun updateSignupStarted(started: Boolean) {
_state.value = _state.value.copy(isSignupStarted = started)
}

sealed class SignupEvent {
object SignupStart : SignupEvent()
object SignupFinish : SignupEvent()
object SignupError : SignupEvent()
}
}
2 changes: 1 addition & 1 deletion android/app/src/main/res/drawable/ic_back.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:tint="?attr/colorOnSurfaceVariant" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>
Loading