diff --git a/SPMS.API/Controllers/PushController.cs b/SPMS.API/Controllers/PushController.cs new file mode 100644 index 0000000..ba3e5e6 --- /dev/null +++ b/SPMS.API/Controllers/PushController.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using SPMS.Application.DTOs.Push; +using SPMS.Application.Interfaces; +using SPMS.Domain.Common; + +namespace SPMS.API.Controllers; + +[ApiController] +[Route("v1/in/push")] +[ApiExplorerSettings(GroupName = "push")] +public class PushController : ControllerBase +{ + private readonly IPushService _pushService; + + public PushController(IPushService pushService) + { + _pushService = pushService; + } + + [HttpPost("send")] + [SwaggerOperation(Summary = "단건 발송", Description = "특정 디바이스에 푸시 메시지를 즉시 발송합니다.")] + public async Task SendAsync([FromBody] PushSendRequestDto request) + { + var serviceId = GetServiceId(); + var result = await _pushService.SendAsync(serviceId, request); + return Ok(ApiResponse.Success(result)); + } + + [HttpPost("send/tag")] + [SwaggerOperation(Summary = "태그 발송", Description = "태그 조건에 해당하는 디바이스에 푸시 메시지를 발송합니다.")] + public async Task SendByTagAsync([FromBody] PushSendTagRequestDto request) + { + var serviceId = GetServiceId(); + var result = await _pushService.SendByTagAsync(serviceId, request); + return Ok(ApiResponse.Success(result)); + } + + private long GetServiceId() + { + if (HttpContext.Items.TryGetValue("ServiceId", out var serviceIdObj) && serviceIdObj is long serviceId) + return serviceId; + + throw new Domain.Exceptions.SpmsException(ErrorCodes.BadRequest, "서비스 식별 정보가 없습니다.", 400); + } +} diff --git a/SPMS.Application/DependencyInjection.cs b/SPMS.Application/DependencyInjection.cs index 05675cb..5d01307 100644 --- a/SPMS.Application/DependencyInjection.cs +++ b/SPMS.Application/DependencyInjection.cs @@ -18,6 +18,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/SPMS.Application/Interfaces/IPushService.cs b/SPMS.Application/Interfaces/IPushService.cs new file mode 100644 index 0000000..b393220 --- /dev/null +++ b/SPMS.Application/Interfaces/IPushService.cs @@ -0,0 +1,9 @@ +using SPMS.Application.DTOs.Push; + +namespace SPMS.Application.Interfaces; + +public interface IPushService +{ + Task SendAsync(long serviceId, PushSendRequestDto request); + Task SendByTagAsync(long serviceId, PushSendTagRequestDto request); +} diff --git a/SPMS.Application/Services/PushService.cs b/SPMS.Application/Services/PushService.cs new file mode 100644 index 0000000..200b364 --- /dev/null +++ b/SPMS.Application/Services/PushService.cs @@ -0,0 +1,124 @@ +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; + + public PushService(IMessageRepository messageRepository, IPushQueueService pushQueueService) + { + _messageRepository = messageRepository; + _pushQueueService = pushQueueService; + } + + 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" + }; + } + + 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); + } +} diff --git a/SPMS.Domain/Common/ErrorCodes.cs b/SPMS.Domain/Common/ErrorCodes.cs index 4b64796..552aa20 100644 --- a/SPMS.Domain/Common/ErrorCodes.cs +++ b/SPMS.Domain/Common/ErrorCodes.cs @@ -36,6 +36,9 @@ public static class ErrorCodes public const string DeviceNotFound = "141"; public const string DeviceTokenDuplicate = "142"; + // === Message (5) === + public const string MessageNotFound = "151"; + // === Push (6) === public const string PushSendFailed = "161"; public const string PushStateChangeNotAllowed = "162";