Files
AX-Copilot-Codex/src/AxCopilot/Services/PdfExportService.cs

118 lines
5.1 KiB
C#

using System.Diagnostics;
using System.IO;
using System.Text;
using AxCopilot.Models;
namespace AxCopilot.Services;
/// <summary>
/// 대화 내역을 PDF로 내보내는 서비스.
/// HTML을 생성한 후 시스템 브라우저의 인쇄 기능을 활용합니다.
/// </summary>
public static class PdfExportService
{
/// <summary>대화를 HTML 파일로 내보내고, 브라우저에서 PDF로 인쇄할 수 있도록 엽니다.</summary>
public static string ExportToPrintableHtml(ChatConversation conversation, string outputPath)
{
var html = BuildHtml(conversation);
File.WriteAllText(outputPath, html, Encoding.UTF8);
return outputPath;
}
/// <summary>대화를 HTML로 변환합니다 (인쇄 최적화 스타일 포함).</summary>
public static string BuildHtml(ChatConversation conversation)
{
var sb = new StringBuilder();
sb.AppendLine("<!DOCTYPE html>");
sb.AppendLine("<html lang=\"ko\">");
sb.AppendLine("<head>");
sb.AppendLine("<meta charset=\"utf-8\">");
sb.AppendLine($"<title>AX Copilot — {EscapeHtml(conversation.Title)}</title>");
sb.AppendLine("<style>");
sb.AppendLine(GetPrintStyles());
sb.AppendLine("</style>");
sb.AppendLine("</head>");
sb.AppendLine("<body>");
// 헤더
sb.AppendLine("<div class=\"header\">");
sb.AppendLine($"<h1>{EscapeHtml(conversation.Title)}</h1>");
sb.AppendLine($"<div class=\"meta\">{conversation.CreatedAt:yyyy-MM-dd HH:mm} · {conversation.Messages.Count}개 메시지</div>");
sb.AppendLine("</div>");
// 메시지
foreach (var msg in conversation.Messages)
{
var roleClass = msg.Role == "user" ? "user" : "assistant";
var roleLabel = msg.Role == "user" ? "사용자" : "AI";
sb.AppendLine($"<div class=\"message {roleClass}\">");
sb.AppendLine($"<div class=\"role\">{roleLabel}</div>");
sb.AppendLine($"<div class=\"content\">{FormatContent(msg.Content)}</div>");
if (msg.Timestamp != default)
sb.AppendLine($"<div class=\"time\">{msg.Timestamp:HH:mm}</div>");
sb.AppendLine("</div>");
}
// 푸터
sb.AppendLine("<div class=\"footer\">");
sb.AppendLine($"AX Copilot · 내보내기 일시: {DateTime.Now:yyyy-MM-dd HH:mm}");
sb.AppendLine("</div>");
sb.AppendLine("<script>window.onload = function() { window.print(); };</script>");
sb.AppendLine("</body></html>");
return sb.ToString();
}
private static string GetPrintStyles() => @"
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Malgun Gothic', 'Segoe UI', sans-serif; font-size: 13px; color: #222; background: #fff; padding: 20px; }
.header { border-bottom: 2px solid #4B5EFC; padding-bottom: 12px; margin-bottom: 20px; }
.header h1 { font-size: 18px; font-weight: 700; color: #1a1b2e; }
.header .meta { font-size: 11px; color: #888; margin-top: 4px; }
.message { margin-bottom: 16px; padding: 12px 16px; border-radius: 8px; page-break-inside: avoid; }
.message.user { background: #f0f4ff; border-left: 3px solid #4B5EFC; }
.message.assistant { background: #fafafa; border-left: 3px solid #10B981; }
.role { font-size: 10px; font-weight: 700; color: #888; text-transform: uppercase; margin-bottom: 6px; }
.content { line-height: 1.7; white-space: pre-wrap; word-break: break-word; }
.content code { background: #f0f0f5; padding: 1px 4px; border-radius: 3px; font-family: Consolas, monospace; font-size: 12px; }
.content pre { background: #f5f5fa; padding: 10px; border-radius: 6px; overflow-x: auto; margin: 8px 0; }
.time { font-size: 10px; color: #aaa; text-align: right; margin-top: 4px; }
.footer { margin-top: 30px; padding-top: 12px; border-top: 1px solid #ddd; font-size: 10px; color: #aaa; text-align: center; }
@media print { body { padding: 0; } .message { break-inside: avoid; } }
@page { margin: 15mm; }
";
private static string FormatContent(string content)
{
if (string.IsNullOrEmpty(content)) return "";
var escaped = EscapeHtml(content);
// 코드 블록
escaped = System.Text.RegularExpressions.Regex.Replace(escaped,
@"```(\w*)\n([\s\S]*?)```",
m => $"<pre><code>{m.Groups[2].Value}</code></pre>");
// 인라인 코드
escaped = System.Text.RegularExpressions.Regex.Replace(escaped,
@"`([^`]+)`", "<code>$1</code>");
// 줄바꿈
escaped = escaped.Replace("\n", "<br>");
return escaped;
}
private static string EscapeHtml(string text) =>
text.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
/// <summary>HTML 파일을 기본 브라우저에서 엽니다 (인쇄 대화상자 자동 표시).</summary>
public static void OpenInBrowser(string htmlPath)
{
try
{
Process.Start(new ProcessStartInfo(htmlPath) { UseShellExecute = true });
}
catch (Exception ex)
{
LogService.Warn($"PDF 내보내기 브라우저 열기 실패: {ex.Message}");
}
}
}