using System.Diagnostics; using System.IO; using System.Text; using AxCopilot.Models; namespace AxCopilot.Services; /// /// 대화 내역을 PDF로 내보내는 서비스. /// HTML을 생성한 후 시스템 브라우저의 인쇄 기능을 활용합니다. /// public static class PdfExportService { /// 대화를 HTML 파일로 내보내고, 브라우저에서 PDF로 인쇄할 수 있도록 엽니다. public static string ExportToPrintableHtml(ChatConversation conversation, string outputPath) { var html = BuildHtml(conversation); File.WriteAllText(outputPath, html, Encoding.UTF8); return outputPath; } /// 대화를 HTML로 변환합니다 (인쇄 최적화 스타일 포함). public static string BuildHtml(ChatConversation conversation) { var sb = new StringBuilder(); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine($"AX Copilot — {EscapeHtml(conversation.Title)}"); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); // 헤더 sb.AppendLine("
"); sb.AppendLine($"

{EscapeHtml(conversation.Title)}

"); sb.AppendLine($"
{conversation.CreatedAt:yyyy-MM-dd HH:mm} · {conversation.Messages.Count}개 메시지
"); sb.AppendLine("
"); // 메시지 foreach (var msg in conversation.Messages) { var roleClass = msg.Role == "user" ? "user" : "assistant"; var roleLabel = msg.Role == "user" ? "사용자" : "AI"; sb.AppendLine($"
"); sb.AppendLine($"
{roleLabel}
"); sb.AppendLine($"
{FormatContent(msg.Content)}
"); if (msg.Timestamp != default) sb.AppendLine($"
{msg.Timestamp:HH:mm}
"); sb.AppendLine("
"); } // 푸터 sb.AppendLine("
"); sb.AppendLine($"AX Copilot · 내보내기 일시: {DateTime.Now:yyyy-MM-dd HH:mm}"); sb.AppendLine("
"); sb.AppendLine(""); sb.AppendLine(""); 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 => $"
{m.Groups[2].Value}
"); // 인라인 코드 escaped = System.Text.RegularExpressions.Regex.Replace(escaped, @"`([^`]+)`", "$1"); // 줄바꿈 escaped = escaped.Replace("\n", "
"); return escaped; } private static string EscapeHtml(string text) => text.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """); /// HTML 파일을 기본 브라우저에서 엽니다 (인쇄 대화상자 자동 표시). public static void OpenInBrowser(string htmlPath) { try { Process.Start(new ProcessStartInfo(htmlPath) { UseShellExecute = true }); } catch (Exception ex) { LogService.Warn($"PDF 내보내기 브라우저 열기 실패: {ex.Message}"); } } }