using System.Globalization; using System.Text.Json; using SPMS.Application.DTOs.Notice; using SPMS.Application.DTOs.Push; using SPMS.Application.Interfaces; using SPMS.Domain.Common; using SPMS.Domain.Enums; 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; private readonly IPushSendLogRepository _pushSendLogRepository; public PushService( IMessageRepository messageRepository, IPushQueueService pushQueueService, IScheduleCancelStore scheduleCancelStore, IPushSendLogRepository pushSendLogRepository) { _messageRepository = messageRepository; _pushQueueService = pushQueueService; _scheduleCancelStore = scheduleCancelStore; _pushSendLogRepository = pushSendLogRepository; } 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); } public async Task GetLogAsync(long serviceId, PushLogRequestDto request) { long? messageId = null; if (!string.IsNullOrWhiteSpace(request.MessageCode)) { var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId); if (message != null) messageId = message.Id; else return new PushLogResponseDto { Items = [], Pagination = new PaginationDto { Page = request.Page, Size = request.Size, TotalCount = 0, TotalPages = 0 } }; } PushResult? status = null; if (!string.IsNullOrWhiteSpace(request.Status)) { status = request.Status.ToLowerInvariant() switch { "success" => PushResult.Success, "failed" => PushResult.Failed, _ => null }; } DateTime? startDate = null; if (!string.IsNullOrWhiteSpace(request.StartDate) && DateTime.TryParseExact(request.StartDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedStart)) startDate = parsedStart; DateTime? endDate = null; if (!string.IsNullOrWhiteSpace(request.EndDate) && DateTime.TryParseExact(request.EndDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedEnd)) endDate = parsedEnd; var (items, totalCount) = await _pushSendLogRepository.GetPagedWithMessageAsync( serviceId, request.Page, request.Size, messageId, request.DeviceId, status, startDate, endDate); var totalPages = (int)Math.Ceiling((double)totalCount / request.Size); return new PushLogResponseDto { Items = items.Select(l => new PushLogItemDto { SendId = l.Id, MessageCode = l.Message?.MessageCode ?? string.Empty, DeviceId = l.DeviceId, Status = l.Status.ToString().ToLowerInvariant(), FailReason = l.FailReason, SentAt = l.SentAt }).ToList(), Pagination = new PaginationDto { Page = request.Page, Size = request.Size, TotalCount = totalCount, TotalPages = totalPages } }; } 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); } }