Add FluentValidation to application services
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m45s

This commit is contained in:
2026-07-07 16:31:29 +09:00
parent 68cc97eda6
commit c54599304b
9 changed files with 110 additions and 38 deletions
@@ -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>