ScheduledSmsSender.java
package com.deveagles.be15_deveagles_be.features.messages.command.application.service;
import com.deveagles.be15_deveagles_be.common.exception.BusinessException;
import com.deveagles.be15_deveagles_be.common.exception.ErrorCode;
import com.deveagles.be15_deveagles_be.features.customers.query.service.CustomerQueryService;
import com.deveagles.be15_deveagles_be.features.messages.command.application.dto.SmsSendUnit;
import com.deveagles.be15_deveagles_be.features.messages.command.application.dto.response.MessageSendResult;
import com.deveagles.be15_deveagles_be.features.messages.command.domain.aggregate.MessageDeliveryStatus;
import com.deveagles.be15_deveagles_be.features.messages.command.domain.aggregate.MessageSendingType;
import com.deveagles.be15_deveagles_be.features.messages.command.domain.aggregate.MessageSettings;
import com.deveagles.be15_deveagles_be.features.messages.command.domain.aggregate.Sms;
import com.deveagles.be15_deveagles_be.features.messages.command.domain.repository.MessageSettingRepository;
import com.deveagles.be15_deveagles_be.features.messages.command.domain.repository.SmsRepository;
import com.deveagles.be15_deveagles_be.features.messages.command.infrastructure.CoolSmsClient;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class ScheduledSmsSender {
private final SmsRepository smsRepository;
private final CustomerQueryService customerQueryService;
private final MessageSettingRepository messageSettingRepository;
private final CoolSmsClient coolSmsClient;
private final MessageCommandService messageCommandService;
@Scheduled(fixedDelay = 60000) // 이전 작업이 끝난 후 60초 뒤에 실행
public void sendScheduledMessages() {
log.info("✅ 예약 메시지 스케줄러 실행됨 - {}", LocalDateTime.now());
LocalDateTime now = LocalDateTime.now().withSecond(0).withNano(0);
// 1. 현재 시간에 예약된 메시지 조회
List<Sms> scheduledMessages =
smsRepository
.findAllByMessageSendingTypeAndScheduledAtLessThanEqualAndMessageDeliveryStatus(
MessageSendingType.RESERVATION, now, MessageDeliveryStatus.PENDING);
if (scheduledMessages.isEmpty()) return;
// 2. 고객 전화번호 매핑
List<Long> customerIds = scheduledMessages.stream().map(Sms::getCustomerId).distinct().toList();
List<String> phoneNumbers = customerQueryService.getCustomerPhoneNumbers(customerIds);
Map<Long, String> phoneNumberMap = new HashMap<>();
for (int i = 0; i < customerIds.size(); i++) {
phoneNumberMap.put(customerIds.get(i), phoneNumbers.get(i));
}
// ✅ 3. shopId → senderNumber 캐싱 처리
Map<Long, String> senderNumberMap = new HashMap<>();
for (Sms sms : scheduledMessages) {
senderNumberMap.computeIfAbsent(
sms.getShopId(),
shopId ->
messageSettingRepository
.findByShopId(shopId)
.map(MessageSettings::getSenderNumber)
.orElseThrow(() -> new BusinessException(ErrorCode.MESSAGE_SETTINGS_NOT_FOUND)));
}
// ✅ 4. senderNumber + messageContent 로 그룹핑
Map<String, List<Sms>> grouped =
scheduledMessages.stream()
.filter(sms -> phoneNumberMap.containsKey(sms.getCustomerId()))
.collect(
Collectors.groupingBy(
sms -> senderNumberMap.get(sms.getShopId()) + "|" + sms.getMessageContent()));
List<MessageSendResult> allResults = new ArrayList<>();
// 5. 그룹별 전송
for (Map.Entry<String, List<Sms>> entry : grouped.entrySet()) {
String[] keyParts = entry.getKey().split("\\|", 2);
String senderNumber = keyParts[0];
String messageContent = keyParts[1];
List<Sms> smsGroup = entry.getValue();
List<SmsSendUnit> units =
smsGroup.stream()
.map(
sms ->
new SmsSendUnit(sms.getMessageId(), phoneNumberMap.get(sms.getCustomerId())))
.toList();
List<MessageSendResult> results = coolSmsClient.sendMany(senderNumber, messageContent, units);
allResults.addAll(results);
}
// 6. 성공/실패 반영
List<Long> successIds =
allResults.stream()
.filter(MessageSendResult::success)
.map(MessageSendResult::messageId)
.toList();
List<Long> failedIds =
allResults.stream().filter(r -> !r.success()).map(MessageSendResult::messageId).toList();
if (!successIds.isEmpty()) {
messageCommandService.markSmsAsSent(successIds);
}
if (!failedIds.isEmpty()) {
messageCommandService.markSmsAsFailed(failedIds);
}
}
}