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" }; } }