sodalive-backend-spring-boot/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt

171 lines
5.7 KiB
Kotlin

package kr.co.vividnext.sodalive.fcm
import com.google.firebase.messaging.AndroidConfig
import com.google.firebase.messaging.ApnsConfig
import com.google.firebase.messaging.Aps
import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.FirebaseMessagingException
import com.google.firebase.messaging.Message
import com.google.firebase.messaging.MessagingErrorCode
import com.google.firebase.messaging.MulticastMessage
import com.google.firebase.messaging.Notification
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Service
@Service
class FcmService {
private val logger = LoggerFactory.getLogger(this::class.java)
@Async
fun send(
tokens: List<String>,
title: String,
message: String,
container: String,
roomId: Long? = null,
messageId: Long? = null,
contentId: Long? = null,
creatorId: Long? = null,
auditionId: Long? = null
) {
if (tokens.isEmpty()) return
logger.info("os: $container")
var targets = tokens
val maxAttempts = 3
var attempt = 1
while (attempt <= maxAttempts && targets.isNotEmpty()) {
val multicastMessage = MulticastMessage.builder()
.addAllTokens(tokens)
multicastMessage.setAndroidConfig(
AndroidConfig.builder()
.setPriority(AndroidConfig.Priority.HIGH)
.build()
)
multicastMessage.setApnsConfig(
ApnsConfig.builder()
.setAps(
Aps.builder()
.setSound("default")
.build()
)
.build()
)
if (container == "ios") {
multicastMessage
.setNotification(
Notification.builder()
.setTitle(title)
.setBody(message)
.build()
)
} else {
multicastMessage
.putData("title", title)
.putData("message", message)
}
if (roomId != null) {
multicastMessage.putData("room_id", roomId.toString())
}
if (messageId != null) {
multicastMessage.putData("message_id", messageId.toString())
}
if (contentId != null) {
multicastMessage.putData("content_id", contentId.toString())
}
if (creatorId != null) {
multicastMessage.putData("channel_id", creatorId.toString())
}
if (auditionId != null) {
multicastMessage.putData("audition_id", auditionId.toString())
}
val response = FirebaseMessaging.getInstance().sendEachForMulticast(multicastMessage.build())
val failedTokens = mutableListOf<String>()
response.responses.forEachIndexed { index, res ->
if (!res.isSuccessful) {
val exception = res.exception
val token = targets[index]
if (exception?.messagingErrorCode == MessagingErrorCode.UNREGISTERED) {
logger.error("[FCM] ❌ UNREGISTERED → $token")
// 필요 시 DB에서 삭제
} else {
logger.error("[FCM] ❌ 실패: $token / ${exception?.messagingErrorCode}")
failedTokens.add(token)
}
}
}
if (failedTokens.isEmpty()) {
logger.info("[FCM] ✅ 전체 전송 성공")
return
}
targets = failedTokens
attempt++
}
if (targets.isNotEmpty()) {
logger.error("[FCM] ❌ 최종 실패 대상 ${targets.size}명 → $targets")
}
}
fun sendPointGranted(token: String, point: Int) {
val data = mapOf(
"type" to "POINT_GRANTED",
"point" to point.toString(),
"message" to "${point}포인트가 지급되었습니다!"
)
var attempts = 0
val maxAttempts = 3
while (attempts < maxAttempts) {
try {
val message = Message.builder()
.setToken(token)
.putAllData(data)
.build()
val response = FirebaseMessaging.getInstance().send(message)
logger.info("[FCM] ✅ 성공 (attempt ${attempts + 1}): messageId=$response")
return // 성공 시 즉시 종료
} catch (e: FirebaseMessagingException) {
attempts++
// "registration-token-not-registered" 예외 코드 확인
if (e.messagingErrorCode == MessagingErrorCode.UNREGISTERED) {
logger.error("[FCM] ❌ 실패: 토큰이 등록되지 않음 (등록 해제됨) → 재시도 안함")
return
}
logger.error("[FCM] ❌ 실패 (attempt $attempts): ${e.errorCode} - ${e.message}")
if (attempts >= maxAttempts) {
logger.error("[FCM] ❌ 최종 실패: 전송 불가")
}
} catch (e: Exception) {
// Firebase 이외의 예외도 잡기
attempts++
logger.error("[FCM] ❌ 실패 (attempt $attempts): ${e.message}")
if (attempts >= maxAttempts) {
logger.error("[FCM] ❌ 최종 실패: 알 수 없는 오류")
}
}
}
}
}