Add FluentValidation to application services
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m45s
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m45s
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
namespace TaxBaik.Application.DTOs;
|
||||
|
||||
using FluentValidation;
|
||||
|
||||
public sealed class CreateBlogPostDtoValidator : AbstractValidator<CreateBlogPostDto>
|
||||
{
|
||||
public CreateBlogPostDtoValidator()
|
||||
{
|
||||
RuleFor(x => x.Title).NotEmpty().MaximumLength(ValidationRules.TitleMaxLength);
|
||||
RuleFor(x => x.Content).NotEmpty().MinimumLength(ValidationRules.MessageMinLength).MaximumLength(ValidationRules.ContentMaxLength);
|
||||
RuleFor(x => x.Tags).MaximumLength(ValidationRules.TagsMaxLength);
|
||||
RuleFor(x => x.SeoTitle).MaximumLength(ValidationRules.SeoTitleMaxLength);
|
||||
RuleFor(x => x.SeoDescription).MaximumLength(ValidationRules.SeoDescriptionMaxLength);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SubmitInquiryDtoValidator : AbstractValidator<SubmitInquiryDto>
|
||||
{
|
||||
public SubmitInquiryDtoValidator()
|
||||
{
|
||||
ApplyInquiryRules(this);
|
||||
}
|
||||
|
||||
internal static void ApplyInquiryRules(AbstractValidator<SubmitInquiryDto> validator) { }
|
||||
}
|
||||
|
||||
public sealed class UpdateInquiryDtoValidator : AbstractValidator<UpdateInquiryDto>
|
||||
{
|
||||
public UpdateInquiryDtoValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(ValidationRules.NameMaxLength);
|
||||
RuleFor(x => x.Phone).NotEmpty().Matches(ValidationRules.KoreanPhonePattern);
|
||||
RuleFor(x => x.Email).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.Email));
|
||||
RuleFor(x => x.ServiceType).MaximumLength(ValidationRules.ServiceTypeMaxLength);
|
||||
RuleFor(x => x.Message).NotEmpty().MinimumLength(ValidationRules.MessageMinLength).MaximumLength(ValidationRules.MessageMaxLength);
|
||||
RuleFor(x => x.Status).NotEmpty();
|
||||
RuleFor(x => x.AdminMemo).MaximumLength(ValidationRules.MemoMaxLength);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CreateClientDtoValidator : AbstractValidator<CreateClientDto>
|
||||
{
|
||||
public CreateClientDtoValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(ValidationRules.NameMaxLength);
|
||||
RuleFor(x => x.CompanyName).MaximumLength(ValidationRules.NameMaxLength);
|
||||
RuleFor(x => x.Phone).MaximumLength(ValidationRules.PhoneMaxLength);
|
||||
RuleFor(x => x.Email).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.Email));
|
||||
RuleFor(x => x.ServiceType).MaximumLength(ValidationRules.ServiceTypeMaxLength);
|
||||
RuleFor(x => x.TaxType).MaximumLength(ValidationRules.ServiceTypeMaxLength);
|
||||
RuleFor(x => x.Status).NotEmpty();
|
||||
RuleFor(x => x.Source).MaximumLength(ValidationRules.ServiceTypeMaxLength);
|
||||
RuleFor(x => x.Memo).MaximumLength(ValidationRules.MemoMaxLength);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
namespace TaxBaik.Application.Services;
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using TaxBaik.Application.DTOs;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
public class BlogService(IBlogPostRepository repository, IMemoryCache memoryCache)
|
||||
public class BlogService(IBlogPostRepository repository, IMemoryCache memoryCache, IValidator<CreateBlogPostDto> validator)
|
||||
{
|
||||
public async Task<BlogPost?> GetByIdAsync(int id, CancellationToken ct = default) =>
|
||||
await repository.GetByIdAsync(id, ct);
|
||||
@@ -60,6 +61,7 @@ public class BlogService(IBlogPostRepository repository, IMemoryCache memoryCach
|
||||
|
||||
public async Task<BlogPost> CreateAsync(CreateBlogPostDto dto, CancellationToken ct = default)
|
||||
{
|
||||
validator.ValidateAndThrow(dto);
|
||||
var post = new BlogPost
|
||||
{
|
||||
Title = dto.Title,
|
||||
@@ -87,6 +89,7 @@ public class BlogService(IBlogPostRepository repository, IMemoryCache memoryCach
|
||||
|
||||
public async Task<BlogPost?> UpdateAsync(int id, CreateBlogPostDto dto, CancellationToken ct = default)
|
||||
{
|
||||
validator.ValidateAndThrow(dto);
|
||||
var post = await repository.GetByIdAsync(id, ct);
|
||||
if (post == null)
|
||||
return null;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
namespace TaxBaik.Application.Services;
|
||||
|
||||
using FluentValidation;
|
||||
using TaxBaik.Application.DTOs;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class ClientService(IClientRepository repository)
|
||||
public class ClientService(IClientRepository repository, IValidator<CreateClientDto> validator)
|
||||
{
|
||||
public async Task<(IEnumerable<Client> Items, int Total)> GetPagedAsync(
|
||||
int page, int pageSize, string? status = null, string? search = null, CancellationToken ct = default) =>
|
||||
@@ -24,8 +25,7 @@ public class ClientService(IClientRepository repository)
|
||||
|
||||
public async Task<int> CreateAsync(CreateClientDto dto, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Name))
|
||||
throw new ValidationException("고객명을 입력하세요.");
|
||||
validator.ValidateAndThrow(dto);
|
||||
|
||||
var client = new Client
|
||||
{
|
||||
@@ -45,8 +45,7 @@ public class ClientService(IClientRepository repository)
|
||||
|
||||
public async Task UpdateAsync(int id, CreateClientDto dto, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Name))
|
||||
throw new ValidationException("고객명을 입력하세요.");
|
||||
validator.ValidateAndThrow(dto);
|
||||
|
||||
var client = await repository.GetByIdAsync(id, ct)
|
||||
?? throw new KeyNotFoundException($"고객 ID {id}를 찾을 수 없습니다.");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
namespace TaxBaik.Application.Services;
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using TaxBaik.Application.DTOs;
|
||||
using TaxBaik.Domain.Entities;
|
||||
@@ -10,7 +11,9 @@ using TaxBaik.Domain.Interfaces;
|
||||
public class InquiryService(
|
||||
IInquiryRepository repository,
|
||||
IInquiryNotificationService notificationService,
|
||||
IMemoryCache memoryCache)
|
||||
IMemoryCache memoryCache,
|
||||
IValidator<SubmitInquiryDto> submitValidator,
|
||||
IValidator<UpdateInquiryDto> updateValidator)
|
||||
{
|
||||
// 한국 전화번호 정규식
|
||||
// 휴대폰: 010~019, 070, 0505~0509
|
||||
@@ -18,15 +21,18 @@ public class InquiryService(
|
||||
private static readonly Regex PhoneRegex = new(
|
||||
@"^(0(?:2|3[1-3]|4[1-4]|5[1-5]|6[1-4]|70|50[5-9]|[7-9](?:\d{1,2})?)\d{7,8}|0\d{9,10})$");
|
||||
|
||||
private const int MinMessageLength = 10;
|
||||
private const int MaxMessageLength = 5000;
|
||||
|
||||
public async Task<int> SubmitAsync(
|
||||
string name, string phone, string serviceType, string message,
|
||||
string? email = null, string? ipAddress = null, bool suppressNotification = false, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ValidationException("이름을 입력하세요.");
|
||||
submitValidator.ValidateAndThrow(new SubmitInquiryDto
|
||||
{
|
||||
Name = name,
|
||||
Phone = phone,
|
||||
Email = email,
|
||||
ServiceType = serviceType,
|
||||
Message = message
|
||||
});
|
||||
|
||||
var cleanPhone = phone?.Replace("-", "").Replace(" ", "").Trim() ?? "";
|
||||
if (!PhoneRegex.IsMatch(cleanPhone) || cleanPhone.Length < 10)
|
||||
@@ -34,15 +40,7 @@ public class InquiryService(
|
||||
|
||||
var formattedPhone = FormatPhoneNumber(cleanPhone);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
throw new ValidationException("문의 내용을 입력하세요.");
|
||||
|
||||
var trimmedMessage = message.Trim();
|
||||
if (trimmedMessage.Length < MinMessageLength)
|
||||
throw new ValidationException($"문의 내용은 최소 {MinMessageLength}자 이상이어야 합니다.");
|
||||
|
||||
if (trimmedMessage.Length > MaxMessageLength)
|
||||
throw new ValidationException($"문의 내용은 최대 {MaxMessageLength}자 이하여야 합니다.");
|
||||
|
||||
var inquiry = new Inquiry
|
||||
{
|
||||
@@ -95,23 +93,13 @@ public class InquiryService(
|
||||
var inquiry = await repository.GetByIdAsync(id, ct);
|
||||
if (inquiry == null)
|
||||
return null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dto.Name))
|
||||
throw new ValidationException("이름을 입력하세요.");
|
||||
updateValidator.ValidateAndThrow(dto);
|
||||
|
||||
var cleanPhone = dto.Phone?.Replace("-", "").Replace(" ", "").Trim() ?? "";
|
||||
if (!PhoneRegex.IsMatch(cleanPhone) || cleanPhone.Length < 10)
|
||||
throw new ValidationException("올바른 전화번호를 입력하세요. (예: 01012345678 또는 010-1234-5678)");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dto.Message))
|
||||
throw new ValidationException("문의 내용을 입력하세요.");
|
||||
|
||||
var trimmedUpdateMessage = dto.Message.Trim();
|
||||
if (trimmedUpdateMessage.Length < MinMessageLength)
|
||||
throw new ValidationException($"문의 내용은 최소 {MinMessageLength}자 이상이어야 합니다.");
|
||||
|
||||
if (trimmedUpdateMessage.Length > MaxMessageLength)
|
||||
throw new ValidationException($"문의 내용은 최대 {MaxMessageLength}자 이하여야 합니다.");
|
||||
|
||||
if (!InquiryStatusMapper.TryParse(dto.Status, out var parsedStatus))
|
||||
throw new ValidationException("지원하지 않는 문의 상태입니다.");
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
Reference in New Issue
Block a user