Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 39 additions & 14 deletions src/main/kotlin/com/moa/service/FcmService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,58 @@ import org.springframework.stereotype.Service
class FcmService(
private val fcmTokenRepository: FcmTokenRepository,
) {

private val log = LoggerFactory.getLogger(javaClass)

fun send(token: String, data: Map<String, String>): Boolean {
val message = Message.builder()
fun sendEach(requests: List<Pair<String, Map<String, String>>>): List<Boolean> {
if (requests.isEmpty()) return emptyList()
val results = ArrayList<Boolean>(requests.size)
requests.chunked(MAX_BATCH_SIZE).forEach { batch ->
try {
val messages = batch.map { (token, data) -> buildMessage(token, data) }
val response = FirebaseMessaging.getInstance().sendEach(messages)
response.responses.forEachIndexed { i, sendResponse ->
if (!sendResponse.isSuccessful) {
handleFcmException(sendResponse.exception, batch[i].first)
}
results.add(sendResponse.isSuccessful)
}
} catch (ex: Exception) {
log.error("FCM batch send failed for {} messages", batch.size, ex)
repeat(batch.size) { results.add(false) }
}
}
return results
}

private fun buildMessage(token: String, data: Map<String, String>): Message =
Message.builder()
.setToken(token)
.setNotification(
Notification.builder()
.setTitle(data["title"])
.setBody(data["body"])
.build()
)
.putAllData(data)
.build()

return try {
FirebaseMessaging.getInstance().send(message)
true
} catch (ex: FirebaseMessagingException) {
handleFcmException(ex, token)
false
private fun handleFcmException(ex: Exception?, token: String) {
val fcmEx = ex as? FirebaseMessagingException ?: run {
log.error("FCM 전송 중 예상치 못한 예외: {}", token, ex)
return
}
}

private fun handleFcmException(ex: FirebaseMessagingException, token: String) {
when (ex.messagingErrorCode) {
when (fcmEx.messagingErrorCode) {
MessagingErrorCode.UNREGISTERED,
MessagingErrorCode.INVALID_ARGUMENT -> {
log.warn("유효하지 않은 FCM 토큰 삭제: {}", token)
fcmTokenRepository.deleteByToken(token)
}

else -> log.error("FCM 전송 실패", ex)
else -> log.error("FCM 전송 실패: {}", token, fcmEx)
}
}

companion object {
private const val MAX_BATCH_SIZE = 500
}
}
5 changes: 4 additions & 1 deletion src/main/kotlin/com/moa/service/WorkdayService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ class WorkdayService(
} else {
val policy = workPolicyVersionRepository
.findTopByMemberIdAndEffectiveFromLessThanEqualOrderByEffectiveFromDesc(memberId, date)
?: throw NotFoundException()
?: workPolicyVersionRepository.findTopByMemberIdAndEffectiveFromLessThanEqualOrderByEffectiveFromDesc(
memberId,
LocalDate.now()
) ?: throw NotFoundException()

// 요청값이 하나라도 비어있으면 정책의 기본 시간을 할당
policy.clockInTime to policy.clockOutTime
Expand Down
Original file line number Diff line number Diff line change
@@ -1,62 +1,66 @@
package com.moa.service.notification

import com.moa.entity.NotificationLog
import com.moa.entity.NotificationStatus
import com.moa.repository.FcmTokenRepository
import com.moa.repository.NotificationLogRepository
import com.moa.service.FcmService
import com.moa.service.FcmTokenService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate
import java.time.LocalTime

@Service
class NotificationDispatchService(
private val notificationLogRepository: NotificationLogRepository,
private val fcmTokenService: FcmTokenService,
private val fcmTokenRepository: FcmTokenRepository,
private val fcmService: FcmService,
private val notificationMessageBuilder: NotificationMessageBuilder,
) {

private val log = LoggerFactory.getLogger(javaClass)

@Transactional
fun processNotifications(date: LocalDate, currentTime: LocalTime) {
val pendingLogs = notificationLogRepository
.findAllByScheduledDateAndScheduledTimeLessThanEqualAndStatus(
scheduledDate = date,
scheduledTime = currentTime,
status = NotificationStatus.PENDING,
)

if (pendingLogs.isEmpty()) return

log.info("Dispatching {} pending notifications", pendingLogs.size)

for (notification in pendingLogs) {
val tokens = fcmTokenService.getTokensByMemberId(notification.memberId)
val memberIds = pendingLogs.map { it.memberId }.distinct()
val tokensByMemberId = fcmTokenRepository.findAllByMemberIdIn(memberIds)
.groupBy { it.memberId }

data class DispatchItem(
val notification: NotificationLog,
val token: String,
val data: Map<String, String>,
)

val dispatchItems = mutableListOf<DispatchItem>()
for (notification in pendingLogs) {
val tokens = tokensByMemberId[notification.memberId].orEmpty()
if (tokens.isEmpty()) {
notification.status = NotificationStatus.FAILED
log.warn("No FCM tokens for member {}, marking as FAILED", notification.memberId)
continue
}
val data = notificationMessageBuilder.buildMessage(notification).toData()
tokens.forEach { dispatchItems.add(DispatchItem(notification, it.token, data)) }
}

val message = notificationMessageBuilder.buildMessage(notification)
val data = mapOf(
"title" to message.title,
"body" to message.body,
"type" to message.type.name,
)

var anySuccess = false
for (token in tokens) {
if (fcmService.send(token.token, data)) {
anySuccess = true
}
if (dispatchItems.isNotEmpty()) {
val results = fcmService.sendEach(dispatchItems.map { it.token to it.data })
results.forEachIndexed { i, success ->
if (success) dispatchItems[i].notification.status = NotificationStatus.SENT
}

notification.status = if (anySuccess) NotificationStatus.SENT else NotificationStatus.FAILED
pendingLogs.filter { it.status == NotificationStatus.PENDING }
.forEach { it.status = NotificationStatus.FAILED }
}

notificationLogRepository.saveAll(pendingLogs)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import com.moa.entity.DailyWorkScheduleType
import com.moa.entity.NotificationLog
import com.moa.entity.NotificationType
import com.moa.repository.DailyWorkScheduleRepository
import com.moa.repository.ProfileRepository
import com.moa.repository.WorkPolicyVersionRepository
import com.moa.service.EarningsCalculator
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.text.NumberFormat
Expand All @@ -17,12 +15,10 @@ import java.util.*

@Service
class NotificationMessageBuilder(
private val profileRepository: ProfileRepository,
private val workPolicyVersionRepository: WorkPolicyVersionRepository,
private val dailyWorkScheduleRepository: DailyWorkScheduleRepository,
private val earningsCalculator: EarningsCalculator,
) {
private val log = LoggerFactory.getLogger(javaClass)

fun buildMessage(notification: NotificationLog): NotificationMessage {
val title = notification.notificationType.title
Expand Down Expand Up @@ -66,4 +62,10 @@ class NotificationMessageBuilder(
}
}

data class NotificationMessage(val title: String, val body: String, val type: NotificationType)
data class NotificationMessage(val title: String, val body: String, val type: NotificationType) {
fun toData(): Map<String, String> = mapOf(
"title" to title,
"body" to body,
"type" to type.name,
)
}
Loading