using System.Text.Json; using SPMS.Application.DTOs.Push; using SPMS.Application.Interfaces; using SPMS.Domain.Common; using SPMS.Domain.Exceptions; using SPMS.Domain.Interfaces; namespace SPMS.Application.Services; public class PushService : IPushService { private readonly IMessageRepository _messageRepository; private readonly IPushQueueService _pushQueueService; private readonly IScheduleCancelStore _scheduleCancelStore; public PushService( IMessageRepository messageRepository, IPushQueueService pushQueueService, IScheduleCancelStore scheduleCancelStore) { _messageRepository = messageRepository; _pushQueueService = pushQueueService; _scheduleCancelStore = scheduleCancelStore; } public async Task SendAsync(long serviceId, PushSendRequestDto request) { var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId); if (message == null) throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404); var title = ApplyVariables(message.Title, request.Variables); var body = ApplyVariables(message.Body, request.Variables); var requestId = Guid.NewGuid().ToString("N"); var pushMessage = new PushMessageDto { MessageId = message.Id.ToString(), RequestId = requestId, ServiceId = serviceId, SendType = "single", Title = title, Body = body, ImageUrl = message.ImageUrl, LinkUrl = message.LinkUrl, CustomData = ParseCustomData(message.CustomData), Target = new PushTargetDto { Type = "device_ids", Value = JsonSerializer.SerializeToElement(new[] { request.DeviceId }) }, CreatedBy = message.CreatedBy, CreatedAt = DateTime.UtcNow.ToString("o") }; await _pushQueueService.PublishPushMessageAsync(pushMessage); return new PushSendResponseDto { RequestId = requestId, SendType = "single", Status = "queued" }; } public async Task SendByTagAsync(long serviceId, PushSendTagRequestDto request) { var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId); if (message == null) throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404); var requestId = Guid.NewGuid().ToString("N"); var pushMessage = new PushMessageDto { MessageId = message.Id.ToString(), RequestId = requestId, ServiceId = serviceId, SendType = "group", Title = message.Title, Body = message.Body, ImageUrl = message.ImageUrl, LinkUrl = message.LinkUrl, CustomData = ParseCustomData(message.CustomData), Target = new PushTargetDto { Type = "tags", Value = JsonSerializer.SerializeToElement(new { tags = request.Tags, match = request.TagMatch }) }, CreatedBy = message.CreatedBy, CreatedAt = DateTime.UtcNow.ToString("o") }; await _pushQueueService.PublishPushMessageAsync(pushMessage); return new PushSendResponseDto { RequestId = requestId, SendType = "group", Status = "queued" }; } public async Task ScheduleAsync(long serviceId, PushScheduleRequestDto request) { var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId); if (message == null) throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404); var sendType = request.SendType.ToLowerInvariant(); if (sendType != "single" && sendType != "tag") throw new SpmsException(ErrorCodes.BadRequest, "send_type은 single 또는 tag만 허용됩니다.", 400); if (sendType == "single" && request.DeviceId == null) throw new SpmsException(ErrorCodes.BadRequest, "send_type=single 시 device_id는 필수입니다.", 400); if (sendType == "tag" && (request.Tags == null || request.Tags.Count == 0)) throw new SpmsException(ErrorCodes.BadRequest, "send_type=tag 시 tags는 필수입니다.", 400); var requestId = Guid.NewGuid().ToString("N"); var scheduleId = $"sch_{DateTime.UtcNow:yyyyMMdd}_{requestId[..8]}"; var title = ApplyVariables(message.Title, request.Variables); var body = ApplyVariables(message.Body, request.Variables); PushTargetDto target; if (sendType == "single") { target = new PushTargetDto { Type = "device_ids", Value = JsonSerializer.SerializeToElement(new[] { request.DeviceId!.Value }) }; } else { target = new PushTargetDto { Type = "tags", Value = JsonSerializer.SerializeToElement(new { tags = request.Tags, match = "or" }) }; } var pushMessage = new PushMessageDto { MessageId = message.Id.ToString(), RequestId = requestId, ServiceId = serviceId, SendType = sendType, Title = title, Body = body, ImageUrl = message.ImageUrl, LinkUrl = message.LinkUrl, CustomData = ParseCustomData(message.CustomData), Target = target, CreatedBy = message.CreatedBy, CreatedAt = DateTime.UtcNow.ToString("o") }; var scheduleMessage = new ScheduleMessageDto { ScheduleId = scheduleId, MessageId = message.Id.ToString(), ServiceId = serviceId, ScheduledAt = request.ScheduledAt, PushMessage = pushMessage }; await _pushQueueService.PublishScheduleMessageAsync(scheduleMessage); return new PushScheduleResponseDto { ScheduleId = scheduleId, ScheduledAt = request.ScheduledAt, Status = "scheduled" }; } public async Task CancelScheduleAsync(PushScheduleCancelRequestDto request) { await _scheduleCancelStore.MarkCancelledAsync(request.ScheduleId); } private static string ApplyVariables(string template, Dictionary? variables) { if (variables == null || variables.Count == 0) return template; var result = template; foreach (var (key, value) in variables) { result = result.Replace($"{{{{{key}}}}}", value); } return result; } private static Dictionary? ParseCustomData(string? customData) { if (string.IsNullOrWhiteSpace(customData)) return null; return JsonSerializer.Deserialize>(customData); } }