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, 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() 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] ❌ 최종 실패: 알 수 없는 오류") } } } } }