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.Enums; 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 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 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 = SendStatus.Determine(p.TotalSendCount, p.SuccessCount) }).ToList(), Pagination = new PaginationDto { Page = page, Size = size, TotalCount = totalCount, TotalPages = totalPages } }; } public async Task GetInfoAsync(long serviceId, MessageInfoRequestDto request) { if (string.IsNullOrWhiteSpace(request.MessageCode)) throw new SpmsException(ErrorCodes.BadRequest, "메시지 코드는 필수입니다.", 400); var message = await _messageRepository.GetByMessageCodeWithDetailsAsync(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(message.CustomData); } var (totalSend, successCount) = await _messageRepository.GetSendStatsAsync(message.Id); 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, ServiceName = message.Service.ServiceName, ServiceCode = message.Service.ServiceCode, CreatedByName = message.CreatedByAdmin.Name, LatestSendStatus = SendStatus.Determine(totalSend, successCount), 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 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, LinkType = message.LinkType }; } private async Task 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 ExtractVariables(string title, string body) { var variables = new HashSet(); 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? 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; } }