From 73de5efd84643aa96fa20c4f2cbdb56beb75566a Mon Sep 17 00:00:00 2001 From: SEAN Date: Tue, 10 Feb 2026 16:38:51 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A6=89=EC=8B=9C=20=EB=B0=9C=EC=86=A1?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20API=20=EA=B5=AC=ED=98=84=20(#114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /v1/in/push/send (단건 발송) - POST /v1/in/push/send/tag (태그 발송) - PushService: 메시지 조회 → 변수 치환 → RabbitMQ 큐 발행 - MessageNotFound(151) 에러 코드 추가 Closes #114 --- SPMS.API/Controllers/PushController.cs | 46 ++++++++ SPMS.Application/DependencyInjection.cs | 1 + SPMS.Application/Interfaces/IPushService.cs | 9 ++ SPMS.Application/Services/PushService.cs | 124 ++++++++++++++++++++ SPMS.Domain/Common/ErrorCodes.cs | 3 + 5 files changed, 183 insertions(+) create mode 100644 SPMS.API/Controllers/PushController.cs create mode 100644 SPMS.Application/Interfaces/IPushService.cs create mode 100644 SPMS.Application/Services/PushService.cs 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";