improvement: 공통 응답/에러 포맷 고정 (#164)
All checks were successful
SPMS_API/pipeline/head This commit looks good
All checks were successful
SPMS_API/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/SPMS/SPMS_API/pulls/198
This commit is contained in:
commit
a37e57f789
|
|
@ -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<object>.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<object>.Fail("101", "인증 정보가 유효하지 않습니다."));
|
||||
}
|
||||
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
||||
|
||||
await _authService.ChangePasswordAsync(adminId, request);
|
||||
return Ok(ApiResponse.Success());
|
||||
|
|
|
|||
|
|
@ -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<object>.Fail("101", "인증 정보가 유효하지 않습니다."));
|
||||
}
|
||||
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
||||
|
||||
var result = await _authService.GetProfileAsync(adminId);
|
||||
return Ok(ApiResponse<ProfileResponseDto>.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<object>.Fail("101", "인증 정보가 유효하지 않습니다."));
|
||||
}
|
||||
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
||||
|
||||
var result = await _authService.UpdateProfileAsync(adminId, request);
|
||||
return Ok(ApiResponse<ProfileResponseDto>.Success(result));
|
||||
|
|
|
|||
|
|
@ -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<object>.Fail("101", "인증 정보가 유효하지 않습니다."));
|
||||
}
|
||||
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
||||
|
||||
var result = await _serviceManagementService.CreateAsync(request, adminId);
|
||||
return Ok(ApiResponse<CreateServiceResponseDto>.Success(result));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
using SPMS.Domain.Common;
|
||||
|
||||
namespace SPMS.Application.DTOs.Message;
|
||||
|
||||
public class MessageValidationResultDto
|
||||
{
|
||||
public bool IsValid { get; set; }
|
||||
public List<ValidationErrorDto> Errors { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ValidationErrorDto
|
||||
{
|
||||
public string Field { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public List<FieldError> Errors { get; set; } = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ValidationErrorDto>();
|
||||
var errors = new List<FieldError>();
|
||||
|
||||
ValidateTitle(request.Title, errors);
|
||||
ValidateBody(request.Body, errors);
|
||||
|
|
@ -29,59 +30,59 @@ public class MessageValidationService : IMessageValidationService
|
|||
};
|
||||
}
|
||||
|
||||
private static void ValidateTitle(string title, List<ValidationErrorDto> errors)
|
||||
private static void ValidateTitle(string title, List<FieldError> 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<ValidationErrorDto> errors)
|
||||
private static void ValidateBody(string body, List<FieldError> 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<ValidationErrorDto> errors)
|
||||
private static void ValidateImageUrl(string? imageUrl, List<FieldError> 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<ValidationErrorDto> errors)
|
||||
private static void ValidateLinkUrl(string? linkUrl, List<FieldError> 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<ValidationErrorDto> errors)
|
||||
private static void ValidateLinkType(string? linkType, List<FieldError> 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<ValidationErrorDto> errors)
|
||||
private static void ValidateData(string? data, List<FieldError> 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를 초과할 수 없습니다." });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,3 +34,15 @@ public class ApiResponse<T> : ApiResponse
|
|||
public new static ApiResponse<T> Fail(string code, string msg)
|
||||
=> new() { Result = false, Code = code, Msg = msg };
|
||||
}
|
||||
|
||||
public class ValidationErrorData
|
||||
{
|
||||
[JsonPropertyName("fieldErrors")]
|
||||
public List<FieldError> FieldErrors { get; init; } = [];
|
||||
}
|
||||
|
||||
public static class ApiResponseExtensions
|
||||
{
|
||||
public static ApiResponse<ValidationErrorData> ValidationFail(string code, string msg, List<FieldError> fieldErrors)
|
||||
=> new() { Result = false, Code = code, Msg = msg, Data = new ValidationErrorData { FieldErrors = fieldErrors } };
|
||||
}
|
||||
|
|
|
|||
12
SPMS.Domain/Common/FieldError.cs
Normal file
12
SPMS.Domain/Common/FieldError.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user