improvement: 메시지 상세/프리뷰 응답 강화 (#226) #227

Merged
seonkyu.kim merged 1 commits from improvement/#226-message-detail-preview into develop 2026-02-25 05:51:21 +00:00
7 changed files with 63 additions and 6 deletions

View File

@ -41,7 +41,7 @@ public class MessageController : ControllerBase
} }
[HttpPost("info")] [HttpPost("info")]
[SwaggerOperation(Summary = "메시지 상세 조회", Description = "메시지 코드로 상세 정보를 조회합니다. 템플릿 변수 목록을 포함합니다.")] [SwaggerOperation(Summary = "메시지 상세 조회", Description = "메시지 코드로 상세 정보를 조회합니다. 서비스 정보(service_name, service_code), 작성자(created_by_name), 발송 상태(latest_send_status), 템플릿 변수 목록을 포함합니다.")]
public async Task<IActionResult> GetInfoAsync([FromBody] MessageInfoRequestDto request) public async Task<IActionResult> GetInfoAsync([FromBody] MessageInfoRequestDto request)
{ {
var serviceId = GetServiceId(); var serviceId = GetServiceId();
@ -67,7 +67,7 @@ public class MessageController : ControllerBase
} }
[HttpPost("preview")] [HttpPost("preview")]
[SwaggerOperation(Summary = "메시지 미리보기", Description = "메시지 템플릿에 변수를 치환하여 미리보기를 생성합니다.")] [SwaggerOperation(Summary = "메시지 미리보기", Description = "메시지 템플릿에 변수를 치환하여 미리보기를 생성합니다. 응답에 link_type을 포함합니다.")]
public async Task<IActionResult> PreviewAsync([FromBody] MessagePreviewRequestDto request) public async Task<IActionResult> PreviewAsync([FromBody] MessagePreviewRequestDto request)
{ {
var serviceId = GetServiceId(); var serviceId = GetServiceId();

View File

@ -31,6 +31,18 @@ public class MessageInfoResponseDto
[JsonPropertyName("is_active")] [JsonPropertyName("is_active")]
public bool IsActive { get; set; } 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")] [JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
} }

View File

@ -1,11 +1,12 @@
using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization;
namespace SPMS.Application.DTOs.Message; namespace SPMS.Application.DTOs.Message;
public class MessagePreviewRequestDto public class MessagePreviewRequestDto
{ {
[Required] [JsonPropertyName("message_code")]
public string MessageCode { get; set; } = string.Empty; public string MessageCode { get; set; } = string.Empty;
[JsonPropertyName("variables")]
public Dictionary<string, string>? Variables { get; set; } public Dictionary<string, string>? Variables { get; set; }
} }

View File

@ -1,9 +1,21 @@
using System.Text.Json.Serialization;
namespace SPMS.Application.DTOs.Message; namespace SPMS.Application.DTOs.Message;
public class MessagePreviewResponseDto public class MessagePreviewResponseDto
{ {
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
[JsonPropertyName("body")]
public string Body { get; set; } = string.Empty; public string Body { get; set; } = string.Empty;
[JsonPropertyName("image_url")]
public string? ImageUrl { get; set; } public string? ImageUrl { get; set; }
[JsonPropertyName("link_url")]
public string? LinkUrl { get; set; } public string? LinkUrl { get; set; }
[JsonPropertyName("link_type")]
public string? LinkType { get; set; }
} }

View File

@ -107,7 +107,7 @@ public class MessageService : IMessageService
if (string.IsNullOrWhiteSpace(request.MessageCode)) if (string.IsNullOrWhiteSpace(request.MessageCode))
throw new SpmsException(ErrorCodes.BadRequest, "메시지 코드는 필수입니다.", 400); 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) if (message == null)
throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404); throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404);
@ -119,6 +119,8 @@ public class MessageService : IMessageService
data = JsonSerializer.Deserialize<JsonElement>(message.CustomData); data = JsonSerializer.Deserialize<JsonElement>(message.CustomData);
} }
var (totalSend, successCount) = await _messageRepository.GetSendStatsAsync(message.Id);
return new MessageInfoResponseDto return new MessageInfoResponseDto
{ {
MessageCode = message.MessageCode, MessageCode = message.MessageCode,
@ -130,6 +132,10 @@ public class MessageService : IMessageService
Data = data, Data = data,
Variables = variables, Variables = variables,
IsActive = !message.IsDeleted, IsActive = !message.IsDeleted,
ServiceName = message.Service.ServiceName,
ServiceCode = message.Service.ServiceCode,
CreatedByName = message.CreatedByAdmin.Name,
LatestSendStatus = DetermineSendStatus(totalSend, successCount),
CreatedAt = message.CreatedAt CreatedAt = message.CreatedAt
}; };
} }
@ -163,7 +169,8 @@ public class MessageService : IMessageService
Title = title, Title = title,
Body = body, Body = body,
ImageUrl = message.ImageUrl, ImageUrl = message.ImageUrl,
LinkUrl = message.LinkUrl LinkUrl = message.LinkUrl,
LinkType = message.LinkType
}; };
} }

View File

@ -14,6 +14,8 @@ public interface IMessageRepository : IRepository<Message>
Task<(IReadOnlyList<MessageListProjection> Items, int TotalCount)> GetPagedForListAsync( Task<(IReadOnlyList<MessageListProjection> Items, int TotalCount)> GetPagedForListAsync(
long? serviceId, int page, int size, long? serviceId, int page, int size,
string? keyword = null, bool? isActive = null, string? sendStatus = null); string? keyword = null, bool? isActive = null, string? sendStatus = null);
Task<Message?> GetByMessageCodeWithDetailsAsync(string messageCode, long serviceId);
Task<(int TotalSendCount, int SuccessCount)> GetSendStatsAsync(long messageId);
} }
public class MessageListProjection public class MessageListProjection

View File

@ -54,6 +54,29 @@ public class MessageRepository : Repository<Message>, IMessageRepository
return (items, totalCount); return (items, totalCount);
} }
public async Task<Message?> 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<MessageListProjection> Items, int TotalCount)> GetPagedForListAsync( public async Task<(IReadOnlyList<MessageListProjection> Items, int TotalCount)> GetPagedForListAsync(
long? serviceId, int page, int size, long? serviceId, int page, int size,
string? keyword = null, bool? isActive = null, string? sendStatus = null) string? keyword = null, bool? isActive = null, string? sendStatus = null)