diff --git a/src/TaxBaik.Web/Endpoints/Admin/ValidationEndpoints.cs b/src/TaxBaik.Web/Endpoints/Admin/ValidationEndpoints.cs
new file mode 100644
index 0000000..2fcee50
--- /dev/null
+++ b/src/TaxBaik.Web/Endpoints/Admin/ValidationEndpoints.cs
@@ -0,0 +1,73 @@
+using FastEndpoints;
+using TaxBaik.Web.Services;
+
+namespace TaxBaik.Web.Endpoints.Admin;
+
+///
+/// Sitemap 및 RSS 검증 엔드포인트 (관리자 전용)
+///
+public class ValidateSitemapEndpoint : EndpointWithoutRequest
+{
+ private readonly SitemapValidationService _validationService;
+
+ public ValidateSitemapEndpoint(SitemapValidationService validationService)
+ {
+ _validationService = validationService;
+ }
+
+ public override void Configure()
+ {
+ Get("/api/admin/validate/sitemap");
+ Roles("admin");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var result = await _validationService.ValidateSitemapAsync();
+ await SendOkAsync(result, cancellation: ct);
+ }
+}
+
+public class ValidateRssEndpoint : EndpointWithoutRequest
+{
+ private readonly SitemapValidationService _validationService;
+
+ public ValidateRssEndpoint(SitemapValidationService validationService)
+ {
+ _validationService = validationService;
+ }
+
+ public override void Configure()
+ {
+ Get("/api/admin/validate/rss");
+ Roles("admin");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var result = await _validationService.ValidateRssFeedAsync();
+ await SendOkAsync(result, cancellation: ct);
+ }
+}
+
+public class ValidateConsistencyEndpoint : EndpointWithoutRequest
+{
+ private readonly SitemapValidationService _validationService;
+
+ public ValidateConsistencyEndpoint(SitemapValidationService validationService)
+ {
+ _validationService = validationService;
+ }
+
+ public override void Configure()
+ {
+ Get("/api/admin/validate/consistency");
+ Roles("admin");
+ }
+
+ public override async Task HandleAsync(CancellationToken ct)
+ {
+ var result = await _validationService.ValidateConsistencyAsync();
+ await SendOkAsync(result, cancellation: ct);
+ }
+}
diff --git a/src/TaxBaik.Web/Program.cs b/src/TaxBaik.Web/Program.cs
index 2fde560..fa8734e 100644
--- a/src/TaxBaik.Web/Program.cs
+++ b/src/TaxBaik.Web/Program.cs
@@ -301,6 +301,7 @@ builder.Services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
builder.Services.AddInfrastructure();
builder.Services.AddApplication();
builder.Services.AddScoped();
+builder.Services.AddScoped();
// Register version info
var versionInfo = new VersionInfo();
diff --git a/src/TaxBaik.Web/Services/SitemapValidationService.cs b/src/TaxBaik.Web/Services/SitemapValidationService.cs
new file mode 100644
index 0000000..495d8d8
--- /dev/null
+++ b/src/TaxBaik.Web/Services/SitemapValidationService.cs
@@ -0,0 +1,260 @@
+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"
+ };
+ }
+}