improvement: 메시지 저장/검증 계약 통일 (#222) #223

Merged
seonkyu.kim merged 1 commits from improvement/#222-message-validate-contract into develop 2026-02-25 05:10:24 +00:00
3 changed files with 47 additions and 12 deletions
Showing only changes of commit b373d59710 - Show all commits

View File

@ -22,7 +22,7 @@ public class MessageController : ControllerBase
} }
[HttpPost("save")] [HttpPost("save")]
[SwaggerOperation(Summary = "메시지 저장", Description = "메시지 템플릿을 저장합니다. 메시지 코드가 자동 생성됩니다.")] [SwaggerOperation(Summary = "메시지 저장", Description = "메시지 템플릿을 저장합니다. 메시지 코드가 자동 생성됩니다. 필드명은 snake_case(title, body, image_url, link_url, link_type, data)를 사용합니다. 저장 전 validate API로 사전 검증을 권장합니다.")]
public async Task<IActionResult> SaveAsync([FromBody] MessageSaveRequestDto request) public async Task<IActionResult> SaveAsync([FromBody] MessageSaveRequestDto request)
{ {
var serviceId = GetServiceId(); var serviceId = GetServiceId();
@ -59,7 +59,7 @@ public class MessageController : ControllerBase
} }
[HttpPost("validate")] [HttpPost("validate")]
[SwaggerOperation(Summary = "메시지 유효성 검사", Description = "메시지 내용의 유효성을 검사합니다.")] [SwaggerOperation(Summary = "메시지 유효성 검사", Description = "메시지 내용의 유효성을 검사합니다. save API와 동일한 snake_case 필드명(title, body, image_url, link_url, link_type, data)을 사용합니다. 검증 실패 시 data.errors[] field + message .")]
public IActionResult ValidateAsync([FromBody] MessageValidateRequestDto request) public IActionResult ValidateAsync([FromBody] MessageValidateRequestDto request)
{ {
var result = _validationService.Validate(request); var result = _validationService.Validate(request);

View File

@ -1,20 +1,27 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace SPMS.Application.DTOs.Message; namespace SPMS.Application.DTOs.Message;
public class MessageValidateRequestDto public class MessageValidateRequestDto
{ {
[Required] [Required]
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
[Required] [Required]
[JsonPropertyName("body")]
public string Body { get; set; } = string.Empty; public string Body { get; set; } = string.Empty;
[JsonPropertyName("image_url")]
public string? ImageUrl { get; set; } public string? ImageUrl { get; set; }
[JsonPropertyName("link_url")]
public string? LinkUrl { get; set; } public string? LinkUrl { get; set; }
[JsonPropertyName("link_type")]
public string? LinkType { get; set; } public string? LinkType { get; set; }
public string? Data { get; set; } [JsonPropertyName("data")]
public object? Data { get; set; }
} }

View File

@ -82,14 +82,34 @@ public class MessageValidationService : IMessageValidationService
errors.Add(new FieldError { Field = "link_type", Message = "link_type은 deeplink, web, none 중 하나여야 합니다." }); errors.Add(new FieldError { Field = "link_type", Message = "link_type은 deeplink, web, none 중 하나여야 합니다." });
} }
private static void ValidateData(string? data, List<FieldError> errors) private static void ValidateData(object? data, List<FieldError> errors)
{ {
if (string.IsNullOrWhiteSpace(data)) if (data is null)
return;
// object? → JSON 문자열로 변환
string jsonString;
if (data is JsonElement jsonElement)
{
if (jsonElement.ValueKind == JsonValueKind.Null || jsonElement.ValueKind == JsonValueKind.Undefined)
return;
if (jsonElement.ValueKind != JsonValueKind.Object)
{
errors.Add(new FieldError { Field = "data", Message = "data는 JSON 객체여야 합니다." });
return;
}
jsonString = jsonElement.GetRawText();
}
else if (data is string strData)
{
if (string.IsNullOrWhiteSpace(strData))
return; return;
try try
{ {
using var doc = JsonDocument.Parse(data); using var doc = JsonDocument.Parse(strData);
if (doc.RootElement.ValueKind != JsonValueKind.Object) if (doc.RootElement.ValueKind != JsonValueKind.Object)
{ {
errors.Add(new FieldError { Field = "data", Message = "data는 JSON 객체여야 합니다." }); errors.Add(new FieldError { Field = "data", Message = "data는 JSON 객체여야 합니다." });
@ -102,7 +122,15 @@ public class MessageValidationService : IMessageValidationService
return; return;
} }
if (System.Text.Encoding.UTF8.GetByteCount(data) > MaxDataSizeBytes) jsonString = strData;
}
else
{
// 기타 타입은 직렬화하여 검증
jsonString = JsonSerializer.Serialize(data);
}
if (System.Text.Encoding.UTF8.GetByteCount(jsonString) > MaxDataSizeBytes)
errors.Add(new FieldError { Field = "data", Message = $"data는 {MaxDataSizeBytes / 1024}KB를 초과할 수 없습니다." }); errors.Add(new FieldError { Field = "data", Message = $"data는 {MaxDataSizeBytes / 1024}KB를 초과할 수 없습니다." });
} }
} }