- 메시지 저장 API (POST /v1/in/message/save)
- 메시지 목록 조회 API (POST /v1/in/message/list)
- 메시지 상세 조회 API (POST /v1/in/message/info)
- 메시지 삭제 API (POST /v1/in/message/delete)
- message_code 자동 생성 (접두3+순번4+접미3)
- 변수 추출 ({{변수명}} 패턴)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
210 lines
7.6 KiB
C#
210 lines
7.6 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.GetPagedByServiceAsync(
|
|
serviceId, page, size,
|
|
m => (request.Keyword == null || m.Title.Contains(request.Keyword) || m.Body.Contains(request.Keyword))
|
|
&& (request.IsActive == null || m.IsDeleted != request.IsActive));
|
|
|
|
var totalPages = (int)Math.Ceiling((double)totalCount / size);
|
|
|
|
return new MessageListResponseDto
|
|
{
|
|
Items = items.Select(m => new MessageSummaryDto
|
|
{
|
|
MessageCode = m.MessageCode,
|
|
Title = m.Title,
|
|
IsActive = !m.IsDeleted,
|
|
CreatedAt = m.CreatedAt
|
|
}).ToList(),
|
|
Pagination = new PaginationDto
|
|
{
|
|
Page = page,
|
|
Size = size,
|
|
TotalCount = totalCount,
|
|
TotalPages = totalPages
|
|
}
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|