Merge pull request 'feat: 메시지 유효성 검사 서비스 구현 (#118)' (#119) from feature/#118-message-validation into develop
Some checks failed
SPMS_API/pipeline/head There was a failure building this commit
Some checks failed
SPMS_API/pipeline/head There was a failure building this commit
Reviewed-on: https://git.ipstein.myds.me/SPMS/SPMS_API/pulls/119
This commit is contained in:
commit
f9d3fa0b6f
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<IFileService, FileService>();
|
||||
services.AddScoped<IPushService, PushService>();
|
||||
services.AddSingleton<IMessageValidationService, MessageValidationService>();
|
||||
|
||||
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