From 0eacf25eb357eae52da1e96e57238222066f6ba0 Mon Sep 17 00:00:00 2001 From: SEAN Date: Wed, 25 Feb 2026 14:43:29 +0900 Subject: [PATCH] =?UTF-8?q?improvement:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=83=81=EC=84=B8/=ED=94=84=EB=A6=AC=EB=B7=B0=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B0=95=ED=99=94=20(#226)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MessageInfoResponseDto에 service_name, service_code, created_by_name, latest_send_status 추가 - MessagePreviewRequestDto/ResponseDto에 JsonPropertyName snake_case 적용 - MessagePreviewResponseDto에 link_type 필드 추가 - Repository에 GetByMessageCodeWithDetailsAsync (Navigation Include), GetSendStatsAsync 추가 - MessageService.GetInfoAsync에서 서비스/작성자/발송상태 매핑 - MessageService.PreviewAsync에서 link_type 반환 Closes #226 --- SPMS.API/Controllers/MessageController.cs | 4 ++-- .../DTOs/Message/MessageInfoResponseDto.cs | 12 ++++++++++ .../DTOs/Message/MessagePreviewRequestDto.cs | 5 ++-- .../DTOs/Message/MessagePreviewResponseDto.cs | 12 ++++++++++ SPMS.Application/Services/MessageService.cs | 11 +++++++-- SPMS.Domain/Interfaces/IMessageRepository.cs | 2 ++ .../Repositories/MessageRepository.cs | 23 +++++++++++++++++++ 7 files changed, 63 insertions(+), 6 deletions(-) diff --git a/SPMS.API/Controllers/MessageController.cs b/SPMS.API/Controllers/MessageController.cs index eedbab8..239a928 100644 --- a/SPMS.API/Controllers/MessageController.cs +++ b/SPMS.API/Controllers/MessageController.cs @@ -41,7 +41,7 @@ public class MessageController : ControllerBase } [HttpPost("info")] - [SwaggerOperation(Summary = "메시지 상세 조회", Description = "메시지 코드로 상세 정보를 조회합니다. 템플릿 변수 목록을 포함합니다.")] + [SwaggerOperation(Summary = "메시지 상세 조회", Description = "메시지 코드로 상세 정보를 조회합니다. 서비스 정보(service_name, service_code), 작성자(created_by_name), 발송 상태(latest_send_status), 템플릿 변수 목록을 포함합니다.")] public async Task GetInfoAsync([FromBody] MessageInfoRequestDto request) { var serviceId = GetServiceId(); @@ -67,7 +67,7 @@ public class MessageController : ControllerBase } [HttpPost("preview")] - [SwaggerOperation(Summary = "메시지 미리보기", Description = "메시지 템플릿에 변수를 치환하여 미리보기를 생성합니다.")] + [SwaggerOperation(Summary = "메시지 미리보기", Description = "메시지 템플릿에 변수를 치환하여 미리보기를 생성합니다. 응답에 link_type을 포함합니다.")] public async Task PreviewAsync([FromBody] MessagePreviewRequestDto request) { var serviceId = GetServiceId(); diff --git a/SPMS.Application/DTOs/Message/MessageInfoResponseDto.cs b/SPMS.Application/DTOs/Message/MessageInfoResponseDto.cs index 66669fd..67f25b9 100644 --- a/SPMS.Application/DTOs/Message/MessageInfoResponseDto.cs +++ b/SPMS.Application/DTOs/Message/MessageInfoResponseDto.cs @@ -31,6 +31,18 @@ public class MessageInfoResponseDto [JsonPropertyName("is_active")] public bool IsActive { get; set; } + [JsonPropertyName("service_name")] + public string ServiceName { get; set; } = string.Empty; + + [JsonPropertyName("service_code")] + public string ServiceCode { get; set; } = string.Empty; + + [JsonPropertyName("created_by_name")] + public string CreatedByName { get; set; } = string.Empty; + + [JsonPropertyName("latest_send_status")] + public string LatestSendStatus { get; set; } = "pending"; + [JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; } } diff --git a/SPMS.Application/DTOs/Message/MessagePreviewRequestDto.cs b/SPMS.Application/DTOs/Message/MessagePreviewRequestDto.cs index ee9172e..b3ec3d9 100644 --- a/SPMS.Application/DTOs/Message/MessagePreviewRequestDto.cs +++ b/SPMS.Application/DTOs/Message/MessagePreviewRequestDto.cs @@ -1,11 +1,12 @@ -using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; namespace SPMS.Application.DTOs.Message; public class MessagePreviewRequestDto { - [Required] + [JsonPropertyName("message_code")] public string MessageCode { get; set; } = string.Empty; + [JsonPropertyName("variables")] public Dictionary? Variables { get; set; } } diff --git a/SPMS.Application/DTOs/Message/MessagePreviewResponseDto.cs b/SPMS.Application/DTOs/Message/MessagePreviewResponseDto.cs index 5dcb15e..42b732e 100644 --- a/SPMS.Application/DTOs/Message/MessagePreviewResponseDto.cs +++ b/SPMS.Application/DTOs/Message/MessagePreviewResponseDto.cs @@ -1,9 +1,21 @@ +using System.Text.Json.Serialization; + namespace SPMS.Application.DTOs.Message; public class MessagePreviewResponseDto { + [JsonPropertyName("title")] public string Title { get; set; } = string.Empty; + + [JsonPropertyName("body")] public string Body { get; set; } = string.Empty; + + [JsonPropertyName("image_url")] public string? ImageUrl { get; set; } + + [JsonPropertyName("link_url")] public string? LinkUrl { get; set; } + + [JsonPropertyName("link_type")] + public string? LinkType { get; set; } } diff --git a/SPMS.Application/Services/MessageService.cs b/SPMS.Application/Services/MessageService.cs index 509abca..87a547c 100644 --- a/SPMS.Application/Services/MessageService.cs +++ b/SPMS.Application/Services/MessageService.cs @@ -107,7 +107,7 @@ public class MessageService : IMessageService if (string.IsNullOrWhiteSpace(request.MessageCode)) throw new SpmsException(ErrorCodes.BadRequest, "메시지 코드는 필수입니다.", 400); - var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId); + var message = await _messageRepository.GetByMessageCodeWithDetailsAsync(request.MessageCode, serviceId); if (message == null) throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404); @@ -119,6 +119,8 @@ public class MessageService : IMessageService data = JsonSerializer.Deserialize(message.CustomData); } + var (totalSend, successCount) = await _messageRepository.GetSendStatsAsync(message.Id); + return new MessageInfoResponseDto { MessageCode = message.MessageCode, @@ -130,6 +132,10 @@ public class MessageService : IMessageService Data = data, Variables = variables, IsActive = !message.IsDeleted, + ServiceName = message.Service.ServiceName, + ServiceCode = message.Service.ServiceCode, + CreatedByName = message.CreatedByAdmin.Name, + LatestSendStatus = DetermineSendStatus(totalSend, successCount), CreatedAt = message.CreatedAt }; } @@ -163,7 +169,8 @@ public class MessageService : IMessageService Title = title, Body = body, ImageUrl = message.ImageUrl, - LinkUrl = message.LinkUrl + LinkUrl = message.LinkUrl, + LinkType = message.LinkType }; } diff --git a/SPMS.Domain/Interfaces/IMessageRepository.cs b/SPMS.Domain/Interfaces/IMessageRepository.cs index 419244e..a393b5d 100644 --- a/SPMS.Domain/Interfaces/IMessageRepository.cs +++ b/SPMS.Domain/Interfaces/IMessageRepository.cs @@ -14,6 +14,8 @@ public interface IMessageRepository : IRepository Task<(IReadOnlyList Items, int TotalCount)> GetPagedForListAsync( long? serviceId, int page, int size, string? keyword = null, bool? isActive = null, string? sendStatus = null); + Task GetByMessageCodeWithDetailsAsync(string messageCode, long serviceId); + Task<(int TotalSendCount, int SuccessCount)> GetSendStatsAsync(long messageId); } public class MessageListProjection diff --git a/SPMS.Infrastructure/Persistence/Repositories/MessageRepository.cs b/SPMS.Infrastructure/Persistence/Repositories/MessageRepository.cs index 91e7244..1cb4e4e 100644 --- a/SPMS.Infrastructure/Persistence/Repositories/MessageRepository.cs +++ b/SPMS.Infrastructure/Persistence/Repositories/MessageRepository.cs @@ -54,6 +54,29 @@ public class MessageRepository : Repository, IMessageRepository return (items, totalCount); } + public async Task GetByMessageCodeWithDetailsAsync(string messageCode, long serviceId) + { + return await _dbSet + .Include(m => m.Service) + .Include(m => m.CreatedByAdmin) + .FirstOrDefaultAsync(m => m.MessageCode == messageCode && m.ServiceId == serviceId && !m.IsDeleted); + } + + public async Task<(int TotalSendCount, int SuccessCount)> GetSendStatsAsync(long messageId) + { + var stats = await _context.PushSendLogs + .Where(l => l.MessageId == messageId) + .GroupBy(_ => 1) + .Select(g => new + { + Total = g.Count(), + Success = g.Count(l => l.Status == PushResult.Success) + }) + .FirstOrDefaultAsync(); + + return stats != null ? (stats.Total, stats.Success) : (0, 0); + } + public async Task<(IReadOnlyList Items, int TotalCount)> GetPagedForListAsync( long? serviceId, int page, int size, string? keyword = null, bool? isActive = null, string? sendStatus = null)