diff --git a/SPMS.API/Controllers/MessageController.cs b/SPMS.API/Controllers/MessageController.cs index 60f66f4..eedbab8 100644 --- a/SPMS.API/Controllers/MessageController.cs +++ b/SPMS.API/Controllers/MessageController.cs @@ -32,10 +32,10 @@ public class MessageController : ControllerBase } [HttpPost("list")] - [SwaggerOperation(Summary = "메시지 목록 조회", Description = "서비스별 메시지 목록을 페이지 단위로 조회합니다.")] + [SwaggerOperation(Summary = "메시지 목록 조회", Description = "메시지 목록을 페이지 단위로 조회합니다. X-Service-Code 헤더가 있으면 해당 서비스만, 없으면 전체 서비스 메시지를 반환합니다. send_status 필터(complete/pending/failed)를 지원합니다.")] public async Task GetListAsync([FromBody] MessageListRequestDto request) { - var serviceId = GetServiceId(); + var serviceId = GetServiceIdOrNull(); var result = await _messageService.GetListAsync(serviceId, request); return Ok(ApiResponse.Success(result)); } @@ -83,6 +83,13 @@ public class MessageController : ControllerBase throw new SpmsException(ErrorCodes.BadRequest, "서비스 식별 정보가 없습니다.", 400); } + private long? GetServiceIdOrNull() + { + if (HttpContext.Items.TryGetValue("ServiceId", out var serviceIdObj) && serviceIdObj is long serviceId) + return serviceId; + return null; + } + private long GetAdminId() { var adminIdClaim = User.FindFirst("adminId")?.Value; diff --git a/SPMS.API/Middlewares/ServiceCodeMiddleware.cs b/SPMS.API/Middlewares/ServiceCodeMiddleware.cs index 781d481..f582ba1 100644 --- a/SPMS.API/Middlewares/ServiceCodeMiddleware.cs +++ b/SPMS.API/Middlewares/ServiceCodeMiddleware.cs @@ -31,7 +31,8 @@ public class ServiceCodeMiddleware // === OPTIONAL_FOR_ADMIN: 관리자는 X-Service-Code 선택 === if (path.StartsWithSegments("/v1/in/stats") || - path.StartsWithSegments("/v1/in/device/list")) + path.StartsWithSegments("/v1/in/device/list") || + path.StartsWithSegments("/v1/in/message/list")) { if (context.Request.Headers.TryGetValue("X-Service-Code", out var optionalCode) && !string.IsNullOrWhiteSpace(optionalCode)) diff --git a/SPMS.Application/DTOs/Message/MessageListRequestDto.cs b/SPMS.Application/DTOs/Message/MessageListRequestDto.cs index 5e44cbe..b423db7 100644 --- a/SPMS.Application/DTOs/Message/MessageListRequestDto.cs +++ b/SPMS.Application/DTOs/Message/MessageListRequestDto.cs @@ -15,4 +15,10 @@ public class MessageListRequestDto [JsonPropertyName("is_active")] public bool? IsActive { get; set; } + + [JsonPropertyName("service_code")] + public string? ServiceCode { get; set; } + + [JsonPropertyName("send_status")] + public string? SendStatus { get; set; } } diff --git a/SPMS.Application/DTOs/Message/MessageListResponseDto.cs b/SPMS.Application/DTOs/Message/MessageListResponseDto.cs index 74f2b04..e216a34 100644 --- a/SPMS.Application/DTOs/Message/MessageListResponseDto.cs +++ b/SPMS.Application/DTOs/Message/MessageListResponseDto.cs @@ -25,4 +25,13 @@ public class MessageSummaryDto [JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; } + + [JsonPropertyName("service_name")] + public string ServiceName { get; set; } = string.Empty; + + [JsonPropertyName("service_code")] + public string ServiceCode { get; set; } = string.Empty; + + [JsonPropertyName("latest_send_status")] + public string LatestSendStatus { get; set; } = "pending"; } diff --git a/SPMS.Application/Interfaces/IMessageService.cs b/SPMS.Application/Interfaces/IMessageService.cs index b410ab2..1d86fdd 100644 --- a/SPMS.Application/Interfaces/IMessageService.cs +++ b/SPMS.Application/Interfaces/IMessageService.cs @@ -5,7 +5,7 @@ namespace SPMS.Application.Interfaces; public interface IMessageService { Task SaveAsync(long serviceId, long adminId, MessageSaveRequestDto request); - Task GetListAsync(long serviceId, MessageListRequestDto request); + Task GetListAsync(long? serviceId, MessageListRequestDto request); Task GetInfoAsync(long serviceId, MessageInfoRequestDto request); Task DeleteAsync(long serviceId, MessageDeleteRequestDto request); Task PreviewAsync(long serviceId, MessagePreviewRequestDto request); diff --git a/SPMS.Application/Services/MessageService.cs b/SPMS.Application/Services/MessageService.cs index 792b34f..509abca 100644 --- a/SPMS.Application/Services/MessageService.cs +++ b/SPMS.Application/Services/MessageService.cs @@ -63,26 +63,28 @@ public class MessageService : IMessageService }; } - public async Task GetListAsync(long serviceId, MessageListRequestDto request) + public async Task GetListAsync(long? serviceId, MessageListRequestDto request) { var page = Math.Max(1, request.Page); var size = Math.Clamp(request.Size, 1, 100); - var (items, totalCount) = await _messageRepository.GetPagedByServiceAsync( + var (items, totalCount) = await _messageRepository.GetPagedForListAsync( serviceId, page, size, - m => (request.Keyword == null || m.Title.Contains(request.Keyword) || m.Body.Contains(request.Keyword)) - && (request.IsActive == null || m.IsDeleted != request.IsActive)); + request.Keyword, request.IsActive, request.SendStatus); var totalPages = (int)Math.Ceiling((double)totalCount / size); return new MessageListResponseDto { - Items = items.Select(m => new MessageSummaryDto + Items = items.Select(p => new MessageSummaryDto { - MessageCode = m.MessageCode, - Title = m.Title, - IsActive = !m.IsDeleted, - CreatedAt = m.CreatedAt + MessageCode = p.MessageCode, + Title = p.Title, + IsActive = p.IsActive, + CreatedAt = p.CreatedAt, + ServiceName = p.ServiceName, + ServiceCode = p.ServiceCode, + LatestSendStatus = DetermineSendStatus(p.TotalSendCount, p.SuccessCount) }).ToList(), Pagination = new PaginationDto { @@ -94,6 +96,12 @@ public class MessageService : IMessageService }; } + private static string DetermineSendStatus(int totalSend, int successCount) + { + if (totalSend == 0) return "pending"; + return successCount > 0 ? "complete" : "failed"; + } + public async Task GetInfoAsync(long serviceId, MessageInfoRequestDto request) { if (string.IsNullOrWhiteSpace(request.MessageCode)) diff --git a/SPMS.Domain/Interfaces/IMessageRepository.cs b/SPMS.Domain/Interfaces/IMessageRepository.cs index 0e71b48..419244e 100644 --- a/SPMS.Domain/Interfaces/IMessageRepository.cs +++ b/SPMS.Domain/Interfaces/IMessageRepository.cs @@ -11,4 +11,19 @@ public interface IMessageRepository : IRepository Task<(IReadOnlyList Items, int TotalCount)> GetPagedByServiceAsync( long serviceId, int page, int size, Expression>? predicate = null); + Task<(IReadOnlyList Items, int TotalCount)> GetPagedForListAsync( + long? serviceId, int page, int size, + string? keyword = null, bool? isActive = null, string? sendStatus = null); +} + +public class MessageListProjection +{ + public string MessageCode { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public bool IsActive { get; set; } + public DateTime CreatedAt { get; set; } + public string ServiceName { get; set; } = string.Empty; + public string ServiceCode { get; set; } = string.Empty; + public int TotalSendCount { get; set; } + public int SuccessCount { get; set; } } diff --git a/SPMS.Infrastructure/Persistence/Repositories/MessageRepository.cs b/SPMS.Infrastructure/Persistence/Repositories/MessageRepository.cs index cbcff91..91e7244 100644 --- a/SPMS.Infrastructure/Persistence/Repositories/MessageRepository.cs +++ b/SPMS.Infrastructure/Persistence/Repositories/MessageRepository.cs @@ -1,6 +1,7 @@ using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using SPMS.Domain.Entities; +using SPMS.Domain.Enums; using SPMS.Domain.Interfaces; namespace SPMS.Infrastructure.Persistence.Repositories; @@ -52,4 +53,50 @@ public class MessageRepository : Repository, IMessageRepository return (items, totalCount); } + + public async Task<(IReadOnlyList Items, int TotalCount)> GetPagedForListAsync( + long? serviceId, int page, int size, + string? keyword = null, bool? isActive = null, string? sendStatus = null) + { + var query = _dbSet.Where(m => !m.IsDeleted); + + if (serviceId.HasValue) + query = query.Where(m => m.ServiceId == serviceId.Value); + + if (!string.IsNullOrWhiteSpace(keyword)) + query = query.Where(m => m.Title.Contains(keyword) || m.Body.Contains(keyword)); + + if (isActive.HasValue) + query = query.Where(m => m.IsDeleted != isActive.Value); + + var projected = query.Select(m => new MessageListProjection + { + MessageCode = m.MessageCode, + Title = m.Title, + IsActive = !m.IsDeleted, + CreatedAt = m.CreatedAt, + ServiceName = m.Service.ServiceName, + ServiceCode = m.Service.ServiceCode, + TotalSendCount = _context.PushSendLogs.Count(l => l.MessageId == m.Id), + SuccessCount = _context.PushSendLogs.Count(l => l.MessageId == m.Id && l.Status == PushResult.Success) + }); + + // 발송 상태 필터 + if (sendStatus == "complete") + projected = projected.Where(p => p.SuccessCount > 0); + else if (sendStatus == "failed") + projected = projected.Where(p => p.TotalSendCount > 0 && p.SuccessCount == 0); + else if (sendStatus == "pending") + projected = projected.Where(p => p.TotalSendCount == 0); + + var totalCount = await projected.CountAsync(); + + var items = await projected + .OrderByDescending(p => p.CreatedAt) + .Skip((page - 1) * size) + .Take(size) + .ToListAsync(); + + return (items, totalCount); + } }