diff --git a/src/AxCopilot/App.Helpers.cs b/src/AxCopilot/App.Helpers.cs new file mode 100644 index 0000000..d22bb04 --- /dev/null +++ b/src/AxCopilot/App.Helpers.cs @@ -0,0 +1,92 @@ +using Microsoft.Win32; +using AxCopilot.Services; + +namespace AxCopilot; + +public partial class App +{ + // ─── 자동 시작 / 앱 종료 헬퍼 ────────────────────────────────────── + + private static System.Drawing.Icon LoadAppIcon() + { + // DPI 인식 아이콘 크기 (기본 16 → 고DPI에서 20/24/32) + var iconSize = System.Windows.Forms.SystemInformation.SmallIconSize; + + // 1) 파일 시스템에서 로드 (개발 환경) + try + { + var exeDir = System.IO.Path.GetDirectoryName(Environment.ProcessPath) + ?? AppContext.BaseDirectory; + var path = System.IO.Path.Combine(exeDir, "Assets", "icon.ico"); + if (System.IO.File.Exists(path)) + return new System.Drawing.Icon(path, iconSize); + } + catch (Exception) { } + + // 2) 내장 리소스에서 로드 (PublishSingleFile 배포) + try + { + var uri = new Uri("pack://application:,,,/Assets/icon.ico"); + var stream = System.Windows.Application.GetResourceStream(uri)?.Stream; + if (stream != null) + return new System.Drawing.Icon(stream, iconSize); + } + catch (Exception) { } + + return System.Drawing.SystemIcons.Application; + } + + private const string AutoRunKey = @"Software\Microsoft\Windows\CurrentVersion\Run"; + private const string AutoRunName = "AxCopilot"; + + private static bool IsAutoStartEnabled() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey(AutoRunKey, writable: false); + return key?.GetValue(AutoRunName) != null; + } + catch (Exception) { return false; } + } + + private static void SetAutoStart(bool enable) + { + try + { + using var key = Registry.CurrentUser.OpenSubKey(AutoRunKey, writable: true); + if (key == null) return; + + if (enable) + { + var exePath = Environment.ProcessPath + ?? System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName + ?? string.Empty; + if (!string.IsNullOrEmpty(exePath)) + key.SetValue(AutoRunName, $"\"{exePath}\""); + } + else + { + key.DeleteValue(AutoRunName, throwOnMissingValue: false); + } + } + catch (Exception ex) + { + LogService.Warn($"자동 시작 레지스트리 설정 실패: {ex.Message}"); + } + } + + protected override void OnExit(System.Windows.ExitEventArgs e) + { + _chatWindow?.ForceClose(); // 미리 생성된 ChatWindow 진짜 닫기 + _inputListener?.Dispose(); + _clipboardHistory?.Dispose(); + _indexService?.Dispose(); + _sessionTracking?.Dispose(); + _worktimeReminder?.Dispose(); + _trayIcon?.Dispose(); + try { _singleInstanceMutex?.ReleaseMutex(); } catch (Exception) { } + _singleInstanceMutex?.Dispose(); + LogService.Info("=== AX Copilot 종료 ==="); + base.OnExit(e); + } +} diff --git a/src/AxCopilot/App.Settings.cs b/src/AxCopilot/App.Settings.cs new file mode 100644 index 0000000..68859bc --- /dev/null +++ b/src/AxCopilot/App.Settings.cs @@ -0,0 +1,252 @@ +using System.Windows; +using System.Windows.Forms; +using AxCopilot.Core; +using AxCopilot.Services; +using AxCopilot.ViewModels; +using AxCopilot.Views; + +namespace AxCopilot; + +public partial class App +{ + // ─── 설정창 / ChatWindow ───────────────────────────────────────────── + + private void SetupTrayIcon(PluginHost pluginHost, SettingsService settings) + { + _trayIcon = new NotifyIcon + { + Text = "AX Copilot", + Visible = true, + Icon = LoadAppIcon() + }; + + // ─── WPF 커스텀 트레이 메뉴 구성 ────────────────────────────────── + _trayMenu = new Views.TrayMenuWindow(); + _trayMenu + .AddItem("\uE7C5", "AX Commander 호출하기", () => + Dispatcher.Invoke(() => _launcher?.Show())) + .AddItem("\uE8BD", "AX Agent 대화하기", () => + Dispatcher.Invoke(() => OpenAiChat()), out var aiTrayItem) + .AddItem("\uE8A7", "독 바 표시", () => + Dispatcher.Invoke(() => ToggleDockBar())) + .AddSeparator() + .AddItem("\uE72C", "플러그인 재로드", () => + { + pluginHost.Reload(); + _trayIcon!.ShowBalloonTip(2000, "AX Copilot", "플러그인이 재로드되었습니다.", ToolTipIcon.None); + }) + .AddItem("\uE838", "로그 폴더 열기", () => + { + var logDir = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "AxCopilot", "logs"); + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("explorer.exe", logDir) + { UseShellExecute = true }); + }) + .AddItem("\uE9D9", "사용 통계", () => + Dispatcher.Invoke(() => new StatisticsWindow().Show())) + .AddItem("\uE736", "사용 가이드 문서보기", () => + { + try { Dispatcher.Invoke(() => new Views.GuideViewerWindow().Show()); } + catch (Exception ex) { LogService.Error($"사용 가이드 열기 실패: {ex.Message}"); } + }) + .AddSeparator() + .AddToggleItem("\uE82F", "Windows 시작 시 자동 실행", IsAutoStartEnabled(), + isChecked => SetAutoStart(isChecked), out _, out _) + .AddSeparator() + .AddItem("\uE713", "설정", () => + Dispatcher.Invoke(OpenSettings)) + .AddItem("\uE946", "개발 정보", () => + Dispatcher.Invoke(() => new AboutWindow().Show())) + .AddItem("\uE711", "종료", () => + { + _inputListener?.Dispose(); + _trayIcon?.Dispose(); + Shutdown(); + }); + + // AI 기능 활성화 여부에 따라 메뉴 항목 가시성 동적 업데이트 + _trayMenu.Opening += () => + { + aiTrayItem.Visibility = settings.Settings.AiEnabled + ? System.Windows.Visibility.Visible + : System.Windows.Visibility.Collapsed; + }; + + // 우클릭 → WPF 메뉴 표시, 좌클릭 → 런처 토글 + _trayIcon.MouseClick += (_, e) => + { + if (e.Button == System.Windows.Forms.MouseButtons.Left) + Dispatcher.Invoke(() => _launcher?.Show()); + else if (e.Button == System.Windows.Forms.MouseButtons.Right) + Dispatcher.Invoke(() => _trayMenu?.ShowWithUpdate()); + }; + + // 타이머/알람 풍선 알림 서비스 연결 + NotificationService.Initialize((title, msg) => + { + Dispatcher.Invoke(() => + _trayIcon?.ShowBalloonTip(4000, title, msg, ToolTipIcon.None)); + }); + } + + /// ChatWindow 등 외부에서 설정 창을 여는 공개 메서드. + public void OpenSettingsFromChat() => Dispatcher.Invoke(OpenSettings); + + /// AX Agent 창 열기 (트레이 메뉴 등에서 호출). + private Views.ChatWindow? _chatWindow; + + /// + /// ChatWindow를 백그라운드에서 미리 생성합니다 (앱 시작 후 저우선순위로 호출). + /// 이후 OpenAiChat() 시 창 생성 비용 없이 즉시 Show/Activate만 수행합니다. + /// + internal void PrewarmChatWindow() + { + if (_chatWindow != null || _settings == null) return; + _chatWindow = new Views.ChatWindow(_settings); + } + + private void OpenAiChat() + { + if (_settings == null) return; + if (_chatWindow == null) + { + _chatWindow = new Views.ChatWindow(_settings); + } + _chatWindow.Show(); + _chatWindow.Activate(); + } + + public void ToggleDockBar() + { + if (_dockBar != null && _dockBar.IsVisible) + { + _dockBar.Hide(); + return; + } + + if (_dockBar == null) + { + _dockBar = new DockBarWindow(); + _dockBar.OnQuickSearch = query => + { + if (_launcher == null) return; + _launcher.Show(); + _launcher.Activate(); // 독 바 뒤가 아닌 전면에 표시 + if (!string.IsNullOrEmpty(query)) + Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, + () => _launcher.SetInputText(query)); + }; + _dockBar.OnCapture = async () => + { + WindowTracker.Capture(); + if (_captureHandler != null) + await _captureHandler.CaptureDirectAsync("region"); + }; + _dockBar.OnOpenAgent = () => + { + if (_launcher == null) return; + _launcher.Show(); + Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, + () => _launcher.SetInputText("!")); + }; + } + var launcher = _settings?.Settings.Launcher; + var dockItems = launcher?.DockBarItems ?? new() { "launcher", "clipboard", "capture", "agent", "clock", "cpu" }; + _dockBar.BuildFromSettings(dockItems); + _dockBar.OnPositionChanged = (left, top) => + { + if (_settings != null) + { + _settings.Settings.Launcher.DockBarLeft = left; + _settings.Settings.Launcher.DockBarTop = top; + _settings.Save(); + } + }; + _dockBar.Show(); + _dockBar.ApplySettings( + launcher?.DockBarOpacity ?? 0.92, + launcher?.DockBarLeft ?? -1, + launcher?.DockBarTop ?? -1, + launcher?.DockBarRainbowGlow ?? false); + } + + /// 독 바를 현재 설정으로 즉시 새로고침합니다. + public void RefreshDockBar() + { + if (_dockBar == null || !_dockBar.IsVisible) return; + var launcher = _settings?.Settings.Launcher; + var dockItems = launcher?.DockBarItems ?? new() { "launcher", "clipboard", "capture", "agent", "clock", "cpu" }; + _dockBar.BuildFromSettings(dockItems); + _dockBar.ApplySettings( + launcher?.DockBarOpacity ?? 0.92, + launcher?.DockBarLeft ?? -1, + launcher?.DockBarTop ?? -1, + launcher?.DockBarRainbowGlow ?? false); + } + + private void OpenSettings() + { + if (_settingsWindow != null && _settingsWindow.IsVisible) + { + _settingsWindow.Activate(); + return; + } + + if (_settings == null || _launcher == null) return; + + var vm = new ViewModels.SettingsViewModel(_settings); + + // 미리보기 콜백: 현재 편집 중인 색상(vm.ColorRows)으로 런처에 즉시 반영 + void PreviewCallback(string themeKey) + { + if (themeKey == "custom") + { + var tempColors = new AxCopilot.Models.CustomThemeColors(); + foreach (var row in vm.ColorRows) + { + var prop = typeof(AxCopilot.Models.CustomThemeColors).GetProperty(row.Property); + prop?.SetValue(tempColors, row.Hex); + } + _launcher.ApplyTheme(themeKey, tempColors); + } + else + { + _launcher.ApplyTheme(themeKey, _settings.Settings.Launcher.CustomTheme); + } + } + + // 취소/X 닫기 콜백: 파일에 저장된 원본 설정으로 복원 + void RevertCallback() + { + _launcher.ApplyTheme( + _settings.Settings.Launcher.Theme ?? "system", + _settings.Settings.Launcher.CustomTheme); + } + + _settingsWindow = new Views.SettingsWindow(vm, PreviewCallback, RevertCallback) + { + // 핫키 녹화 중 글로벌 핫키 일시 정지 + SuspendHotkeyCallback = suspend => + { + if (_inputListener != null) + _inputListener.SuspendHotkey = suspend; + } + }; + + // 저장 완료 시 InputListener 핫키 갱신 + 알림 타이머 재시작 + vm.SaveCompleted += (_, _) => + { + if (_inputListener != null && _settings != null) + { + _inputListener.UpdateHotkey(_settings.Settings.Hotkey); + _inputListener.UpdateCaptureHotkey( + _settings.Settings.ScreenCapture.GlobalHotkey, + _settings.Settings.ScreenCapture.GlobalHotkeyEnabled); + } + _worktimeReminder?.RestartTimer(); + }; + + _settingsWindow.Show(); + } +} diff --git a/src/AxCopilot/App.xaml.cs b/src/AxCopilot/App.xaml.cs index c11e840..2f40050 100644 --- a/src/AxCopilot/App.xaml.cs +++ b/src/AxCopilot/App.xaml.cs @@ -449,328 +449,4 @@ public partial class App : System.Windows.Application ToolTipIcon.Warning); }); } - - private void SetupTrayIcon(PluginHost pluginHost, SettingsService settings) - { - _trayIcon = new NotifyIcon - { - Text = "AX Copilot", - Visible = true, - Icon = LoadAppIcon() - }; - - // ─── WPF 커스텀 트레이 메뉴 구성 ────────────────────────────────── - _trayMenu = new Views.TrayMenuWindow(); - _trayMenu - .AddItem("\uE7C5", "AX Commander 호출하기", () => - Dispatcher.Invoke(() => _launcher?.Show())) - .AddItem("\uE8BD", "AX Agent 대화하기", () => - Dispatcher.Invoke(() => OpenAiChat()), out var aiTrayItem) - .AddItem("\uE8A7", "독 바 표시", () => - Dispatcher.Invoke(() => ToggleDockBar())) - .AddSeparator() - .AddItem("\uE72C", "플러그인 재로드", () => - { - pluginHost.Reload(); - _trayIcon!.ShowBalloonTip(2000, "AX Copilot", "플러그인이 재로드되었습니다.", ToolTipIcon.None); - }) - .AddItem("\uE838", "로그 폴더 열기", () => - { - var logDir = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "AxCopilot", "logs"); - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("explorer.exe", logDir) - { UseShellExecute = true }); - }) - .AddItem("\uE9D9", "사용 통계", () => - Dispatcher.Invoke(() => new StatisticsWindow().Show())) - .AddItem("\uE736", "사용 가이드 문서보기", () => - { - try { Dispatcher.Invoke(() => new Views.GuideViewerWindow().Show()); } - catch (Exception ex) { LogService.Error($"사용 가이드 열기 실패: {ex.Message}"); } - }) - .AddSeparator() - .AddToggleItem("\uE82F", "Windows 시작 시 자동 실행", IsAutoStartEnabled(), - isChecked => SetAutoStart(isChecked), out _, out _) - .AddSeparator() - .AddItem("\uE713", "설정", () => - Dispatcher.Invoke(OpenSettings)) - .AddItem("\uE946", "개발 정보", () => - Dispatcher.Invoke(() => new AboutWindow().Show())) - .AddItem("\uE711", "종료", () => - { - _inputListener?.Dispose(); - _trayIcon?.Dispose(); - Shutdown(); - }); - - // AI 기능 활성화 여부에 따라 메뉴 항목 가시성 동적 업데이트 - _trayMenu.Opening += () => - { - aiTrayItem.Visibility = settings.Settings.AiEnabled - ? System.Windows.Visibility.Visible - : System.Windows.Visibility.Collapsed; - }; - - // 우클릭 → WPF 메뉴 표시, 좌클릭 → 런처 토글 - _trayIcon.MouseClick += (_, e) => - { - if (e.Button == System.Windows.Forms.MouseButtons.Left) - Dispatcher.Invoke(() => _launcher?.Show()); - else if (e.Button == System.Windows.Forms.MouseButtons.Right) - Dispatcher.Invoke(() => _trayMenu?.ShowWithUpdate()); - }; - - // 타이머/알람 풍선 알림 서비스 연결 - NotificationService.Initialize((title, msg) => - { - Dispatcher.Invoke(() => - _trayIcon?.ShowBalloonTip(4000, title, msg, ToolTipIcon.None)); - }); - } - - /// ChatWindow 등 외부에서 설정 창을 여는 공개 메서드. - public void OpenSettingsFromChat() => Dispatcher.Invoke(OpenSettings); - - /// AX Agent 창 열기 (트레이 메뉴 등에서 호출). - private Views.ChatWindow? _chatWindow; - - /// - /// ChatWindow를 백그라운드에서 미리 생성합니다 (앱 시작 후 저우선순위로 호출). - /// 이후 OpenAiChat() 시 창 생성 비용 없이 즉시 Show/Activate만 수행합니다. - /// - internal void PrewarmChatWindow() - { - if (_chatWindow != null || _settings == null) return; - _chatWindow = new Views.ChatWindow(_settings); - } - - private void OpenAiChat() - { - if (_settings == null) return; - if (_chatWindow == null) - { - _chatWindow = new Views.ChatWindow(_settings); - } - _chatWindow.Show(); - _chatWindow.Activate(); - } - - public void ToggleDockBar() - { - if (_dockBar != null && _dockBar.IsVisible) - { - _dockBar.Hide(); - return; - } - - if (_dockBar == null) - { - _dockBar = new DockBarWindow(); - _dockBar.OnQuickSearch = query => - { - if (_launcher == null) return; - _launcher.Show(); - _launcher.Activate(); // 독 바 뒤가 아닌 전면에 표시 - if (!string.IsNullOrEmpty(query)) - Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, - () => _launcher.SetInputText(query)); - }; - _dockBar.OnCapture = async () => - { - WindowTracker.Capture(); - if (_captureHandler != null) - await _captureHandler.CaptureDirectAsync("region"); - }; - _dockBar.OnOpenAgent = () => - { - if (_launcher == null) return; - _launcher.Show(); - Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, - () => _launcher.SetInputText("!")); - }; - } - var launcher = _settings?.Settings.Launcher; - var dockItems = launcher?.DockBarItems ?? new() { "launcher", "clipboard", "capture", "agent", "clock", "cpu" }; - _dockBar.BuildFromSettings(dockItems); - _dockBar.OnPositionChanged = (left, top) => - { - if (_settings != null) - { - _settings.Settings.Launcher.DockBarLeft = left; - _settings.Settings.Launcher.DockBarTop = top; - _settings.Save(); - } - }; - _dockBar.Show(); - _dockBar.ApplySettings( - launcher?.DockBarOpacity ?? 0.92, - launcher?.DockBarLeft ?? -1, - launcher?.DockBarTop ?? -1, - launcher?.DockBarRainbowGlow ?? false); - } - - /// 독 바를 현재 설정으로 즉시 새로고침합니다. - public void RefreshDockBar() - { - if (_dockBar == null || !_dockBar.IsVisible) return; - var launcher = _settings?.Settings.Launcher; - var dockItems = launcher?.DockBarItems ?? new() { "launcher", "clipboard", "capture", "agent", "clock", "cpu" }; - _dockBar.BuildFromSettings(dockItems); - _dockBar.ApplySettings( - launcher?.DockBarOpacity ?? 0.92, - launcher?.DockBarLeft ?? -1, - launcher?.DockBarTop ?? -1, - launcher?.DockBarRainbowGlow ?? false); - } - - private void OpenSettings() - { - if (_settingsWindow != null && _settingsWindow.IsVisible) - { - _settingsWindow.Activate(); - return; - } - - if (_settings == null || _launcher == null) return; - - var vm = new ViewModels.SettingsViewModel(_settings); - - // 미리보기 콜백: 현재 편집 중인 색상(vm.ColorRows)으로 런처에 즉시 반영 - void PreviewCallback(string themeKey) - { - if (themeKey == "custom") - { - var tempColors = new AxCopilot.Models.CustomThemeColors(); - foreach (var row in vm.ColorRows) - { - var prop = typeof(AxCopilot.Models.CustomThemeColors).GetProperty(row.Property); - prop?.SetValue(tempColors, row.Hex); - } - _launcher.ApplyTheme(themeKey, tempColors); - } - else - { - _launcher.ApplyTheme(themeKey, _settings.Settings.Launcher.CustomTheme); - } - } - - // 취소/X 닫기 콜백: 파일에 저장된 원본 설정으로 복원 - void RevertCallback() - { - _launcher.ApplyTheme( - _settings.Settings.Launcher.Theme ?? "system", - _settings.Settings.Launcher.CustomTheme); - } - - _settingsWindow = new Views.SettingsWindow(vm, PreviewCallback, RevertCallback) - { - // 핫키 녹화 중 글로벌 핫키 일시 정지 - SuspendHotkeyCallback = suspend => - { - if (_inputListener != null) - _inputListener.SuspendHotkey = suspend; - } - }; - - // 저장 완료 시 InputListener 핫키 갱신 + 알림 타이머 재시작 - vm.SaveCompleted += (_, _) => - { - if (_inputListener != null && _settings != null) - { - _inputListener.UpdateHotkey(_settings.Settings.Hotkey); - _inputListener.UpdateCaptureHotkey( - _settings.Settings.ScreenCapture.GlobalHotkey, - _settings.Settings.ScreenCapture.GlobalHotkeyEnabled); - } - _worktimeReminder?.RestartTimer(); - }; - - _settingsWindow.Show(); - } - - // ─── 자동 시작 레지스트리 헬퍼 ────────────────────────────────────────── - - private static System.Drawing.Icon LoadAppIcon() - { - // DPI 인식 아이콘 크기 (기본 16 → 고DPI에서 20/24/32) - var iconSize = System.Windows.Forms.SystemInformation.SmallIconSize; - - // 1) 파일 시스템에서 로드 (개발 환경) - try - { - var exeDir = System.IO.Path.GetDirectoryName(Environment.ProcessPath) - ?? AppContext.BaseDirectory; - var path = System.IO.Path.Combine(exeDir, "Assets", "icon.ico"); - if (System.IO.File.Exists(path)) - return new System.Drawing.Icon(path, iconSize); - } - catch (Exception) { } - - // 2) 내장 리소스에서 로드 (PublishSingleFile 배포) - try - { - var uri = new Uri("pack://application:,,,/Assets/icon.ico"); - var stream = System.Windows.Application.GetResourceStream(uri)?.Stream; - if (stream != null) - return new System.Drawing.Icon(stream, iconSize); - } - catch (Exception) { } - - return System.Drawing.SystemIcons.Application; - } - - private const string AutoRunKey = @"Software\Microsoft\Windows\CurrentVersion\Run"; - private const string AutoRunName = "AxCopilot"; - - private static bool IsAutoStartEnabled() - { - try - { - using var key = Registry.CurrentUser.OpenSubKey(AutoRunKey, writable: false); - return key?.GetValue(AutoRunName) != null; - } - catch (Exception) { return false; } - } - - private static void SetAutoStart(bool enable) - { - try - { - using var key = Registry.CurrentUser.OpenSubKey(AutoRunKey, writable: true); - if (key == null) return; - - if (enable) - { - var exePath = Environment.ProcessPath - ?? System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName - ?? string.Empty; - if (!string.IsNullOrEmpty(exePath)) - key.SetValue(AutoRunName, $"\"{exePath}\""); - } - else - { - key.DeleteValue(AutoRunName, throwOnMissingValue: false); - } - } - catch (Exception ex) - { - LogService.Warn($"자동 시작 레지스트리 설정 실패: {ex.Message}"); - } - } - - protected override void OnExit(ExitEventArgs e) - { - _chatWindow?.ForceClose(); // 미리 생성된 ChatWindow 진짜 닫기 - _inputListener?.Dispose(); - _clipboardHistory?.Dispose(); - _indexService?.Dispose(); - _sessionTracking?.Dispose(); - _worktimeReminder?.Dispose(); - _trayIcon?.Dispose(); - try { _singleInstanceMutex?.ReleaseMutex(); } catch (Exception) { } - _singleInstanceMutex?.Dispose(); - LogService.Info("=== AX Copilot 종료 ==="); - base.OnExit(e); - } } diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.HtmlReport.cs b/src/AxCopilot/Services/Agent/AgentLoopService.HtmlReport.cs new file mode 100644 index 0000000..de5beca --- /dev/null +++ b/src/AxCopilot/Services/Agent/AgentLoopService.HtmlReport.cs @@ -0,0 +1,151 @@ +namespace AxCopilot.Services.Agent; + +public partial class AgentLoopService +{ + /// LLM 텍스트 응답을 HTML 보고서 파일로 자동 저장합니다. + private string? AutoSaveAsHtml(string textContent, string userQuery, AgentContext context) + { + try + { + // 파일명 생성 — 동사/명령어를 제거하여 깔끔한 파일명 만들기 + var title = userQuery.Length > Defaults.QueryTitleMaxLength ? userQuery[..Defaults.QueryTitleMaxLength] : userQuery; + // 파일명에 불필요한 동사/명령어 제거 + var removeWords = new[] { "작성해줘", "작성해 줘", "만들어줘", "만들어 줘", "써줘", "써 줘", + "생성해줘", "생성해 줘", "작성해", "만들어", "생성해", "해줘", "해 줘", "부탁해" }; + var safeTitle = title; + foreach (var w in removeWords) + safeTitle = safeTitle.Replace(w, "", StringComparison.OrdinalIgnoreCase); + foreach (var c in System.IO.Path.GetInvalidFileNameChars()) + safeTitle = safeTitle.Replace(c, '_'); + safeTitle = safeTitle.Trim().TrimEnd('.').Trim(); + + var fileName = $"{safeTitle}.html"; + var fullPath = FileReadTool.ResolvePath(fileName, context.WorkFolder); + if (context.ActiveTab == "Cowork") + fullPath = AgentContext.EnsureTimestampedPath(fullPath); + + var dir = System.IO.Path.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(dir)) System.IO.Directory.CreateDirectory(dir); + + // 텍스트 → HTML 변환 + var css = TemplateService.GetCss("professional"); + var htmlBody = ConvertTextToHtml(textContent); + + var html = $@" + + + +{EscapeHtml(title)} + + + +
+

{EscapeHtml(title)}

+
작성일: {DateTime.Now:yyyy-MM-dd} | AX Copilot 자동 생성
+{htmlBody} +
+ +"; + + System.IO.File.WriteAllText(fullPath, html, System.Text.Encoding.UTF8); + LogService.Info($"[AgentLoop] 문서 자동 저장 완료: {fullPath}"); + return fullPath; + } + catch (Exception ex) + { + LogService.Warn($"[AgentLoop] 문서 자동 저장 실패: {ex.Message}"); + return null; + } + } + + /// LLM 텍스트(마크다운 형식)를 HTML로 변환합니다. + private static string ConvertTextToHtml(string text) + { + var sb = new System.Text.StringBuilder(); + var lines = text.Split('\n'); + var inList = false; + var listType = "ul"; + + foreach (var rawLine in lines) + { + var line = rawLine.TrimEnd(); + + // 빈 줄 + if (string.IsNullOrWhiteSpace(line)) + { + if (inList) { sb.AppendLine($""); inList = false; } + continue; + } + + // 마크다운 제목 + if (line.StartsWith("### ")) + { + if (inList) { sb.AppendLine($""); inList = false; } + sb.AppendLine($"

{EscapeHtml(line[4..])}

"); + continue; + } + if (line.StartsWith("## ")) + { + if (inList) { sb.AppendLine($""); inList = false; } + sb.AppendLine($"

{EscapeHtml(line[3..])}

"); + continue; + } + if (line.StartsWith("# ")) + { + if (inList) { sb.AppendLine($""); inList = false; } + sb.AppendLine($"

{EscapeHtml(line[2..])}

"); + continue; + } + + // 번호 리스트 (1. 2. 등) - 대제목급이면 h2로 + if (System.Text.RegularExpressions.Regex.IsMatch(line, @"^\d+\.\s+\S")) + { + var content = System.Text.RegularExpressions.Regex.Replace(line, @"^\d+\.\s+", ""); + // 짧고 제목 같으면 h2, 길면 리스트 + if (content.Length < 80 && !content.Contains('.') && !line.StartsWith(" ")) + { + if (inList) { sb.AppendLine($""); inList = false; } + sb.AppendLine($"

{EscapeHtml(line)}

"); + } + else + { + if (!inList) { sb.AppendLine("
    "); inList = true; listType = "ol"; } + sb.AppendLine($"
  1. {EscapeHtml(content)}
  2. "); + } + continue; + } + + // 불릿 리스트 + if (line.TrimStart().StartsWith("- ") || line.TrimStart().StartsWith("* ") || line.TrimStart().StartsWith("• ")) + { + var content = line.TrimStart()[2..].Trim(); + if (!inList) { sb.AppendLine("