using System.Text.RegularExpressions;
using TaxBaik.Application.Services;
namespace TaxBaik.Web.Services;
///
/// Sitemap 및 RSS 피드의 정합성 검증
///
public class SitemapValidationService
{
private readonly BlogService _blogService;
private readonly ILogger _logger;
public SitemapValidationService(BlogService blogService, ILogger logger)
{
_blogService = blogService;
_logger = logger;
}
///
/// Sitemap 검증 결과
///
public class SitemapValidationResult
{
public bool IsValid { get; set; }
public List Errors { get; set; } = [];
public List Warnings { get; set; } = [];
public int TotalUrls { get; set; }
public int BlogPostUrls { get; set; }
public DateTime ValidatedAt { get; set; } = DateTime.UtcNow;
}
///
/// Sitemap 전체 검증
///
public async Task 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;
}
///
/// RSS 피드 검증
///
public async Task 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();
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;
}
///
/// Sitemap과 RSS의 일관성 검증
///
public async Task 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;
}
///
/// URL 형식 검증
///
private static void ValidateUrls(List 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}");
}
}
}
///
/// 정적 페이지 URL 목록
///
private static List GetStaticUrls()
{
var baseUrl = "https://www.taxbaik.com/taxbaik";
return new List
{
$"{baseUrl}",
$"{baseUrl}/about",
$"{baseUrl}/services",
$"{baseUrl}/contact",
$"{baseUrl}/privacy",
$"{baseUrl}/terms",
$"{baseUrl}/blog",
$"{baseUrl}/faq",
$"{baseUrl}/announcement",
$"{baseUrl}/inquiry"
};
}
}