");
sb.AppendLine($"
{Escape(title)}
");
sb.AppendLine($"
{Escape(coverSubtitle)}
");
sb.AppendLine($"
{DateTime.Now:yyyy년 MM월 dd일}
");
sb.AppendLine("
");
}
sb.AppendLine("");
if (string.IsNullOrWhiteSpace(coverSubtitle))
sb.AppendLine($"
{Escape(title)}
");
// TOC 생성
if (toc && sections.Count > 1)
{
sb.AppendLine("
");
sb.AppendLine("
📋 목차
");
sb.AppendLine("
");
for (int i = 0; i < sections.Count; i++)
{
var indent = sections[i].Level > 1 ? " style=\"padding-left:20px\"" : "";
sb.AppendLine($"- {Escape(sections[i].Heading)}
");
}
sb.AppendLine("
");
sb.AppendLine("
");
}
// 섹션 본문
for (int i = 0; i < sections.Count; i++)
{
var (heading, content, level) = sections[i];
var tag = level <= 1 ? "h2" : "h3";
sb.AppendLine($"<{tag} id=\"section-{i + 1}\">{Escape(heading)}{tag}>");
// LLM이 생성한 깨진 태그 자동 수정
var sanitized = HtmlSkill.SanitizeHtmlTagsPublic(content);
sb.AppendLine($"
{sanitized}
");
}
sb.AppendLine("
");
sb.AppendLine("");
sb.AppendLine("");
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
var issues = ValidateBasic(sb.ToString());
var review = ArtifactQualityReviewService.ReviewHtml(title, sb.ToString(), !string.IsNullOrWhiteSpace(coverSubtitle), toc, printReady: true);
var reviewSummary = $" HTML 품질 리뷰: {review.Score}/100 | 강점 {review.Strengths.Count} | 이슈 {review.Issues.Count}";
return issues.Count > 0
? $"{reviewSummary} | 기본 검토 이슈 {issues.Count}건: {string.Join("; ", issues)}"
: reviewSummary;
}
private string AssembleDocx(string path, string title, List<(string Heading, string Content, int Level)> sections,
bool useToc, string? coverSubtitle, string? templatePath, string? headerText, string? footerText, bool showPageNumbers)
{
var templateApplied = !string.IsNullOrWhiteSpace(templatePath);
if (templateApplied)
File.Copy(templatePath!, path, true);
using var doc = templateApplied
? DocumentFormat.OpenXml.Packaging.WordprocessingDocument.Open(path, true)
: DocumentFormat.OpenXml.Packaging.WordprocessingDocument.Create(path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
var mainPart = doc.MainDocumentPart ?? doc.AddMainDocumentPart();
EnsureAssemblerStyles(mainPart, templateApplied);
var body = InitializeAssemblerBody(mainPart, templateApplied);
DocumentFormat.OpenXml.Wordprocessing.RunFonts KoreanFonts() => new()
{
Ascii = "맑은 고딕",
HighAnsi = "맑은 고딕",
EastAsia = "맑은 고딕",
ComplexScript = "맑은 고딕"
};
string DecodeHtml(string text)
=> text.Replace(" ", " ")
.Replace("&", "&")
.Replace("<", "<")
.Replace(">", ">")
.Replace(""", "\"");
string ExtractStructuredText(string html)
{
if (string.IsNullOrWhiteSpace(html)) return "";
var text = Regex.Replace(html, @"