feat: harden auth ops and deployment baseline
This commit is contained in:
@@ -2,13 +2,13 @@ namespace TaxBaik.Application.DTOs;
|
||||
|
||||
public class CreateBlogPostDto
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public required string Content { get; set; }
|
||||
public int? CategoryId { get; set; }
|
||||
public string Tags { get; set; }
|
||||
public string SeoTitle { get; set; }
|
||||
public string SeoDescription { get; set; }
|
||||
public string ThumbnailUrl { get; set; }
|
||||
public string? Tags { get; set; }
|
||||
public string? SeoTitle { get; set; }
|
||||
public string? SeoDescription { get; set; }
|
||||
public string? ThumbnailUrl { get; set; }
|
||||
public bool IsPublished { get; set; }
|
||||
public int AuthorId { get; set; }
|
||||
public int? AuthorId { get; set; }
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public class BlogService(IBlogPostRepository repository)
|
||||
|
||||
public async Task<(IEnumerable<BlogPost>, int)> GetPublishedPagedAsync(
|
||||
int page, int pageSize, int? categoryId = null, CancellationToken ct = default) =>
|
||||
await repository.GetPublishedPagedAsync(page, pageSize, categoryId, ct);
|
||||
await repository.GetPublishedPagedAsync(NormalizePage(page), NormalizePageSize(pageSize), categoryId, ct);
|
||||
|
||||
public async Task<IEnumerable<BlogPost>> GetAllAsync(CancellationToken ct = default) =>
|
||||
await repository.GetAllForAdminAsync(ct);
|
||||
@@ -22,8 +22,11 @@ public class BlogService(IBlogPostRepository repository)
|
||||
|
||||
public async Task<int> CreateAsync(BlogPost post, CancellationToken ct = default)
|
||||
{
|
||||
post.Slug = GenerateSlug(post.Title);
|
||||
post.IsPublished = false;
|
||||
ValidatePost(post);
|
||||
post.Title = post.Title.Trim();
|
||||
post.Content = post.Content.Trim();
|
||||
post.Slug = await GenerateUniqueSlugAsync(post.Title, ct: ct);
|
||||
post.PublishedAt = post.IsPublished ? DateTime.UtcNow : null;
|
||||
return await repository.CreateAsync(post, ct);
|
||||
}
|
||||
|
||||
@@ -65,6 +68,10 @@ public class BlogService(IBlogPostRepository repository)
|
||||
post.SeoDescription = dto.SeoDescription;
|
||||
post.ThumbnailUrl = dto.ThumbnailUrl;
|
||||
post.IsPublished = dto.IsPublished;
|
||||
post.PublishedAt = dto.IsPublished
|
||||
? post.PublishedAt ?? DateTime.UtcNow
|
||||
: null;
|
||||
ValidatePost(post);
|
||||
|
||||
await UpdateAsync(post, ct);
|
||||
return post;
|
||||
@@ -81,6 +88,50 @@ public class BlogService(IBlogPostRepository repository)
|
||||
var slug = Regex.Replace(title.ToLowerInvariant(), @"[^\w\s-]", "");
|
||||
slug = Regex.Replace(slug, @"\s+", "-");
|
||||
slug = Regex.Replace(slug, @"-+", "-").Trim('-');
|
||||
if (string.IsNullOrWhiteSpace(slug))
|
||||
slug = $"post-{DateTime.UtcNow:yyyyMMddHHmmss}";
|
||||
return slug.Length > 100 ? slug[..100] : slug;
|
||||
}
|
||||
|
||||
private async Task<string> GenerateUniqueSlugAsync(string title, int? existingPostId = null, CancellationToken ct = default)
|
||||
{
|
||||
var baseSlug = GenerateSlug(title);
|
||||
var slug = baseSlug;
|
||||
var suffix = 2;
|
||||
var allPosts = (await repository.GetAllForAdminAsync(ct)).ToList();
|
||||
|
||||
while (allPosts.Any(x => x.Id != existingPostId && string.Equals(x.Slug, slug, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var suffixText = $"-{suffix++}";
|
||||
var maxBaseLength = Math.Max(1, 100 - suffixText.Length);
|
||||
slug = $"{baseSlug[..Math.Min(baseSlug.Length, maxBaseLength)]}{suffixText}";
|
||||
}
|
||||
|
||||
return slug;
|
||||
}
|
||||
|
||||
private static void ValidatePost(BlogPost post)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(post.Title))
|
||||
throw new ValidationException("제목을 입력하세요.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(post.Content))
|
||||
throw new ValidationException("본문을 입력하세요.");
|
||||
|
||||
if (post.IsPublished && string.IsNullOrWhiteSpace(post.SeoTitle))
|
||||
throw new ValidationException("발행하려면 SEO 제목을 입력하세요.");
|
||||
|
||||
if (post.IsPublished && string.IsNullOrWhiteSpace(post.SeoDescription))
|
||||
throw new ValidationException("발행하려면 SEO 설명을 입력하세요.");
|
||||
}
|
||||
|
||||
private static int NormalizePage(int page) => Math.Max(1, page);
|
||||
|
||||
private static int NormalizePageSize(int pageSize) => Math.Clamp(pageSize, 1, 100);
|
||||
|
||||
public async Task<(int TotalPosts, int PublishedPosts)> GetStatsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var posts = (await repository.GetAllForAdminAsync(ct)).ToList();
|
||||
return (posts.Count, posts.Count(x => x.IsPublished));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ namespace TaxBaik.Application.Services;
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Enums;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class InquiryService(IInquiryRepository repository)
|
||||
@@ -10,7 +11,7 @@ public class InquiryService(IInquiryRepository repository)
|
||||
|
||||
public async Task<int> SubmitAsync(
|
||||
string name, string phone, string serviceType, string message,
|
||||
string? ipAddress = null, CancellationToken ct = default)
|
||||
string? email = null, string? ipAddress = null, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ValidationException("이름을 입력하세요.");
|
||||
@@ -25,10 +26,11 @@ public class InquiryService(IInquiryRepository repository)
|
||||
{
|
||||
Name = name.Trim(),
|
||||
Phone = phone.Trim(),
|
||||
Email = string.IsNullOrWhiteSpace(email) ? null : email.Trim(),
|
||||
ServiceType = serviceType ?? "기타",
|
||||
Message = message.Trim(),
|
||||
IpAddress = ipAddress,
|
||||
Status = "new",
|
||||
Status = InquiryStatusMapper.ToStorageValue(InquiryStatus.New),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
@@ -40,10 +42,30 @@ public class InquiryService(IInquiryRepository repository)
|
||||
|
||||
public async Task<(IEnumerable<Inquiry>, int)> GetPagedAsync(
|
||||
int page, int pageSize, string? status = null, CancellationToken ct = default) =>
|
||||
await repository.GetPagedAsync(page, pageSize, status, ct);
|
||||
await repository.GetPagedAsync(NormalizePage(page), NormalizePageSize(pageSize), NormalizeOptionalStatus(status), ct);
|
||||
|
||||
public async Task UpdateStatusAsync(int id, string status, CancellationToken ct = default) =>
|
||||
await repository.UpdateStatusAsync(id, status, ct);
|
||||
public async Task UpdateStatusAsync(int id, string status, CancellationToken ct = default)
|
||||
{
|
||||
if (!InquiryStatusMapper.TryParse(status, out var parsed))
|
||||
throw new ValidationException("지원하지 않는 문의 상태입니다.");
|
||||
|
||||
await repository.UpdateStatusAsync(id, InquiryStatusMapper.ToStorageValue(parsed), ct);
|
||||
}
|
||||
|
||||
private static int NormalizePage(int page) => Math.Max(1, page);
|
||||
|
||||
private static int NormalizePageSize(int pageSize) => Math.Clamp(pageSize, 1, 100);
|
||||
|
||||
private static string? NormalizeOptionalStatus(string? status)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(status))
|
||||
return null;
|
||||
|
||||
if (!InquiryStatusMapper.TryParse(status, out var parsed))
|
||||
throw new ValidationException("지원하지 않는 문의 상태입니다.");
|
||||
|
||||
return InquiryStatusMapper.ToStorageValue(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
public class ValidationException : Exception
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace TaxBaik.Application.Services;
|
||||
|
||||
using TaxBaik.Domain.Enums;
|
||||
|
||||
public static class InquiryStatusMapper
|
||||
{
|
||||
public static string ToStorageValue(InquiryStatus status) => status switch
|
||||
{
|
||||
InquiryStatus.New => "new",
|
||||
InquiryStatus.Contacted => "contacted",
|
||||
InquiryStatus.Completed => "completed",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null)
|
||||
};
|
||||
|
||||
public static bool TryParse(string? value, out InquiryStatus status)
|
||||
{
|
||||
status = value?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"new" => InquiryStatus.New,
|
||||
"contacted" => InquiryStatus.Contacted,
|
||||
"completed" => InquiryStatus.Completed,
|
||||
_ => default
|
||||
};
|
||||
|
||||
return value?.Trim().ToLowerInvariant() is "new" or "contacted" or "completed";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user