From 4bc08715fa474a8b52de16d8c618551b1aa4fe3d Mon Sep 17 00:00:00 2001 From: SEAN Date: Tue, 24 Feb 2026 16:24:56 +0900 Subject: [PATCH] =?UTF-8?q?improvement:=20=EA=B3=B5=ED=86=B5=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5/=EC=97=90=EB=9F=AC=20=ED=8F=AC=EB=A7=B7=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20(#164)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FieldError DTO 공통화 (SPMS.Domain/Common) - ValidationErrorData + ApiResponse.ValidationFail() 추가 - InvalidModelStateResponseFactory로 ModelState 에러 ApiResponse 변환 - Controller Unauthorized 응답 throw SpmsException으로 통일 (에러코드 102) - MessageValidationService ValidationErrorDto → FieldError 교체 Closes #164 --- SPMS.API/Controllers/AuthController.cs | 9 ++--- SPMS.API/Controllers/ProfileController.cs | 9 ++--- SPMS.API/Controllers/ServiceController.cs | 5 ++- SPMS.API/Program.cs | 21 ++++++++++- .../Message/MessageValidationResultDto.cs | 10 ++---- .../Services/MessageValidationService.cs | 35 ++++++++++--------- SPMS.Domain/Common/ApiResponse.cs | 12 +++++++ SPMS.Domain/Common/FieldError.cs | 12 +++++++ 8 files changed, 73 insertions(+), 40 deletions(-) create mode 100644 SPMS.Domain/Common/FieldError.cs 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; +} -- 2.45.1