diff --git a/SPMS.API/Controllers/AuthController.cs b/SPMS.API/Controllers/AuthController.cs index 5ea9567..907079b 100644 --- a/SPMS.API/Controllers/AuthController.cs +++ b/SPMS.API/Controllers/AuthController.cs @@ -4,6 +4,7 @@ using Swashbuckle.AspNetCore.Annotations; using SPMS.Application.DTOs.Auth; using SPMS.Application.Interfaces; using SPMS.Domain.Common; +using SPMS.Domain.Exceptions; namespace SPMS.API.Controllers; @@ -83,9 +84,7 @@ public class AuthController : ControllerBase { var adminIdClaim = User.FindFirst("adminId")?.Value; if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId)) - { - return Unauthorized(ApiResponse.Fail("101", "인증 정보가 유효하지 않습니다.")); - } + throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다."); await _authService.LogoutAsync(adminId); return Ok(ApiResponse.Success()); @@ -116,9 +115,7 @@ public class AuthController : ControllerBase { var adminIdClaim = User.FindFirst("adminId")?.Value; if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId)) - { - return Unauthorized(ApiResponse.Fail("101", "인증 정보가 유효하지 않습니다.")); - } + throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다."); await _authService.ChangePasswordAsync(adminId, request); return Ok(ApiResponse.Success()); diff --git a/SPMS.API/Controllers/ProfileController.cs b/SPMS.API/Controllers/ProfileController.cs index b5c0795..db8479f 100644 --- a/SPMS.API/Controllers/ProfileController.cs +++ b/SPMS.API/Controllers/ProfileController.cs @@ -4,6 +4,7 @@ using Swashbuckle.AspNetCore.Annotations; using SPMS.Application.DTOs.Account; using SPMS.Application.Interfaces; using SPMS.Domain.Common; +using SPMS.Domain.Exceptions; namespace SPMS.API.Controllers; @@ -30,9 +31,7 @@ public class ProfileController : ControllerBase { var adminIdClaim = User.FindFirst("adminId")?.Value; if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId)) - { - return Unauthorized(ApiResponse.Fail("101", "인증 정보가 유효하지 않습니다.")); - } + throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다."); var result = await _authService.GetProfileAsync(adminId); return Ok(ApiResponse.Success(result)); @@ -49,9 +48,7 @@ public class ProfileController : ControllerBase { var adminIdClaim = User.FindFirst("adminId")?.Value; if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId)) - { - return Unauthorized(ApiResponse.Fail("101", "인증 정보가 유효하지 않습니다.")); - } + throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다."); var result = await _authService.UpdateProfileAsync(adminId, request); return Ok(ApiResponse.Success(result)); diff --git a/SPMS.API/Controllers/ServiceController.cs b/SPMS.API/Controllers/ServiceController.cs index 36eb0df..0f8801e 100644 --- a/SPMS.API/Controllers/ServiceController.cs +++ b/SPMS.API/Controllers/ServiceController.cs @@ -4,6 +4,7 @@ using Swashbuckle.AspNetCore.Annotations; using SPMS.Application.DTOs.Service; using SPMS.Application.Interfaces; using SPMS.Domain.Common; +using SPMS.Domain.Exceptions; namespace SPMS.API.Controllers; @@ -33,9 +34,7 @@ public class ServiceController : ControllerBase { var adminIdClaim = User.FindFirst("adminId")?.Value; if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId)) - { - return Unauthorized(ApiResponse.Fail("101", "인증 정보가 유효하지 않습니다.")); - } + throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다."); var result = await _serviceManagementService.CreateAsync(request, adminId); return Ok(ApiResponse.Success(result)); diff --git a/SPMS.API/Program.cs b/SPMS.API/Program.cs index 26dfdd7..d9831c5 100644 --- a/SPMS.API/Program.cs +++ b/SPMS.API/Program.cs @@ -20,7 +20,26 @@ builder.Services.AddApplication(); builder.Services.AddInfrastructure(builder.Configuration); // ===== 3. Presentation ===== -builder.Services.AddControllers(); +builder.Services.AddControllers() + .ConfigureApiBehaviorOptions(options => + { + options.InvalidModelStateResponseFactory = context => + { + var fieldErrors = context.ModelState + .Where(e => e.Value?.Errors.Count > 0) + .SelectMany(e => e.Value!.Errors.Select(err => new FieldError + { + Field = e.Key, + Message = err.ErrorMessage + })) + .ToList(); + + var response = ApiResponseExtensions.ValidationFail( + ErrorCodes.BadRequest, "입력값 검증 실패", fieldErrors); + + return new Microsoft.AspNetCore.Mvc.BadRequestObjectResult(response); + }; + }); builder.Services.AddSwaggerDocumentation(); builder.Services.AddJwtAuthentication(builder.Configuration); builder.Services.AddAuthorizationPolicies(); diff --git a/SPMS.Application/DTOs/Message/MessageValidationResultDto.cs b/SPMS.Application/DTOs/Message/MessageValidationResultDto.cs index 67296db..c8427d0 100644 --- a/SPMS.Application/DTOs/Message/MessageValidationResultDto.cs +++ b/SPMS.Application/DTOs/Message/MessageValidationResultDto.cs @@ -1,13 +1,9 @@ +using SPMS.Domain.Common; + namespace SPMS.Application.DTOs.Message; public class MessageValidationResultDto { public bool IsValid { get; set; } - public List Errors { get; set; } = []; -} - -public class ValidationErrorDto -{ - public string Field { get; set; } = string.Empty; - public string Message { get; set; } = string.Empty; + public List Errors { get; set; } = []; } diff --git a/SPMS.Application/Services/MessageValidationService.cs b/SPMS.Application/Services/MessageValidationService.cs index 24ec45e..bb2f513 100644 --- a/SPMS.Application/Services/MessageValidationService.cs +++ b/SPMS.Application/Services/MessageValidationService.cs @@ -1,6 +1,7 @@ using System.Text.Json; using SPMS.Application.DTOs.Message; using SPMS.Application.Interfaces; +using SPMS.Domain.Common; namespace SPMS.Application.Services; @@ -13,7 +14,7 @@ public class MessageValidationService : IMessageValidationService public MessageValidationResultDto Validate(MessageValidateRequestDto request) { - var errors = new List(); + var errors = new List(); ValidateTitle(request.Title, errors); ValidateBody(request.Body, errors); @@ -29,59 +30,59 @@ public class MessageValidationService : IMessageValidationService }; } - private static void ValidateTitle(string title, List errors) + private static void ValidateTitle(string title, List errors) { if (string.IsNullOrWhiteSpace(title)) { - errors.Add(new ValidationErrorDto { Field = "title", Message = "제목은 필수입니다." }); + errors.Add(new FieldError { Field = "title", Message = "제목은 필수입니다." }); return; } if (title.Length > MaxTitleLength) - errors.Add(new ValidationErrorDto { Field = "title", Message = $"제목은 {MaxTitleLength}자를 초과할 수 없습니다." }); + errors.Add(new FieldError { Field = "title", Message = $"제목은 {MaxTitleLength}자를 초과할 수 없습니다." }); } - private static void ValidateBody(string body, List errors) + private static void ValidateBody(string body, List errors) { if (string.IsNullOrWhiteSpace(body)) { - errors.Add(new ValidationErrorDto { Field = "body", Message = "본문은 필수입니다." }); + errors.Add(new FieldError { Field = "body", Message = "본문은 필수입니다." }); return; } if (body.Length > MaxBodyLength) - errors.Add(new ValidationErrorDto { Field = "body", Message = $"본문은 {MaxBodyLength}자를 초과할 수 없습니다." }); + errors.Add(new FieldError { Field = "body", Message = $"본문은 {MaxBodyLength}자를 초과할 수 없습니다." }); } - private static void ValidateImageUrl(string? imageUrl, List errors) + 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 ValidationErrorDto { Field = "image_url", Message = "유효한 URL 형식이 아닙니다." }); + errors.Add(new FieldError { Field = "image_url", Message = "유효한 URL 형식이 아닙니다." }); } - private static void ValidateLinkUrl(string? linkUrl, List errors) + private static void ValidateLinkUrl(string? linkUrl, List errors) { if (string.IsNullOrWhiteSpace(linkUrl)) return; if (!Uri.TryCreate(linkUrl, UriKind.Absolute, out _)) - errors.Add(new ValidationErrorDto { Field = "link_url", Message = "유효한 URL 형식이 아닙니다." }); + errors.Add(new FieldError { Field = "link_url", Message = "유효한 URL 형식이 아닙니다." }); } - private static void ValidateLinkType(string? linkType, List errors) + private static void ValidateLinkType(string? linkType, List errors) { if (string.IsNullOrWhiteSpace(linkType)) return; if (!AllowedLinkTypes.Contains(linkType.ToLowerInvariant())) - errors.Add(new ValidationErrorDto { 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 errors) + private static void ValidateData(string? data, List errors) { if (string.IsNullOrWhiteSpace(data)) return; @@ -91,17 +92,17 @@ public class MessageValidationService : IMessageValidationService using var doc = JsonDocument.Parse(data); if (doc.RootElement.ValueKind != JsonValueKind.Object) { - errors.Add(new ValidationErrorDto { Field = "data", Message = "data는 JSON 객체여야 합니다." }); + errors.Add(new FieldError { Field = "data", Message = "data는 JSON 객체여야 합니다." }); return; } } catch (JsonException) { - errors.Add(new ValidationErrorDto { Field = "data", Message = "유효한 JSON 형식이 아닙니다." }); + errors.Add(new FieldError { Field = "data", Message = "유효한 JSON 형식이 아닙니다." }); return; } if (System.Text.Encoding.UTF8.GetByteCount(data) > MaxDataSizeBytes) - errors.Add(new ValidationErrorDto { Field = "data", Message = $"data는 {MaxDataSizeBytes / 1024}KB를 초과할 수 없습니다." }); + errors.Add(new FieldError { Field = "data", Message = $"data는 {MaxDataSizeBytes / 1024}KB를 초과할 수 없습니다." }); } } diff --git a/SPMS.Domain/Common/ApiResponse.cs b/SPMS.Domain/Common/ApiResponse.cs index a6282d2..744746a 100644 --- a/SPMS.Domain/Common/ApiResponse.cs +++ b/SPMS.Domain/Common/ApiResponse.cs @@ -34,3 +34,15 @@ public class ApiResponse : ApiResponse public new static ApiResponse Fail(string code, string msg) => new() { Result = false, Code = code, Msg = msg }; } + +public class ValidationErrorData +{ + [JsonPropertyName("fieldErrors")] + public List FieldErrors { get; init; } = []; +} + +public static class ApiResponseExtensions +{ + public static ApiResponse ValidationFail(string code, string msg, List fieldErrors) + => new() { Result = false, Code = code, Msg = msg, Data = new ValidationErrorData { FieldErrors = fieldErrors } }; +} diff --git a/SPMS.Domain/Common/FieldError.cs b/SPMS.Domain/Common/FieldError.cs new file mode 100644 index 0000000..a0af310 --- /dev/null +++ b/SPMS.Domain/Common/FieldError.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace SPMS.Domain.Common; + +public class FieldError +{ + [JsonPropertyName("field")] + public string Field { get; init; } = string.Empty; + + [JsonPropertyName("message")] + public string Message { get; init; } = string.Empty; +}