[Android / Kotlin] 회원가입시 필드 유효성 체크

Subeen·2023년 12월 19일
1

Android

목록 보기
22/73

과제의 목표

  • 공통
    • EditText를 실시간으로 입력 받아 유효성을 체크한다.
    • EditText Focus out 할 때 입력하지 않았을 경우 에러 텍스트를 노출한다.
  • 이메일
    • EditText Focus 할 때 이메일을 입력하지 않았을 경우 에러 텍스트를 노출한다.
    • 서비스 제공사를 Spinner로 변경, 직접 입력일 경우 EditText를 노출한다.
  • 비밀번호
    • 비밀번호 강도 체크하여 영문과 숫자를 포함하지 않고 길이가 10자 미만일 경우 에러 텍스트를 노출한다.
  • 회원가입 버튼
    • 모든 항목의 유효성 체크를 완료했을 때 버튼 활성화, 그러지 못할 경우 비활성화 한다.

결과 화면

ValidationActivity.kt

class ValidationActivity : AppCompatActivity() {
    private val etName: EditText by lazy {
        findViewById(R.id.et_name)
    }
    private val etEmail: EditText by lazy {
        findViewById(R.id.et_email)
    }
    private val etEmailServiceProvider: EditText by lazy {
        findViewById(R.id.et_email_service_provider)
    }
    private val etPwd: EditText by lazy {
        findViewById(R.id.et_pwd)
    }
    private val etPwdCheck: EditText by lazy {
        findViewById(R.id.et_pwd_check)
    }
    private val spServiceProvider: Spinner by lazy {
        findViewById(R.id.sp_service_provider)
    }
    private val tvNameMessage: TextView by lazy {
        findViewById(R.id.tv_name_message)
    }
    private val tvEmailMessage: TextView by lazy {
        findViewById(R.id.tv_email_message)
    }
    private val tvPwdMessage: TextView by lazy {
        findViewById(R.id.tv_pwd_message)
    }
    private val tvPwdCheckMessage: TextView by lazy {
        findViewById(R.id.tv_pwd_check_message)
    }
    private val btnSignUp: Button by lazy {
        findViewById(R.id.btn_signUp)
    }

    private val editTexts
        get() = listOf(
            etName,
            etEmail,
            etEmailServiceProvider,
            etPwd,
            etPwdCheck
        )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_validation)

        initView()
    }

    private fun setSpinnerServiceProvider() {
    	/*
        스피너에 어댑터 등록한다. 
        R.array.emailList : strings.xml 내의 emailList 아이템 목록을 추가한다.
        */
        spServiceProvider.adapter = ArrayAdapter.createFromResource(
            this,
            R.array.emailList,
            android.R.layout.simple_spinner_item
        )

        val lastIndex = spServiceProvider.adapter.count - 1 // 스피너의 마지막 요소 
        /*
        onItemSelectedListener가 최초에 실행되는 문제를 방지하고자 Listener 이전에 setselection(position, false)을 해준다.
        마지막 요소인 "직접 입력"으로 초기화 
        */
        spServiceProvider.setSelection(lastIndex, false)  

		// 사용자가 선택한 값을 알기 위해 Listener를 추가한다.
        spServiceProvider.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
            override fun onItemSelected(
                parent: AdapterView<*>?,
                view: View?,
                position: Int,
                id: Long
            ) {
                if (position == lastIndex) {  // "직접 입력" 일 경우
                	// EditText가 활성화 된다. 
                    etEmailServiceProvider.isEnabled = true
                    etEmailServiceProvider.setText("")
                } else {
                    /*
                    EditText가 비활성화 되고 선택한 값을 출력해준다.
                    gmail.com 선택시 @gmail.com 출력
                    */
                    etEmailServiceProvider.isEnabled = false
                    val text = "@${spServiceProvider.selectedItem}"
                    etEmailServiceProvider.setText(text)
                }
            }

            override fun onNothingSelected(p0: AdapterView<*>?) = Unit
        }
    }

    private fun initView() {
        setTextChangeListener()
        setOnFocusChangedListener()
        setSpinnerServiceProvider()

        btnSignUp.setOnClickListener {
            // TODO 회원가입 버튼 클릭시
        }
    }

    private fun setTextChangeListener() {  // addTextChangedListener
        editTexts.forEach { editText ->
            editText.addTextChangedListener(EditTextWatcher(
                onChanged = { _, _, _, _ ->
                    editText.setErrorMessage()  // EditText 유효성 체크 후 에러 텍스트 노출
                    setButtonEnable()  // 유효성 체크에 따라 버튼 활성화 상태 변경 
                }
            ))
        }
    }

    private fun setOnFocusChangedListener() {  // setOnFocusChangeListener
        editTexts.forEach { editText ->
            editText.setOnFocusChangeListener { _, hasFocus ->
                if (!hasFocus) {
                    editText.setErrorMessage()  // EditText 유효성 체크 후 에러 텍스트 노출
                    setButtonEnable()  // 유효성 체크에 따라 버튼 활성화 상태 변경
                }
            }
        }
    }

	// EditText별 유효성 체크 후 에러 텍스트 노출 및 Visibility 상태 변경
    private fun EditText.setErrorMessage() {
        val message = when (this) {
            etName -> getMessageValidName()
            etEmail -> getMessageValidEmail()
            etEmailServiceProvider -> getMessageValidEmailRegex()
            etPwd -> getMessageValidPassword()
            etPwdCheck -> getConfirmPassword()
            else -> return
        }

        val textView = when (this) {
            etName -> tvNameMessage
            etEmail, etEmailServiceProvider -> tvEmailMessage
            etPwd -> tvPwdMessage
            etPwdCheck -> tvPwdCheckMessage
            else -> return
        }

        textView.text = message
        textView.setVisibility()
    }

    /*
    TextView가 isBlank()가 아닐 경우 에러 메시지가 노출 된 상태기에 TextView를 VISIBLE
    isBlank()일 경우는 유효성 검사에 성공한 상태기에 GONE 처리 해준다.
    */
    private fun TextView.setVisibility() {
        visibility = if (text.isBlank()) GONE else VISIBLE
    }

    private fun getMessageForInput(editText: EditText, message: String): String =
        if (editText.text.isBlank()) {	// EditText에 입력 된 값이 없을 경우
            message
        } else {
            ""
        }

	// 이름 유효성 체크
    private fun getMessageValidName(): String =
        getMessageForInput(etName, getString(R.string.text_input_name))

	// 이메일 유효성 체크
    private fun getMessageValidEmail(): String =
        getMessageForInput(etEmail, getString(R.string.text_input_email))

	// 이메일 정규 표현식 체크
    private fun getMessageValidEmailRegex(): String {
        val text = etEmailServiceProvider.text.toString()
        val email = etEmail.text.toString() + text
        return when {
            text.isBlank() -> getString(R.string.text_input_service_provider)
            !Patterns.EMAIL_ADDRESS.matcher(email).matches() -> getString(R.string.text_check_email)
            else -> ""
        }
    }

	// 비밀번호 유효성 체크
    private fun getMessageValidPassword(): String {
        val text = etPwd.text.toString()
        val pattern = "^(?=.*[A-Za-z])(?=.*[0-9])[A-Za-z[0-9]]{10,}\$"
        return when {
            text.length < 10 -> getString(R.string.text_check_pwd_length)
            !Pattern.matches(pattern, text) -> getString(R.string.text_check_pwd_regex)
            else -> ""
        }
    }

	// 비밀번호가 일치하는지 체크 
    private fun getConfirmPassword(): String =  
        if (etPwd.text.toString() != etPwdCheck.text.toString()) {
            getString(R.string.text_pwd_mismach)
        } else {
            ""
        }

	// 버튼 활성화 상태 변경을 위한 함수 
    private fun setButtonEnable() {
        btnSignUp.isEnabled = getMessageValidName().isBlank()
                && getMessageValidEmail().isBlank()
                && getMessageValidEmailRegex().isBlank()
                && getMessageValidPassword().isBlank()
                && getConfirmPassword().isBlank()
    }

}

EditTextWatcher.kt

/*
TextWatcher를 공통 클래스로 만들어 사용한다.
원하는 함수만 사용할 수 있게 하기 위해 nullable한 람다 함수를 매개 변수로 받도록 한다.
*/
class EditTextWatcher(
    private val afterChanged: ((Editable?) -> Unit) = {},
    private val beforeChanged: ((CharSequence?, Int, Int, Int) -> Unit) = { _, _, _, _ -> },
    private val onChanged: ((CharSequence?, Int, Int, Int) -> Unit) = { _, _, _, _ -> }
) : TextWatcher {
    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        beforeChanged(s, start, count, after)
    }

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        onChanged(s, start, before, count)
    }

    override fun afterTextChanged(s: Editable?) {
        afterChanged(s)
    }
}

activity_validation.xml

  • 비밀번호 입력시 텍스트를 가리기 위해 EditTextinputTypetextPassword로 지정 했으며 입력된 문자는 *** 로 표시된다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ValidationActivity">

    <TextView
        android:id="@+id/tv_name"
        style="@style/ThemeTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/text_name"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/et_name"
        style="@style/ThemeEditText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_name" />

    <TextView
        android:id="@+id/tv_name_message"
        style="@style/ThemeValidTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_name" />

    <TextView
        android:id="@+id/tv_email"
        style="@style/ThemeTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/text_email"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_name_message" />

    <EditText
        android:id="@+id/et_email"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="32dp"
        android:textSize="14sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_email" />

    <EditText
        android:id="@+id/et_email_service_provider"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        app:layout_constraintBottom_toBottomOf="@+id/et_email"
        app:layout_constraintStart_toEndOf="@id/et_email"
        app:layout_constraintTop_toBottomOf="@id/tv_email"
        app:layout_constraintTop_toTopOf="@id/et_email" />

    <Spinner
        android:id="@+id/sp_service_provider"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="32dp"
        app:layout_constraintBottom_toBottomOf="@id/et_email_service_provider"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/et_email_service_provider"
        app:layout_constraintTop_toTopOf="@id/et_email_service_provider" />

    <TextView
        android:id="@+id/tv_email_message"
        style="@style/ThemeValidTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_email" />

    <TextView
        android:id="@+id/tv_pwd"
        style="@style/ThemeTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/text_pwd"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_email" />

    <EditText
        android:id="@+id/et_pwd"
        style="@style/ThemeEditText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/text_pwd_condition"
        android:inputType="textPassword"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_pwd" />

    <TextView
        android:id="@+id/tv_pwd_message"
        style="@style/ThemeValidTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_pwd" />

    <TextView
        android:id="@+id/tv_pwd_check"
        style="@style/ThemeTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/text_pwd_check"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_pwd" />

    <EditText
        android:id="@+id/et_pwd_check"
        style="@style/ThemeEditText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textPassword"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_pwd_check" />

    <TextView
        android:id="@+id/tv_pwd_check_message"
        style="@style/ThemeValidTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_pwd_check" />

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn_signUp"
        style="@style/ThemeButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/btn_pressed_selector"
        android:enabled="false"
        android:text="@string/text_sign_up"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_pwd_check_message" />
</androidx.constraintlayout.widget.ConstraintLayout>

btn_pressed_selector.xml

  • Update (2023.12.20)
    • 회원 가입 버튼이 눌렸을 때의 액션 추가
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <solid android:color="@color/light_purple" />
            <stroke android:width="2dp" android:color="@color/purple" />
            <corners android:radius="10dp" />
        </shape>
    </item>
    <item android:state_pressed="false">
        <shape android:shape="rectangle">
            <solid android:color="#00FFFFFF" />
            <stroke android:width="2dp" android:color="@color/purple" />
            <corners android:radius="10dp" />
        </shape>
    </item>

</selector>

themes.xml

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Base.Theme.SelfIntroduction" parent="Theme.Material3.DayNight.NoActionBar">
        <!-- Customize your light theme here. -->
        <!-- <item name="colorPrimary">@color/my_light_primary</item> -->
    </style>

    <style name="Theme.SelfIntroduction" parent="Base.Theme.SelfIntroduction" />

    <style name="ThemeEditText" parent="Widget.AppCompat.EditText">
        <item name="android:textColor">@color/black</item>
        <item name="android:textSize">16sp</item>
        <item name="android:layout_marginStart">32dp</item>
        <item name="android:layout_marginEnd">32dp</item>
    </style>

    <style name="ThemeTextView" parent="Widget.AppCompat.TextView">
        <item name="android:textColor">@color/black</item>
        <item name="android:textSize">16sp</item>
        <item name="android:textStyle">bold</item>
        <item name="android:layout_marginStart">32dp</item>
        <item name="android:layout_marginEnd">32dp</item>
        <item name="android:layout_marginTop">16dp</item>
    </style>

    <style name="ThemeValidTextView" parent="Widget.AppCompat.TextView">
        <item name="android:textColor">@color/red</item>
        <item name="android:textSize">13sp</item>
        <item name="android:layout_marginStart">32dp</item>
        <item name="android:layout_marginEnd">32dp</item>
    </style>
    
    <style name="ThemeButton" parent="Widget.AppCompat.Button">
        <item name="android:textStyle">bold</item>
        <item name="android:layout_marginStart">32dp</item>
        <item name="android:layout_marginEnd">32dp</item>
        <item name="android:layout_marginBottom">32dp</item>
        <item name="android:textColor">@color/black</item>
        <item name="android:layout_marginTop">16dp</item>
    </style>

</resources>

strings.xml

<resources>
    <string name="text_input_name">이름을 입력하세요.</string>
    <string name="text_input_email">이메일을 입력해주세요.</string>
    <string name="text_input_service_provider">서비스 제공사를 입력해주세요.</string>
    <string name="text_check_email">올바른 이메일 주소를 입력해주세요.</string>
    <string name="text_check_pwd_length">10자 이상 입력해주세요.</string>
    <string name="text_check_pwd_regex">영문과 숫자를 포함해주세요.</string>
    <string name="text_pwd_mismach">비밀번호가 일치하지 않습니다.</string>
    <string name="text_pwd_condition">영문, 숫자, 10자 이상</string>
    <string name="text_name">이름</string>
    <string name="text_email">이메일</string>
    <string name="text_pwd">비밀번호</string>
    <string name="text_pwd_check">비밀번호 확인</string>
    <string name="text_sign_up">회원가입</string>

    <string-array name="emailList">
        <item>gmail.com</item>
        <item>kakao.com</item>
        <item>naver.com</item>
        <item>직접 입력</item>
    </string-array>
</resources>

내일배움캠프를 수강하게 된 이유 중 하나가 내 입맛대로 작성하던 코드를 개선하고 코드를 효율적으로 작성하는 방법을 배우고 싶은 마음에 수강하게 되었는데, 수준별 학습을 들으며 수강하길 정말 잘했다..! 고 생각했다. 🥹 튜터님의 실시간 코딩을 보며 코드는 이렇게 작성하는거구나.. 이렇게도 작성할 수 있구나 ! 하고 감탄 했고 내 코드는 중복 되는 부분이 많으니 이런 부분을 우선적으로 수정하여 재사용성을 높여야겠다고 느꼈다. 나름 중복 되는 부분을 함수로 만들어 사용했다고 생각했는데 더 개선할 수 있도록 많이 배워야겠다. 앞으로 새로 배우게 될 내용들이 너무나 기대된다 🤭

profile
개발 공부 기록 🌱

0개의 댓글