앱 프로젝트 - 13 - 1 (틴더 앱) - Firebase Realtime Database, Firebase Authenetication ㅡ 로그인 관리 ( Email Login, Facebook Login ), github 에서 사람들이 만들어 놓은, 내가 원하는 오픈소스 찾기

소개


레이아웃 소개

[ 로그인 화면 ]

[ 로그인하여 들어간 틴더화면 ]


알아야 할 내용

Firebase Realtime Database

Firebase 세팅

  1. 해당 글 참조 ( 프로젝트 등록 ) : https://velog.io/@odesay97/%EC%95%B1-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-09-1-%ED%91%B8%EC%8B%9C-%EC%95%8C%EB%A6%BC-%EC%88%98%EC%8B%A0%EA%B8%B0-firebase

  2. 프로잭트에서 앱 수준의 gradle에 Realtime Database에 대한 의존성 추가

    implementation platform('com.google.firebase:firebase-bom:29.1.0') ㅡ 이 코드 아래에 추가해줘야 함
    implementation 'com.google.firebase:firebase-database-ktx'

  3. Realtime Database 클릭 및 데이터베이스 만들기 클릭

  4. 데이터베이스 기본 설정 -> 기본 설정이며, 이후에 코딩을 통해 변경해줄 수 있다.

    [ 잠금 모드 ] --> 권한이 있는 사용자만 해당 DB를 제어할 수 있음

    [ 테스트 모드 ] --> 주석으로 되어 있는 기간까지, 권한이 있든 없든 해당 DB를 제어할 수 있음

  5. 이렇게 Realtime DB를 만들었으면,
    해당 DB에 대한 권한 코드가 쓰여있는 google-services.json를 다시 받아야 한다.

    -> 프로젝트 개요로 가서 프로젝트 클릭

    -> 톱니바퀴 클릭 -> 해당 Json파일을 받아서 안드로이드 프로젝트의 App폴더에 넣는다.

    ( 넣는 위치는 위의 링크 참조 )

Firebase Realtime Database 사용하기

  • 예제 => LikeActivity.kt
    --> 이번 앱에서 Realtime Database를 활용한 부분들 중에서
    중요 기능과 관련있는 것들을 가져온 것이다.

    --> 실제 활용을 보고 싶다면, 맨아래에 코드 소개 부분을 보길 바란다.

    
    ......
    
    class LikeActivity : AppCompatActivity() {
    
        private lateinit var userDB: DatabaseReference
        
            override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           setContentView(R.layout.activity_like)
           
                   userDB = Firebase.database.reference.child("Users")
                   
                   ......
                   
          // getCurrentUserId()는 현재 기기의 id를 반환하도록 제작한 메소드임
            val currentUserDB = userDB.child(getCurrentUserId())
                   
                           // DB에서 값을 가져오는 방법은 모두 리스너를 통해서만 가능함
           // 이번에는 단일 값을 가져오는 기능만 할 것이므로 addListenerForSingleValueEvent를 달아줌 -> 파라미터로 ValueEventListener를 구현하여 사용
           currentUserDB.addListenerForSingleValueEvent(object : ValueEventListener {
               override fun onDataChange(snapshot: DataSnapshot) {
                   // TODO DataChange가 일어났을 때 CallBack이 넘어오면서 호출됨
                   // 처음에 리스너를 달았을 때는 처음에 리스너가 없었기 때문에 해당 DB에 데이터가 존재하면 이벤트가 내려오게 됨
                   // 즉, 리스너를 처음 걸었을 경우와 DB의 데이터가 변경되었을 경우 호출되는 리스너라고 보면 된다.
    
                   Log.e("SingleValueEvent",snapshot.key.toString().orEmpty())
    
                   if (snapshot.child("name").value == null) {
                       showNameInputPopUp()
                       return
                   }
                   // todo 유저 정보를 갱신해라
                   getUnSelectedUsers()
               }
    
               override fun onCancelled(error: DatabaseError) {
                   // TODO Cancel이 일어났을 때 CallBack이 넘어오면서 호출됨
               }
           })
                   
            
            ......
            
            
       private fun getUnSelectedUsers() {
    
           // DB에서 값을 가져오는 방법은 모두 리스너를 통해서만 가능함
           // DB에서 데이터를 유동적으로 가져오기 위해서 사용되는 리스너
           userDB.addChildEventListener(object : ChildEventListener {
    
               // 해당 위치의 데이터를 하나씩 snapshot에 담아서 가져옴
               override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
    
                   Log.e("ChildAdd",snapshot.key.toString().orEmpty())
                       
                       ......
                   }
               }
    
               // 해당 위치에서 데이터가 변경되었을 경우, 변경이 있었던 영역을 가져옴
               override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
    
                   Log.e("ChildChange",snapshot.key.toString().orEmpty())
    
                      ......
               }
    
               // 데이터 삭제
               override fun onChildRemoved(snapshot: DataSnapshot) {}
    
               // 데이터가 이동
               override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
    
               // 데이터 제어중 오류 발생
               override fun onCancelled(error: DatabaseError) {}
           })
       }
       
           // Realtime Database에 데이터를 넣는 메소드
       private fun saveUserName(name: String) {
    
          // getCurrentUserId()는 현재 기기의 id를 반환하도록 제작한 메소드임
           val userId = getCurrentUserId()
           val currentUserDB = userDB.child(userId)
           val user = mutableMapOf<String, Any>()
           user["name"] = name
           currentUserDB.updateChildren(user)
           // 해당 경로에 데이터 추가 ( 추가할 데이터는 딕셔너리 형태여야 함 -> Json형태의 DB이기 떄문 )
    
           // Todo 유저 정보를 가져와라
           getUnSelectedUsers()
       }
    

Realtime Database를 불러오기 - 최상위 영역에 접근하기

  • 위의 예제 코드에서 해당 부분이 Realtime Database를 불러오는 부분이다.

        private lateinit var userDB: DatabaseReference
        
            userDB = Firebase.database.reference

    --> 이 코드를 통해 userDB라는 변수에
    현재 프로젝트에 등록했던( 위에 등록부분에서 ) Realtime Database의
    최상위 영역에 접근할 수 있다.

Realtime Database의 하부 영역으로 이동하기 + 하부 영역 생성하기

  • 위에서 Realtime Database의 최상위 영역에 접근했다면,
    이제 하부 영역으로 이동
    혹은 하부 영역을 생성하는 방법이다.

  • Realtime Database는 Json형식으로 저장하는 DB이므로,
    reference( -> DB의 최상위항목 ) 에서부터 child()를 연속해나가는 것으로
    원하는 지점에 접근할 수 있다.

    Firebase.database.reference.child("aa").child("bb").child("cc")
    --> 최상위에 있는 aa의 내부에 있는 bb의 내부에 있는 cc로 접근

    이렇게 파고 들어갔을 때 해당 영역이 없다면 새로 생성하여 가져오고ㅡ,
    있으면 있는 것을 가져온다.

    즉, 위의 Firebase.database.reference.child("aa").child("bb").child("cc")에서
    만약에 bb와 cc가 없다면
    aa리스트 내부에 bb라는 이름의 키에 대한 리스트를 가진 딕셔너리 객체를 새롭게 만들고,
    bb의 리스트의 내부에 cc라는 이름의 키에 대한 리스트를 가진 딕셔너리 객체를 새롭게 만듬

    이후 이 영역에 데이터를 넣는다고 하면 ㅡ, cc의 값에 해당하는 리스트 영역에 딕셔너리 형태로 데이터가 들어갈 것이다.

    • 예제 1)

      최상위 영역에서 child() 멤버함수를 사용해서
      하위 영역에 접근 및 하위 영역 생성을 해줄 수 있다.

          private lateinit var userDB: DatabaseReference
          
             userDB = Firebase.database.reference.child("Users")
      

      예제 1) 에서 접근한 영역은 최상위 영역의 Users라는 하부 영역이다.

      이때 Users라는 영역이 이미 존재한다면 그냥 접근하고,
      존재하지 않는다면 Users라는 이름으로 영역을 생성한 뒤 접근한다.

      [ 최상위 영역.의 Users라는 영역. ]

    • 예제 2)

      최상위 영역의 하위 영역의 하위 영역으로 접근하고 싶다면 다음과 같이
      child() 멤버함수를 연속적으로 써주면 된다.

       private lateinit var userDB: DatabaseReference
       
          userDB = Firebase.database.reference.child("Users").child(getCurrentUserId())
          
          
      private fun getCurrentUserId(): String{
         // 대충 현재 기기의 ID를 반환하는 코드
      }
      

      [ 최상위 영역.의 Users라는 영역.의 기기ID를 이름으로 하는 영역. ]

    • 위의 예제들을 통해 명확해진 내용은 다음과 같다.

      • Realtime Database에서는 child() 멤버함수를 연속적으로 사용하는 것으로
        더욱 하위 영역에 접근할 수 있다.

        ( Users안에 people안에 male안에 money로 접근한다고 치면 )

        Firebase.database.reference.child("Users").child("people").child("male").child("money")
      • Realtime Database의 영역에 child()를 통해 접근 시
        해당 영역이 없다면 새로 생성한 뒤 접근한다.

      --> 즉, Realtime Database에서는 Child()를 연속하는 것을 통해
      key들을 타고 데이터에 접근하여,
      최후에 있는 데이터만 Value이라는 것을 알 수 있다.
      ( Json 형태의 Key-Value )

    • 주의할 점
      위와 같이 Realtime Database의 영역명을 하드코딩( 직접입력 ) 하게 될 경우
      코드가 길어지게 되었을 때, 오타로 인한 오류의 여지가 많다.

      따라서 아래와 같이 전역상수로 따로 모아서 사용할 것을 추천한다.

      예제) DBKey.kt

      class DBKey {
       companion object{
           const val USERS = "Users"
           const val LIKED_BY = "likedBy"
           const val  LIKE ="like"
           const val DIS_LIKE = "dislike"
           const val NAME = "name"
       }
      }

Realtime Database 데이터 삽입하기

  • 위에서 설명한대로 Realtime Database에서 원하는 영역에 접근했다면,
    이 부분은 해당 영역에 데이터를 삽입하는 방법이다.

     private lateinit var userDB: DatabaseReference
    
    userDB = Firebase.database.reference.child("Users")       
                       ......                    
                      
    private fun saveUserName(name: String) {
    
      // getCurrentUserId()는 현재 기기의 id를 반환하도록 제작한 메소드임
       val userId = getCurrentUserId()
       val currentUserDB = userDB.child(userId) 
       
       val user = mutableMapOf<String, Any>()
       user["name"] = name
       
       // 해당 경로에 데이터 추가 ( 추가할 데이터는 딕셔너리 형태여야 함 -> Json형태의 DB이기 때문 )
       currentUserDB.updateChildren(user)
    
    }
    • 임의로 구현한 saveUserName()이라는 메소드는 String을 매개변수로 받아서
      Realtime Database의 Users내부의 기기ID명에 해당하는 영역에
      name을 키값으로 하는 딕셔너리를 넣는 메소드이다.

    • 이 때 Realtime Database내의 원하는 영역으로 이동하는 위해서
      위에서 설명했듯이 child() 멤버함수를 사용한다.

    • Realtime Database에 넣을 데이터를 구성한다.
      ( Realtime Database는 Json형태로 데이터를 저장하므로
      해당 영역에 특정 데이터를 넣기 위해서 딕셔너리 형태의 데이터를 준비했다. )

    • Realtime Database에 데이터를 넣기 위해서
      updateChildren() 멤버함수를 사용했다.
      --> updateChildren()는 매개변수로 넣을 데이터를 받으며ㅡ,
      해당 데이터는 updateChildren()를 호출한 객체가 참조하고 있는 영역에
      삽입된다.

Realtime Database에서 데이터 가져오기

  • Realtime Database에서 데이터를 가져오는 작업은
    모두 Listener설정을 통해 가능하다.

  • 이 부분은 Realtime Database에서 데이터를 가져오는
    대표적인 2가지 리스너에 대한 부분이다.

    • addListenerForSingleValueEvent(object : ValueEventListener{}) 사용

         private lateinit var userDB: DatabaseReference
      
          userDB = Firebase.database.reference.child("Users")
          
           // getCurrentUserId()는 현재 기기의 id를 반환하도록 제작한 메소드임
          val currentUserDB = userDB.child(getCurrentUserId())   
                    ......
      
            currentUserDB.addListenerForSingleValueEvent(object : ValueEventListener {
                override fun onDataChange(snapshot: DataSnapshot) {
                    // TODO DataChange가 일어났을 때 CallBack이 넘어오면서 호출됨
                    // 처음에 리스너를 달았을 때는 처음에 리스너가 없었기 때문에 해당 DB에 데이터가 존재하면 이벤트가 내려오게 됨
                    // 즉, 리스너를 처음 걸었을 경우와 DB의 데이터가 변경되었을 경우 호출되는 리스너라고 보면 된다.
      
                    Log.e("SingleValueEvent",snapshot.key.toString().orEmpty())
      
                }
      
                override fun onCancelled(error: DatabaseError) {
                    // TODO Cancel이 일어났을 때 CallBack이 넘어오면서 호출됨
                }
            })
      • 데이터를 한번만 가져오고 싶을 때 사용한다.

      • 이 메소드를 최초에 한번만 실행된다.

      • ValueEventListener{}는 2개의 메소드를 구현해야 한다.

        • onDataChange(snapshot: DataSnapshot) 은 최초에 해당 영역에서
          데이터를 온전히 가져오는데 성공했을 때 호출된다.

        • onCancelled(error: DatabaseError) 은 최초에 해당 영역에서
          데이터를 온전히 가져오는데 실패했을 때 호출된다.

      • 이 메소드는 최초에 한번 실행되어,
        해당 영역에 대한 데이터를 snapshot의 인자로 받아온다.

      • 이후, snapshot.key나 snapshot.value 등을 통해
        영역에서 원하는 데이터를 추출해서 사용하면 된다.

    • addChildEventListener(object : ChildEventListener {}) 사용

         private lateinit var userDB: DatabaseReference
      
          userDB = Firebase.database.reference.child("Users")
          
                    ......
      
        private fun getUnSelectedUsers() {
      
            // DB에서 값을 가져오는 방법은 모두 리스너를 통해서만 가능함
            // DB에서 데이터를 유동적으로 가져오기 위해서 사용되는 리스너
            userDB.addChildEventListener(object : ChildEventListener {
      
                // 해당 위치의 데이터를 하나씩 snapshot에 담아서 가져옴
                override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
      
                    Log.e("ChildAdd",snapshot.key.toString().orEmpty())
                        
                        ......
                    }
                }
      
                // 해당 위치에서 데이터가 변경되었을 경우, 변경이 있었던 영역을 가져옴
                override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
      
                    Log.e("ChildChange",snapshot.key.toString().orEmpty())
      
                       ......
                }
      
                // 데이터 삭제
                override fun onChildRemoved(snapshot: DataSnapshot) {}
      
                // 데이터가 이동
                override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
      
                // 데이터 제어중 오류 발생
                override fun onCancelled(error: DatabaseError) {}
            })
        }
      • 해당 영역에서 수신대기하여,
        해당 영역에서의 데이터의 변경에 맞춰 호출되며,
        변경된 데이터를 가져오기 위해 사용한다.

      • ChildEventListener {}는 데이터 변경의 종류들에 대응하기 위해
        5가지 메소드를 구현해야 한다.

        • onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {}

          • 해당 영역 내부에서 데이터의 삽입이 이루어지면 호출됨

          • 최초에는 데이터가 없는 것으로 인식하고 있기 때문에,
            시작할 때, 해당 영역의 데이터를 모두 가져오기 위해
            한번씩 실행됨

          • 예를 들어
            Realtime Database에 아래와 같이 데이터가 들어있는 상태에서 앱을 실행하면,
            최초에 해당 영역에 데이터가 없다고 인식한 상태에서
            아래의 3개의 데이터가 새롭게 들어왔다고 인식하므로
            데이터 각각에 대해 onChildAdded()를 호출하여
            하나씩 가져오게 된다.

            [ Realtime Database ]

            [ onChildAdded가 하나씩 가져와서 출력한 데이터 ]

        • onChildChanged(snapshot: DataSnapshot, previousChildName: String?)

          • 해당 영역의 내부에서 데이터의 변경이 이루어지면 호출됨
            --> 변경한 데이터를 가져옴
        • onChildRemoved(snapshot: DataSnapshot)

          • 해당 영역의 내부에서 데이터의 삭제가 이루어지면 호출됨
            --> 삭제한 데이터를 가져옴
        • onChildMoved(snapshot: DataSnapshot, previousChildName: String?)

          • 해당 영역에서 데이터의 이동이 이루어지면 호출됨
            --> 이동한 데이터를 가져옴
        • onCancelled(error: DatabaseError)

          • 데이터를 제어하는 것에 문제가 발생했을 경우 호출됨

Firebase Authentication

Firebase 세팅

  1. 해당 글 참조 ( 프로젝트 등록 ) : https://velog.io/@odesay97/%EC%95%B1-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-09-1-%ED%91%B8%EC%8B%9C-%EC%95%8C%EB%A6%BC-%EC%88%98%EC%8B%A0%EA%B8%B0-firebase

  2. 프로잭트에서 앱 수준의 gradle에 Authentication에 대한 의존성 추가

    implementation platform('com.google.firebase:firebase-bom:29.1.0') ㅡ 이 코드 아래에 추가해줘야 함
    implementation 'com.google.firebase:firebase-auth-ktx'

  1. Authentication 클릭 및 시작하기 클릭

    Firebase Authentication 콘솔 설명

  • Users
    현재 로그인되어 있는 유저들이 목록에 나타남

  • Sign-in method
    로그인에 대한 설정을 해줄 수 있음
    --> 어떤 로그인 방식으로 유저가 로그인 할 수 있는지
    --> 어떤 기업의 계정과 연동하여 로그인 할 수 있는지
    --> 한 개의 이메일로 몇개까지 계정을 만들 수 있는지

    등등

Firebase 일반 로그인 설정

1. Authentication 콘솔에서 Sign-in-method로 가서 원하는 로그인 방식들을 선택

( 일단 계정 연동없이 기본적인 방식인 이메일/비밀번호를 예시로 할 것임 )

2. 해당 로그인 방식에 대한 세부 설정을 함

  • 이메일/비밀번호
    이메일과 비밀번호를 통한 로그인 설정

  • 이메일 링크( 비밀번호가 없는 로그인 )
    이메일과 인증번호를 통한 로그인 설정

3. 로그인 구현 -> 로그인 화면 구성

( 일반적으로 회원가입과 로그인을 따로 구현하여 나타내지만, 이번에는 설명 목적으므로 한 액티비티에서 처리할 것이다. )

예시)

<?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"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:padding="24dp">

   <EditText
       android:id="@+id/emailEditText"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <EditText
       android:id="@+id/passwordEditText"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:inputType="textPassword"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/emailEditText" />

   <Button
       android:id="@+id/loginButton"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:enabled="false"
       android:text="로그인"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/passwordEditText" />

   <Button
       android:id="@+id/signUpButton"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_marginEnd="4dp"
       android:enabled="false"
       android:text="회원가입"
       app:layout_constraintEnd_toStartOf="@+id/loginButton"
       app:layout_constraintTop_toBottomOf="@+id/passwordEditText" />
</androidx.constraintlayout.widget.ConstraintLayout>

4. 로그인 구성 2 -> Authentication을 이용하여 로그인 코드 구성

( 일반적으로 회원가입과 로그인을 따로 구현하여 나타내지만, 이번에는 설명 목적으므로 한 액티비티에서 처리할 것이다. )

[ LoginActivity ] --> 로그인 기능을 구현한 액티비티

......

class LoginActivity : AppCompatActivity() {

   val emailEditText : EditText by lazy {
       findViewById<EditText>(R.id.emailEditText)
   }

   val passwordEditText : EditText by lazy {
       findViewById<EditText>(R.id.passwordEditText)
   }

   val loginButton: Button by lazy {
       findViewById<Button>(R.id.loginButton)
   }
   val signUpButton: Button by lazy {
       findViewById<Button>(R.id.signUpButton)
   }

   // firebase Authentication기능에 대한 변수
   private lateinit var auth: FirebaseAuth

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

        // firebase Authentication기능을 가져옴
       auth = Firebase.auth

       initLoginButton()
       initSignUpButton()
       initEmailAndPAsswordEditText()

   }

 // 입력값 없음에 대한 예외처리 
   private fun initEmailAndPAsswordEditText(){

       emailEditText.addTextChangedListener {
           val enable = emailEditText.text.isNotEmpty() && passwordEditText.text.isNotEmpty()
           loginButton.isEnabled = enable
           signUpButton.isEnabled = enable
       }
       passwordEditText.addTextChangedListener {
           val enable = emailEditText.text.isNotEmpty() && passwordEditText.text.isNotEmpty()
           loginButton.isEnabled = enable
           signUpButton.isEnabled = enable
       }

   }

// 로그인 구현
   private fun initLoginButton() {
       loginButton.setOnClickListener {

           val email = getInputEmail()
           val password = getInputPassword()

           auth.signInWithEmailAndPassword(email,password)
               .addOnCompleteListener(this) { task ->
                   if (task.isSuccessful){
                       finish()
                   }else{
                       Toast.makeText(this, "로그인에 실패했습니다. 이메일 또는 비밀번호를 확인해주세요.", Toast.LENGTH_SHORT).show()
                   }
               }
       }
   }

// 회원가입 구현
   private fun initSignUpButton() {
       signUpButton.setOnClickListener {

           val email = getInputEmail()
           val password = getInputPassword()

           auth.createUserWithEmailAndPassword(email,password)
               .addOnCompleteListener(this){ task ->
                   if(task.isSuccessful){
                       Toast.makeText(this, "회원가입에 성공했습니다. 로그인 버튼을 눌러 로그인해주세요",Toast.LENGTH_SHORT).show()

                   }else{
                       Toast.makeText(this, "이미 가입한 이메일이거나, 회원가입에 실패했습니다.",Toast.LENGTH_SHORT).show()
                   }
               }
       }
   }
   private fun getInputEmail() = emailEditText.text.toString()
   private fun getInputPassword() = passwordEditText.text.toString()
} 
  • 이 부분에서 Firebase의 Authentication기능을 가져옴

       private lateinit var auth: FirebaseAuth
       
       ......
       
              auth = Firebase.auth
  • 이 부분에서 로그인 기능을 구현함
    --> 로그인 버튼을 클릭시 실행

    val emailEditText: EditText = findViewById<EditText>(R.id.emailEditText)
    val passwordEditText: EditText = findViewById<EditText>(R.id.passwordEditText)
    val loginButton: Button = findViewById<Button>(R.id.loginButton)
    
    ......
    
    private fun initLoginButton() {
       loginButton.setOnClickListener {
    
           val email = emailEditText.text.toString()
           val password = passwordEditText.text.toString()
    
           auth.signInWithEmailAndPassword(email,password)
               .addOnCompleteListener(this) { task ->
                   if (task.isSuccessful){
                       finish()
                   }else{
                       Toast.makeText(this, "로그인에 실패했습니다. 이메일 또는 비밀번호를 확인해주세요.", Toast.LENGTH_SHORT).show()
                   }
               }
       }
    }
    
    • FirebaseAuth의 매소드인 signInWithEmailAndPassword()는 Email과 password를 통해 로그인을 시도해주는 메소드임
      ( Firebase Authentication 콘솔에서 설정해놓은 로그인 방식에 따라 다양한 로그인 메소드가 있음 )

    • 따라서 signInWithEmailAndPassword()는

      • 첫번째 파라미터로 Email값
      • 두번째 파라미터로 password값

      을 받아서 로그인을 시도함

    • 로그인 코드가 실행되어 작업이 완료되었다면
      ( 로그인 성공 여부와 상관없이 )
      addOnCompleteListener() 메소드가 호출됨

    • addOnCompleteListener() 메소드는
      task를 반환하는데, task의 isSuccessful변수는
      로그인이 성공하면 true가
      로그인에 실패하면 false가 할당되므로
      이 변수를 사용하여 로그인의 성공실패 여부를 알 수 있음

      따라서 addOnCompleteListener() 메소드를 재구성하여
      로그인이 성공했을 경우와 실패했을 경우에 따른 처리를 해주면 됨

      --> 일반적으로는 위의 코드와 같이
      성공하였다면, 로그인화면에 대한 액티비티를 종료하여 MainActivity로 돌아가주고
      실패하였다면, Toast메시지를 반환하여 실패하였음을 알리거나, 비밀번호 찾기에 대한 권유ㅡ, 등의 것들을 하면 된다.

  • 이 부분에서 회원가입 기능을 구현함
    --> 회원가입 버튼을 클릭시 실행

    ( 일반적으로는 회원가입은 따로 액티비티를 만들어서 처리하지만, 이번에는 설명이 목적이므로 같이 처리하였다. )

    val emailEditText: EditText = findViewById<EditText>(R.id.emailEditText)
    val passwordEditText: EditText = findViewById<EditText>(R.id.passwordEditText)
    val loginButton: Button = findViewById<Button>(R.id.loginButton)
    
    ......
    
     private fun initSignUpButton() {
       signUpButton.setOnClickListener {
    
           val email = emailEditText.text.toString()
           val password = passwordEditText.text.toString()
    
           auth.createUserWithEmailAndPassword(email,password)
               .addOnCompleteListener(this){ task ->
                   if(task.isSuccessful){
                       Toast.makeText(this, "회원가입에 성공했습니다. 로그인 버튼을 눌러 로그인해주세요",Toast.LENGTH_SHORT).show()
    
                   }else{
                       Toast.makeText(this, "이미 가입한 이메일이거나, 회원가입에 실패했습니다.",Toast.LENGTH_SHORT).show()
                   }
               }
       }
    }
    • FirebaseAuth의 메소드인 createUserWithEmailAndPassword() 메소드는 Email과 password를 통해 회원가입을 시도해주는 메소드임
      ( Firebase Authentication 콘솔에서 설정해놓은 로그인 방식에 따라 다양한 회원가입 메소드가 있음 )

    • 따라서 createUserWithEmailAndPassword()는

      • 첫번째 파라미터로 Email값
      • 두번째 파라미터로 password값

      을 받아서 회원가입을 시도함

    • 회원가입 코드가 실행되어 작업이 완료되었다면
      ( 회원가입 성공 여부와 상관없이 )
      addOnCompleteListener() 메소드가 호출됨

    • addOnCompleteListener() 메소드는
      task를 반환하는데, task의 isSuccessful변수는
      회원가입에 성공하면 true가
      회원가입에 실패하면 false가 할당되므로
      이 변수를 사용하여 회원가입의 성공실패 여부를 알 수 있음

      따라서 addOnCompleteListener() 메소드를 재구성하여
      회원가입이 성공했을 경우와 실패했을 경우에 따른 처리를 해주면 됨

      --> 일반적으로는 위의 코드와 같이
      성공하였다면, 회원가입 완료가 로그인 완료의 의미를 가지지는 않으므로ㅡ, 로그인 화면으로 돌아가며ㅡ, Toast등을 통해 그 사실을 알린다.
      실패하였다면, Toast메시지를 반환하여 실패하였음을 알리면서ㅡ, 실패의 이유를 제시한다.

  • 비고
    -> 회원가입시 이메일의 형태("[email protected]")를 가져야 한다.
    -> 회원가입시 비밀번호는 6자리 이상이어야 한다.

  • 이 부분에서 이메일 및 비밀번호 입력이 없을 경우에 대한 예외처리를 하고 있다.

    
           emailEditText.addTextChangedListener {
             val enable = emailEditText.text.isNotEmpty() && passwordEditText.text.isNotEmpty()
             loginButton.isEnabled = enable
             signUpButton.isEnabled = enable
         }
         passwordEditText.addTextChangedListener {
             val enable = emailEditText.text.isNotEmpty() && passwordEditText.text.isNotEmpty()
             loginButton.isEnabled = enable
             signUpButton.isEnabled = enable
         }
    
    • 위의 코드를 보면 EditText에서 값을 받아와서 사용하는 데ㅡ, 만약에 EditText에 아무 값도 입력되어 있지 않으면
      NullSafe 오류가 발생할 것이다.

      따라서 Email값과 password값이 모두 입력되어 있는 상황을 제외하면
      로그인과 회원가입 버튼이 비활성화 되도록 만들어주고 있다.

    • EditText에 값이 입력되어 있는지를 확인하기 위해
      EditText에 addTextChangedListener{}를 달아서 확인하고 있다.
      ( 해당 EditText의 Text변수의 내용이 변경될 때마다 호출되는 리스너 )

5. MainActivity에서 앱 시작시에 로그인 여부를 확인ㅡ, 로그인이 되어있지 않다면 LoginActivity를 실행하도록 코딩

......

class MainActivity : AppCompatActivity() {

    private var auth: FirebaseAuth = Firebase.auth

......

    override fun onStart() {
      super.onStart()

      // 로그인이 되어있을 경우, auth의 currentUser에 해당 로그인 정보가 들어있게 된다.
      if (auth.currentUser == null){
          startActivity(Intent(this,LoginActivity::class.java))
      }
  }

......
  • 앱이 실행할 때 호출되는 메소드중 하나인 onStart()메소드를 재정의하여
    시작시에 로그인되어있지 않을 경우
    Intent를 통해 LoginActivity를 실행한다.

  • 로그인 여부를 확인하기 위해서는 FirebaseAuth의 currentUser를 확인하면 된다.

    --> FirebaseAuth의 currentUser 변수에 로그인 정보들이 들어있음
    즉, 로그인 되어있지 않다면 해당 변수가 null로 비어있음

Firebase SNS 연동 로그인 설정 -> ex) 페이스북 예시

1. Firebase Authentication 콘솔에서 로그인 방식으로 페이스북 추가

2. 연동 서버 ( 여기서는 Facebook )의 승인을 받아 등록

  • 2-1. 1번에서 Facebook을 선택하면 다음과 같은 창이 뜬다.

    • 위의 일반 로그인 과는 다르게 연동 로그인의 경우ㅡ,

      • 우리가 사용자를 관리하는 것이 아닌 해당 연동 서버가 관리하는 사용자의 데이터를 가져와서 사용하는 방식이다.

      • 따라서 연동 서버의 입장에서는 우리가 누군지 알아야 우리가 필요한 최소한의 User정보도 넘겨줄 수 있고,
        우리 입장에서도 우리 앱에서 해당 User가 로그인 했다라고 연동 서버에 알려줘야하기 때문에

        --> 이를 위해서 해당 연동서버 ( 여기서는 Facebook )의 승인을 받아야 한다.

    아래의 3번에서 Facebook Developer로 이동하여 이번 앱에 사용할 Facebook 앱 프로젝트를 만들고 난 후ㅡ,
    아래의 내용을 진행할 것 !!

  • 2-2. 아래의 3번에서 생성한 Facebook 앱 프로젝트에서 기본 설정에 들어가면
    앱 ID와 앱 시크릿 코드가 있는데,
    해당 부분을 위에 앱 ID와 앱 비밀번호에 각각 넣는다.
    --> Firebase 앱 프로젝트에 Facebook 앱 프로젝트 등록하는 것

  • 2-3. 이번에는 반대로 아래의 내용을 복사한다.

  • 2-4. Facebook 앱 프로젝트의 해당 부분에 복사한 내용을 붙여넣기 한다.
    --> Facebook 앱 프로젝트에 Firebase 앱 프로젝트 등록하는 것

3. Facebook developer로 이동하여 Facebook 앱 프로젝트 만들기

    1. Facebook Developer 로 이동

    https://developers.facebook.com/

    1. 로그인 후 시작하기( 혹은 내 앱 ) 클릭
    1. 앱만들기 클릭
    1. 앱에서 Facebook이 담당할 기능 선택
      ( 이 부분에서 Facebook이 앱에 제공해야하는 데이터가 어떤 것들인지 결정됨 )

      --> 이번에는 Facebook 로그인만 구현하면 되기 때문에 "소비자" 선택

    1. 이후 앱 이름 설정 후 앱 만들기 클릭
    1. 앱 프로젝트가 만들어 졌으면, Facebook 로그인에 설정 클릭
      ( 우리는 로그인 기능을 사용할 것이므로 )

4. 안드로이드 스튜디오의 앱 프로젝트를 Facebook 앱 프로젝트에 등록

안드로이드용 Facebook 로그인 공식문서 : https://developers.facebook.com/docs/facebook-login/android

해당 공식 문서에 들어가서 하나씩 해나가면 등록된다.

아래의 내용은 글을 쓰는 현재의 절차이므로ㅡ,
절차가 바뀌었을 가능성이 있다.
그러니까 위의 공식문서 들어가서 그냥 따라해라

  1. 앱 선택 또는 새 앱 만들기

    위에서 이미 앱 프로젝트를 만들었으므로ㅡ,
    ( 만들지 않았다면 새 앱 만들기 클릭해서 만들면 됨 )
    만든 앱 프로젝트를 목록에서 선택해줌

    --> 이렇게 해주면 해당 페이지에서 이후 절차에 해당하는 코드들에서
    앱ID가 자동으로 들어가기 때문에 편하다.

    ( 즉, 이후의 절차에서 코드상 앱 ID를 넣어서 붙여넣기 해야될 부분에 위의 앱 ID가 자동으로 삽입되므로 코드를 그냥 복붙해도 되서 편해진다. )

  2. 페이스북 앱 다운 -> 기기에서 Facebook을 다운하면 된다.

  3. Facebook SDK 통합

    • 써있는 대로 build.gradle에서 추가해주면 된다.
      ( 단, 위의 "[8.1)"이라고 써있는 부분은 8.1버전 이상의 버전을 사용하라는 의미이므로ㅡ,
      조건을 만족하는 버전의 버전넘버를 써준다. )

      이 글을 쓰는 타이밍에는

      implementation 'com.facebook.android:facebook-login:8.2.0'

      을 사용했음

    • 또한 프로젝트 수준의 build.gradle에서 repositories 섹션에

      mavenCentral()

      이 없다면 추가해줘야 한다.

  4. 리소스 및 메니페스트 수정

    --> 아까 1번에서 앱 ID 등록한 것이 빛을 바라는 부분이다.

    써있는 대로 해당 위치에 코드 복사해서 붙여넣기 해주면 된다.

  5. 패키지 이름 및 기본 클래스를 앱과 연결
    패키지 이름과 기본 클래스를 등록하는 부분이다.

    • A는 앱에서 아래와 같이 manifest의 pachage명을 넣어주면 된다.

    • B는 앱에서 아래와 같이 manifest의 기본 액티비티를 넣어주면 된다.
      ( 그런데 주의할 점이 액티비티의 주소도 포함해서 넣어야 한다. )

      --> com.example.aop_part3_chapter13.MainActivity.kt 이런식으로

  6. 앱용 개발 및 릴리스 키 해시 제공

    해당 앱을 마켓에 릴리스 할때 필요한 부분인데ㅡ,
    이 앱은 연습용이므로 여기서는 이 부분은 다루지 않는다.

  7. 앱에 대한 SSO 활성화

    위와 동일하게 현재는 패스

--> 여기까지 되었으면 다음으로 넘어가서 안드로이드 스튜디오에서 구현하면 된다.

5. 안드로이드 스튜디오에서 Facebook 로그인 구현

  1. Facebook 로그인 버튼 추가

      <com.facebook.login.widget.LoginButton
           android:id="@+id/facebookLoginButton"
           android:layout_width="0dp"
           android:layout_height="wrap_content"
           android:layout_marginTop="30dp"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintTop_toBottomOf="@+id/loginButton" />
  2. Facebook 로그인 구현

  • Facebook 로그인에 대한 대략적인 설명

    • SNS 연동의 경우ㅡ, ( 여기서는 페이스북 연동 로그인의 경우 )
      해당 버튼을 누르면 페이스북에 로그인하기 위한 액티비티가 실행된다.

      위의 4번에서 페이스북에 앱 프로젝트 등록했을 때ㅡ, 4번째 항목에서
      Manifest에 설정한 부분을 보면 Facebook에 대한 Activity를 등록한 것을 볼 수 있는데

      위에 Facebook라이브러리에서 가져온 LoginButton 위젯을 클릭하면
      Facebook 라이브러리에서 내부적으로 Facebook 액티비티를 Intent로 열음
      ( startActivityForResult()를 사용하여 )

      --> 이 액티비티는 내가 구현하는 것이 아니라
      facebook login에 대한 라이브러리를 build.gradle에서 가져올 때 구현되어서 들어옴

    • 해당 Facebook Activity를 통해 로그인에 대한 작업이 완료가 되었으면ㅡ,
      ( 로그인 되었든 안되었든 사용자가 취소했든 상관없이 작업이 완료가 되었다면 )
      Activity Callback으로 로그인에 대한 데이터가 넘어오게 된다.

      ( 이해를 돕자면 Intent로 액티비티끼리 데이터를 주고 받는 상황이라고 볼 수 있다. )

      이에 따라 onAcitivityResult() 메소드가 호출되게 되며ㅡ,
      ( startActivityForResult()로 실행한 액티비티가 종료되면서 반환한 값을 받기 위해 호출되는 메소드 )

      이때 CallbackManager라는 것을 사용해서 onActivityResult()가 호출되었을 때
      페이스북 SDK의 onActivityResult()도 호출되도록 만들며,
      동시에 해당 메소드를 이용하여 Callback한 데이터를 페이스북 SDK에 전달한다.

      페이스북 SDK는
      실제로 로그인이 되었는지 안되었는지 결과를 체크한다.

  • 최종적으로 Facebook측에 정상적으로 로그인이 되었다고 판단되면 ㅡ,
    해당 로그인에 대한 AccessToken을 가져와서 Firebase측에 넘기는 것으로
    Firebase Authetication에 Facebook과 연동하여 로그인을 시도한다.
  • 페이스북 로그인 예시 코드

    ......
    
    class LoginActivity : AppCompatActivity() {
    
        val facebookLoginButton: LoginButton by lazy {
          findViewById<LoginButton>(R.id.facebookLoginButton)
      }
    
      val callbackManager: CallbackManager by lazy {
          CallbackManager.Factory.create()
      }
    
        // firebase Authentication기능에 대한 변수
      private lateinit var auth: FirebaseAuth
      
      override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContentView(R.layout.activity_login)
          
                  // firebase Authentication기능을 가져옴
          auth = Firebase.auth
          
          initFacbookLoginButton()
    
      }
      
          private fun initFacbookLoginButton(){
    
          // 해당 버튼을 통해 페이스북에 로그인이 완료하였을 때ㅡ,
          // 페이스북 측의 계정에서 어떤 정보를 가져올 것인지를 적음
          // ( 즉, 가져올 정보에 대한 Permission을 추가해준 것임 ) ㅡ,
          // facebook 문서를 확인해서 원하는 정보에 대한 Permission을 추가해주면 됨
          facebookLoginButton.setPermissions("email","public_profile")
    
          facebookLoginButton.registerCallback(callbackManager,object : FacebookCallback<LoginResult>{
              override fun onSuccess(result: LoginResult) {
                 // TODO 로그인이 성공적
                  // 로그인이 성공적이었을 경우,
                  // LoginResult에서 AccessToken을 가져와서 ㅡ, ( 우리가 다루는 것이 아니라 ) Firebase측에 넘겨줘야하기 떄문에
                  // LoginResult에서 AccessToken을 가져와서 Firebase에 넘겨주는 코드를 추가해야 한다.
    
                  //LoginResult에서 AccessToken을 가져옴
                  val credential = FacebookAuthProvider.getCredential(result.accessToken.token)
    
                  // 가져온 AccessToken을 Firebase측에 넘겨주면서 로그인 및 계정 관리
                  auth.signInWithCredential(credential)
                      .addOnCompleteListener(this@LoginActivity){ task->
                          if(task.isSuccessful){
                              finish()
                          }else{
                              Toast.makeText(this@LoginActivity,"페이스북 로그인에 실패하였습니다.",Toast.LENGTH_SHORT).show()
                          }
    
                      }
    
              }
    
              override fun onCancel() {
                 // TODO 로그인이 취소됨 -> 사용자가 취소를 누름
              }
    
              override fun onError(error: FacebookException?) {
                 // TODO 로그인중 에러가 발생함
                  Toast.makeText(this@LoginActivity,"페이스북 로그인이 실패했습니다.",Toast.LENGTH_SHORT).show()
                  // 해당 코드가 Callback 안에 있으므로 어느 Context를 가져올 것인지 명시
              }
          })
      }
      
      
      
          override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
          super.onActivityResult(requestCode, resultCode, data)
    
          callbackManager.onActivityResult(requestCode,resultCode,data)
      }
    
    • 이 부분에서 Firebase의 Authentication기능을 가져옴

         private lateinit var auth: FirebaseAuth
         
         ......
         
                auth = Firebase.auth
    • 이 부분에서 Facebook 라이브러리에서 가져온 LoginButton에 대한 설정을 해주고 있음

          val facebookLoginButton: LoginButton by lazy {
           findViewById<LoginButton>(R.id.facebookLoginButton)
       }
       
       ......
      
         facebookLoginButton.setPermissions("email","public_profile")
      • 해당 버튼을 통해 페이스북에 로그인이 완료하였을 때ㅡ,
        페이스북 측의 계정에서 어떤 정보를 가져올 것인지를 적음
        ( 즉, 가져올 정보에 대한 Permission을 추가해준 것임 )

      • facebook 문서를 확인해서 원하는 정보에 대한 Permission을 추가해주면 됨

    • 이 부분에서 CallbackManager 구성 및 onActivityResult 재정의

        val callbackManager: CallbackManager by lazy {
         CallbackManager.Factory.create()
      }
      
      ......
      
           override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
              super.onActivityResult(requestCode, resultCode, data)
      
              callbackManager.onActivityResult(requestCode,resultCode,data)
      }
      • startActivityForResult()로 실행된 액티비티의 작업이 종료되면 호출되는 메소드인
        onActivityResult를 구현하고 있음

        --> 페이스북 라이브러리에서 가져온 LoginButton이 클릭되면
        내부적으로 startActivityForResult로 페이스북 로그인 액티비티가 실행되게 되며
        이후 로그인에 대한 작업이 완료되면 ㅡ,
        로그인 성공, 실패, 취소 여부와 관계없이 onAcitivityResult가 호출된다.

      • 본래 액티비티의 onActivityResult() 메소드가 실행되었을 때ㅡ,

        • 연쇄적으로 페이스북 라이브러리의 onActivityResult()도 실행되도록 만들기 위해,
        • MainActivity의 onActivityResult() 메소드의 값을 페이스북에 전달하기 위해

        CallbackManager를 사용하고 있다.

      • CallbackManager는 CallbackManager 내부의 Factory 클래스의 create() 메소드를 통해서 생성할 수 있다.

      • 이렇게 생성된 CallbackManager는

        • 본래 액티비티의 onActivityResult()메소드 내부에서
          페이스북 라이브러리의 onActivityResult()메소드를 호출하는 것으로
          페이스북 라이브러리의 메소드가 연쇄적으로 호출되도록 만든다.

        • 또한 본래 액티비티의 onActivityResult()가 파라미터로 받은 데이터를
          페이스북 라이브러리의 onActivityResult()의 파라미터로 전달하여
          페이스북 라이브러리에 로그인 데이터를 전달하고 있다.

           override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
                    super.onActivityResult(requestCode, resultCode, data)
          
                    callbackManager.onActivityResult(requestCode,resultCode,data)
          }
    • 이 부분에서 Facebook LoginButton에 Callback을 등록하고 있음

       val auth : FirebaseAuth by lazy { 
           Firebase.auth
       }
          val callbackManager: CallbackManager by lazy {
           CallbackManager.Factory.create()
       }
           val loginButton: Button by lazy {
           findViewById<Button>(R.id.loginButton)
       }
      
      ......
      
           facebookLoginButton.registerCallback(callbackManager,object : FacebookCallback<LoginResult>{
               override fun onSuccess(result: LoginResult) {
                  // TODO 로그인이 성공적
                   // 로그인이 성공적이었을 경우,
                   // LoginResult에서 AccessToken을 가져와서 ㅡ, ( 우리가 다루는 것이 아니라 ) Firebase측에 넘겨줘야하기 떄문에
                   // LoginResult에서 AccessToken을 가져와서 Firebase에 넘겨주는 코드를 추가해야 한다.
      
                   //LoginResult에서 AccessToken을 가져옴
                   val credential = FacebookAuthProvider.getCredential(result.accessToken.token)
      
                   // 가져온 AccessToken을 Firebase측에 넘겨주면서 로그인 및 계정 관리
                   auth.signInWithCredential(credential)
                       .addOnCompleteListener(this@LoginActivity){ task->
                           if(task.isSuccessful){
                               finish()
                           }else{
                               Toast.makeText(this@LoginActivity,"페이스북 로그인에 실패하였습니다.",Toast.LENGTH_SHORT).show()
                           }
      
                       }
      
               }
      
               override fun onCancel() {
                  // TODO 로그인이 취소됨 -> 사용자가 취소를 누름
               }
      
               override fun onError(error: FacebookException?) {
                  // TODO 로그인중 에러가 발생함
                   Toast.makeText(this@LoginActivity,"페이스북 로그인이 실패했습니다.",Toast.LENGTH_SHORT).show()
                   // 해당 코드가 Callback 안에 있으므로 어느 Context를 가져올 것인지 명시
               }
           })

      --> CallbackManager를 LoginButton에 넘겨주어
      페이스북 로그인 액티비티의 Callback( 작업 종료 )에 대한 리스너를 설정하고,
      동시에 페이스북 로그인 액티비티가 Callback한 데이터를
      페이스북 SDK( 라이브러리 )에 넘겨주고 있다

      --> 그렇게 받은 Callback데이터에서 로그인 여부를 파악하여
      로그인 여부에 따라 다른 처리를 해주고 있다.

      --> 최종적으로 로그인이 되었다고 판단되면
      CallBack한 데이터의 Access Token을 Firebase로 넘기면서 로그인 요청을 하고 있다.

      • Facebook LoginButton은 registerCallback()메소드를 사용해서
        페이스북 로그인 여부에 따른 처리를 해준다.

      • registerCallback() 메소드는

        • 첫번째 파라미터로 페이스북 액티비티가 Callback한 Callback데이터를 받은
          CallbackManager 객체를 받고,

        • 두번째 파라미터로 CallBack의 내용( 로그인 성공, 실패, 취소 )에 따라
          따로 처리하기 위한 FacebookCallback<LoginResult> 인터페이스를 구현한 객체를 받는다.

        • 여기서 registerCallback() 메소드의 첫번째 파라미터에는
          아까 생성하여 onActivityResult()가 받아온 값을 넣었던 CallBackManager를 넣고

        • registerCallback() 메소드의 두번째 파라미터에는
          FacebookCallback<LoginResult>를 구현한 클래스의 객체를 넣어주면 된다.

      • 일반적으로 FacebookCallback<LoginResult> 인터페이스는
        무명클래스로 구현하여 넣는데,
        이때 해당 무명클래스는 3개의 메소드를 구현해야 한다.

        • onSuccess(result: LoginResult)
          --> 로그인이 성공했을 때 호출되는 메소드

          • result파라미터의 인자로 로그인에 대한 데이터를 가지고 옴
            --> 해당 파라미터를 통해 가져온 인자에서 AccessToken을 가져와서
            로그인 요청과 함께 Firebase에 전달해야 함
        • override fun onCancel()
          --> 사용자가 로그인을 취소했을 때 호출되는 메소드

        • onError(error: FacebookException?)
          --> 에러로 인해 로그인이 실패했을 때 호출되는 메소드
          --> 파라미터로 error에 대한 정보를 가지고 옴

      • 로그인이 성공했을 경우에
        FacebookCallback<LoginResult>는
        onSuccess(result: LoginResult) 메소드를 호출하는데ㅡ,
        이 메소드에서 인자로 들어오는 로그인 데이터에서 AccessToken을 가져와서
        Firebase에 로그인 요청과 함께 넘겨줘야 한다.

        • 이 부분에서 AccessToken을 가져와서 변수에 저장하고 있다.

          ......
          
             override fun onSuccess(result: LoginResult) {
              // TODO 로그인이 성공적
          
               val credential = FacebookAuthProvider.getCredential(result.accessToken.token)
               
               ......
          

          --> 파라미터로 들어온 LoginResult에서 AccessToken을 가져옴

          • FacebookAuthProvider의 getCredential() 메소드를 사용하여
            반환한 AccessToken을
            Firebase Authentication에서 사용가능한 객체로 감싸고 있다.
        • 이 부분에서 AccessToken을 통해
          Firebase Authentication 기능에 로그인 요청을 하고 있다.

             auth.signInWithCredential(credential)
                  .addOnCompleteListener(this@LoginActivity){ task->
                      if(task.isSuccessful){
                           finish()
                      }else{
                           Toast.makeText(this@LoginActivity,"페이스북 로그인에 실패하였습니다.",Toast.LENGTH_SHORT).show()
                           }
          
                       }

즉 Facebook 연동 로그인을 대략적으로 정리해보면

  1. 사용자가 Facebook LoginButton 클릭

  2. Facebook 라이브러리에 따라 startActivityForResult()로 Facebook 로그인 액티비티 실행됨
    ( Facebook라이브러리가 내장하고 있는 액티비티 )

  3. 사용자가 로그인을 하거나, 로그인을 취소하거나, 로그인에 실패하여 액티비티의 작업이 종료
    ( 이때 startActivityForResult()로 실행시킨 액티비티이므로
    본래의 액티비티에서 onActivityResult()메소드가
    페이스북 로그인 액티비티의 CallBack 데이터를 인자로 받아서 호출됨 )

  4. 해당 액티비티에서는 CallBackManager가

    • onActivityResult()내부에서 onActivityResult()가 받은 CallBack데이터를 받고,
    • 동시에 페이스북 라이브러리(SDK)의 onActivityResult()를 호출하여

    페이스북 라이브러리(SDK)의 코드가 실행되도록 만듬

  5. 페이스북 라이브러리(SDK)에서는 CallBack데이터를 통해
    로그인이 어떻게 되었는지 확인하고( 성공?, 실패? 취소? ),
    이에 따라 다른 메소드를 호출하여 처리해줌

  6. 그 중에서 만약 로그인이 성공하였다면 CallBack데이터에서
    AccessToken을 가져와서
    Firebase에 로그인 요청과 함께 전달해줌

  7. Firebase에서는 AccessToken을 바탕으로 로그인 요청에 대한 처리

로그아웃하기

......
    private val auth: FirebaseAuth = Firebase.auth
    
    ......
    
    // 로그인 되어있는 상태에서 
    
            auth.signOut()

github에서 원하는 오픈소스 찾기

github : https://github.com/

github사이트에 들어가서 내가 원하는 기능을 검색해보면 찾을 수 있다.

그렇다면 검색어를 어떻게해야 찾을 수 있을 지 고민될텐데,

  • 검색어에는

    • 내가 해당 오픈소스를 사용할 개발 도구
    • 해당 오픈소스의 기능
    • 내가 원하는 방향에 좀 더 가까운 오픈소스를 찾기 위한 Keyword

    등등이 들어가면 된다.

  • 예를 들어
    이번 앱에서는 "yuyakaido / CardStackView" 을 사용할 것인데ㅡ,

    이것을 찾는 과정은 다음과 같았다.

    1. "Swipe Android" 를 검색
      -> 오픈소스의 기능 + 오픈소스를 사용할 개발도구

    2. 그런데 이렇게 하니까 Android에서 사용할 수 있는
      Swipe와 관련된 오픈소스들이 모두 뜸

    3. 그래서 틴더앱과 가까운 Swipe기능을 찾기위해
      어떤 Keyword를 넣어야할지 고민

    4. 틴더에 들어가는 Swipe기능은 일반적으로 Stack을 사용하므로
      "Swipe Android Stack" 검색

    5. 좋아요가 많은 것부터 차례대로 보면서 원하는 오픈소스 찾기

      --> 대략적으로 이런 과정을 거쳐서 찾으면 된다.

  • 이렇게 찾은 오픈소스가 사용해도 괜찮은 오픈소스인지 구분하는 기준은
    다음과 같은 부분을 보면 된다.

    • 좋아요가 많은지
    • 비교적 최근까지 개발 및 업데이트가 이루어졌는지
  • 만약에 해당 오픈소스를 사용하다가 버그를 발견했다면,
    그 오픈소스를 받은 곳에 가서 issues 탭을 클릭하면
    다른 사람들이 발견한 버그들과 해당 버그를 어떻게 처리했는지 확인해 볼 수 있다.

    --> 만약 내가 경험한 버그가 올라와있지 않다면ㅡ,
    내가 직접 제작자에게 질문해 볼 수도 있다.


코드 소개

MainActivity.kt

package com.example.aop_part3_chapter13

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase

class MainActivity : AppCompatActivity() {

    private var auth: FirebaseAuth = Firebase.auth

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

    }

    override fun onStart() {
        super.onStart()

        // 로그인이 되어있을 경우, auth의 currentUser에 해당 로그인 정보가 들어있게 된다.
        if (auth.currentUser == null){
            startActivity(Intent(this,LoginActivity::class.java))
        }else{
            startActivity(Intent(this,LikeActivity::class.java))
            finish()
        }
    }
}

activity_main.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"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

LoginActivity.kt

package com.example.aop_part3_chapter13

import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener
import com.example.aop_part3_chapter13.DBKey.Companion.USERS
import com.example.aop_part3_chapter13.DBKey.Companion.USER_ID
import com.facebook.CallbackManager
import com.facebook.FacebookCallback
import com.facebook.FacebookException
import com.facebook.login.LoginResult
import com.facebook.login.widget.LoginButton
import com.google.firebase.auth.FacebookAuthProvider
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase

class LoginActivity : AppCompatActivity() {

    val emailEditText : EditText by lazy {
        findViewById<EditText>(R.id.emailEditText)
    }

    val passwordEditText : EditText by lazy {
        findViewById<EditText>(R.id.passwordEditText)
    }

    val loginButton: Button by lazy {
        findViewById<Button>(R.id.loginButton)
    }
    val signUpButton: Button by lazy {
        findViewById<Button>(R.id.signUpButton)
    }
    val facebookLoginButton: LoginButton by lazy {
        findViewById<LoginButton>(R.id.facebookLoginButton)
    }

    val callbackManager: CallbackManager by lazy {
        CallbackManager.Factory.create()
    }

    // firebase Authentication기능에 대한 변수
    private lateinit var auth: FirebaseAuth

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

        // firebase Authentication기능을 가져옴
        auth = Firebase.auth

        initLoginButton()
        initSignUpButton()
        
        // EmailEditText과 PasswordEditText에 입력값이 없다면 로그인버튼과 패스워드 버튼을 비활성화 ( 입력값 없음에 대한 예외처리 )
        initEmailAndPasswordEditText()
        initFacbookLoginButton()

    }

    private fun initEmailAndPasswordEditText(){

        emailEditText.addTextChangedListener {
            val enable = emailEditText.text.isNotEmpty() && passwordEditText.text.isNotEmpty()
            loginButton.isEnabled = enable
            signUpButton.isEnabled = enable
        }
        passwordEditText.addTextChangedListener {
            val enable = emailEditText.text.isNotEmpty() && passwordEditText.text.isNotEmpty()
            loginButton.isEnabled = enable
            signUpButton.isEnabled = enable
        }
    }

    private fun initLoginButton() {
        loginButton.setOnClickListener {

            val email = getInputEmail()
            val password = getInputPassword()

            // Email값과 Password값을 이용해서 Firebase Auth의 SignIn 기능을 이용할 수 있음 -------------( 로그인 기능 )
            // auth를 통해 해당 Email과 Password로 로그인을 시도ㅡ, 이후 해당 작업이 완료되었다면 addOnCompleteListener를 호출ㅡ,
            // 만약에 작업이 정상적으로 마무리 되었다면( task.isSuccessful가  true라면 ) 로그인이 완료되었다는 의미이므로 해당 액티비티 종료
            // 해당 액티비티는 로그인이 되어있지 않을 경우 로그인하기 위해 나타나는 액티비티임
            // 따라서 해당 코드를 통해 로그인이 된 시점에서 해당 액티비티를 종료시켜줌 ( finish() )
            auth.signInWithEmailAndPassword(email,password)
                .addOnCompleteListener(this) { task ->
                    if (task.isSuccessful){
                        handleSuccessLogin()
                    }else{
                        Toast.makeText(this, "로그인에 실패했습니다. 이메일 또는 비밀번호를 확인해주세요.", Toast.LENGTH_SHORT).show()
                    }
                }
        }
    }

    private fun initSignUpButton() {
        signUpButton.setOnClickListener {

            val email = getInputEmail()
            val password = getInputPassword()

            // Email값과 Password값을 이용해서 Firebase Auth의 SignUp 기능을 이용할 수 있음 -------------( 화원가입 기능 )
            // 회원가입의 경우, 회원가입이 되었다고 로그인 된 것은 아니므로 액티비티를 닫을 필요 없이 회원가입의 결과에 대한 Toast 메시지만 보내주면 될 것이다.
            auth.createUserWithEmailAndPassword(email,password)
                .addOnCompleteListener(this){ task ->
                    if(task.isSuccessful){
                        Toast.makeText(this, "회원가입에 성공했습니다. 로그인 버튼을 눌러 로그인해주세요",Toast.LENGTH_SHORT).show()

                    }else{
                        Toast.makeText(this, "이미 가입한 이메일이거나, 회원가입에 실패했습니다.",Toast.LENGTH_SHORT).show()
                    }
                }
        }
    }

    private fun initFacbookLoginButton(){

        // 해당 버튼을 통해 페이스북에 로그인이 완료하였을 때ㅡ,
        // 페이스북 측의 계정에서 어떤 정보를 가져올 것인지를 적음
        // ( 즉, 가져올 정보에 대한 Permission을 추가해준 것임 ) ㅡ,
        // facebook 문서를 확인해서 원하는 정보에 대한 Permission을 추가해주면 됨
        facebookLoginButton.setPermissions("email","public_profile")

        facebookLoginButton.registerCallback(callbackManager,object : FacebookCallback<LoginResult>{
            override fun onSuccess(result: LoginResult) {
               // TODO 로그인이 성공적
                // 로그인이 성공적이었을 경우,
                // LoginResult에서 AccessToken을 가져와서 ㅡ, ( 우리가 다루는 것이 아니라 ) Firebase측에 넘겨줘야하기 떄문에
                // LoginResult에서 AccessToken을 가져와서 Firebase에 넘겨주는 코드를 추가해야 한다.

                //LoginResult에서 AccessToken을 가져옴
                val credential = FacebookAuthProvider.getCredential(result.accessToken.token)

                // 가져온 AccessToken을 Firebase측에 넘겨주면서 로그인 및 계정 관리
                auth.signInWithCredential(credential)
                    .addOnCompleteListener(this@LoginActivity){ task->
                        if(task.isSuccessful){
                            handleSuccessLogin()
                        }else{
                            Toast.makeText(this@LoginActivity,"페이스북 로그인에 실패하였습니다.",Toast.LENGTH_SHORT).show()
                        }
                    }
            }

            override fun onCancel() {
               // TODO 로그인이 취소됨 -> 사용자가 취소를 누름
            }

            override fun onError(error: FacebookException?) {
               // TODO 로그인중 에러가 발생함
                Toast.makeText(this@LoginActivity,"페이스북 로그인이 실패했습니다.",Toast.LENGTH_SHORT).show()
                // 해당 코드가 Callback 안에 있으므로 어느 Context를 가져올 것인지 명시
            }
        })
    }


    private fun getInputEmail() = emailEditText.text.toString()
    private fun getInputPassword() = passwordEditText.text.toString()

    private fun handleSuccessLogin(){
        if(auth.currentUser == null){
            Toast.makeText(this,"로그인에 실패했습니다.",Toast.LENGTH_SHORT).show()
            return
        }

        // 현재 로그인 되어있는 계정이 있다면 해당 계정의 uid를 가져옴
        val userId = auth.currentUser?.uid.orEmpty()
        val currentUserDB = Firebase.database.reference.child(USERS).child(userId)
        val user = mutableMapOf<String,Any>()
        user[USER_ID] = userId
        currentUserDB.updateChildren(user)
        // 위에서 지정한 영역에 해당 데이터를 넣음

        finish()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        callbackManager.onActivityResult(requestCode,resultCode,data)
    }
}

activity_login.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="24dp">

    <EditText
        android:id="@+id/emailEditText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/passwordEditText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:inputType="textPassword"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/emailEditText" />

    <Button
        android:id="@+id/loginButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:enabled="false"
        android:text="로그인"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/passwordEditText" />

    <Button
        android:id="@+id/signUpButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="4dp"
        android:enabled="false"
        android:text="회원가입"
        app:layout_constraintEnd_toStartOf="@+id/loginButton"
        app:layout_constraintTop_toBottomOf="@+id/passwordEditText" />

    <com.facebook.login.widget.LoginButton
        android:id="@+id/facebookLoginButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/loginButton" />

</androidx.constraintlayout.widget.ConstraintLayout>

LikeActivity.kt

package com.example.aop_part3_chapter13

import android.content.Intent
import android.os.Bundle
import android.text.TextPaint
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ListAdapter
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.example.aop_part3_chapter13.DBKey.Companion.DIS_LIKE
import com.example.aop_part3_chapter13.DBKey.Companion.LIKE
import com.example.aop_part3_chapter13.DBKey.Companion.LIKED_BY
import com.example.aop_part3_chapter13.DBKey.Companion.NAME
import com.example.aop_part3_chapter13.DBKey.Companion.USERS
import com.example.aop_part3_chapter13.DBKey.Companion.USER_ID
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.*
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import com.yuyakaido.android.cardstackview.CardStackLayoutManager
import com.yuyakaido.android.cardstackview.CardStackListener
import com.yuyakaido.android.cardstackview.CardStackView
import com.yuyakaido.android.cardstackview.Direction
import java.util.*

// CardStackView에 리스너로 넣을 인터페이스를 해당 클래스에서 구현해서 this로 넣음 -> 이를 위해 CardStackListener 상속받음
class LikeActivity : AppCompatActivity(), CardStackListener {

    private val auth: FirebaseAuth = Firebase.auth
    private lateinit var userDB: DatabaseReference

    private val stackView: CardStackView by lazy {
        findViewById(R.id.cardStackView)
    }
    private val adapter = CardItemAdapter()
    private val cardItems = mutableListOf<CardItem>()

    private val cardStackManager by lazy {
        // 해당 클래스에서 CardStackListener 인터페이스를 상속받아 구현했기 때문에 this로 넣어줄 수 있음
        CardStackLayoutManager(this, this)
    }

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

        userDB = Firebase.database.reference.child(USERS)
        // 최상위 항목에 있는 Users 항목에 접근한 것

        // Users 항목 내부에 현재 사용자에 uid를 이름으로 하는 항목을 추가 및 접근 해줌
        val currentUserDB = userDB.child(getCurrentUserId())

        // DB에서 값을 가져오는 방법은 모두 리스너를 통해서만 가능함
        // 이번에는 단일 값을 가져오는 기능만 할 것이므로 addListenerForSingleValueEvent를 달아줌 -> 파라미터로 ValueEventListener를 구현하여 사용
        // 해당 리스너는 최초에 한번만 실행되며, 이후에는 실행되지 않는다.
        currentUserDB.addListenerForSingleValueEvent(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                // TODO DataChange가 일어났을 때 CallBack이 넘어오면서 호출됨
                // 처음에 리스너를 달았을 때는 처음에 리스너가 없었기 때문에 해당 DB에 데이터가 존재하면 이벤트가 내려오게 됨

                Log.e("SingleValueEvent",snapshot.key.toString().orEmpty())

                if (snapshot.child(NAME).value == null) {
                    showNameInputPopUp()
                    return
                }
                // todo 유저 정보를 갱신해라
                getUnSelectedUsers()
            }

            override fun onCancelled(error: DatabaseError) {
                // TODO Cancel이 일어났을 때 CallBack이 넘어오면서 호출됨
            }
        })

        initCardStackView()
        initSignOutButton()
        initMatchedListButton()
    }

    private fun initSignOutButton(){
        val signOutButton = findViewById<Button>(R.id.signOutButton)

        signOutButton.setOnClickListener {
            
            // 로그 아웃 ( Authentication에서 로그아웃 )
            auth.signOut()
            startActivity(Intent(this,MainActivity::class.java))
            finish()
        }
    }

    private fun initMatchedListButton() {
        val matchedListButton = findViewById<Button>(R.id.matchListButton)
        matchedListButton.setOnClickListener {
            startActivity(Intent(this, MatchedUserActivity::class.java))
        }
    }

    private fun initCardStackView() {
        stackView.layoutManager = cardStackManager
        stackView.adapter = adapter
    }

    private fun getUnSelectedUsers() {

        // DB에서 값을 가져오는 방법은 모두 리스너를 통해서만 가능함
        // DB에서 데이터를 유동적으로 가져오기 위해서 사용되는 리스너
        userDB.addChildEventListener(object : ChildEventListener {

            // 해당 영역내에서 데이터가 삽입되면 호출되는 메소드
            // 최초에는 해당 영역에 데이터가 없다고 인식하며,
            // 이후 데이터가 하나씩 들어간다고 인식되므로 
            // 해당 위치의 데이터 각각에 대해 해당 메소드가 따로 호출되어 하나씩 snapshot에 담아서 가져옴
            override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {

                Log.e("ChildAdd",snapshot.key.toString().orEmpty())

                if (snapshot.child(USER_ID).value != getCurrentUserId()
                    && snapshot.child(LIKED_BY).child(LIKE).hasChild(getCurrentUserId()).not()
                    && snapshot.child(LIKED_BY).child(DIS_LIKE).hasChild(getCurrentUserId()).not()
                ) {

                    val userId = snapshot.child(USER_ID).value.toString()
                    var name = "undecided"
                    if (snapshot.child(NAME).value != null) {
                        name = snapshot.child(NAME).value.toString()
                    }
                    cardItems.add(CardItem(userId, name))
                    adapter.submitList(cardItems)
                    adapter.notifyDataSetChanged()
                }
            }

            // 해당 위치에서 데이터가 변경되었을 경우, 변경이 있었던 데이터를 가져옴
            override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {

                Log.e("ChildChange",snapshot.key.toString().orEmpty())

                cardItems.find { it.userId == snapshot.key }?.let {
                    it.name = snapshot.child(NAME).value.toString()
                }
                adapter.submitList(cardItems)
                adapter.notifyDataSetChanged()
            }

            // 데이터 삭제
            override fun onChildRemoved(snapshot: DataSnapshot) {}

            // 데이터가 이동
            override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}

            // 데이터 제어중 오류 발생
            override fun onCancelled(error: DatabaseError) {}
        })
    }

    private fun showNameInputPopUp() {
        // EditText를 생성
        val editText = EditText(this)

        // AlertDialog에는 View를 추가할 수도 있음
        AlertDialog.Builder(this)
            .setTitle("이름을 입력해주세요")
            .setView(editText)
            .setPositiveButton("저장") { _, _ ->
                if (editText.text.isEmpty()) {
                    showNameInputPopUp()
                } else {
                    saveUserName(editText.text.toString())
                }
            }
            .setCancelable(false)
            .show()
    }

    // Realtime Database에 데이터를 넣는 메소드
    private fun saveUserName(name: String) {

        val userId = getCurrentUserId()
        val currentUserDB = userDB.child(userId)
        val user = mutableMapOf<String, Any>()
        user[NAME] = name
        currentUserDB.updateChildren(user)
        // 해당 경로에 데이터 추가 ( 추가할 데이터는 딕셔너리 형태여야 함 -> Json형태의 DB이기 떄문 )

        // Todo 유저 정보를 가져와라
        getUnSelectedUsers()
    }

    private fun getCurrentUserId(): String {
        if (auth.currentUser == null) {
            Toast.makeText(this, "로그인이 되어있지 않습니다.", Toast.LENGTH_SHORT).show()
            finish()
        }
        return auth.currentUser?.uid.orEmpty()
    }

    private fun like(){
        // CardStackView의 현재 최상위 항목은 CardStackLayoutManager의 topPosition을 보면 된다고, github의 가이드에 나와있었음ㅡ, 이런 부분들을 참고하는 것
        val card = cardItems[cardStackManager.topPosition-1]
        cardItems.removeFirst()

        // like를 하면 상대방의 DB에 누가 자신을 Like했는지 저장됨
        userDB.child(card.userId)
            .child(LIKED_BY)
            .child(LIKE)
            .child(getCurrentUserId())
            .setValue(true)

        // todo 매칭이 된 시점을 봐야한다.
        // todo 상대방에게 like를 누른 시점에 내 liked에 상대방의 id가 저장되어 있다면( 즉, 상대방이 나를 like 했다는 의미임 ), 서로 like 했다는 의미이므로 매칭 시켜줌 -> 상대방 입장에서도 동일
        saveMatchIfOtherUserLikedMe(card.userId)

        Toast.makeText(this,"${card.name}님을 like 하셨습니다",Toast.LENGTH_SHORT).show()
    }

    private fun disLike(){
        val card = cardItems[cardStackManager.topPosition-1]
        cardItems.removeFirst()

        // disLike를 하면 상대방의 DB에 누가 자신을 disLike했는지 저장됨
        userDB.child(card.userId)
            .child(LIKED_BY)
            .child(DIS_LIKE)
            .child(getCurrentUserId())
            .setValue(true)

        Toast.makeText(this,"${card.name}님을 disLike 하셨습니다",Toast.LENGTH_SHORT).show()
    }

    private fun saveMatchIfOtherUserLikedMe(otherUserId: String){

        // 내 DB속의 like안에 상대방의 id가 있는지 ㅡ, 즉, 상대방이 나를 like 했는지
        val otherUserDB = userDB.child(getCurrentUserId()).child(LIKED_BY).child(LIKE).child(otherUserId)

        // DB에서 값을 가져오는 방법은 모두 리스너를 통해서만 가능함
        otherUserDB.addListenerForSingleValueEvent(object : ValueEventListener{
            override fun onDataChange(snapshot: DataSnapshot) {

                // 만약에 상대방이 나를 이미 like 한 상태라면 ( 해당 상황 자체가 내가 상대방을 like 하는 상황이므로  ), 매칭이 성립
                if (snapshot.value == true){
                    // 내 DB에 상대방 id를 매칭에 추가
                    userDB.child(getCurrentUserId())
                        .child(LIKED_BY)
                        .child("match")
                        .child(otherUserId)
                        .setValue(true)

                    // 상대방의 DB에 내 id를 매칭에 추가
                    userDB.child(otherUserId)
                        .child(LIKED_BY)
                        .child("match")
                        .child(getCurrentUserId())
                        .setValue(true)
                }
            }

            override fun onCancelled(error: DatabaseError) {
            }
        })
    }

    // TODO -> 여기부터 아래 부분은 CardStackView의 리스너로 사용될 CardStackListener인터페이스를 LikeActivity 클래스 차원에서 구현하기 위한 부분임
    //  -> 따라서 CardStackView에는 this 키워드로 리스너를 넣어줄 수 있음

    override fun onCardDragging(direction: Direction?, ratio: Float) {}
    override fun onCardSwiped(direction: Direction?) {
        // Todo 카드를 스와이프 했을 때 리스너

        when (direction) {
            Direction.Right -> like()
            Direction.Left -> disLike()
            else ->{

            }
        }
    }

    override fun onCardRewound() {}
    override fun onCardCanceled() {}
    override fun onCardAppeared(view: View?, position: Int) {}
    override fun onCardDisappeared(view: View?, position: Int) {}
}

activity_like.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="매칭할 카드가 없습니다."
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.yuyakaido.android.cardstackview.CardStackView
        android:id="@+id/cardStackView"
        android:layout_width="0dp"
        android:layout_height="300dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="LIKE"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="DISLIKE"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/matchListButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="매치 리스트 보기"
        app:layout_constraintBottom_toTopOf="@+id/signOutButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/signOutButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="로그아웃 하기"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

CardItemAdapter.kt -> LikeActivity에서 CardStackView에 사용될 Adapter

package com.example.aop_part3_chapter13

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView

class CardItemAdapter : ListAdapter<CardItem, CardItemAdapter.ViewHolder>(diffUtil){

    inner class ViewHolder(private val view: View): RecyclerView.ViewHolder(view){

        fun bind(cardItem: CardItem){
            view.findViewById<TextView>(R.id.nameTextView).text = cardItem.name
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return ViewHolder(inflater.inflate(R.layout.item_card,parent,false))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(currentList[position])
    }

    companion object{
        val diffUtil = object : DiffUtil.ItemCallback<CardItem>() {
            override fun areItemsTheSame(oldItem: CardItem, newItem: CardItem): Boolean {
              //  TODO("Not yet implemented")

                return oldItem.userId == newItem.userId
            }

            override fun areContentsTheSame(oldItem: CardItem, newItem: CardItem): Boolean {
               // TODO("Not yet implemented")

                return oldItem == newItem
            }
        }
    }
}

item_card.xml -> LikeActivity에서 CardStackView에 사용될 Layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_margin="24dp"
    app:cardCornerRadius="16dp"
    android:layout_height="match_parent">
    
    <LinearLayout
        android:background="#FFC107"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <TextView
            android:id="@+id/nameTextView"
            android:text="name"
            android:textSize="40sp"
            android:textColor="@color/black"
            android:gravity="center"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
        
    </LinearLayout>
    
</androidx.cardview.widget.CardView>

MatchedUserActivity.kt

package com.example.aop_part3_chapter13

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.*
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase

class MatchedUserActivity: AppCompatActivity() {

    private val auth: FirebaseAuth = Firebase.auth
    private lateinit var userDB: DatabaseReference
    private val adapter = MatchedUserAdapter()
    private val cardItems = mutableListOf<CardItem>()

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

        userDB = Firebase.database.reference.child("Users")

        initMatchedUserRecyclerView()
        getMatchUsers()
    }

    private fun initMatchedUserRecyclerView(){
        val recyclerView = findViewById<RecyclerView>(R.id.matchedUserRecyclerView)

        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter

    }

    private fun getMatchUsers(){
        val matchedDB = userDB.child(getCurrentUserId()).child("likedBy").child("match")
        matchedDB.addChildEventListener(object : ChildEventListener{
            override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
               // TODO("Not yet implemented")
                if (snapshot.key?.isNotEmpty() == true) {
                    getUserByKey(snapshot.key.orEmpty())

                }
            }
            override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {}
            override fun onChildRemoved(snapshot: DataSnapshot) {}
            override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
            override fun onCancelled(error: DatabaseError) {}
        })
    }

    private fun getUserByKey(userId:String){
        userDB.child(userId).addListenerForSingleValueEvent(object : ValueEventListener{
            override fun onDataChange(snapshot: DataSnapshot) {
               // TODO("Not yet implemented")

                cardItems.add(CardItem(userId,snapshot.child("name").value.toString()))
                adapter.submitList(cardItems)
            }
            override fun onCancelled(error: DatabaseError) {}
        })
    }

    private fun getCurrentUserId(): String {
        if (auth.currentUser == null) {
            Toast.makeText(this, "로그인이 되어있지 않습니다.", Toast.LENGTH_SHORT).show()
            finish()
        }
        return auth.currentUser?.uid.orEmpty()
    }
}

activity_match.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/matchedUserRecyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MatchedUserAdapter.kt -> MatchedUserActivity에서 RecyclerView에 사용될 Adapter

package com.example.aop_part3_chapter13

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ListAdapter
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView

class  MatchedUserAdapter: androidx.recyclerview.widget.ListAdapter<CardItem,MatchedUserAdapter.ViewHolder>(diffUtil){

    inner class ViewHolder(private val view: View): RecyclerView.ViewHolder(view){

        fun bind(cardItem: CardItem){
            view.findViewById<TextView>(R.id.userNameTextView).text = cardItem.name
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return ViewHolder(inflater.inflate(R.layout.item_matched_user,parent,false))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(currentList[position])
    }

    companion object{
        val diffUtil = object : DiffUtil.ItemCallback<CardItem>() {
            override fun areItemsTheSame(oldItem: CardItem, newItem: CardItem): Boolean {
                //  TODO("Not yet implemented")

                return oldItem.userId == newItem.userId
            }

            override fun areContentsTheSame(oldItem: CardItem, newItem: CardItem): Boolean {
                // TODO("Not yet implemented")

                return oldItem == newItem
            }
        }
    }
}

item_matched_user.xml -> MatchedUserActivity에서 RecyclerView에 사용될 Layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="30sp"
        android:id="@+id/userNameTextView"/>

</LinearLayout>

CardItem.kt -> data class ( 앱에서 데이터를 더 효율적으로 다루기 위해 데이터 객체 구성 )

package com.example.aop_part3_chapter13

data class CardItem (
    val userId: String,
    var name: String
        )

DBKey.kt -> Realtime Database에 대한 접근을 설계할 때, 오타를 방지하기 위해서 전역상수로 지정해놓은 클래스

package com.example.aop_part3_chapter13


// 아직 적용하진 않았음

// 단어 실수를 방지하기 위해 중요하고 자주쓰이는 단어는 이렇게 따로 정리해 주는 것이 좋다. 
class DBKey {
    companion object {
        const val USERS = "Users"
        const val LIKED_BY = "likedBy"
        const val LIKE = "like"
        const val DIS_LIKE = "dislike"
        const val NAME = "name"
        const val USER_ID = "userId"
    }
}

좋은 웹페이지 즐겨찾기