소개팅앱 5. 좋아요 목록 표시 & 알림 전송하기

변현섭·2023년 8월 27일
0
post-thumbnail

이번 포스팅에서는 크게 두 가지 기능을 추가해보려 합니다. 첫번째는 내가 좋아요 표시한 유저가 나를 똑같이 좋아요 표시했을 때, 매칭 알림을 전송해주는 기능이고 두번째는 내가 좋아요 표시한 유저를 List View로 나타내는 기능입니다.

1. 좋아요 표시한 사람 저장하기

① 좋아요한 사람들의 목록을 저장할 경로를 FirebaseRef 클래스에 추가한다.

val userLike = database.getReference("userLike")

② 좋아요 표시한 사람의 UID는 카드뷰를 오른쪽으로 넘겼을 때 받아오면 된다. MainActivity의 onCardSwiped 메서드를 아래와 같이 수정하자.

override fun onCardSwiped(direction: Direction?) {
    if(direction == Direction.Right) {
        saveUserLike(uid, userInfoList[userCount].uid.toString())
    }
    userCount++
    if(userCount == userInfoList.count()) {
        getUserInfoList(currentUserGender)
        Toast.makeText(baseContext, "유저 정보를 다시 가져옵니다.", Toast.LENGTH_SHORT).show()
    }
}

③ 이제 좋아요 표시한 유저의 목록을 저장하는 메서드를 정의하자.

private fun saveUserLike(uid : String, otherUid : String) {
    FirebaseRef.userLike.child(uid).child(otherUid).setValue("like")
}

④ 코드를 실행시켜보면, 파이어베이스의 본인의 UID와 본인이 좋아요 표시한 사람의 UID가 잘 저장되고 있음을 확인할 수 있을 것이다.

2. 매칭 로직 구현하기

① 매칭을 구현하기 위해서는 내가 좋아요를 표시한 사람의 좋아요 목록에 내가 있는지를 확인하는 로직이 필요하다. MainActivity에 아래의 메서드를 추가하자.

  • Toast.makeText에서 context 부분에서 에러가 나면 @을 이용해 클래스 명을 명시해주면 된다.
private fun getOtherUserLike(uid : String, otherUid : String) {
    val postListener = object : ValueEventListener {
        override fun onDataChange(dataSnapshot: DataSnapshot) {
            for(dataModel in dataSnapshot.children) {
                if(dataModel.key.toString() == uid) {
                    Toast.makeText(this@MainActivity, "매칭되었습니다", Toast.LENGTH_SHORT).show()
                }
            }
        }
        
        override fun onCancelled(databseError: DatabaseError) {
            Log.w("MainActivity", "onCancelled", databseError.toException())
        }
    }
    FirebaseRef.userLike.child(otherUid).addValueEventListener(postListener)
}

② 방금 만든 saveUserLike 메서드 하단에 위 메서드를 추가하면 된다.

private fun saveUserLike(uid : String, otherUid : String) {
    FirebaseRef.userLike.child(uid).child(otherUid).setValue("like")
    getOtherUserLike(uid, otherUid)
}

③ 매칭이 잘 되는지 확인해보자. 두 개의 디바이스를 선택한 후 코드를 실행시켜보면, 서로 좋아요를 표시했을 때 Toast 메시지가 나와야한다.

3. 매칭 시 알림 보내기

① 안드로이드 알림과 관련한 자세한 설명은 아래의 공식문서를 참조하기 바란다.
>> 안드로이드 알림 관련 공식문서

② MainActivity에 아래의 메서드를 추가한다.

private fun createNotificationChannel() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val name = "name"
        val descriptionText = "description"
        val importance = NotificationManager.IMPORTANCE_DEFAULT
        val channel = NotificationChannel("test", name, importance).apply {
            description = descriptionText
        }
        // Register the channel with the system
        val notificationManager: NotificationManager =
            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(channel)
    }
}
  • if문: Android 버전이 8.0 (Oreo) 이상일 때에만 알림 채널을 생성한다. 이는 Android 8.0 이상부터 알림을 보내는 방식이 변경되었기 때문이다.
  • name: 알림 채널의 이름을 설정한다.
  • descriptionText: 알림 채널에 대한 간단한 설명이다.
  • importance: 알림의 중요도를 기본 중요도로 설정한다.
  • NotificationChannel("test", name, importance).apply: 위에서 설정한 이름, 중요도 및 설명을 가지고 알림 채널 객체를 생성한다. test는 알림 채널의 id이다.
  • notificationManager.createNotificationChannel(channel): 알림 체널을 시스템에 등록한다. 이제 해당 채널을 통해 알림이 전송된다.

③ 알림을 보내는 메서드도 추가한다.

private fun sendNotification() {
    if(NotificationManagerCompat.from(this).areNotificationsEnabled()) {
        var builder = NotificationCompat.Builder(this, "test")
            .setSmallIcon(R.drawable.icon)
            .setContentTitle("매칭 완료")
            .setContentText("내가 좋아요 표시한 사람이 나를 좋아요 표시하였습니다.")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setAutoCancel(true)
        with(NotificationManagerCompat.from(this)) {
            if (ActivityCompat.checkSelfPermission(
                    this@MainActivity,
                    Manifest.permission.POST_NOTIFICATIONS
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                return
            }
            notify(1, builder.build())
        }
    }
    else {
        Log.w("notification", "알림 수신이 차단된 상태입니다.")
    }
}
  • NotificationManagerCompat.from(this).areNotificationsEnabled(): 기기의 알림 설정이 허용되어있는지 확인한다.
  • setSmallIcon: 알림의 작은 아이콘을 설정한다.
  • setPriority: 알림의 중요도를 기본 중요도로 설정한다.
  • setAutoCancel(true): 알림을 탭하면 알림이 자동으로 사라지도록 설정한다. 이 설정은 푸시 알림에 대해 화면전환(PendingIntent) 기능을 추가할 때, 제대로 사용된다.
  • with(NotificationManagerCompat.from(this)): 알림 전송에 필요한 알림 관리자를 생성한다.
  • ActivityCompat.checkSelfPermission: 알림 권한(POST_NOTIFICATIONS)을 확인하고, 권한이 없을 경우 알림 전송을 중단한다. 빨간 줄이 생길텐데, 우클릭 > Show Context Actions > Add Permission POST NOTIFICATIONS를 클릭하면 된다.

④ AndroidManifest.xml 파일에는 아래의 권한이 허용되어있어야 한다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

⑤ getOtherUserLike의 onDataChange 메서드를 아래와 같이 수정한다.

for(datamodel in dataSnapshot.children) {
    if(datamodel.key.toString() == uid) {
        Toast.makeText(this@MainActivity, "매칭되었습니다", Toast.LENGTH_SHORT).show()
        createNotificationChannel()
        sendNotification()
    }
}

⑥ 이제 코드를 실행시켜보면, 매칭 알림이 잘 전송되는 것을 확인할 수 있을 것이다.

  • 앱 설정에서 알림을 허용해야 결과를 제대로 확인할 수 있다.

4. 매칭 리스트 표시하기

1) 내가 좋아요 표시한 유저의 정보 가져오기

① default 패키지 하위로, chat이라는 이름의 패키지를 추가하고, chat 패키지 하위로 MatchingListActivity를 추가한다.

② activity_setting.xml 파일에 아래의 Button 태그를 추가한다.

 <Button
     android:id="@+id/myMatchingList"
     android:text="매칭 리스트"
     android:textStyle="bold"
     android:layout_margin="10dp"
     android:textSize="20sp"
     android:layout_width="match_parent"
     android:layout_height="60dp"/>

③ 버튼에 대한 클릭 이벤트 리스너를 SettingActivity 파일에 작성한다.

val matchingListBtn = findViewById<Button>(R.id.myMatchingList)

likeListBtn.setOnClickListener {
    startActivity(Intent(this, LikeListActivity::class.java))
}

④ MatchingListActivity 파일에 내가 좋아요 표시한 유저의 UID를 담는 List를 생성한다.

private val myLikeList = mutableListOf<String>()

⑤ 내가 좋아요 표시한 유저의 UID를 방금 생성한 list에 넣는 메서드를 정의한다.

private fun getMyLikeList() {
    val postListener = object : ValueEventListener {
        override fun onDataChange(dataSnapshot: DataSnapshot) {
            for(dataModel in dataSnapshot.children) {
    			myLikeList.add(dataModel.key.toString())
			}
        }
        
        override fun onCancelled(databseError: DatabaseError) {
            Log.w("MatchingListActivity", "onCancelled", databseError.toException())
        }
    }
    
    FirebaseRef.userLike.child(uid).addValueEventListener(postListener)
}

⑥ 이번에는 좋아요 표시한 유저의 정보를 담는 List를 생성한다.

private val myLikeUserInfo = mutableListOf<UserInfo>()

⑦ 내가 좋아요 표시한 유저의 정보만 List에 추가하기 위해 아래의 메서드를 정의한다.

private fun getUserInfoList() {
    val postListener = object : ValueEventListener {
        override fun onDataChange(dataSnapshot: DataSnapshot) {
            for(dataModel in dataSnapshot.children) {
                val userInfo = dataModel.getValue(UserInfo::class.java)
            }
        }
        override fun onCancelled(databseError: DatabaseError) {
            Log.w("MatchingListActivity", "onCancelled", databseError.toException())
        }
    }
    FirebaseRef.userInfo.addValueEventListener(postListener)
}

⑧ onCreate 메서드에서 위에서 작성한 메서드를 호출한다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_matching_list)
    getUserInfoList()
    getMyLikeList()
}

2) 리스트 뷰 구성하기

① layout 디렉토리 하위로 list_view_item.xml 파일을 추가한다.

② 아래와 같이 TextView 태그를 추가한다.

<TextView
	android:id="@+id/lvNickname"
    android:text="닉네임 :"
    android:textSize="20sp"
    android:layout_margin="20dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
    
<TextView
	android:id="@+id/lvNicknameArea"
    android:text="크롬"
    android:textSize="20sp"
    android:textStyle="bold"
    android:layout_margin="20dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

③ 이제 chat 디렉토리 하위로, LVAdapter 클래스를 생성하자.

class LVAdapter(val context : Context, val dataList : MutableList<UserInfo>) : BaseAdapter() {

    override fun getCount(): Int {
        return dataList.size
    }

    override fun getItem(position: Int): Any {
        return dataList[position]
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
    	var convertView = convertView
        
    	if(convertView == null) {
        	convertView = LayoutInflater.from(parent?.context).inflate(R.layout.list_view_item, parent, false)
    	}
    
    	val nickname = convertView!!.findViewById<TextView>(R.id.lvNicknameArea)
    	nickname!!.text = dataList[position].nickname
    	return convertView!!
	}
}

④ activity_matching_list.xml 파일에 ListView 태그를 추가한다.

<ListView
	android:id="@+id/listViewItems"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

⑤ MatchingListActivity 파일에서 lateinit으로 어댑터를 선언한다.

  • onCreate 메서드에서 어댑터를 연결하고, getUserInfoList 메서드에서 어댑터를 동기화시켜야 하므로 함수 외부에 lateinit으로 선언해야 한다.
lateinit var listViewAdapter : LVAdapter

⑥ onCreate 메서드를 아래와 같이 수정한다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_matching_list)
    
    val listview = findViewById<ListView>(R.id.listViewItems)
    listViewAdapter = LVAdapter(this, myLikeUserInfo)
    listview.adapter = listViewAdapter
    
    getUserInfoList()
    getMyLikeList()
}

⑦ getUserInfoList 메서드의 for문이 끝나는 시점에 Adapter를 동기화 시켜야한다.

for(dataModel in dataSnapshot.children) {
    val userInfo = dataModel.getValue(UserInfo::class.java)
    if(myLikeList.contains(userInfo?.uid)) {
        myLikeUserInfo.add(userInfo!!)
    }
}
listViewAdapter.notifyDataSetChanged()

5. 비동기처리로 인한 문제점

아마 코드를 실행시킨 후 매칭 리스트에 들어가보면, 결과가 제대로 나타나지 않을 것이다. 이는 비동기처리에 인한 결과로, myLikeList가 다 채워지기도 전에 if(myLikeList.contains(userInfo?.uid)) 조건을 확인하면서 발생하는 문제이다. 그래서 if(myLikeList.contains(userInfo?.uid)) 직전에 Log.d로 myLikeList.toString()의 결과를 확인해보면 빈 리스트가 나올 것이다. Android Kotlin에서 비동기로 처리되는 작업은 아래와 같다.

  • (Retrofit, OkHttp 등) 서버와의 통신
  • 파일 I/O
  • (클릭 등) UI 이벤트
  • 백그라운드 프로세스

나중에 Api 연동을 다룰 때에는 Coroutine을 사용하여 이 문제를 해결하겠지만, 여기서는 getMyLikeList()와 getUserInfoList() 메서드를 순차적으로(Blocking 방식으로) 동작시키는 방식으로 해결해보자. 메서드를 Blocking 방식으로 실행하는 방법에는 크게 두 가지가 있다.

1) delay 발생시키기

이 아이디어는 "코드는 한줄씩 순차적으로 수행된다"는 점에서 착안한다. 위 경우에서도 당연히 첫번째 메서드가 먼저 실행되긴 하지만, 두번째 메서드가 Non-Blocking 방식으로 실행됨에 따라 두 메서드는 거의 동시에 실행되는 것처럼 보일 것이다. 따라서 두 메서드 사이에 delay 타임을 주어 첫번째 메서드가 완료될 때까지 충분히 기다렸다가, 두번째 메서드를 실행해야 한다.

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

        val listview = findViewById<ListView>(R.id.listViewItems)
        listViewAdapter = LVAdapter(this, myLikeUserInfo)

        listview.adapter = listViewAdapter
        runBlocking {
            launch {
                getMyLikeList()
            }
            delay(200) // 0.2초
            launch {
                getUserInfoList()
            }
        }
    }

2) 메서드 내부 호출

delay를 발생시키는 방법에는 명확한 한계가 있다. delay가 너무 짧아도, 너무 길어도 문제가 된다는 점에서 적절한 delay 시간을 찾기도 어렵고 심지어는 List의 사이즈에 따라 delay를 변경해야 할 수도 있다.

사실 더 좋은 방법은 메서드를 내부 호출하는 방식이다. 다시말해 onCreate 메서드에서 getMyLikeList()만 호출하고, getUserInfoList()를 아래와 같이 getMyLikeList()에서 내부 호출하는 것이다.

override fun onCreate(savedInstanceState: Bundle?) {
	...
	getMyLikeList()
}

private fun getMyLikeList() {
    val postListener = object : ValueEventListener {
        override fun onDataChange(dataSnapshot: DataSnapshot) {
            for(dataModel in dataSnapshot.children) {
                myLikeList.add(dataModel.key.toString())
            }
            getUserInfoList()
        }
        override fun onCancelled(databseError: DatabaseError) {
            Log.w("MatchingListActivity", "onCancelled", databseError.toException())
        }
    }
    FirebaseRef.userLike.child(uid).addValueEventListener(postListener)
}

우리가 여기서 알아야 할 것은, 순서가 보장되어야하는 작업을 처리할 때에는 항상 비동기처리에 대한 부분을 염두에 두어야 한다는 것이다. 이제 코드를 실행시켜보면, 매칭 리스트에 닉네임이 잘 출력될 것이다.

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

0개의 댓글