using System.Text.Json; using SPMS.Application.DTOs.Message; using SPMS.Application.Interfaces; using SPMS.Domain.Common; 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(); 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 errors) { if (string.IsNullOrWhiteSpace(title)) { errors.Add(new FieldError { Field = "title", Message = "제목은 필수입니다." }); return; } if (title.Length > MaxTitleLength) errors.Add(new FieldError { Field = "title", Message = $"제목은 {MaxTitleLength}자를 초과할 수 없습니다." }); } private static void ValidateBody(string body, List errors) { if (string.IsNullOrWhiteSpace(body)) { errors.Add(new FieldError { Field = "body", Message = "본문은 필수입니다." }); return; } if (body.Length > MaxBodyLength) errors.Add(new FieldError { Field = "body", Message = $"본문은 {MaxBodyLength}자를 초과할 수 없습니다." }); } private static void ValidateImageUrl(string? imageUrl, List errors) { if (string.IsNullOrWhiteSpace(imageUrl)) return; if (!Uri.TryCreate(imageUrl, UriKind.Absolute, out var uri) || (uri.Scheme != "http" && uri.Scheme != "https")) errors.Add(new FieldError { Field = "image_url", Message = "유효한 URL 형식이 아닙니다." }); } private static void ValidateLinkUrl(string? linkUrl, List errors) { if (string.IsNullOrWhiteSpace(linkUrl)) return; if (!Uri.TryCreate(linkUrl, UriKind.Absolute, out _)) errors.Add(new FieldError { Field = "link_url", Message = "유효한 URL 형식이 아닙니다." }); } private static void ValidateLinkType(string? linkType, List errors) { if (string.IsNullOrWhiteSpace(linkType)) return; if (!AllowedLinkTypes.Contains(linkType.ToLowerInvariant())) errors.Add(new FieldError { Field = "link_type", Message = "link_type은 deeplink, web, none 중 하나여야 합니다." }); } private static void ValidateData(object? data, List errors) { 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; 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(jsonString) > MaxDataSizeBytes) errors.Add(new FieldError { Field = "data", Message = $"data는 {MaxDataSizeBytes / 1024}KB를 초과할 수 없습니다." }); } }