이번 포스팅에서는 먼저 회원가입 및 로그인 페이지에서 발생할 수 있는 예외를 처리하고, 유저의 정보를 저장할 것입니다. 또한 이 작업을 완료하면, 로그인 및 회원가입 페이지에서 해주어야 할 모든 내용이 끝나기 때문에, 앞으로는 바로 메인 페이지로 이동할 수 있도록 자동 로그인 기능과 메인페이지의 레이아웃을 구성하는 작업을 진행해보도록 하겠습니다.
회원가입 및 로그인에 필요한 예외 처리는 프론트엔드에서 처리할 수 있는 부분이 많기 때문에, 최대한 프론트엔드에서 예외를 처리하도록 하겠다. 당연한 이야기겠지만, 프론트엔드에서 예외를 처리하는 것이 훨씬 더 빠른 피드백을 제공할 수 있기 때문에, 데이터베이스가 필요하지 않은 간단한 예외는 최대한 프론트엔드 측에서 처리하도록 하겠다.
① 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도 아래와 같이 수정하자.
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", "회원가입 실패")
}
}
}
}
① 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")
}
}
① 푸시 알림은 지금 구현할 것은 아니지만, 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에 잘 저장될 것이다.
① 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)
}
}
}
① 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
① 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 파일을 아래와 같이 수정한다.
<?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가 잘 전환될 것이다.