- ServiceCodeMiddleware: message/list를 OPTIONAL_FOR_ADMIN에 추가 - MessageListRequestDto: service_code, send_status 필터 필드 추가 - MessageSummaryDto: service_name, service_code, latest_send_status 추가 - IMessageRepository + MessageRepository: GetPagedForListAsync 구현 (Service 조인 + PushSendLog 집계 한 번의 쿼리) - IMessageService + MessageService: serviceId nullable 변경, DetermineSendStatus 헬퍼 - MessageController: GetServiceIdOrNull() 헬퍼 + Swagger 업데이트 Closes #224
218 lines
7.8 KiB
C#
218 lines
7.8 KiB
C#
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using SPMS.Application.DTOs.Message;
|
|
using SPMS.Application.DTOs.Notice;
|
|
using SPMS.Application.Interfaces;
|
|
using SPMS.Domain.Common;
|
|
using SPMS.Domain.Exceptions;
|
|
using SPMS.Domain.Interfaces;
|
|
|
|
namespace SPMS.Application.Services;
|
|
|
|
public class MessageService : IMessageService
|
|
{
|
|
private readonly IMessageRepository _messageRepository;
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private static readonly Regex VariablePattern = new(@"\{\{(\w+)\}\}", RegexOptions.Compiled);
|
|
|
|
public MessageService(IMessageRepository messageRepository, IUnitOfWork unitOfWork)
|
|
{
|
|
_messageRepository = messageRepository;
|
|
_unitOfWork = unitOfWork;
|
|
}
|
|
|
|
public async Task<MessageSaveResponseDto> SaveAsync(long serviceId, long adminId, MessageSaveRequestDto request)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.Title))
|
|
throw new SpmsException(ErrorCodes.BadRequest, "제목은 필수입니다.", 400);
|
|
if (string.IsNullOrWhiteSpace(request.Body))
|
|
throw new SpmsException(ErrorCodes.BadRequest, "본문은 필수입니다.", 400);
|
|
|
|
var messageCode = await GenerateMessageCodeAsync(serviceId);
|
|
|
|
string? customData = null;
|
|
if (request.Data != null)
|
|
{
|
|
customData = request.Data is JsonElement jsonElement
|
|
? jsonElement.GetRawText()
|
|
: JsonSerializer.Serialize(request.Data);
|
|
}
|
|
|
|
var message = new Domain.Entities.Message
|
|
{
|
|
ServiceId = serviceId,
|
|
MessageCode = messageCode,
|
|
Title = request.Title,
|
|
Body = request.Body,
|
|
ImageUrl = request.ImageUrl,
|
|
LinkUrl = request.LinkUrl,
|
|
LinkType = request.LinkType,
|
|
CustomData = customData,
|
|
CreatedAt = DateTime.UtcNow,
|
|
CreatedBy = adminId,
|
|
IsDeleted = false
|
|
};
|
|
|
|
await _messageRepository.AddAsync(message);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
|
|
return new MessageSaveResponseDto
|
|
{
|
|
MessageCode = messageCode,
|
|
CreatedAt = message.CreatedAt
|
|
};
|
|
}
|
|
|
|
public async Task<MessageListResponseDto> 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.GetPagedForListAsync(
|
|
serviceId, page, size,
|
|
request.Keyword, request.IsActive, request.SendStatus);
|
|
|
|
var totalPages = (int)Math.Ceiling((double)totalCount / size);
|
|
|
|
return new MessageListResponseDto
|
|
{
|
|
Items = items.Select(p => new MessageSummaryDto
|
|
{
|
|
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
|
|
{
|
|
Page = page,
|
|
Size = size,
|
|
TotalCount = totalCount,
|
|
TotalPages = totalPages
|
|
}
|
|
};
|
|
}
|
|
|
|
private static string DetermineSendStatus(int totalSend, int successCount)
|
|
{
|
|
if (totalSend == 0) return "pending";
|
|
return successCount > 0 ? "complete" : "failed";
|
|
}
|
|
|
|
public async Task<MessageInfoResponseDto> GetInfoAsync(long serviceId, MessageInfoRequestDto request)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.MessageCode))
|
|
throw new SpmsException(ErrorCodes.BadRequest, "메시지 코드는 필수입니다.", 400);
|
|
|
|
var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId);
|
|
if (message == null)
|
|
throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404);
|
|
|
|
var variables = ExtractVariables(message.Title, message.Body);
|
|
|
|
object? data = null;
|
|
if (!string.IsNullOrEmpty(message.CustomData))
|
|
{
|
|
data = JsonSerializer.Deserialize<JsonElement>(message.CustomData);
|
|
}
|
|
|
|
return new MessageInfoResponseDto
|
|
{
|
|
MessageCode = message.MessageCode,
|
|
Title = message.Title,
|
|
Body = message.Body,
|
|
ImageUrl = message.ImageUrl,
|
|
LinkUrl = message.LinkUrl,
|
|
LinkType = message.LinkType,
|
|
Data = data,
|
|
Variables = variables,
|
|
IsActive = !message.IsDeleted,
|
|
CreatedAt = message.CreatedAt
|
|
};
|
|
}
|
|
|
|
public async Task DeleteAsync(long serviceId, MessageDeleteRequestDto request)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.MessageCode))
|
|
throw new SpmsException(ErrorCodes.BadRequest, "메시지 코드는 필수입니다.", 400);
|
|
|
|
var message = await _messageRepository.GetByMessageCodeAndServiceAsync(request.MessageCode, serviceId);
|
|
if (message == null)
|
|
throw new SpmsException(ErrorCodes.MessageNotFound, "존재하지 않는 메시지 코드입니다.", 404);
|
|
|
|
message.IsDeleted = true;
|
|
message.DeletedAt = DateTime.UtcNow;
|
|
_messageRepository.Update(message);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task<MessagePreviewResponseDto> PreviewAsync(long serviceId, MessagePreviewRequestDto 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);
|
|
|
|
return new MessagePreviewResponseDto
|
|
{
|
|
Title = title,
|
|
Body = body,
|
|
ImageUrl = message.ImageUrl,
|
|
LinkUrl = message.LinkUrl
|
|
};
|
|
}
|
|
|
|
private async Task<string> GenerateMessageCodeAsync(long serviceId)
|
|
{
|
|
var today = DateTime.UtcNow.Date;
|
|
var seed = (int)(today.Ticks ^ serviceId);
|
|
var random = new Random(seed);
|
|
|
|
var prefix = GenerateRandomChars(random, 3);
|
|
var suffix = GenerateRandomChars(new Random(seed + 1), 3);
|
|
|
|
var sequence = await _messageRepository.GetTodaySequenceAsync(serviceId) + 1;
|
|
if (sequence > 9999)
|
|
throw new SpmsException(ErrorCodes.LimitExceeded, "일일 메시지 생성 한도(9999건)를 초과했습니다.", 429);
|
|
|
|
return $"{prefix}{sequence:D4}{suffix}";
|
|
}
|
|
|
|
private static string GenerateRandomChars(Random random, int length)
|
|
{
|
|
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
var result = new char[length];
|
|
for (var i = 0; i < length; i++)
|
|
result[i] = chars[random.Next(chars.Length)];
|
|
return new string(result);
|
|
}
|
|
|
|
private static List<string> ExtractVariables(string title, string body)
|
|
{
|
|
var variables = new HashSet<string>();
|
|
foreach (Match match in VariablePattern.Matches(title))
|
|
variables.Add(match.Groups[1].Value);
|
|
foreach (Match match in VariablePattern.Matches(body))
|
|
variables.Add(match.Groups[1].Value);
|
|
return variables.ToList();
|
|
}
|
|
|
|
private static string ApplyVariables(string template, Dictionary<string, string>? 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;
|
|
}
|
|
}
|