d59440efbc
TaxBaik CI/CD / build-and-deploy (push) Has been cancelled
New validation service for ensuring feed consistency: - SitemapValidationService: Complete feed validation • URL format validation (protocol, domain, scheme) • Duplicate URL detection • Blog post date validation • Sitemap ↔ RSS consistency checks - ValidationEndpoints (FastEndpoints): Admin API • GET /api/admin/validate/sitemap • GET /api/admin/validate/rss • GET /api/admin/validate/consistency Validation checks: ✓ URL validity (Uri.TryCreate) ✓ HTTPS protocol ✓ Correct domain ✓ GUID validity ✓ RFC 2822 date format ✓ Required fields ✓ Duplicate detection ✓ Post count consistency Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
261 lines
8.3 KiB
C#
261 lines
8.3 KiB
C#
using System.Text.RegularExpressions;
|
|
using TaxBaik.Application.Services;
|
|
|
|
namespace TaxBaik.Web.Services;
|
|
|
|
/// <summary>
|
|
/// Sitemap 및 RSS 피드의 정합성 검증
|
|
/// </summary>
|
|
public class SitemapValidationService
|
|
{
|
|
private readonly BlogService _blogService;
|
|
private readonly ILogger<SitemapValidationService> _logger;
|
|
|
|
public SitemapValidationService(BlogService blogService, ILogger<SitemapValidationService> logger)
|
|
{
|
|
_blogService = blogService;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sitemap 검증 결과
|
|
/// </summary>
|
|
public class SitemapValidationResult
|
|
{
|
|
public bool IsValid { get; set; }
|
|
public List<string> Errors { get; set; } = [];
|
|
public List<string> Warnings { get; set; } = [];
|
|
public int TotalUrls { get; set; }
|
|
public int BlogPostUrls { get; set; }
|
|
public DateTime ValidatedAt { get; set; } = DateTime.UtcNow;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sitemap 전체 검증
|
|
/// </summary>
|
|
public async Task<SitemapValidationResult> ValidateSitemapAsync()
|
|
{
|
|
var result = new SitemapValidationResult();
|
|
|
|
try
|
|
{
|
|
// 1. 정적 페이지 검증
|
|
var staticUrls = GetStaticUrls();
|
|
ValidateUrls(staticUrls, result);
|
|
|
|
result.TotalUrls = staticUrls.Count;
|
|
|
|
// 2. 동적 블로그 포스트 검증
|
|
var (posts, _) = await _blogService.GetPublishedPagedAsync(1, 1000, categoryId: null, ct: default);
|
|
var blogUrls = posts.Select(p => $"https://www.taxbaik.com/taxbaik/blog/{p.Slug}").ToList();
|
|
|
|
ValidateUrls(blogUrls, result);
|
|
|
|
result.TotalUrls += blogUrls.Count;
|
|
result.BlogPostUrls = blogUrls.Count;
|
|
|
|
// 3. 중복 검증
|
|
var allUrls = staticUrls.Concat(blogUrls).ToList();
|
|
var duplicates = allUrls.GroupBy(u => u)
|
|
.Where(g => g.Count() > 1)
|
|
.Select(g => g.Key)
|
|
.ToList();
|
|
|
|
if (duplicates.Any())
|
|
{
|
|
result.Errors.Add($"중복 URL 발견: {string.Join(", ", duplicates)}");
|
|
}
|
|
|
|
// 4. 블로그 포스트 날짜 검증
|
|
foreach (var post in posts)
|
|
{
|
|
if (post.PublishedAt == null || post.PublishedAt == default)
|
|
{
|
|
result.Warnings.Add($"포스트 '{post.Slug}'의 발행일이 없습니다.");
|
|
}
|
|
}
|
|
|
|
result.IsValid = !result.Errors.Any();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Errors.Add($"검증 실패: {ex.Message}");
|
|
result.IsValid = false;
|
|
}
|
|
|
|
_logger.LogInformation("Sitemap 검증 완료: {IsValid}, 총 {TotalUrls}개 URL, {BlogPostUrls}개 포스트",
|
|
result.IsValid, result.TotalUrls, result.BlogPostUrls);
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// RSS 피드 검증
|
|
/// </summary>
|
|
public async Task<SitemapValidationResult> ValidateRssFeedAsync()
|
|
{
|
|
var result = new SitemapValidationResult();
|
|
|
|
try
|
|
{
|
|
var (posts, _) = await _blogService.GetPublishedPagedAsync(1, 50, categoryId: null, ct: default);
|
|
|
|
if (!posts.Any())
|
|
{
|
|
result.Warnings.Add("RSS 피드에 포스트가 없습니다.");
|
|
}
|
|
|
|
// 필수 필드 검증
|
|
var postsList = posts.ToList();
|
|
foreach (var post in postsList)
|
|
{
|
|
var errors = new List<string>();
|
|
|
|
if (string.IsNullOrEmpty(post.Title))
|
|
errors.Add("제목 없음");
|
|
|
|
if (string.IsNullOrEmpty(post.Slug))
|
|
errors.Add("Slug 없음");
|
|
|
|
if (string.IsNullOrEmpty(post.Content))
|
|
errors.Add("콘텐츠 없음");
|
|
|
|
if (post.PublishedAt == null || post.PublishedAt == default)
|
|
errors.Add("발행일 없음");
|
|
|
|
if (errors.Any())
|
|
{
|
|
result.Errors.Add($"포스트 '{post.Title}': {string.Join(", ", errors)}");
|
|
}
|
|
|
|
// GUID 유효성 (URL 형식)
|
|
var guid = $"https://www.taxbaik.com/taxbaik/blog/{post.Slug}";
|
|
if (!Uri.TryCreate(guid, UriKind.Absolute, out _))
|
|
{
|
|
result.Errors.Add($"GUID 유효하지 않음: {guid}");
|
|
}
|
|
|
|
// pubDate 형식 검증 (RFC 2822)
|
|
if (post.PublishedAt.HasValue)
|
|
{
|
|
try
|
|
{
|
|
var rfc2822 = post.PublishedAt.Value.ToString("R"); // RFC 2822 형식
|
|
if (string.IsNullOrEmpty(rfc2822))
|
|
result.Warnings.Add($"포스트 '{post.Slug}' pubDate 형식 변환 실패");
|
|
}
|
|
catch
|
|
{
|
|
result.Errors.Add($"포스트 '{post.Slug}' pubDate 형식 오류");
|
|
}
|
|
}
|
|
}
|
|
|
|
result.TotalUrls = postsList.Count;
|
|
result.IsValid = !result.Errors.Any();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Errors.Add($"RSS 검증 실패: {ex.Message}");
|
|
result.IsValid = false;
|
|
}
|
|
|
|
_logger.LogInformation("RSS 검증 완료: {IsValid}, 총 {TotalUrls}개 포스트",
|
|
result.IsValid, result.TotalUrls);
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sitemap과 RSS의 일관성 검증
|
|
/// </summary>
|
|
public async Task<SitemapValidationResult> ValidateConsistencyAsync()
|
|
{
|
|
var result = new SitemapValidationResult();
|
|
|
|
try
|
|
{
|
|
var sitemapValidation = await ValidateSitemapAsync();
|
|
var rssValidation = await ValidateRssFeedAsync();
|
|
|
|
// 에러 통합
|
|
result.Errors.AddRange(sitemapValidation.Errors);
|
|
result.Errors.AddRange(rssValidation.Errors);
|
|
|
|
// 경고 통합
|
|
result.Warnings.AddRange(sitemapValidation.Warnings);
|
|
result.Warnings.AddRange(rssValidation.Warnings);
|
|
|
|
// 블로그 포스트 수 비교
|
|
if (sitemapValidation.BlogPostUrls != rssValidation.TotalUrls)
|
|
{
|
|
result.Warnings.Add(
|
|
$"Sitemap의 블로그 포스트({sitemapValidation.BlogPostUrls})와 " +
|
|
$"RSS 포스트({rssValidation.TotalUrls}) 수가 다릅니다. " +
|
|
$"(Sitemap은 전체, RSS는 최근 50개)"
|
|
);
|
|
}
|
|
|
|
result.IsValid = !result.Errors.Any();
|
|
result.TotalUrls = sitemapValidation.TotalUrls;
|
|
result.BlogPostUrls = sitemapValidation.BlogPostUrls;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Errors.Add($"일관성 검증 실패: {ex.Message}");
|
|
result.IsValid = false;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// URL 형식 검증
|
|
/// </summary>
|
|
private static void ValidateUrls(List<string> urls, SitemapValidationResult result)
|
|
{
|
|
foreach (var url in urls)
|
|
{
|
|
// URL 형식 검증
|
|
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
|
{
|
|
result.Errors.Add($"유효하지 않은 URL: {url}");
|
|
continue;
|
|
}
|
|
|
|
// HTTPS 검증
|
|
if (uri.Scheme != "https")
|
|
{
|
|
result.Warnings.Add($"HTTPS가 아닌 URL: {url}");
|
|
}
|
|
|
|
// 도메인 검증
|
|
if (uri.Host != "www.taxbaik.com" && uri.Host != "taxbaik.com")
|
|
{
|
|
result.Errors.Add($"잘못된 도메인: {url}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 정적 페이지 URL 목록
|
|
/// </summary>
|
|
private static List<string> GetStaticUrls()
|
|
{
|
|
var baseUrl = "https://www.taxbaik.com/taxbaik";
|
|
return new List<string>
|
|
{
|
|
$"{baseUrl}",
|
|
$"{baseUrl}/about",
|
|
$"{baseUrl}/services",
|
|
$"{baseUrl}/contact",
|
|
$"{baseUrl}/privacy",
|
|
$"{baseUrl}/terms",
|
|
$"{baseUrl}/blog",
|
|
$"{baseUrl}/faq",
|
|
$"{baseUrl}/announcement",
|
|
$"{baseUrl}/inquiry"
|
|
};
|
|
}
|
|
}
|