회원가입 & 로그인 - 유저 정보 저장하기

변현섭·2023년 9월 1일
0
post-thumbnail

이번 포스팅에서는 먼저 회원가입 및 로그인 페이지에서 발생할 수 있는 예외를 처리하고, 유저의 정보를 저장할 것입니다. 또한 이 작업을 완료하면, 로그인 및 회원가입 페이지에서 해주어야 할 모든 내용이 끝나기 때문에, 앞으로는 바로 메인 페이지로 이동할 수 있도록 자동 로그인 기능과 메인페이지의 레이아웃을 구성하는 작업을 진행해보도록 하겠습니다.

1. 예외 처리

회원가입 및 로그인에 필요한 예외 처리는 프론트엔드에서 처리할 수 있는 부분이 많기 때문에, 최대한 프론트엔드에서 예외를 처리하도록 하겠다. 당연한 이야기겠지만, 프론트엔드에서 예외를 처리하는 것이 훨씬 더 빠른 피드백을 제공할 수 있기 때문에, 데이터베이스가 필요하지 않은 간단한 예외는 최대한 프론트엔드 측에서 처리하도록 하겠다.

① JoinActivity의 joinBtn의 setOnClickListener를 아래와 같이 수정하자.

val joinBtn = findViewById<Button>(R.id.joinBtn)
joinBtn.setOnClickListener {
    val nickname = findViewById<TextInputEditText>(R.id.nickname)
    val email = findViewById<TextInputEditText>(R.id.joinEmail)
    val password = findViewById<TextInputEditText>(R.id.joinPassword)
    val passwordChk = findViewById<TextInputEditText>(R.id.passwordChk)
    
    if(nickname.text.toString().isEmpty()) {
        Toast.makeText(this, "닉네임을 입력해주세요", Toast.LENGTH_SHORT).show()
    }
    
    else if(email.text.toString().isEmpty()) {
        Toast.makeText(this, "이메일을 입력해주세요", Toast.LENGTH_SHORT).show()
    }
    
    else if(password.text.toString().isEmpty()) {
        Toast.makeText(this, "비밀번호를 입력해주세요", Toast.LENGTH_SHORT).show()
    }
    
    else if(password.text.toString().length > 12 || password.text.toString().length < 6) {
        Toast.makeText(this, "비밀번호는 6~12자로만 입력할 수 있습니다.", Toast.LENGTH_SHORT).show()
    }
    
    else if(password.text.toString() != passwordChk.text.toString()) {
        Toast.makeText(this, "비밀번호와 비밀번호 확인의 입력 값이 다릅니다.", Toast.LENGTH_SHORT).show()
    }
    
    else {
        val postUserReq = PostUserReq(nickname.text.toString(), email.text.toString(),
            password.text.toString(), passwordChk.text.toString())
            
        auth.createUserWithEmailAndPassword(email.text.toString(), password.text.toString())
            .addOnCompleteListener(this) { task ->
                if (task.isSuccessful) {
                    CoroutineScope(Dispatchers.IO).launch {
                        val response = createUser(postUserReq)
                        if (response.isSuccess) {
                            Log.d("JoinActivity", "회원가입 완료: " + uid)
                            
                            // UI 업데이트는 Dispatchers.Main을 사용하여 메인 스레드에서 실행
                            withContext(Dispatchers.Main) {
                                Toast.makeText(this@JoinActivity, "가입을 환영합니다!", Toast.LENGTH_SHORT).show()
                                val intent = Intent(this@JoinActivity, LoginActivity::class.java)
                                startActivity(intent)
                            }
                        } else {
                            Log.d("JoinActivity", "회원가입 실패")
                            val message = response.message
                            Log.d("JoinActivity", message)
                            withContext(Dispatchers.Main) {
                                Toast.makeText(this@JoinActivity, message, Toast.LENGTH_SHORT).show()
                            }
                        }
                    }
                } else {
                    // UI 업데이트는 Dispatchers.Main을 사용하여 메인 스레드에서 실행
                    runOnUiThread {
                        Toast.makeText(this@JoinActivity, "이메일 형식이 잘못되었거나, 이미 존재하는 이메일입니다.", Toast.LENGTH_SHORT).show()
                    }
                }
            }
    }
}

② LoginActivity의 loginBtn의 setOnClickListener도 아래와 같이 수정하자.

  • 유저 로그인을 파이어베이스에서 처리하기 때문에, 프론트엔드에서 empty와 비밀번호 일치 여부를 모두 확인할 수 있다.
  • 백엔드에서는 로그인한 유저의 JWT를 생성해서 DB에 넣는다.
loginBtn.setOnClickListener {
    val email = findViewById<TextInputEditText>(R.id.email)
    val password = findViewById<TextInputEditText>(R.id.password)
    val uid = FirebaseAuthUtils.getUid()
    val postLoginReq = PostLoginReq(uid, email.text.toString(), password.text.toString())
    
    if(email.text.toString().isEmpty()) {
        Toast.makeText(this, "이메일을 입력해주세요", Toast.LENGTH_SHORT).show()
    }
    
    else if(password.text.toString().isEmpty()) {
        Toast.makeText(this, "비밀번호를 입력해주세요", Toast.LENGTH_SHORT).show()
    }
    
    else {
    	auth.createUserWithEmailAndPassword(email.text.toString(), password.text.toString())
			.addOnCompleteListener(this) { task ->
            	if (task.isSuccessful) {
                	createUser(postUserReq)
                    Toast.makeText(this, "가입을 환영합니다!", Toast.LENGTH_SHORT).show()
                    val intent = Intent(this, LoginActivity::class.java)
                    startActivity(intent)
                } else {
                	Log.d("JoinActivity", "회원가입 실패")
                }
            }
    }
}

2. 유저의 정보 저장하기

1) 파이어베이스 Realtime Database

① Realtime Database를 잠금모드로 시작한다.

② Realtime Database의 규칙 탭에 들어가 read와 write 권한을 모두 허용(true)해주자.

③ Module 수준의 build.gradle 파일에 아래의 의존성을 추가한다.

implementation("com.google.firebase:firebase-database-ktx")

④ 유저의 정보를 Realtime Database에 저장하기 위해 authentication 패키지 하위로, UserInfo data class를 생성한다.

data class UserInfo (
    val uid : String? = null,
    val userId : Long? = null,
    val deviceToken : String? = null,
    val accessToken : String? = null,
    val refreshToken : String? = null,
)

⑤ 이번에는 utils 패키지 하위로 FirebaseRef라는 Kotlin 클래스를 생성하여 Realtime Database에 값을 저장하기 위한 정적 멤버 함수를 만들자.

class FirebaseRef {
    companion object {
        val database = Firebase.database
        val userInfo = database.getReference("userInfo")
    }
}

2) 파이어베이스 Cloud Messaging

① 푸시 알림은 지금 구현할 것은 아니지만, device token을 UserInfo에 저장하기 위한 용도로만 잠깐 사용하겠다.

② Module 수준의 build.gradle 파일에 아래의 의존성을 추가한다.

implementation("com.google.firebase:firebase-messaging-ktx")

③ device token을 받아올 수 있도록 LoginActivity를 아래와 같이 수정한다.

auth.signInWithEmailAndPassword(email.text.toString(), password.text.toString())
    .addOnCompleteListener(this) { task ->
        if (task.isSuccessful) {
                CoroutineScope(Dispatchers.IO).launch {
                val response = loginUser(postLoginReq)
                Log.d("LoginActivity", response.toString())
                if (response.isSuccess) {
                    FirebaseMessaging.getInstance().token.addOnCompleteListener(
                        OnCompleteListener { task ->
                            if (!task.isSuccessful) {
                                Log.w("MyToken", "Fetching FCM registration token failed", task.exception)
                                return@OnCompleteListener
                            }
                            
                            val deviceToken = task.result
                            val userInfo = UserInfo(uid, response.result?.userId,
                                deviceToken, response.result?.accessToken, response.result?.refreshToken)
                            Log.d("userInfo", userInfo.toString())
                            
                            FirebaseRef.userInfo.child(uid).setValue(userInfo)
                            val intent = Intent(this@LoginActivity, MainActivity::class.java)
                            startActivity(intent)
                        })
                    Log.d("LoginActivity", "로그인 완료")
                } else {
                    Log.d("LoginActivity", "로그인 실패")
                    Toast.makeText(this@LoginActivity, "이메일 또는 비밀번호를 확인해주세요", Toast.LENGTH_SHORT).show()
                }
            }
        } else {
            Log.d("LoginActivity", "로그인 실패")
            Toast.makeText(this@LoginActivity, "이메일 또는 비밀번호를 확인해주세요", Toast.LENGTH_SHORT).show()
        }
    }

이제 회원가입 후 로그인을 하면, 유저의 정보가 Realtime Database와 AWS RDS에 잘 저장될 것이다.

3. 자동 로그인 구현하기

① utils 패키를 생성하고, 그 하위로 FirebaseAuthUtils를 추가한다.

class FirebaseAuth {
    companion object {
        private lateinit var auth : FirebaseAuth

        fun getUid() : String {
            auth = Firebase.auth
            return auth.currentUser?.uid.toString()
        }
    }
}

② SplashActivity를 아래와 같이 수정한다.

@Suppress("DEPRECATION")
class SplashActivity : AppCompatActivity() {

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

        val uid = FirebaseAuthUtils.getUid()

        if(uid == null) {
            Handler().postDelayed({
                startActivity(Intent(this, IntroActivity::class.java))
                finish()
            }, 3000)
        }

        else {
            Handler().postDelayed({
                startActivity(Intent(this, MainActivity::class.java))
                finish()
            }, 3000)
        }
    }
}

4. 메인페이지의 레이아웃 구성하기

1) navigation bar 만들기

① app 우클릭 > New > Android Resource File을 클릭하고 아래와 같이 설정한다.

② 아래의 경고창에서 OK를 누르면 된다.

③ activity_main.xml의 기존 TextView 태그를 삭제한 후, Design 모드로 열어 Containers > NavHostFragment를 드래그앤 드롭으로 흰 바탕 위에 올려 놓는다. 방금 만든 main_nav를 선택하고 OK를 누르면 된다.

④ Code로 돌아와서 width와 height를 match_parent로 변경하자.

<androidx.fragment.app.FragmentContainerView
    android:id="@+id/fragmentContainerView"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/main_nav"
    tools:layout_editor_absoluteX="1dp"
    tools:layout_editor_absoluteY="1dp" />

⑤ 네비게이션 바를 사용하기 위해서는 컴파일 버전이 최소 34 이상이어야 하므로 Module 수준의 build.gradle 파일에서 compileSdk를 34로 수정해야 한다.

android {
    namespace = "com.chrome.chattingapp"
    compileSdk = 34

2) Fragment 만들기

① default 패키지에 우클릭 > New > Fragment > Fragment (Blank)를 클릭한다.

② UserListFragment, ChatListFragment, MyPageFragment라는 이름으로 총 3개의 Fragment를 생성한다.

③ main_nav.xml 파일에 들어와서 휴대폰+ 아이콘을 클릭하면 방금 생성한 Fragment들이 보인다. 3개의 Fragment를 모두 클릭하여 화면에 띄워주자.

④ 1번 Fragment에서 2, 3번으로 연결하고, 2번 Fragment에서 1, 3번으로 연결하고, 3번 Fragment에서 1, 2번으로 연결한다.

⑤ fragment에 넣어줄 이미지를 friend, chat, mypage라는 이름으로 drawable 디렉토리에 넣는다. 이미지가 없다면 텍스트로 대체해도 된다.

⑥ frgament_user_list.xml, frgament_chat_list.xml, frgament_my_page.xml 파일을 아래와 같이 수정한다.

  • Fragment의 전환을 확인하기 위해 현재 Fragment에만 main_border를 적용하자.
  • 각 Fragment에 맞게 조금씩 변경해주어야 한다.
<?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"
    android:background="@drawable/main_border"
    tools:context=".ChatListFragment">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="100dp"
        app:layout_constraintBottom_toBottomOf="parent">

        <ImageView
        	android:id="@+id/freind"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="1dp"
            android:gravity="center"
            android:layout_weight="1"
            android:src="@drawable/friend" />

        <ImageView
        	android:id="@+id/chat"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@drawable/main_border"
            android:layout_margin="1dp"
            android:gravity="center"
            android:layout_weight="1"
            android:src="@drawable/chat" />

        <ImageView
        	android:id="@+id/mypage"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="1dp"
            android:gravity="center"
            android:layout_weight="1"
            android:src="@drawable/mypage" />

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

⑦ UserListFragment를 아래와 같이 수정한다.

class UserListFragment : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val view = inflater.inflate(R.layout.fragment_user_list, container, false)

        val chat = view.findViewById<ImageView>(R.id.chat)
        chat.setOnClickListener {
            it.findNavController().navigate(R.id.action_userListFragment_to_chatListFragment)
        }

        val mypage = view.findViewById<ImageView>(R.id.mypage)
        mypage.setOnClickListener {
            it.findNavController().navigate(R.id.action_userListFragment_to_myPageFragment)
        }
        return view
    }
}

⑧ ChatListFragment도 아래와 같이 수정한다.

class ChatListFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val view = inflater.inflate(R.layout.fragment_chat_list, container, false)

        val freind = view.findViewById<ImageView>(R.id.freind)
        freind.setOnClickListener {
            it.findNavController().navigate(R.id.action_chatListFragment_to_userListFragment)
        }

        val mypage = view.findViewById<ImageView>(R.id.mypage)
        mypage.setOnClickListener {
            it.findNavController().navigate(R.id.action_chatListFragment_to_myPageFragment)
        }
        return view
    }
}

⑨ 마지막으로, MyPageFragment를 아래와 같이 수정한다.

class MyPageFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val view = inflater.inflate(R.layout.fragment_my_page, container, false)

        val freind = view.findViewById<ImageView>(R.id.freind)
        freind.setOnClickListener {
            it.findNavController().navigate(R.id.action_myPageFragment_to_userListFragment)
        }

        val chat = view.findViewById<ImageView>(R.id.chat)
        chat.setOnClickListener {
            it.findNavController().navigate(R.id.action_myPageFragment_to_chatListFragment)
        }
        return view
    }
}

⑩ 상태바의 색깔을 main color로 바꿔주기 위해 themes.xml, themes.xml(night)의 style 컨테이너에 아래와 같이 item 컨테이너를 추가한다.

<style name="Base.Theme.ChattingApp" parent="Theme.Material3.DayNight.NoActionBar">
	...
	<item name="android:windowLightStatusBar">true</item>
	<item name="android:statusBarColor">#B8F8FB</item>

이제 코드를 실행시켜보면, 곧바로 메인페이지로 이동하면서 네비게이션 바에 따라 Fragment가 잘 전환될 것이다.

profile
Java Spring, Android Kotlin, Node.js, ML/DL 개발을 공부하는 인하대학교 정보통신공학과 학생입니다.

0개의 댓글