From b56170f10c23250d1eb8d00cdda3338bcf457e40 Mon Sep 17 00:00:00 2001 From: SEAN Date: Tue, 10 Feb 2026 17:41:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B0=9C=EC=86=A1=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20(#122)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /v1/in/push/log 엔드포인트 추가 - PushSendLogRepository (페이징, 필터링: message_code, device_id, status, 날짜범위) - PushService.GetLogAsync 구현 - 누락된 Push DTO 파일 포함 (PushSendRequestDto, PushSendResponseDto, PushSendTagRequestDto) --- SPMS.API/Controllers/PushController.cs | 9 +++ .../DTOs/Push/PushLogRequestDto.cs | 27 +++++++ .../DTOs/Push/PushLogResponseDto.cs | 34 ++++++++ .../DTOs/Push/PushSendRequestDto.cs | 14 ++++ .../DTOs/Push/PushSendResponseDto.cs | 8 ++ .../DTOs/Push/PushSendTagRequestDto.cs | 15 ++++ SPMS.Application/Interfaces/IPushService.cs | 1 + SPMS.Application/Services/PushService.cs | 79 ++++++++++++++++++- .../Interfaces/IPushSendLogRepository.cs | 13 +++ SPMS.Infrastructure/DependencyInjection.cs | 1 + .../Repositories/PushSendLogRepository.cs | 47 +++++++++++ 11 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 SPMS.Application/DTOs/Push/PushLogRequestDto.cs create mode 100644 SPMS.Application/DTOs/Push/PushLogResponseDto.cs create mode 100644 SPMS.Application/DTOs/Push/PushSendRequestDto.cs create mode 100644 SPMS.Application/DTOs/Push/PushSendResponseDto.cs create mode 100644 SPMS.Application/DTOs/Push/PushSendTagRequestDto.cs create mode 100644 SPMS.Domain/Interfaces/IPushSendLogRepository.cs create mode 100644 SPMS.Infrastructure/Persistence/Repositories/PushSendLogRepository.cs diff --git a/SPMS.API/Controllers/PushController.cs b/SPMS.API/Controllers/PushController.cs index e64b5d1..dd72d66 100644 --- a/SPMS.API/Controllers/PushController.cs +++ b/SPMS.API/Controllers/PushController.cs @@ -54,6 +54,15 @@ public class PushController : ControllerBase return Ok(ApiResponse.Success()); } + [HttpPost("log")] + [SwaggerOperation(Summary = "발송 로그 조회", Description = "푸시 발송 이력을 페이지 단위로 조회합니다.")] + public async Task GetLogAsync([FromBody] PushLogRequestDto request) + { + var serviceId = GetServiceId(); + var result = await _pushService.GetLogAsync(serviceId, request); + return Ok(ApiResponse.Success(result)); + } + private long GetServiceId() { if (HttpContext.Items.TryGetValue("ServiceId", out var serviceIdObj) && serviceIdObj is long serviceId) diff --git a/SPMS.Application/DTOs/Push/PushLogRequestDto.cs b/SPMS.Application/DTOs/Push/PushLogRequestDto.cs new file mode 100644 index 0000000..d304493 --- /dev/null +++ b/SPMS.Application/DTOs/Push/PushLogRequestDto.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace SPMS.Application.DTOs.Push; + +public class PushLogRequestDto +{ + [JsonPropertyName("page")] + public int Page { get; set; } = 1; + + [JsonPropertyName("size")] + public int Size { get; set; } = 20; + + [JsonPropertyName("message_code")] + public string? MessageCode { get; set; } + + [JsonPropertyName("device_id")] + public long? DeviceId { get; set; } + + [JsonPropertyName("status")] + public string? Status { get; set; } + + [JsonPropertyName("start_date")] + public string? StartDate { get; set; } + + [JsonPropertyName("end_date")] + public string? EndDate { get; set; } +} diff --git a/SPMS.Application/DTOs/Push/PushLogResponseDto.cs b/SPMS.Application/DTOs/Push/PushLogResponseDto.cs new file mode 100644 index 0000000..f271ee2 --- /dev/null +++ b/SPMS.Application/DTOs/Push/PushLogResponseDto.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; +using SPMS.Application.DTOs.Notice; + +namespace SPMS.Application.DTOs.Push; + +public class PushLogResponseDto +{ + [JsonPropertyName("items")] + public List Items { get; set; } = []; + + [JsonPropertyName("pagination")] + public PaginationDto Pagination { get; set; } = new(); +} + +public class PushLogItemDto +{ + [JsonPropertyName("send_id")] + public long SendId { get; set; } + + [JsonPropertyName("message_code")] + public string MessageCode { get; set; } = string.Empty; + + [JsonPropertyName("device_id")] + public long DeviceId { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + [JsonPropertyName("fail_reason")] + public string? FailReason { get; set; } + + [JsonPropertyName("sent_at")] + public DateTime SentAt { get; set; } +} diff --git a/SPMS.Application/DTOs/Push/PushSendRequestDto.cs b/SPMS.Application/DTOs/Push/PushSendRequestDto.cs new file mode 100644 index 0000000..fd0bb36 --- /dev/null +++ b/SPMS.Application/DTOs/Push/PushSendRequestDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace SPMS.Application.DTOs.Push; + +public class PushSendRequestDto +{ + [Required] + public string MessageCode { get; set; } = string.Empty; + + [Required] + public long DeviceId { get; set; } + + public Dictionary? Variables { get; set; } +} diff --git a/SPMS.Application/DTOs/Push/PushSendResponseDto.cs b/SPMS.Application/DTOs/Push/PushSendResponseDto.cs new file mode 100644 index 0000000..de2ed65 --- /dev/null +++ b/SPMS.Application/DTOs/Push/PushSendResponseDto.cs @@ -0,0 +1,8 @@ +namespace SPMS.Application.DTOs.Push; + +public class PushSendResponseDto +{ + public string RequestId { get; set; } = string.Empty; + public string SendType { get; set; } = string.Empty; + public string Status { get; set; } = "queued"; +} diff --git a/SPMS.Application/DTOs/Push/PushSendTagRequestDto.cs b/SPMS.Application/DTOs/Push/PushSendTagRequestDto.cs new file mode 100644 index 0000000..40a1432 --- /dev/null +++ b/SPMS.Application/DTOs/Push/PushSendTagRequestDto.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace SPMS.Application.DTOs.Push; + +public class PushSendTagRequestDto +{ + [Required] + public string MessageCode { get; set; } = string.Empty; + + [Required] + [MinLength(1)] + public List Tags { get; set; } = []; + + public string TagMatch { get; set; } = "or"; +} diff --git a/SPMS.Application/Interfaces/IPushService.cs b/SPMS.Application/Interfaces/IPushService.cs index faf36e4..3360979 100644 --- a/SPMS.Application/Interfaces/IPushService.cs +++ b/SPMS.Application/Interfaces/IPushService.cs @@ -8,4 +8,5 @@ public interface IPushService Task SendByTagAsync(long serviceId, PushSendTagRequestDto request); Task ScheduleAsync(long serviceId, PushScheduleRequestDto request); Task CancelScheduleAsync(PushScheduleCancelRequestDto request); + Task GetLogAsync(long serviceId, PushLogRequestDto request); } diff --git a/SPMS.Application/Services/PushService.cs b/SPMS.Application/Services/PushService.cs index 9dc885a..993dd9a 100644 --- a/SPMS.Application/Services/PushService.cs +++ b/SPMS.Application/Services/PushService.cs @@ -1,7 +1,10 @@ +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; @@ -12,15 +15,18 @@ 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) + IScheduleCancelStore scheduleCancelStore, + IPushSendLogRepository pushSendLogRepository) { _messageRepository = messageRepository; _pushQueueService = pushQueueService; _scheduleCancelStore = scheduleCancelStore; + _pushSendLogRepository = pushSendLogRepository; } public async Task SendAsync(long serviceId, PushSendRequestDto request) @@ -186,6 +192,77 @@ public class PushService : IPushService 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) diff --git a/SPMS.Domain/Interfaces/IPushSendLogRepository.cs b/SPMS.Domain/Interfaces/IPushSendLogRepository.cs new file mode 100644 index 0000000..1574976 --- /dev/null +++ b/SPMS.Domain/Interfaces/IPushSendLogRepository.cs @@ -0,0 +1,13 @@ +using SPMS.Domain.Entities; +using SPMS.Domain.Enums; + +namespace SPMS.Domain.Interfaces; + +public interface IPushSendLogRepository : IRepository +{ + Task<(IReadOnlyList Items, int TotalCount)> GetPagedWithMessageAsync( + long serviceId, int page, int size, + long? messageId = null, long? deviceId = null, + PushResult? status = null, + DateTime? startDate = null, DateTime? endDate = null); +} diff --git a/SPMS.Infrastructure/DependencyInjection.cs b/SPMS.Infrastructure/DependencyInjection.cs index 42f0899..cadd401 100644 --- a/SPMS.Infrastructure/DependencyInjection.cs +++ b/SPMS.Infrastructure/DependencyInjection.cs @@ -39,6 +39,7 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // External Services services.AddScoped(); diff --git a/SPMS.Infrastructure/Persistence/Repositories/PushSendLogRepository.cs b/SPMS.Infrastructure/Persistence/Repositories/PushSendLogRepository.cs new file mode 100644 index 0000000..a8d8fa5 --- /dev/null +++ b/SPMS.Infrastructure/Persistence/Repositories/PushSendLogRepository.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore; +using SPMS.Domain.Entities; +using SPMS.Domain.Enums; +using SPMS.Domain.Interfaces; + +namespace SPMS.Infrastructure.Persistence.Repositories; + +public class PushSendLogRepository : Repository, IPushSendLogRepository +{ + public PushSendLogRepository(AppDbContext context) : base(context) { } + + public async Task<(IReadOnlyList Items, int TotalCount)> GetPagedWithMessageAsync( + long serviceId, int page, int size, + long? messageId = null, long? deviceId = null, + PushResult? status = null, + DateTime? startDate = null, DateTime? endDate = null) + { + var query = _dbSet + .Include(l => l.Message) + .Where(l => l.ServiceId == serviceId); + + if (messageId.HasValue) + query = query.Where(l => l.MessageId == messageId.Value); + + if (deviceId.HasValue) + query = query.Where(l => l.DeviceId == deviceId.Value); + + if (status.HasValue) + query = query.Where(l => l.Status == status.Value); + + if (startDate.HasValue) + query = query.Where(l => l.SentAt >= startDate.Value); + + if (endDate.HasValue) + query = query.Where(l => l.SentAt < endDate.Value.AddDays(1)); + + var totalCount = await query.CountAsync(); + + var items = await query + .OrderByDescending(l => l.SentAt) + .Skip((page - 1) * size) + .Take(size) + .ToListAsync(); + + return (items, totalCount); + } +} -- 2.45.1