채팅 - 메시지 PUSH 알림

변현섭·2023년 9월 11일
0

지난 포스팅에 이어서 새로운 메시지를 사용자에게 알려주는 기능을 더 추가해보도록 하겠습니다. 채팅방 목록에서 각 채팅방의 마지막 메시지를 보여주고, 새로운 메시지가 도착했다는 푸시알림도 전송해보겠습니다.

1. 채팅방 목록에서 마지막 메시지 보여주기

1) 백엔드

① ChatRoomController에 아래의 API를 추가한다.

  • 기존에는 닉네임 리스트를 GetChatRoomRes에 실어서 보냈지만, 이제부터는 따로 API로 만들어주었다.
// 채팅방에 참여한 유저의 닉네임 List를 String으로 반환
@GetMapping("/userList/{roomId}")
public BaseResponse<String> getUserStrList(@PathVariable String roomId) {
    try {
        return new BaseResponse<>(chatRoomService.getUserStrList(roomId));
    } catch (BaseException exception) {
        return new BaseResponse<>(exception.getStatus());
    }
}

② 이제 GetChatRoomRes에서도 nickNameList를 반환할 필요가 없다.

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class GetChatRoomRes {
    private String chatRoomId;
    private String roomName;
}

2) 프론트엔드

① ChatRoom에서 userList는 지우고, lastMessage 필드를 추가한다.

data class ChatRoom(
    val chatRoomId : String? = null,
    val roomName : String? = null,
    val unreadCount : Int? = 0,
    val lastMessage : String = ""
)

② ChatListFragment의 getUnreadMessageCount 메서드에서 마지막 메시지를 가져와 기존의 userList가 있던 자리에 띄워주도록 하겠다.

private fun getUnreadMessageCount(chatRoomId : String) {
    val postListener = object : ValueEventListener {
        override fun onDataChange(dataSnapshot: DataSnapshot) {
            var count = 0
            var lastMessage = ""
            for (datamModel in dataSnapshot.children) {
                val uidList = datamModel.child("readerUids").getValue(object : GenericTypeIndicator<MutableMap<String, Boolean>>() {})
                if (uidList != null) {
                    Log.d("readerUids", uidList.toString())
                    if (!uidList.containsKey(FirebaseAuthUtils.getUid())) {
                        // readerUid에 내 uid가 없으면
                        count++
                    }
                }
                val lastDataModel = datamModel.getValue(MessageModel::class.java)
                if(lastDataModel != null) {
                    lastMessage = lastDataModel.contents
                }
            }
            FirebaseRef.chatRoom.child(FirebaseAuthUtils.getUid()).child(chatRoomId).child("unreadCount").setValue(count)
            FirebaseRef.chatRoom.child(FirebaseAuthUtils.getUid()).child(chatRoomId).child("lastMessage").setValue(lastMessage)
        }
        override fun onCancelled(databaseError: DatabaseError) {
            Log.w("MyMessage", "onCancelled", databaseError.toException())
        }
    }
    FirebaseRef.message.child(chatRoomId).addValueEventListener(postListener)
}

③ chat_listview_item.xml 파일의 기존 User List TextView를 아래와 같이 수정한다.

<TextView
    android:id="@+id/lvLastMessageArea"
    android:text="Last Message"
    android:textSize="15sp"
    android:layout_marginHorizontal="40dp"
    android:layout_marginBottom="20dp"
    android:maxWidth="170dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

④ ChatRoomAdapter를 아래와 같이 수정한다.

class ChatRoomAdapter(private val context: Context, private val dataList : List<ChatRoom>) : 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(context).inflate(R.layout.chat_listview_item, parent, false)
        }
        val listViewChatRoomName = convertView?.findViewById<TextView>(R.id.lvChatRoomName)
        val listViewLastMessage = convertView?.findViewById<TextView>(R.id.lvLastMessageArea)
        val listViewUnreadCount = convertView?.findViewById<TextView>(R.id.unreadMessageCountTextView)
        val unreadMessageContainer = convertView?.findViewById<FrameLayout>(R.id.unreadMessageContainer)

        listViewUnreadCount!!.text = dataList[position].unreadCount.toString()
        val unreadCount = listViewUnreadCount!!.text.toString().toInt()

        if (unreadCount > 0) {
            unreadMessageContainer!!.visibility = View.VISIBLE
            listViewUnreadCount.visibility = View.VISIBLE
        } else {
            unreadMessageContainer!!.visibility = View.GONE
            listViewUnreadCount.visibility = View.GONE
        }

        listViewChatRoomName!!.text = dataList[position].roomName
        listViewLastMessage!!.text = dataList[position].lastMessage

        return convertView!!
    }
}

⑤ 이외의 기존 user list와 관련된 모든 내용을 삭제한다.

⑥ ChatApi에 아래의 API를 추가한다.

@GET("/chat/userList/{roomId}")
suspend fun getUserStrList(@Path("roomId") roomId : String) : BaseResponse<String>

⑦ ChatRoomActivity에 getUserStrList를 추가하고 Coroutine을 이용해 호출한다.

override fun onCreate(savedInstanceState: Bundle?) {
	...
	CoroutineScope(Dispatchers.IO).launch {
	    val response = getUserCount(chatRoomId!!)
	    Log.d("userCount", response.toString())
	    if (response.isSuccess) {
	        count = response.result.toString()
	        userCount.text = count
	    } else {
	        Log.d("UserListFragment", "유저의 정보를 불러오지 못함")
	    }
	}
	...

	private suspend fun getUserStrList(roomId: String): BaseResponse<String> {
	    return RetrofitInstance.chatApi.getUserStrList(roomId)
	}
	...

이제 코드를 실행해보자. 채팅방 목록에는 마지막 메시지가 보여야 하고, 채팅방에 입장했을 때에는 여전히 유저 수와 유저의 닉네임 리스트가 보여야 한다.

2. Push 알림 보내기

1) 백엔드

푸시 알림을 사용하려면, device token이 필요하다. 현재 device token은 파이어베이스에만 저장되어 있고, 서버에서는 저장하고 있지 않다. 단체 채팅의 경우 채팅방에 속해 있는 모든 유저의 device token 목록이 필요하므로, 서버에서 device token을 저장해두었다가 채팅방에 참여한 유저의 디바이스 토큰을 List로 반환해주기로 하자.

① User에 deviceToken 필드를 nullable로 추가한다.

@Column(nullable = true) 
private String deviceToken;

② User의 createUser메서드도 아래와 같이 수정한다.

public User createUser(String nickName, String email, String password, String uid) {
    this.nickName= nickName;
    this.email = email;
    this.password = password;
    this.uid = uid;
    this.deviceToken = null;
    return this;
}

③ UserController에 유저의 디바이스 토큰을 set하는 API를 추가한다.

/**
 * 디바이스 토큰 저장
 */
@PostMapping("/device-token")
public BaseResponse<String> saveDeviceToken(@RequestBody PostDeviceTokenReq postDeviceTokenReq) {
    try {
        return new BaseResponse<>(userService.saveDeviceToken(postDeviceTokenReq));
    } catch (BaseException exception) {
        return new BaseResponse<>(exception.getStatus());
    }
}

④ UserService에 아래의 메서드를 추가한다.

/**
 * 디바이스 토큰 저장
 */
public String saveDeviceToken(PostDeviceTokenReq postDeviceTokenReq) throws BaseException {
    User user = utilService.findByUserUidWithValidation(postDeviceTokenReq.getUid());
    user.setDeviceToken(postDeviceTokenReq.getDeviceToken());
    userRepository.save(user);
    return "디바이스 토큰이 저장되었습니다.";
}

⑤ User > dto에 PostDeviceTokenReq를 추가한다.

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class PostDeviceTokenReq {
    private String uid;
    private String deviceToken;
}

⑥ ChatRoomController에 채팅방에 참여한 유저의 디바이스 토큰 목록을 반환하는 API를 추가한다.

  • 이 때 메시지를 보내는 사람에게는 푸시 알림을 보내면 안되기 때문에 본인의 디바이스 토큰은 리스트에서 제외시킨다.
// 채팅에 참여한 유저 중 본인을 제외한 유저의 디바이스 토큰 목록 반환
@GetMapping("/tokenList/{roomId}")
public BaseResponse<List<String>> getTokenList(@PathVariable String roomId) {
    try {
        Long userId = jwtService.getUserIdx();
        return new BaseResponse<>(chatRoomService.getTokenList(userId, roomId));
    } catch (BaseException exception) {
        return new BaseResponse<>(exception.getStatus());
    }
}

⑦ ChatRoomService에 아래의 메서드를 추가한다.

// 채팅에 참여한 유저 중 본인을 제외한 유저의 디바이스 토큰 목록 반환
public List<String> getTokenList(Long userId, String chatRoomId) throws BaseException {
    User user = utilService.findByUserIdWithValidation(userId);
    utilService.findChatRoomByChatRoomIdWithValidation(chatRoomId);
    List<UserChatRoom> userChatRooms = userChatRoomRepository.findUserChatRoomByRoomId(chatRoomId);
    String userToken = user.getDeviceToken(); // Get user's token
    List<String> tokenList = userChatRooms.stream()
            .map(userChatRoom -> userChatRoom.getUser().getDeviceToken())
            .filter(token -> token != null && !token.equals(userToken)) // Filter out null tokens and user's token
            .collect(Collectors.toList());
    return tokenList;
}

2) 프론트엔드

① push라는 이름의 새로운 패키지를 생성하고 이 패키지 하위로, FirebaseService라는 이름의 kotlin class를 생성한다.

② AndroidManifest.xml 파일에 아래의 내용을 추가한다.

<application
	...
    <service
    	android:name=".utils.FirebaseService"
    	android:exported="false">
    	<intent-filter>
        	<action android:name="com.google.firebase.MESSAGING_EVENT" />
    	</intent-filter>
	</service>
 
    <activity
    	...

③ FirebaseService에 아래의 내용을 입력한다.

class FirebaseService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        super.onNewToken(token)
    }

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)

        val title = message.data["title"].toString()
        val content = message.data["content"].toString()
        createNotificationChannel()
        sendNotification(title, content)
    }

    private fun createNotificationChannel() {
        // Create the NotificationChannel, but only on API 26+ because
        // the NotificationChannel class is new and not in the support library
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = "name"
            val descriptionText = "description"
            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val channel = NotificationChannel("chatting message", name, importance).apply {
                description = descriptionText
            }
            // Register the channel with the system
            val notificationManager: NotificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(channel)
        }
    }

    private fun sendNotification(title : String, body : String) {
        if(NotificationManagerCompat.from(this).areNotificationsEnabled()) {
            var builder = NotificationCompat.Builder(this, "test")
                .setSmallIcon(R.drawable.icon)
                .setContentTitle(title)
                .setContentText(body)
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                .setAutoCancel(true)
            with(NotificationManagerCompat.from(this)) {
                if (ActivityCompat.checkSelfPermission(
                        this@FirebaseService,
                        Manifest.permission.POST_NOTIFICATIONS
                    ) != PackageManager.PERMISSION_GRANTED
                ) {
                    return
                }
                notify(1, builder.build())
            }
        }
        else {
            Log.w("notification", "알림 수신이 차단된 상태입니다.")
        }
    }
}

④ push 패키지 하위로, PushRepository라는 이름의 kotlin class를 생성한다.

class PushRepository {
    companion object {
        const val BASE_URL = "https://fcm.googleapis.com"
        const val SERVER_KEY = "{Server-Key}"
        const val CONTENT_TYPE = "application/json"
    }
}

⑤ 이번에는 push 패키지 하위로, NoticeApi 인터페이스, NoticeModel, PushNotice라는 이름의 kotlin 클래스를 추가한다.

  • NoticeApi
interface NoticeApi {
    @Headers("Authorization: key=${PushRepository.SERVER_KEY}", "Content-Type:${PushRepository.CONTENT_TYPE}")
    @POST("fcm/send")
    suspend fun postNotification(@Body notification: PushNotice) : retrofit2.Response<ResponseBody>
}
  • NoticeModel
data class NoticeModel (
    val title : String = "",
    val content : String = ""
)
  • PushNotice
data class PushNotice (
    val data : NoticeModel,
    val to : String
)

⑥ ChatApi에 아래의 API를 추가한다.

@GET("/chat/tokenList/{roomId}")
suspend fun getTokenList(
    @Header("Authorization") accessToken : String,
    @Path("roomId") roomId : String
) : BaseResponse<List<String>>

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

class RetrofitInstance {
    companion object {
        private val retrofit by lazy {
            Retrofit.Builder()
                .baseUrl(ApiRepository.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
        }
        val userApi = retrofit.create(UserApi::class.java)
        val myPageApi = retrofit.create(MyPageApi::class.java)
        val chatApi = retrofit.create(ChatApi::class.java)

        private val noticeRetrofit by lazy {
            Retrofit.Builder()
                .baseUrl(PushRepository.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
        }
        val noticeApi = noticeRetrofit.create(NoticeApi::class.java)
    }
}

⑧ ChatRoomActivity를 아래와 같이 수정한다.

  • tokenList를 빈 리스트로 선언한다.
  • access token을 이용해 본인을 제외한 유저의 디바이스 토큰을 받아 push 알림을 전송한다.
class ChatRoomActivity : AppCompatActivity() {

    lateinit var count : String // 채팅방 참여 인원수
    lateinit var messageAdapter : MessageAdapter
    lateinit var recyclerView : RecyclerView
    val messageList = mutableListOf<MessageModel>()
    var tokenList = listOf<String>() // 채팅 참여자의 토큰 목록
    val readerUidMap = mutableMapOf<String, Boolean>()

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_chat_room)

        readerUidMap[FirebaseAuthUtils.getUid()] = true

        val roomName = findViewById<TextView>(R.id.chatRoomName)
        val nickNameList = findViewById<TextView>(R.id.nickNameList)
        val inviteBtn = findViewById<Button>(R.id.invite)
        val userCount = findViewById<TextView>(R.id.userCount)

        // Intent로부터 데이터를 가져옴
        val chatRoomName = intent.getStringExtra("chatRoomName")
        roomName.text = chatRoomName

        val roomId = intent.getStringExtra("chatRoomId")
        val chatRoomId = roomId

        CoroutineScope(Dispatchers.IO).launch {
            val response = getUserCount(chatRoomId!!)
            Log.d("userCount", response.toString())
            if (response.isSuccess) {
                count = response.result.toString()
                userCount.text = count
            } else {
                Log.d("UserListFragment", "유저의 정보를 불러오지 못함")
            }
        }

        CoroutineScope(Dispatchers.IO).launch {
            val response = getUserStrList(chatRoomId!!)
            Log.d("UserNickNameList", response.toString())
            if (response.isSuccess) {
                nickNameList.text = response.result.toString()
            } else {
                Log.d("UserNickNameList", "유저의 정보를 불러오지 못함")
            }
        }

        getAccessToken { accessToken ->
            if (accessToken.isNotEmpty()) {
                CoroutineScope(Dispatchers.IO).launch {
                    val response = getTokenList(chatRoomId!!)
                    if (response.isSuccess) {
                        tokenList = response.result!!
                        Log.d("TokenList", response.toString())
                    } else {
                        Log.d("TokenList", "유저의 정보를 불러오지 못함")
                    }
                }
            } else {
                Log.e("TokenList", "Invalid Token")
            }
        }

        recyclerView = findViewById(R.id.messageRV)
        messageAdapter = MessageAdapter(this, messageList)
        recyclerView.adapter = messageAdapter
        recyclerView.layoutManager = LinearLayoutManager(this)

        getMessageList(chatRoomId!!)
        Log.d("MessageList", messageList.toString())

        inviteBtn.setOnClickListener {
            val intent = Intent(this, InviteActivity::class.java)
            intent.putExtra("chatRoomId", chatRoomId)
            intent.putExtra("chatRoomName", chatRoomName)
            startActivity(intent)
        }

        val participantBtn = findViewById<ImageView>(R.id.participants)
        participantBtn.setOnClickListener {
            val intent = Intent(this, ParticipantsActivity::class.java)
            intent.putExtra("chatRoomId", chatRoomId)
            startActivity(intent)
        }

        val message = findViewById<TextInputEditText>(R.id.message)
        val sendBtn = findViewById<ImageView>(R.id.send)

        lateinit var myNickName : String
        lateinit var myProfileUrl : String
        lateinit var messageModel: MessageModel
        val myUid = FirebaseAuthUtils.getUid()

        sendBtn.setOnClickListener {
            val contents = message.text.toString()
            if(contents.isEmpty()) {
                Toast.makeText(this, "메시지를 입력해주세요", Toast.LENGTH_SHORT).show()
            }
            else {
                val sendTime = getSendTime()
                CoroutineScope(Dispatchers.IO).launch {
                    val response = getUserInfo(myUid)
                    if (response.isSuccess) {
                        myNickName = response.result?.nickName.toString()
                        myProfileUrl = response.result?.imgUrl.toString()
                        val readerUids = mutableMapOf<String, Boolean>()
                        messageModel = MessageModel(myUid, myNickName, myProfileUrl, contents,
                            sendTime, readerUids, 0)
                        Log.d("readerUid", readerUids.size.toString())
                        FirebaseRef.message.child(chatRoomId!!).push().setValue(messageModel)
                        val noticeModel = NoticeModel(myNickName, contents)
                        for(token in tokenList) { // 채팅방의 모든 유저에게 채팅 푸시알림을 전송
                            val pushNotice = PushNotice(noticeModel, token)
                            Log.d("Push", pushNotice.toString())
                            Log.d("Push", tokenList.toString())
                            createNotificationChannel()
                            pushNotification(pushNotice)
                        }
                    } else {
                        Log.d("ChatRoomActivity", "유저의 정보를 불러오지 못함")
                    }
                }
                message.text?.clear()
            }
        }
    }

    override fun onBackPressed() {
        readerUidMap.remove(FirebaseAuthUtils.getUid())
        val intent = Intent(this, MainActivity::class.java)
        startActivity(intent)
    }

    private suspend fun getUserCount(roomId: String) : BaseResponse<String> {
        return RetrofitInstance.chatApi.getUserCount(roomId)
    }

    private suspend fun getUserInfo(uid: String): BaseResponse<GetUserRes> {
        return RetrofitInstance.myPageApi.getUserInfo(uid)
    }

    //메시지 보낸 시각 정보 반환
    @RequiresApi(Build.VERSION_CODES.O)
    private fun getSendTime(): String {
        try {
            val localDateTime = LocalDateTime.now()
            val dateTimeFormatter = DateTimeFormatter.ofPattern("M/d  a  h:mm")
            return localDateTime.format(dateTimeFormatter)
        } catch (e: Exception) {
            e.printStackTrace()
            throw Exception("시간 정보를 불러오지 못함")
        }
    }

    private fun getMessageList(chatRoomId: String) {
        val postListener = object : ValueEventListener {
            override fun onDataChange(dataSnapshot: DataSnapshot) {
                messageList.clear()
                val newMessages = mutableListOf<MessageModel>() // 새로운 메시지를 저장할 리스트 생성

                for (dataModel in dataSnapshot.children) {
                    val messageModel = dataModel.getValue(MessageModel::class.java)
                    val messageId = dataModel.key
                    if (messageModel != null) {
                        if (messageId != null) {
                            // readerUidMap을 업데이트
                            FirebaseRef.message.child(chatRoomId!!).child(messageId).child("readerUids")
                                .updateChildren(readerUidMap as Map<String, Boolean>)
                                .addOnCompleteListener { readerUidTask ->
                                    if (readerUidTask.isSuccessful) {
                                        // readerUid 업데이트가 성공한 경우 unreadUserCount를 계산하여 업데이트합니다.
                                        val unreadUserCount = count.toInt() - messageModel.readerUids.size
                                        FirebaseRef.message.child(chatRoomId!!).child(messageId).child("unreadUserCount")
                                            .setValue(unreadUserCount)
                                    } else {
                                        Log.d("ChatRoomActivity", "reader UID를 업데이트하지 못함")
                                    }
                                }
                        }

                        if (messageModel.senderUid != FirebaseAuthUtils.getUid()) {
                            messageModel.viewType = MessageModel.VIEW_TYPE_YOU
                            if(!readerUidMap.containsKey(FirebaseAuthUtils.getUid())) {

                            }
                        }
                        newMessages.add(messageModel)
                    }
                }

                messageList.addAll(newMessages)
                messageAdapter.notifyDataSetChanged()
                Log.d("MessageList", messageList.toString())

                recyclerView.post {
                    recyclerView.scrollToPosition(recyclerView.adapter?.itemCount?.minus(1) ?: 0)
                }
            }

            override fun onCancelled(databaseError: DatabaseError) {
                Log.w("MyMessage", "onCancelled", databaseError.toException())
            }
        }
        FirebaseRef.message.child(chatRoomId).addValueEventListener(postListener)
    }

    private suspend fun getUserStrList(roomId: String): BaseResponse<String> {
        return RetrofitInstance.chatApi.getUserStrList(roomId)
    }

    private suspend fun getTokenList(roomId: String): BaseResponse<List<String>> {
        return RetrofitInstance.chatApi.getTokenList(roomId)
    }

    private fun createNotificationChannel() {
        // Create the NotificationChannel, but only on API 26+ because
        // the NotificationChannel class is new and not in the support library
        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)
        }
    }

    private fun pushNotification(notification: PushNotice) = CoroutineScope(Dispatchers.IO).launch {
        RetrofitInstance.noticeApi.postNotification(notification)
    }

    private fun getAccessToken(callback: (String) -> Unit) {
        val postListener = object : ValueEventListener {
            override fun onDataChange(dataSnapshot: DataSnapshot) {
                val data = dataSnapshot.getValue(com.chrome.chattingapp.authentication.UserInfo::class.java)
                val accessToken = data?.accessToken ?: ""
                callback(accessToken)
            }

            override fun onCancelled(databaseError: DatabaseError) {
                Log.w("NickNameActivity", "onCancelled", databaseError.toException())
            }
        }

        FirebaseRef.userInfo.child(FirebaseAuthUtils.getUid()).addListenerForSingleValueEvent(postListener)
    }
}

⑨ UserApi에 아래의 API를 추가한다.

@POST("users/device-token")
suspend fun saveDeviceToken(@Body postDeviceTokenReq : PostDeviceTokenReq) : BaseResponse<String>

⑩ api > dto에 PostDeviceTokenReq data class를 추가한다.

data class PostDeviceTokenReq(
    @SerializedName("uid")
    val uid : String,

    @SerializedName("deviceToken")
    val deviceToken : String,
)

⑪ LoginActivity에서 device token을 저장하는 API를 호출한다.

else {
    auth.signInWithEmailAndPassword(email.text.toString(), password.text.toString())
        .addOnCompleteListener(this) { task ->
            if (task.isSuccessful) {
                    CoroutineScope(Dispatchers.IO).launch {
                        val response = loginUser(postLoginReq)
                        Log.d("LoginActivity", response.toString())
                        if (response.isSuccess) {
                            FirebaseMessaging.getInstance().token.addOnCompleteListener(
                                OnCompleteListener { task ->
                                    if (!task.isSuccessful) {
                                        Log.w("MyToken", "Fetching FCM registration token failed", task.e
                                        return@OnCompleteListener
                                    }
                                    val deviceToken = task.result
                                    val userInfo = UserInfo(uid, response.result?.userId,
                                        deviceToken, response.result?.accessToken, response.result?.refre
                                    Log.d("userInfo", userInfo.toString())
                                    FirebaseRef.userInfo.child(uid).setValue(userInfo)
                                    CoroutineScope(Dispatchers.IO).launch {
                                        val postDeviceTokenReq = PostDeviceTokenReq(uid, deviceToken)
                                        val response = saveDeviceToken(postDeviceTokenReq)
                                        Log.d("DeviceToken", response.toString())
                                        if (response.isSuccess) {
                                            Log.d("DeviceToken", "디바이스 토큰 저장 완료")
                                        } else {
                                            Log.d("DeviceToken", "디바이스 토큰 저장 실패")
                                        }
                                    }
                                    val intent = Intent(this@LoginActivity, MainActivity::class.java)
                                    startActivity(intent)
                                })
                            Log.d("LoginActivity", "로그인 완료")
                    } else {
                        // 로그인 실패 처리
                        Log.d("LoginActivity", "로그인 실패")
                        val message = response.message
                        Log.d("JoinActivity", message)
                        withContext(Dispatchers.Main) {
                            Toast.makeText(this@LoginActivity, message, Toast.LENGTH_SHORT).show()
                        }
                    }
                }
            } else {
                Log.d("LoginActivity", "로그인 실패")
                Toast.makeText(this@LoginActivity, "이메일 또는 비밀번호를 확인해주세요", Toast.LENGTH_SHORT).show()
            }
        }
}
...
private suspend fun saveDeviceToken(postDeviceTokenReq: PostDeviceTokenReq): BaseResponse<String> {
    return RetrofitInstance.userApi.saveDeviceToken(postDeviceTokenReq)
}

⑫ 마지막으로, AndroidManifest.xml에 아래의 권한을 추가한다.

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

코드를 실행시켜보면, 새로운 메시지가 도착할 때마다 푸시 알림이 도착하는 것을 확인할 수 있을 것이다.

다만, 채팅방에 입장한 상태이더라도 푸시 알림이 계속해서 간다는 문제가 있다. 이 문제에 대한 해결방법에는 다소 복잡한 로직이 필요하므로, 다음 포스팅에서 다루기로 하겠다.

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

0개의 댓글