feat: 메시지 유효성 검사 서비스 구현 (#118)
- MessageValidationService: title/body/image_url/link_url/link_type/data 검증 - POST /v1/in/message/validate 엔드포인트 추가 - MessageController 기반 구성 Closes #118
This commit is contained in:
parent
fc16884d25
commit
ce7b8b3d35
36
SPMS.API/Controllers/MessageController.cs
Normal file
36
SPMS.API/Controllers/MessageController.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
|
using SPMS.Application.DTOs.Message;
|
||||||
|
using SPMS.Application.Interfaces;
|
||||||
|
using SPMS.Domain.Common;
|
||||||
|
|
||||||
|
namespace SPMS.API.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("v1/in/message")]
|
||||||
|
[ApiExplorerSettings(GroupName = "message")]
|
||||||
|
public class MessageController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMessageValidationService _validationService;
|
||||||
|
|
||||||
|
public MessageController(IMessageValidationService validationService)
|
||||||
|
{
|
||||||
|
_validationService = validationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("validate")]
|
||||||
|
[SwaggerOperation(Summary = "메시지 유효성 검사", Description = "메시지 내용의 유효성을 검사합니다.")]
|
||||||
|
public IActionResult ValidateAsync([FromBody] MessageValidateRequestDto request)
|
||||||
|
{
|
||||||
|
var result = _validationService.Validate(request);
|
||||||
|
return Ok(ApiResponse<MessageValidationResultDto>.Success(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long GetServiceId()
|
||||||
|
{
|
||||||
|
if (HttpContext.Items.TryGetValue("ServiceId", out var serviceIdObj) && serviceIdObj is long serviceId)
|
||||||
|
return serviceId;
|
||||||
|
|
||||||
|
throw new Domain.Exceptions.SpmsException(ErrorCodes.BadRequest, "서비스 식별 정보가 없습니다.", 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
SPMS.Application/DTOs/Message/MessageValidateRequestDto.cs
Normal file
20
SPMS.Application/DTOs/Message/MessageValidateRequestDto.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace SPMS.Application.DTOs.Message;
|
||||||
|
|
||||||
|
public class MessageValidateRequestDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Body { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
|
public string? LinkUrl { get; set; }
|
||||||
|
|
||||||
|
public string? LinkType { get; set; }
|
||||||
|
|
||||||
|
public string? Data { get; set; }
|
||||||
|
}
|
||||||
13
SPMS.Application/DTOs/Message/MessageValidationResultDto.cs
Normal file
13
SPMS.Application/DTOs/Message/MessageValidationResultDto.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace SPMS.Application.DTOs.Message;
|
||||||
|
|
||||||
|
public class MessageValidationResultDto
|
||||||
|
{
|
||||||
|
public bool IsValid { get; set; }
|
||||||
|
public List<ValidationErrorDto> Errors { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ValidationErrorDto
|
||||||
|
{
|
||||||
|
public string Field { get; set; } = string.Empty;
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ public static class DependencyInjection
|
||||||
services.AddScoped<IDeviceService, DeviceService>();
|
services.AddScoped<IDeviceService, DeviceService>();
|
||||||
services.AddScoped<IFileService, FileService>();
|
services.AddScoped<IFileService, FileService>();
|
||||||
services.AddScoped<IPushService, PushService>();
|
services.AddScoped<IPushService, PushService>();
|
||||||
|
services.AddSingleton<IMessageValidationService, MessageValidationService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
8
SPMS.Application/Interfaces/IMessageValidationService.cs
Normal file
8
SPMS.Application/Interfaces/IMessageValidationService.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
using SPMS.Application.DTOs.Message;
|
||||||
|
|
||||||
|
namespace SPMS.Application.Interfaces;
|
||||||
|
|
||||||
|
public interface IMessageValidationService
|
||||||
|
{
|
||||||
|
MessageValidationResultDto Validate(MessageValidateRequestDto request);
|
||||||
|
}
|
||||||
107
SPMS.Application/Services/MessageValidationService.cs
Normal file
107
SPMS.Application/Services/MessageValidationService.cs
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using SPMS.Application.DTOs.Message;
|
||||||
|
using SPMS.Application.Interfaces;
|
||||||
|
|
||||||
|
namespace SPMS.Application.Services;
|
||||||
|
|
||||||
|
public class MessageValidationService : IMessageValidationService
|
||||||
|
{
|
||||||
|
private const int MaxTitleLength = 100;
|
||||||
|
private const int MaxBodyLength = 2000;
|
||||||
|
private const int MaxDataSizeBytes = 4096;
|
||||||
|
private static readonly string[] AllowedLinkTypes = ["deeplink", "web", "none"];
|
||||||
|
|
||||||
|
public MessageValidationResultDto Validate(MessageValidateRequestDto request)
|
||||||
|
{
|
||||||
|
var errors = new List<ValidationErrorDto>();
|
||||||
|
|
||||||
|
ValidateTitle(request.Title, errors);
|
||||||
|
ValidateBody(request.Body, errors);
|
||||||
|
ValidateImageUrl(request.ImageUrl, errors);
|
||||||
|
ValidateLinkUrl(request.LinkUrl, errors);
|
||||||
|
ValidateLinkType(request.LinkType, errors);
|
||||||
|
ValidateData(request.Data, errors);
|
||||||
|
|
||||||
|
return new MessageValidationResultDto
|
||||||
|
{
|
||||||
|
IsValid = errors.Count == 0,
|
||||||
|
Errors = errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateTitle(string title, List<ValidationErrorDto> errors)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(title))
|
||||||
|
{
|
||||||
|
errors.Add(new ValidationErrorDto { Field = "title", Message = "제목은 필수입니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title.Length > MaxTitleLength)
|
||||||
|
errors.Add(new ValidationErrorDto { Field = "title", Message = $"제목은 {MaxTitleLength}자를 초과할 수 없습니다." });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateBody(string body, List<ValidationErrorDto> errors)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
|
{
|
||||||
|
errors.Add(new ValidationErrorDto { Field = "body", Message = "본문은 필수입니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.Length > MaxBodyLength)
|
||||||
|
errors.Add(new ValidationErrorDto { Field = "body", Message = $"본문은 {MaxBodyLength}자를 초과할 수 없습니다." });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateImageUrl(string? imageUrl, List<ValidationErrorDto> errors)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(imageUrl))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!Uri.TryCreate(imageUrl, UriKind.Absolute, out var uri) ||
|
||||||
|
(uri.Scheme != "http" && uri.Scheme != "https"))
|
||||||
|
errors.Add(new ValidationErrorDto { Field = "image_url", Message = "유효한 URL 형식이 아닙니다." });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateLinkUrl(string? linkUrl, List<ValidationErrorDto> errors)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(linkUrl))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!Uri.TryCreate(linkUrl, UriKind.Absolute, out _))
|
||||||
|
errors.Add(new ValidationErrorDto { Field = "link_url", Message = "유효한 URL 형식이 아닙니다." });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateLinkType(string? linkType, List<ValidationErrorDto> errors)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(linkType))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!AllowedLinkTypes.Contains(linkType.ToLowerInvariant()))
|
||||||
|
errors.Add(new ValidationErrorDto { Field = "link_type", Message = "link_type은 deeplink, web, none 중 하나여야 합니다." });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateData(string? data, List<ValidationErrorDto> errors)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(data))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(data);
|
||||||
|
if (doc.RootElement.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
errors.Add(new ValidationErrorDto { Field = "data", Message = "data는 JSON 객체여야 합니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
errors.Add(new ValidationErrorDto { Field = "data", Message = "유효한 JSON 형식이 아닙니다." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (System.Text.Encoding.UTF8.GetByteCount(data) > MaxDataSizeBytes)
|
||||||
|
errors.Add(new ValidationErrorDto { Field = "data", Message = $"data는 {MaxDataSizeBytes / 1024}KB를 초과할 수 없습니다." });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user