improvement: 메시지 저장/검증 계약 통일 (#222)
All checks were successful
SPMS_API/pipeline/head This commit looks good

Reviewed-on: https://git.ipstein.myds.me/SPMS/SPMS_API/pulls/223
This commit is contained in:
김선규 2026-02-25 05:10:22 +00:00
commit b9b3fa2fc0
3 changed files with 47 additions and 12 deletions

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,27 +82,55 @@ 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; return;
try // object? → JSON 문자열로 변환
string jsonString;
if (data is JsonElement jsonElement)
{ {
using var doc = JsonDocument.Parse(data); if (jsonElement.ValueKind == JsonValueKind.Null || jsonElement.ValueKind == JsonValueKind.Undefined)
if (doc.RootElement.ValueKind != JsonValueKind.Object) return;
if (jsonElement.ValueKind != JsonValueKind.Object)
{ {
errors.Add(new FieldError { Field = "data", Message = "data는 JSON 객체여야 합니다." }); errors.Add(new FieldError { Field = "data", Message = "data는 JSON 객체여야 합니다." });
return; return;
} }
jsonString = jsonElement.GetRawText();
} }
catch (JsonException) else if (data is string strData)
{ {
errors.Add(new FieldError { Field = "data", Message = "유효한 JSON 형식이 아닙니다." }); if (string.IsNullOrWhiteSpace(strData))
return; return;
try
{
using var doc = JsonDocument.Parse(strData);
if (doc.RootElement.ValueKind != JsonValueKind.Object)
{
errors.Add(new FieldError { Field = "data", Message = "data는 JSON 객체여야 합니다." });
return;
}
}
catch (JsonException)
{
errors.Add(new FieldError { Field = "data", Message = "유효한 JSON 형식이 아닙니다." });
return;
}
jsonString = strData;
}
else
{
// 기타 타입은 직렬화하여 검증
jsonString = JsonSerializer.Serialize(data);
} }
if (System.Text.Encoding.UTF8.GetByteCount(data) > MaxDataSizeBytes) 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를 초과할 수 없습니다." });
} }
} }