Skip to content

Latest commit

 

History

History
1084 lines (738 loc) · 35.6 KB

README.md

File metadata and controls

1084 lines (738 loc) · 35.6 KB

데이트 DAY2

데이트/매칭 앱 프로젝트

day2_cover

시작

  • FCM을 이용한 데이트/매칭 프로젝트입니다.

개발

기간

  • 22.05.30. ~ 22.07.14.

목표

  • 코틀린으로 주요 기능을 구현합니다.
  • FCM을 사용합니다.

사용

  • Kotlin
  • Jetpack
  • Firebase

기능

1. 인증(Firebase Auth)

auth

  • 1.1. 로그인/로그아웃
    • 유효성 검사 후 로그인합니다.
    • 비회원으로 로그인하면 회원 가입 절차 없이 로그인하는 대신, 한 번 로그아웃하면 같은 계정으로 다시 로그인할 수 없습니다.
    • 로그아웃하면 IntroActivity로 이동합니다.
// LoginActivity.kt

        // 로그인 버튼 클릭하면
        binding.loginBtn.setOnClickListener {

            // 로그인조건 확인
            var emailCheck = true
            var pwCheck = true

            // 이메일, 비밀번호
            val emailTxt = binding.email.text.toString()
            val pwTxt = binding.pw.text.toString()

            // 이메일 정규식
            val emailPattern = Patterns.EMAIL_ADDRESS

            // 이메일 검사
            if(emailTxt.isEmpty()) {
                emailCheck = false
                binding.emailArea.error = "이메일주소를 입력하세요"
            } else if(!emailPattern.matcher(emailTxt).matches()) {
                emailCheck = false
                binding.emailArea.error = "이메일 형식이 잘못되었습니다"
            } else {
                emailCheck = true
                binding.emailArea.error = null
            }

            // 비밀번호 검사
            if(pwTxt.isEmpty()) {
                pwCheck = false
                binding.pwArea.error = "비밀번호를 입력해 주세요"
            } else if (pwTxt.length<6) {
                pwCheck = false
                binding.pwArea.error = "최소 6자 이상 입력하세요"
            } else if (pwTxt.length>20) {
                pwCheck = false
                binding.pwArea.error = "20자 이하로 입력하세요"
            } else {
                pwCheck = true
                binding.pwArea.error = null
            }

            // 로그인 조건을 모두 만족하면
            if(emailCheck and pwCheck) {

                // 로그인
                auth.signInWithEmailAndPassword(emailTxt, pwTxt)
                    .addOnCompleteListener(this) { task ->

                        // 성공하면
                        if (task.isSuccessful) {

                            // 명시적 인텐트 -> 다른 액티비티 호출
                            val intent = Intent(this, MainActivity::class.java)

                            // 메인 액티비티 시작
                            startActivity(intent)

                            // 로그인 액티비티 종료
                            finish()

                        // 오류 등 -> 로그인 실패
                        } else { Toast.makeText(this, "이메일과 비밀번호를 다시 확인하세요", Toast.LENGTH_SHORT).show() }

                    }

            // 조건 불만족하면 로그인 실패
            } else { Toast.makeText(this, "이메일과 비밀번호를 다시 확인하세요", Toast.LENGTH_SHORT).show() }

        }
// MyPageActivity.kt

        // 로그아웃 버튼
        binding.logoutBtn.setOnClickListener {

            // 로그아웃 실행
            Firebase.auth.signOut()

            // 로그아웃 확인 메시지지
            Toast.makeText(this, "로그아웃", Toast.LENGTH_SHORT).show()

            // '새 작업(task) 시작' 또는 '시작하려는 액티비티보다 상위에 존재하는 액티비티 삭제'
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)

            // 명시적 인텐트 -> 다른 액티비티 호출
            val intent = Intent(this, IntroActivity::class.java)

            // 인트로 액티비티 시작
            startActivity(intent)

            // 내 정보 액티비티 종료
            finish()

        }
  • 1.2. 회원가입
    • 유효성 검사 후 가입합니다.
// JoinActivity.kt

        // 회원가입 버튼
        binding.joinBtn.setOnClickListener {

            // 가입조건 확인
            var emailCheck = true
            var pwCheck = true
            var nicknameCheck = true
            var genderCheck = true
            var cityCheck = true
            var ageCheck = true

            // 모든 가입조건
            var allCheck = emailCheck and pwCheck and nicknameCheck and genderCheck and cityCheck and ageCheck

            // 이메일주소, 비밀번호
            val emailTxt = binding.email.text.toString()
            val pwTxt = binding.pw.text.toString()

            // 별명, 성별, 지역, 생년
            nickname = binding.nickname.text.toString()
            gender = binding.gender.text.toString()
            city = binding.city.text.toString()
            age = binding.age.text.toString()

            // 빈 칸 검사
            if(emailTxt.isEmpty() || pwTxt.isEmpty() || nickname.isEmpty() || gender.isEmpty() || city.isEmpty() || age.isEmpty()) {
                allCheck = false
                Toast.makeText(this, "입력란을 모두 작성하세요", Toast.LENGTH_SHORT).show()
            }

            // 이메일주소 정규식
            val emailPattern = Patterns.EMAIL_ADDRESS

            // 이메일주소 검사
            if(emailTxt.isEmpty()) {
                emailCheck = false
                binding.emailArea.error = "이메일주소를 입력하세요"
            } else if(!emailPattern.matcher(emailTxt).matches()) {
                emailCheck = false
                binding.emailArea.error = "이메일 형식이 잘못되었습니다"
            } else {
                emailCheck = true
                binding.emailArea.error = null
            }

            // 비밀번호 검사
            if(pwTxt.isEmpty()) {
                pwCheck = false
                binding.pwArea.error = "비밀번호를 입력하세요"
            } else if (pwTxt.length<6) {
                pwCheck = false
                binding.pwArea.error = "최소 6자 이상 입력하세요"
            } else if (pwTxt.length>20) {
                pwCheck = false
                binding.pwArea.error = "20자 이하로 입력하세요"
            } else {
                pwCheck = true
                binding.pwArea.error = null
            }

            // 별명 검사
            if(nickname.isEmpty()) {
                nicknameCheck = false
                binding.nicknameArea.error = "별명을 입력하세요"
            } else if(nickname.length>20) {
                nicknameCheck = false
                binding.nicknameArea.error = "10자 이하로 입력하세요"
            } else {
                nicknameCheck = true
                binding.nicknameArea.error = null
            }

            // 성별 검사
            if(gender.isEmpty()) {
                genderCheck = false
                binding.genderArea.error = "성별을 입력하세요"
            } else {
                genderCheck = true
                binding.genderArea.error = null
            }

            // 지역 검사
            if(city.isEmpty()) {
                cityCheck = false
                binding.cityArea.error = "지역을 입력하세요"
            } else {
                cityCheck = true
                binding.cityArea.error = null
            }

            // 생년 검사
            if(age.isEmpty()) {
                ageCheck = false
                binding.ageArea.error = "생년을 입력하세요"
            } else {
                ageCheck = true
                binding.ageArea.error = null
            }

            // 가입조건 모두 만족하면
            if(allCheck) {

                // 계정 생성
                auth.createUserWithEmailAndPassword(emailTxt, pwTxt)
                    .addOnCompleteListener(this) { task ->

                        // 회원가입 성공
                        if (task.isSuccessful) {

                            // UID 정의
                            val user = auth.currentUser
                            uid = user?.uid.toString()

                            // 토큰
                            FirebaseMessaging.getInstance().token.addOnCompleteListener(
                                OnCompleteListener { task ->

                                    // 실패시 로그
                                    if (!task.isSuccessful) {
                                        Log.w(TAG, "Fetching FCM registration token failed", task.exception)
                                        return@OnCompleteListener
                                    }

                                    // 새 FCM 등록 토큰
                                    val token = task.result.toString()
                                    Log.e(TAG, "토큰(user token value) - $token")

                                    // 작성한 내용과 토큰값을 데이터 클래스 형태로 만들어
                                    val userModel = UserDataModel(
                                        uid,
                                        nickname,
                                        gender,
                                        city,
                                        age,
                                        token
                                    )

                                    // 파이어베이스에 회원정보 하위값으로 넣고
                                    FirebaseRef.userInfoRef.child(uid).setValue(userModel)

                                    // 프사 업로드 후
                                    uploadImage(uid)

                                    // 명시적 인텐트 -> 다른 액티비티 호출
                                    val intent = Intent(this, MainActivity::class.java)

                                    // 메인 액티비티 시작
                                    startActivity(intent)

                                    // 조인 액티비티 종료
                                    finish()

                                })

                        // 오류, 중복 계정 등 -> 회원가입 실패
                        } else { Toast.makeText(this, "회원가입 실패", Toast.LENGTH_SHORT).show() }

                    }

            // 조건 불만족하면 회원가입 실패
            } else { Toast.makeText(this, "회원가입 실패", Toast.LENGTH_SHORT).show() }

        }

2. 메인 화면(Card Stack View)

main

  • 현재 사용자와 다른 성별인 사용자의 프로필만 카드 더미 형식으로 구현합니다.
  • 카드를 오른쪽으로 넘기면 하트 애니메이션이 활성화되며 좋아요 목록에 해당 사용자를 추가합니다.
  • 카드를 왼쪽으로 넘기면 좋아요를 취소합니다(22.10.06. 업데이트).
  • 모든 프로필을 확인하면 자동으로 새로고침합니다.
// MainActivity.kt

        // 카드스택뷰
        manager = CardStackLayoutManager(baseContext, object: CardStackListener {

            // 카드 넘기기
            override fun onCardSwiped(direction: Direction?) {
            
                // 왼쪽(관심없음)
                if(direction == Direction.Left) {

                    // 해당 카드(사용자) 좋아요 삭제
                    userLikeDelete(uid, usersDataList[userCount].uid.toString())

                }

                // 오른쪽(좋아요)
                if(direction == Direction.Right) {

                    // 하트 애니메이션 및 토스트 메시지
                    binding.ltAnimation.playAnimation()
                    Toast.makeText(this@MainActivity, "좋아요", Toast.LENGTH_SHORT).show()

                    // 해당 카드(사용자) 좋아요 처리
                    userLikeOther(uid, usersDataList[userCount].uid.toString())

                }

                // 넘긴 프로필의 수를 셈
                userCount += 1

                // 프로필 전부 다 봤을 때
                if(userCount == usersDataList.count()) {

                    // 자동으로 새로고침
                    getUserDataList(currentUserGender)
                    Toast.makeText(this@MainActivity, "모든 프로필을 확인했습니다", Toast.LENGTH_SHORT).show()

                }
            }

            // 중략

        })

        // 카드스택어댑터에 데이터 넘김
        cardStackAdapter = CardStackAdapter(baseContext, usersDataList)
        binding.cardStackView.layoutManager = manager
        binding.cardStackView.adapter = cardStackAdapter

        // 현재 사용자 정보
        getMyUserData()

    }

    // 현재 사용자 정보
    private fun getMyUserData() {

        // 데이터베이스에서 컨텐츠의 세부정보를 검색
        val postListener = object : ValueEventListener {

            // 데이터스냅샷 내 사용자 데이터 출력
            override fun onDataChange(dataSnapshot: DataSnapshot) {

                // 프사 제외한 나머지 정보
                val data = dataSnapshot.getValue(UserDataModel::class.java)

                // 현재 사용자의 성별
                currentUserGender = data?.gender.toString()

                // 현재 사용자의 닉네임
                MyInfo.myNickname = data?.nickname.toString()

                // 현재 사용자와 성별이 반대인 사용자 목록
                getUserDataList(currentUserGender)

            }

            // 실패
            override fun onCancelled(databaseError: DatabaseError) { Log.w(TAG, "getMyUserData - loadPost:onCancelled", databaseError.toException()) }

        }

        // 파이어베이스 내 데이터의 변화(추가)를 알려줌
        FirebaseRef.userInfoRef.child(uid).addValueEventListener(postListener)

    }

    // 전체 사용자 정보
    private fun getUserDataList(currentUserGender : String) {

        // 데이터베이스에서 컨텐츠의 세부정보를 검색
        val postListener = object : ValueEventListener {

            // 데이터스냅샷 내 사용자 데이터 출력
            @SuppressLint("NotifyDataSetChanged")
            override fun onDataChange(dataSnapshot: DataSnapshot) {

                // 데이터스냅샷 내 사용자 데이터 출력
                for(dataModel in dataSnapshot.children) {

                    // 다른 사용자들 정보 가져옴
                    val user = dataModel.getValue(UserDataModel::class.java)

                    // 현재 사용자와 같은 성별인 사용자 -> 패스
                    if(user!!.gender.toString() == currentUserGender) {}

                    // 현재 사용자와 다른 성별인 사용자만 불러옴
                    else { usersDataList.add(user) }

                }

                // 동기화(새로고침) -> 리스트 크기 및 아이템 변화를 어댑터에 알림
                cardStackAdapter.notifyDataSetChanged()

            }

            // 실패
            override fun onCancelled(databaseError: DatabaseError) { Log.w(TAG, "getUserDataList - loadPost:onCancelled", databaseError.toException()) }

        }

        // 파이어베이스 내 데이터의 변화(추가)를 알려줌
        FirebaseRef.userInfoRef.addValueEventListener(postListener)

    }

    // 카드 좋아요 하기
    private fun userLikeOther(myUid : String, otherUid : String) {

        // (카드 오른쪽으로 넘기면) 좋아요 값 true로 설정
        FirebaseRef.userLikeRef.child(myUid).child(otherUid).setValue("true")

        // 좋아요 목록
        getMyLikeList(otherUid)
        
    }
    
    // 카드 좋아요 삭제하기
    private fun userLikeDelete(myUid : String, otherUid : String) {

        // (카드 왼쪽으로 넘기면) 좋아요 값 삭제
        FirebaseRef.userLikeRef.child(myUid).child(otherUid).removeValue()

    }
// CardStackAdapter.kt

// 카드스택
class CardStackAdapter(val context: Context, private val items: List<UserDataModel>): RecyclerView.Adapter<CardStackAdapter.ViewHolder>() {

    // ViewHolder : (자식뷰를 포함한) 레이아웃 단위의 뷰를 하나의 뷰홀더로 설정
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardStackAdapter.ViewHolder {

        // 뷰홀더 생성
        val inflater = LayoutInflater.from(parent.context)
        val view: View = inflater.inflate(R.layout.item_card, parent, false)

        return ViewHolder(view)

    }

    // 각 뷰홀더에 데이터 연결
    override fun onBindViewHolder(holder: CardStackAdapter.ViewHolder, position: Int) = holder.binding(items[position])

    // 전체 뷰홀더(아이템) 개수
    override fun getItemCount(): Int = items.size

    // 자식뷰를 포함한 레이아웃 단위의 뷰를 하나의 뷰홀더로 설정
    inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {

        // (카드의) 프사
        private val image = itemView.findViewById<ImageView>(R.id.profileImageArea)

        // 별명
        private val nickname = itemView.findViewById<TextView>(R.id.itemNickname)

        // 지역
        private val city = itemView.findViewById<TextView>(R.id.itemCity)

        // 생년
        private val age = itemView.findViewById<TextView>(R.id.itemAge)

        fun binding(data: UserDataModel) {

            // 프사 저장된 위치
            val storageRef = Firebase.storage.reference.child(data.uid + ".png")

            // 프사 다운로드
            storageRef.downloadUrl.addOnCompleteListener( OnCompleteListener { task ->

                // 수행
                if(task.isSuccessful) {

                    // 글라이드로 불러옴
                    Glide.with(context)
                        .load(task.result)
                        .into(image)

                }

            })

            // 불러온 별명, 지역, 나이 정보를 해당 영역에 매칭
            nickname.text = data.nickname
            city.text = data.city
            age.text = data.age

        }

    }

}

3. 매칭 및 좋아요(Firebase Cloud Messaging)

match

  • 3.1. 매칭
    • 현재 사용자와 상대방이 서로 좋아요한 상태면 양쪽에 매칭을 알리는 푸시 메시지가 전송됩니다.
// MainActivity.kt

    // 현재 사용자의 좋아요 목록
    private fun getMyLikeList(otherUid: String) {

        // 데이터베이스에서 컨텐츠의 세부정보를 검색
        val postListener = object : ValueEventListener {

            // 데이터스냅샷 내 사용자 데이터 출력
            override fun onDataChange(dataSnapshot: DataSnapshot) {

                // "모든" 사용자의 좋아요 리스트 (x)
                // "현재 사용자가 좋아하는" 사용자의 좋아요 리스트 (O)
                for(dataModel in dataSnapshot.children) {

                    // 다른 사용자가 좋아요 한 사용자 목록에
                    val likeUserKey = dataModel.key.toString()

                    // 현재 사용자가 포함돼 있으면
                    if(likeUserKey == uid) {

                        // 알림 채널 시스템에 등록
                        createNotificationChannel()

                        // 알림 보내기
                        sendNotification()

                    }

                }

            }

            // 실패
            override fun onCancelled(databaseError: DatabaseError) { Log.w(TAG, "getMyLikeList - loadPost:onCancelled", databaseError.toException()) }

        }

        // 파이어베이스 내 데이터의 변화(추가)를 알려줌
        FirebaseRef.userLikeRef.child(otherUid).addValueEventListener(postListener)

    }

    // 알림 채널 시스템에 등록
    private fun createNotificationChannel() {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

            val name = "name"
            val descriptionText = "descriptionText"
            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val channel = NotificationChannel("CHANNEL_ID", name, importance).apply { description = descriptionText }
            val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

            notificationManager.createNotificationChannel(channel)

        }

    }

    // 푸시 알림(매칭)
    private fun sendNotification() {

        var builder = NotificationCompat.Builder(this, "CHANNEL_ID")
            .setSmallIcon(R.drawable.ic_baseline_local_fire_department_24)
            .setContentTitle("매칭 완료")
            .setContentText("상대방도 나에게 호감이 있어요! 메시지를 보내볼까요?")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)

        with(NotificationManagerCompat.from(this)) { notify(123, builder.build()) }

    }
// MyLikeActivity.kt

        // 좋아요 목록 길게 클릭하면
        binding.myLikeListView.setOnItemLongClickListener { parent, view, position, id ->

            // 매칭된 상태인 경우 메시지 보낼 수 있음
            checkMatching(myLikeList[position].uid.toString())
            getterUid = myLikeList[position].uid.toString()
            getterToken = myLikeList[position].token.toString()

            return@setOnItemLongClickListener(true)

        }

my_like

  • 3.2. 좋아요
    • 좋아요 목록에서 내가 좋아하는 사용자를 확인합니다.
    • 매칭된 사용자는 서로 메시지를 주고받을 수 있습니다(나만 좋아요 상태면 메시지를 보낼 수 없습니다).
// MyLikeActivity.kt

    // 현재 사용자의 좋아요 리스트
    private fun getMyLikeList() {

        // 데이터베이스에서 컨텐츠의 세부정보를 검색
        val postListener = object : ValueEventListener {

            // 데이터 스냅샷
            override fun onDataChange(dataSnapshot: DataSnapshot) {

                // 데이터스냅샷 내 사용자 데이터 출력 -> 현재 사용자가 좋아하는 사용자들의 UID를 myLikeList에 넣음
                for(dataModel in dataSnapshot.children) { myLikeListUid.add(dataModel.key.toString()) }

                // 전체 사용자 정보 받아옴
                getUserDataList()

            }

            // 실패
            override fun onCancelled(databaseError: DatabaseError) { Log.w(TAG, "getMyLikeList - loadPost:onCancelled", databaseError.toException()) }

        }

        // 파이어베이스 내 데이터의 변화(추가)를 알려줌
        FirebaseRef.userLikeRef.child(uid).addValueEventListener(postListener)

    }

    // 전체 사용자 정보
    private fun getUserDataList() {

        // 데이터베이스에서 컨텐츠의 세부정보를 검색
        val postListener = object : ValueEventListener {

            // 데이터 스냅샷
            override fun onDataChange(dataSnapshot: DataSnapshot) {

                // 데이터스냅샷 내 사용자 데이터 출력
                for(dataModel in dataSnapshot.children) {

                    // 다른 사용자 정보 받아옴
                    val user = dataModel.getValue(UserDataModel::class.java)

                    // 전체 사용자 중
                    if(myLikeListUid.contains(user?.uid)) {

                        // 현재 사용자가 좋아하는 사용자의 정보만 추가
                        myLikeList.add(user!!)

                    }

                }

                // 동기화(새로고침) -> 리스트 크기 및 아이템 변화를 어댑터에 알림
                listviewAdapter.notifyDataSetChanged()
                Log.d(TAG, myLikeList.toString())

            }

            // 실패
            override fun onCancelled(databaseError: DatabaseError) { Log.w(TAG, "getUserDataList - loadPost:onCancelled", databaseError.toException()) }

        }

        // 파이어베이스 내 데이터의 변화(추가)를 알려줌
        FirebaseRef.userInfoRef.addValueEventListener(postListener)

    }
// ListViewAdapter.kt

// 좋아요 목록
class ListViewAdapter(val context : Context, private val items : MutableList<UserDataModel>) : BaseAdapter() {

    // 리스트 전체 개수
    override fun getCount(): Int = items.size

    // 리스트를 하나씩 가져옴
    override fun getItem(position: Int): Any = items[position]

    // 리스트의 ID를 가져옴
    override fun getItemId(position: Int): Long = 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.lvNick)

        // 좋아요 목록에 넣어줌
        nickname.text = items[position].nickname

        return convertView

    }

}

4. 메시지 보내기(Firebase Cloud Messaging)

push_msg1 push_msg2 push_msg3

  • 매칭된 사용자를 길게 클릭하면 메시지를 보낼 수 있는 대화창이 뜹니다.
  • 전송 버튼을 클릭하면 작성한 내용이 푸시 메시지로 전송됩니다.
  • 받은 모든 메시지는 쪽지함에서 확인할 수 있습니다.
// MyLikeActivity.kt

    // 현재 사용자와 상대방이 서로 좋아요 했는지 확인
    private fun checkMatching(otherUid : String) {

        // 데이터베이스에서 컨텐츠의 세부정보를 검색
        val postListener = object : ValueEventListener {

            // 데이터 스냅샷
            override fun onDataChange(dataSnapshot: DataSnapshot) {

                // 다른 사용자 정보 로그로 출력
                Log.d(TAG, otherUid)
                Log.e(TAG, dataSnapshot.toString())

                // 서로 좋아요 한 상태 아니면 메시지 못 보냄
                if(dataSnapshot.children.count() == 0) { Toast.makeText(this@MyLikeActivity, "메시지를 보낼 수 없습니다", Toast.LENGTH_LONG).show() }

                // 서로 좋아요 된 상태면 메시지 보냄
                else {

                    // 데이터스냅샷 내 사용자 데이터 출력
                    for (dataModel in dataSnapshot.children) {

                        // 다른 사용자가 좋아요 한 사용자 목록에
                        val likeUserKey = dataModel.key.toString()

                        // 현재 사용자가 포함돼 있으면
                        if(likeUserKey == uid) {

                            // 메시지 입력창 띄움
                            showDialog()

                        }

                    }

                }

            }

            // 실패
            override fun onCancelled(databaseError: DatabaseError) { Log.w(TAG, "checkMatching - loadPost:onCancelled", databaseError.toException()) }

        }

        // 파이어베이스 내 데이터의 변화(추가)를 알려줌
        FirebaseRef.userLikeRef.child(otherUid).addValueEventListener(postListener)

    }

    // 현재 사용자의 좋아요 리스트
    private fun getMyLikeList() {

        // 중략

    }

    // 전체 사용자 정보
    private fun getUserDataList() {

        // 중략

    }

    // 푸시 메시지
    private fun testPush(notification : PushNotification) = CoroutineScope(Dispatchers.IO).launch {

        // 레트로핏 API 이용
        RetrofitInstance.api.postNotification(notification)

    }

    // 메시지 보내는 대화창
    private fun showDialog() {

        val mDialogView = LayoutInflater.from(this).inflate(R.layout.custom_dialog, null)
        val mBuilder = AlertDialog.Builder(this)
            .setView(mDialogView)
            .setTitle("메시지 보내기")
        val mAlertDialog = mBuilder.show()

        val btn = mAlertDialog.findViewById<Button>(R.id.sendBtn)
        val text = mAlertDialog.findViewById<EditText>(R.id.sendText)

        // 메시지 보내기 버튼을 클릭하면 푸시 메시지 발송
        btn?.setOnClickListener {

            val msgText = text!!.text.toString()
            val msgModel = MsgModel(MyInfo.myNickname, msgText)

            // 파이어베이스에 메시지 업로드
            FirebaseRef.userMsgRef.child(getterUid).push().setValue(msgModel)

            val notiModel = NotiModel(MyInfo.myNickname, msgText)
            val pushModel = PushNotification(notiModel, getterToken)

            // 푸시 메시지
            testPush(pushModel)

            // 대화창
            mAlertDialog.dismiss()

        }

    }
// MyMsgActivity.kt

    // 내 메시지 불러오기
    private fun getMyMsg() {

        // 데이터베이스에서 컨텐츠의 세부정보를 검색
        val postListener = object : ValueEventListener {

            // 데이터 스냅샷
            override fun onDataChange(dataSnapshot: DataSnapshot) {

                // 중복 출력 방지 위해 메시지 목록 비워줌
                msgList.clear()

                // 데이터스냅샷 내 사용자 데이터 출력
                for(dataModel in dataSnapshot.children) {

                    // 메시지 모델에서
                    val msg = dataModel.getValue(MsgModel::class.java)
                    msgList.add(msg!!)
                    Log.d(TAG, msg.toString())

                }

                // 역순 정렬
                msgList.reverse()

                // 동기화(새로고침) -> 리스트 크기 및 아이템 변화를 어댑터에 알림
                listviewAdapter.notifyDataSetChanged()

            }

            // 실패
            override fun onCancelled(databaseError: DatabaseError) { Log.w(TAG, "getMyMsg - loadPost:onCancelled", databaseError.toException()) }

        }

        // 파이어베이스 내 데이터의 변화(추가)를 알려줌
        FirebaseRef.userMsgRef.child(FirebaseAuthUtils.getUid()).addValueEventListener(postListener)

    }
// MsgAdapter.kt

// 메시지 목록
class MsgAdapter(val context : Context, val items : MutableList<MsgModel>) : BaseAdapter() {

    // 리스트 전체 개수
    override fun getCount(): Int = items.size

    // 리스트를 하나씩 가져옴
    override fun getItem(position: Int): Any = items[position]

    // 리스트의 ID를 가져옴
    override fun getItemId(position: Int): Long = 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 nicknameArea = convertView!!.findViewById<TextView>(R.id.lvNickArea)
        val textArea = convertView.findViewById<TextView>(R.id.lvNick)

        // 받은 메시지에 넣어줌
        nicknameArea.text = items[position].senderInfo
        textArea.text = items[position].sendText

        return convertView

    }

}
// FirebaseService.kt

    // 새 토큰
    override fun onNewToken(token: String) { super.onNewToken(token) }

    // 메시지 받기
    override fun onMessageReceived(message: RemoteMessage) {

        super.onMessageReceived(message)

        val title = message.data["title"].toString()
        val body = message.data["content"].toString()

        // 알림 채널 시스템에 등록
        createNotificationChannel()

        // 알림 보내기
        sendNotification(title, body)

    }

    // 알림 채널 시스템에 등록
    private fun createNotificationChannel() {

        // API 26 이상에서만 NotificationChannel을 생성(API 25 이하는 지원하지 않음)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

            val name = "name"
            val descriptionText = "description"
            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val channel = NotificationChannel("CHANNEL_ID", name, importance).apply { description = descriptionText }
            val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

            notificationManager.createNotificationChannel(channel)

        }

    }

    // 푸시 알림(메시지)
    private fun sendNotification(title : String, body : String) {

        var builder = NotificationCompat.Builder(this, "CHANNEL_ID")
            .setSmallIcon(R.drawable.ic_baseline_local_fire_department_24)
            .setContentTitle(title)
            .setContentText(body)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)

        with(NotificationManagerCompat.from(this)) { notify(123, builder.build()) }

    }
// RetrofitInstance.kt

// 레트로핏 인스턴스
class RetrofitInstance {

    companion object {

        // (레트로핏) 객체 지연초기화
        private val retrofit by lazy {

            // 빌더
            Retrofit.Builder()

                // 통신할 서버 URL
                .baseUrl(BASE_URL)

                // 서버로부터 받아온 데이터(컨텐츠)를 원하는 타입으로 바꿈
                .addConverterFactory(GsonConverterFactory.create())

                // 빌드
                .build()

        }

        // (레트로핏) 객체 생성
        val api = retrofit.create(NotiAPI::class.java)

    }

}

DB 설계(Firebase Realtime Database)

db_all

  • DB 전체

info_table

  • userInfo(회원정보)

like_table

  • userLike(좋아요)

msg_table

  • userMsg(메시지)

피드백

문제점

  1. 메인 페이지를 제외한 나머지 화면에 액션바가 없어 현재 페이지의 기능을 알기 어려움.
  2. 좋아요 설정한 사용자를 삭제할 수 없음.

개선점

  1. 모든 페이지에 액션바 및 뒤로가기 버튼 추가(22.10.05 업데이트).
  2. 메인 페이지에 좋아요 취소 기능 추가(22.10.06 업데이트).

저작권

이 프로젝트는 MIT 라이센스에 따라 라이센스가 부여됩니다. 자세한 내용은 LICENSE.md 파일을 참조하십시오.


레퍼런스