improvement: 공통 응답/에러 포맷 고정 (#164) #198
|
|
@ -4,6 +4,7 @@ using Swashbuckle.AspNetCore.Annotations;
|
||||||
using SPMS.Application.DTOs.Auth;
|
using SPMS.Application.DTOs.Auth;
|
||||||
using SPMS.Application.Interfaces;
|
using SPMS.Application.Interfaces;
|
||||||
using SPMS.Domain.Common;
|
using SPMS.Domain.Common;
|
||||||
|
using SPMS.Domain.Exceptions;
|
||||||
|
|
||||||
namespace SPMS.API.Controllers;
|
namespace SPMS.API.Controllers;
|
||||||
|
|
||||||
|
|
@ -83,9 +84,7 @@ public class AuthController : ControllerBase
|
||||||
{
|
{
|
||||||
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
||||||
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
||||||
{
|
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
||||||
return Unauthorized(ApiResponse<object>.Fail("101", "인증 정보가 유효하지 않습니다."));
|
|
||||||
}
|
|
||||||
|
|
||||||
await _authService.LogoutAsync(adminId);
|
await _authService.LogoutAsync(adminId);
|
||||||
return Ok(ApiResponse.Success());
|
return Ok(ApiResponse.Success());
|
||||||
|
|
@ -116,9 +115,7 @@ public class AuthController : ControllerBase
|
||||||
{
|
{
|
||||||
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
||||||
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
||||||
{
|
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
||||||
return Unauthorized(ApiResponse<object>.Fail("101", "인증 정보가 유효하지 않습니다."));
|
|
||||||
}
|
|
||||||
|
|
||||||
await _authService.ChangePasswordAsync(adminId, request);
|
await _authService.ChangePasswordAsync(adminId, request);
|
||||||
return Ok(ApiResponse.Success());
|
return Ok(ApiResponse.Success());
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using Swashbuckle.AspNetCore.Annotations;
|
||||||
using SPMS.Application.DTOs.Account;
|
using SPMS.Application.DTOs.Account;
|
||||||
using SPMS.Application.Interfaces;
|
using SPMS.Application.Interfaces;
|
||||||
using SPMS.Domain.Common;
|
using SPMS.Domain.Common;
|
||||||
|
using SPMS.Domain.Exceptions;
|
||||||
|
|
||||||
namespace SPMS.API.Controllers;
|
namespace SPMS.API.Controllers;
|
||||||
|
|
||||||
|
|
@ -30,9 +31,7 @@ public class ProfileController : ControllerBase
|
||||||
{
|
{
|
||||||
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
||||||
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
||||||
{
|
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
||||||
return Unauthorized(ApiResponse<object>.Fail("101", "인증 정보가 유효하지 않습니다."));
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _authService.GetProfileAsync(adminId);
|
var result = await _authService.GetProfileAsync(adminId);
|
||||||
return Ok(ApiResponse<ProfileResponseDto>.Success(result));
|
return Ok(ApiResponse<ProfileResponseDto>.Success(result));
|
||||||
|
|
@ -49,9 +48,7 @@ public class ProfileController : ControllerBase
|
||||||
{
|
{
|
||||||
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
||||||
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
||||||
{
|
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
||||||
return Unauthorized(ApiResponse<object>.Fail("101", "인증 정보가 유효하지 않습니다."));
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _authService.UpdateProfileAsync(adminId, request);
|
var result = await _authService.UpdateProfileAsync(adminId, request);
|
||||||
return Ok(ApiResponse<ProfileResponseDto>.Success(result));
|
return Ok(ApiResponse<ProfileResponseDto>.Success(result));
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using Swashbuckle.AspNetCore.Annotations;
|
||||||
using SPMS.Application.DTOs.Service;
|
using SPMS.Application.DTOs.Service;
|
||||||
using SPMS.Application.Interfaces;
|
using SPMS.Application.Interfaces;
|
||||||
using SPMS.Domain.Common;
|
using SPMS.Domain.Common;
|
||||||
|
using SPMS.Domain.Exceptions;
|
||||||
|
|
||||||
namespace SPMS.API.Controllers;
|
namespace SPMS.API.Controllers;
|
||||||
|
|
||||||
|
|
@ -33,9 +34,7 @@ public class ServiceController : ControllerBase
|
||||||
{
|
{
|
||||||
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
||||||
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
||||||
{
|
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
|
||||||
return Unauthorized(ApiResponse<object>.Fail("101", "인증 정보가 유효하지 않습니다."));
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _serviceManagementService.CreateAsync(request, adminId);
|
var result = await _serviceManagementService.CreateAsync(request, adminId);
|
||||||
return Ok(ApiResponse<CreateServiceResponseDto>.Success(result));
|
return Ok(ApiResponse<CreateServiceResponseDto>.Success(result));
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,26 @@ builder.Services.AddApplication();
|
||||||
builder.Services.AddInfrastructure(builder.Configuration);
|
builder.Services.AddInfrastructure(builder.Configuration);
|
||||||
|
|
||||||
// ===== 3. Presentation =====
|
// ===== 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.AddSwaggerDocumentation();
|
||||||
builder.Services.AddJwtAuthentication(builder.Configuration);
|
builder.Services.AddJwtAuthentication(builder.Configuration);
|
||||||
builder.Services.AddAuthorizationPolicies();
|
builder.Services.AddAuthorizationPolicies();
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
|
using SPMS.Domain.Common;
|
||||||
|
|
||||||
namespace SPMS.Application.DTOs.Message;
|
namespace SPMS.Application.DTOs.Message;
|
||||||
|
|
||||||
public class MessageValidationResultDto
|
public class MessageValidationResultDto
|
||||||
{
|
{
|
||||||
public bool IsValid { get; set; }
|
public bool IsValid { get; set; }
|
||||||
public List<ValidationErrorDto> Errors { get; set; } = [];
|
public List<FieldError> Errors { get; set; } = [];
|
||||||
}
|
|
||||||
|
|
||||||
public class ValidationErrorDto
|
|
||||||
{
|
|
||||||
public string Field { get; set; } = string.Empty;
|
|
||||||
public string Message { get; set; } = string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using SPMS.Application.DTOs.Message;
|
using SPMS.Application.DTOs.Message;
|
||||||
using SPMS.Application.Interfaces;
|
using SPMS.Application.Interfaces;
|
||||||
|
using SPMS.Domain.Common;
|
||||||
|
|
||||||
namespace SPMS.Application.Services;
|
namespace SPMS.Application.Services;
|
||||||
|
|
||||||
|
|
@ -13,7 +14,7 @@ public class MessageValidationService : IMessageValidationService
|
||||||
|
|
||||||
public MessageValidationResultDto Validate(MessageValidateRequestDto request)
|
public MessageValidationResultDto Validate(MessageValidateRequestDto request)
|
||||||
{
|
{
|
||||||
var errors = new List<ValidationErrorDto>();
|
var errors = new List<FieldError>();
|
||||||
|
|
||||||
ValidateTitle(request.Title, errors);
|
ValidateTitle(request.Title, errors);
|
||||||
ValidateBody(request.Body, 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))
|
if (string.IsNullOrWhiteSpace(title))
|
||||||
{
|
{
|
||||||
errors.Add(new ValidationErrorDto { Field = "title", Message = "제목은 필수입니다." });
|
errors.Add(new FieldError { Field = "title", Message = "제목은 필수입니다." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (title.Length > MaxTitleLength)
|
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))
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
{
|
{
|
||||||
errors.Add(new ValidationErrorDto { Field = "body", Message = "본문은 필수입니다." });
|
errors.Add(new FieldError { Field = "body", Message = "본문은 필수입니다." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.Length > MaxBodyLength)
|
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))
|
if (string.IsNullOrWhiteSpace(imageUrl))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!Uri.TryCreate(imageUrl, UriKind.Absolute, out var uri) ||
|
if (!Uri.TryCreate(imageUrl, UriKind.Absolute, out var uri) ||
|
||||||
(uri.Scheme != "http" && uri.Scheme != "https"))
|
(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))
|
if (string.IsNullOrWhiteSpace(linkUrl))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!Uri.TryCreate(linkUrl, UriKind.Absolute, out _))
|
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))
|
if (string.IsNullOrWhiteSpace(linkType))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!AllowedLinkTypes.Contains(linkType.ToLowerInvariant()))
|
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))
|
if (string.IsNullOrWhiteSpace(data))
|
||||||
return;
|
return;
|
||||||
|
|
@ -91,17 +92,17 @@ public class MessageValidationService : IMessageValidationService
|
||||||
using var doc = JsonDocument.Parse(data);
|
using var doc = JsonDocument.Parse(data);
|
||||||
if (doc.RootElement.ValueKind != JsonValueKind.Object)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (JsonException)
|
catch (JsonException)
|
||||||
{
|
{
|
||||||
errors.Add(new ValidationErrorDto { Field = "data", Message = "유효한 JSON 형식이 아닙니다." });
|
errors.Add(new FieldError { Field = "data", Message = "유효한 JSON 형식이 아닙니다." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (System.Text.Encoding.UTF8.GetByteCount(data) > MaxDataSizeBytes)
|
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)
|
public new static ApiResponse<T> Fail(string code, string msg)
|
||||||
=> new() { Result = false, Code = code, Msg = 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