using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.ComponentModel; using System.Windows.Forms; using AxCopilot.Models; using AxCopilot.Services; using AxCopilot.Themes; using AxCopilot.Views; namespace AxCopilot.ViewModels; public partial class SettingsViewModel { // ─── 인덱스 경로 메서드 ────────────────────────────────────────────── public void AddIndexPath(string path) { var trimmed = path.Trim(); if (string.IsNullOrWhiteSpace(trimmed)) return; if (IndexPaths.Any(p => p.Equals(trimmed, StringComparison.OrdinalIgnoreCase))) return; IndexPaths.Add(trimmed); } public void RemoveIndexPath(string path) => IndexPaths.Remove(path); // ─── 인덱스 확장자 메서드 ────────────────────────────────────────── public void AddExtension(string ext) { var trimmed = ext.Trim().ToLowerInvariant(); if (string.IsNullOrWhiteSpace(trimmed)) return; if (!trimmed.StartsWith(".")) trimmed = "." + trimmed; if (IndexExtensions.Any(e => e.Equals(trimmed, StringComparison.OrdinalIgnoreCase))) return; IndexExtensions.Add(trimmed); } public void RemoveExtension(string ext) => IndexExtensions.Remove(ext); public void BrowseIndexPath() { using var dlg = new FolderBrowserDialog { Description = "인덱스할 폴더 선택", UseDescriptionForTitle = true, ShowNewFolderButton = false }; if (dlg.ShowDialog() != DialogResult.OK) return; AddIndexPath(dlg.SelectedPath); } // ─── 빠른 실행 단축키 메서드 ────────────────────────────────────────── public bool AddShortcut() { if (string.IsNullOrWhiteSpace(_newKey) || string.IsNullOrWhiteSpace(_newTarget)) return false; // 중복 키 확인 if (AppShortcuts.Any(s => s.Key.Equals(_newKey.Trim(), StringComparison.OrdinalIgnoreCase))) return false; AppShortcuts.Add(new AppShortcutModel { Key = _newKey.Trim(), Description = _newDescription.Trim(), Target = _newTarget.Trim(), Type = _newType }); NewKey = ""; NewDescription = ""; NewTarget = ""; NewType = "app"; return true; } public void RemoveShortcut(AppShortcutModel shortcut) => AppShortcuts.Remove(shortcut); /// 파일 선택 대화상자. 선택 시 NewTarget에 자동 설정. public void BrowseTarget() { using var dlg = new OpenFileDialog { Filter = "실행 파일 (*.exe)|*.exe|모든 파일 (*.*)|*.*", Title = "앱 선택" }; if (dlg.ShowDialog() != DialogResult.OK) return; NewTarget = dlg.FileName; NewType = "app"; if (string.IsNullOrWhiteSpace(NewDescription)) NewDescription = System.IO.Path.GetFileNameWithoutExtension(dlg.FileName); } public void SelectTheme(string key) { SelectedThemeKey = key; ThemePreviewRequested?.Invoke(this, EventArgs.Empty); } public void PickColor(ColorRowModel row) { using var dlg = new ColorDialog { FullOpen = true }; try { var color = ThemeResourceHelper.HexColor(row.Hex); dlg.Color = System.Drawing.Color.FromArgb(color.R, color.G, color.B); } catch (Exception) { /* 기본값 사용 */ } if (dlg.ShowDialog() != DialogResult.OK) return; row.Hex = $"#{dlg.Color.R:X2}{dlg.Color.G:X2}{dlg.Color.B:X2}"; if (_selectedThemeKey == "custom") ThemePreviewRequested?.Invoke(this, EventArgs.Empty); } // 시스템 예약 프리픽스 (핸들러에서 이미 사용 중인 키) private static readonly HashSet ReservedPrefixes = new(StringComparer.OrdinalIgnoreCase) { "=", "?", "#", "$", ";", "@", "~", ">", "!", "emoji", "color", "recent", "note", "uninstall", "kill", "media", "info", "*", "json", "encode", "port", "env", "snap", "help", "pick", "date", "svc", "pipe", "journal", "routine", "batch", "diff", "win", "stats", "fav", "rename", "monitor", "scaffold", }; /// 설정 저장 전 프리픽스/키워드 충돌을 검사합니다. 충돌 시 메시지를 반환합니다. public string? ValidateBeforeSave() { // 캡처 프리픽스 충돌 검사 var cap = string.IsNullOrWhiteSpace(_capPrefix) ? "cap" : _capPrefix.Trim(); if (cap != "cap" && ReservedPrefixes.Contains(cap)) return $"캡처 프리픽스 '{cap}'은(는) 이미 사용 중인 예약어입니다."; // 빠른 실행 별칭 키 중복 검사 var aliasKeys = AppShortcuts.Select(s => s.Key.ToLowerInvariant()).ToList(); var duplicateAlias = aliasKeys.GroupBy(k => k).FirstOrDefault(g => g.Count() > 1); if (duplicateAlias != null) return $"빠른 실행 키워드 '{duplicateAlias.Key}'이(가) 중복되었습니다."; // 빠른 실행 별칭 키 vs 예약 프리픽스 충돌 foreach (var key in aliasKeys) { if (ReservedPrefixes.Contains(key)) return $"빠른 실행 키워드 '{key}'은(는) 시스템 예약어와 충돌합니다."; } // 배치 명령 키 중복 검사 var batchKeys = BatchCommands.Select(b => b.Key.ToLowerInvariant()).ToList(); var duplicateBatch = batchKeys.GroupBy(k => k).FirstOrDefault(g => g.Count() > 1); if (duplicateBatch != null) return $"배치 명령 키워드 '{duplicateBatch.Key}'이(가) 중복되었습니다."; return null; // 문제 없음 } public void Save() { // 충돌 검사 var conflict = ValidateBeforeSave(); if (conflict != null) { CustomMessageBox.Show( conflict, "AX Copilot — 설정 저장 오류", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning); return; } var s = _service.Settings; s.Hotkey = _hotkey; s.Launcher.MaxResults = _maxResults; s.Launcher.Opacity = _opacity; s.Launcher.Theme = _selectedThemeKey; s.Launcher.Position = _launcherPosition; s.Launcher.WebSearchEngine = _webSearchEngine; s.Launcher.SnippetAutoExpand = _snippetAutoExpand; s.Launcher.Language = _language; L10n.SetLanguage(_language); // 기능 토글 저장 s.Launcher.ShowNumberBadges = _showNumberBadges; s.Launcher.EnableFavorites = _enableFavorites; s.Launcher.EnableRecent = _enableRecent; s.Launcher.EnableActionMode = _enableActionMode; s.Launcher.CloseOnFocusLost = _closeOnFocusLost; s.Launcher.ShowPrefixBadge = _showPrefixBadge; s.Launcher.EnableIconAnimation = _enableIconAnimation; s.Launcher.EnableRainbowGlow = _enableRainbowGlow; s.Launcher.EnableSelectionGlow = _enableSelectionGlow; s.Launcher.EnableRandomPlaceholder = _enableRandomPlaceholder; s.Launcher.ShowLauncherBorder = _showLauncherBorder; s.Launcher.ShortcutHelpUseThemeColor = _shortcutHelpUseThemeColor; s.Launcher.EnableTextAction = _enableTextAction; s.Launcher.EnableFileDialogIntegration = _enableFileDialogIntegration; s.Launcher.EnableClipboardAutoCategory = _enableClipboardAutoCategory; s.Launcher.MaxPinnedClipboardItems = _maxPinnedClipboardItems; s.Launcher.TextActionTranslateLanguage = _textActionTranslateLanguage; s.Llm.MaxSubAgents = _maxSubAgents; s.Llm.PdfExportPath = _pdfExportPath; s.Llm.TipDurationSeconds = _tipDurationSeconds; // LLM 공통 설정 저장 s.Llm.Service = _llmService; s.Llm.Streaming = _llmStreaming; s.Llm.MaxContextTokens = _llmMaxContextTokens; s.Llm.RetentionDays = _llmRetentionDays; s.Llm.Temperature = _llmTemperature; s.Llm.DefaultAgentPermission = _defaultAgentPermission; s.Llm.DefaultOutputFormat = _defaultOutputFormat; s.Llm.DefaultMood = _defaultMood; s.Llm.AutoPreview = _autoPreview; s.Llm.MaxAgentIterations = _maxAgentIterations; s.Llm.MaxRetryOnError = _maxRetryOnError; s.Llm.AgentLogLevel = _agentLogLevel; s.Llm.AgentDecisionLevel = _agentDecisionLevel; s.Llm.PlanMode = _planMode; s.Llm.EnableMultiPassDocument = _enableMultiPassDocument; s.Llm.EnableCoworkVerification = _enableCoworkVerification; s.Llm.EnableFilePathHighlight = _enableFilePathHighlight; s.Llm.FolderDataUsage = _folderDataUsage; s.Llm.EnableAuditLog = _enableAuditLog; s.Llm.EnableAgentMemory = _enableAgentMemory; s.Llm.EnableProjectRules = _enableProjectRules; s.Llm.MaxMemoryEntries = _maxMemoryEntries; s.Llm.EnableImageInput = _enableImageInput; s.Llm.MaxImageSizeKb = _maxImageSizeKb; s.Llm.EnableToolHooks = _enableToolHooks; s.Llm.ToolHookTimeoutMs = _toolHookTimeoutMs; s.Llm.EnableSkillSystem = _enableSkillSystem; s.Llm.SkillsFolderPath = _skillsFolderPath; s.Llm.SlashPopupPageSize = _slashPopupPageSize; s.Llm.EnableDragDropAiActions = _enableDragDropAiActions; s.Llm.DragDropAutoSend = _dragDropAutoSend; s.Llm.Code.EnableCodeReview = _enableCodeReview; s.Llm.EnableAutoRouter = _enableAutoRouter; s.Llm.AutoRouterConfidence = _autoRouterConfidence; s.Llm.EnableChatRainbowGlow = _enableChatRainbowGlow; s.Llm.NotifyOnComplete = _notifyOnComplete; s.Llm.ShowTips = _showTips; s.Llm.DevMode = _devMode; s.Llm.DevModeStepApproval = _devModeStepApproval; s.Llm.WorkflowVisualizer = _workflowVisualizer; s.Llm.FreeTierMode = _freeTierMode; s.Llm.FreeTierDelaySeconds = _freeTierDelaySeconds; s.Llm.ShowTotalCallStats = _showTotalCallStats; // 서비스별 독립 설정 저장 s.Llm.OllamaEndpoint = _ollamaEndpoint; s.Llm.OllamaModel = _ollamaModel; s.Llm.VllmEndpoint = _vllmEndpoint; s.Llm.VllmModel = _vllmModel; s.Llm.GeminiModel = _geminiModel; s.Llm.ClaudeModel = _claudeModel; s.Llm.GeminiApiKey = _geminiApiKey; s.Llm.ClaudeApiKey = _claudeApiKey; // 내부 서비스 API 키 저장 (암호화 분기) if (!string.IsNullOrEmpty(_ollamaApiKey) && _ollamaApiKey != "(저장됨)") s.Llm.OllamaApiKey = CryptoService.EncryptIfEnabled(_ollamaApiKey, s.Llm.EncryptionEnabled); if (!string.IsNullOrEmpty(_vllmApiKey) && _vllmApiKey != "(저장됨)") s.Llm.VllmApiKey = CryptoService.EncryptIfEnabled(_vllmApiKey, s.Llm.EncryptionEnabled); // 활성 서비스의 설정을 기존 호환 필드에도 동기화 (LlmService.cs 호환) switch (_llmService) { case "ollama": s.Llm.Endpoint = _ollamaEndpoint; s.Llm.Model = _ollamaModel; s.Llm.EncryptedApiKey = s.Llm.OllamaApiKey; break; case "vllm": s.Llm.Endpoint = _vllmEndpoint; s.Llm.Model = _vllmModel; s.Llm.EncryptedApiKey = s.Llm.VllmApiKey; break; case "gemini": s.Llm.ApiKey = _geminiApiKey; s.Llm.Model = _geminiModel; break; case "claude": s.Llm.ApiKey = _claudeApiKey; s.Llm.Model = _claudeModel; break; } // 등록 모델 저장 s.Llm.RegisteredModels = RegisteredModels .Where(rm => !string.IsNullOrWhiteSpace(rm.Alias)) .Select(rm => new RegisteredModel { Alias = rm.Alias, EncryptedModelName = rm.EncryptedModelName, Service = rm.Service, Endpoint = rm.Endpoint, ApiKey = rm.ApiKey, AuthType = rm.AuthType ?? "bearer", Cp4dUrl = rm.Cp4dUrl ?? "", Cp4dUsername = rm.Cp4dUsername ?? "", Cp4dPassword = rm.Cp4dPassword ?? "", }) .ToList(); // 프롬프트 템플릿 저장 s.Llm.PromptTemplates = PromptTemplates .Where(pt => !string.IsNullOrWhiteSpace(pt.Name)) .Select(pt => new PromptTemplate { Name = pt.Name, Content = pt.Content, Icon = pt.Icon }) .ToList(); // 인덱스 경로 + 확장자 저장 s.IndexPaths = IndexPaths.ToList(); s.IndexExtensions = IndexExtensions.ToList(); s.IndexSpeed = _indexSpeed; // 커스텀 색상 + 모양 저장 var c = s.Launcher.CustomTheme ??= new CustomThemeColors(); foreach (var row in ColorRows) { var prop = typeof(CustomThemeColors).GetProperty(row.Property); prop?.SetValue(c, row.Hex); } c.WindowCornerRadius = _customWindowCornerRadius; c.ItemCornerRadius = _customItemCornerRadius; // 빠른 실행 단축키 저장: // batch/api/clipboard type은 그대로 유지, app/url/folder는 ViewModel 내용으로 교체 var otherAliases = s.Aliases .Where(a => a.Type is not ("app" or "url" or "folder" or "batch")) .ToList(); s.Aliases = otherAliases .Concat(AppShortcuts.Select(sc => new AliasEntry { Key = sc.Key, Type = sc.Type, Target = sc.Target, Description = string.IsNullOrWhiteSpace(sc.Description) ? null : sc.Description })) .Concat(BatchCommands.Select(b => new AliasEntry { Key = b.Key, Type = "batch", Target = b.Command, ShowWindow = b.ShowWindow })) .ToList(); // 전용 핫키 저장 s.CustomHotkeys = CustomHotkeys.Select(h => new Models.HotkeyAssignment { Hotkey = h.Hotkey, Target = h.Target, Label = h.Label, Type = h.Type }).ToList(); // 스니펫 저장 s.Snippets = Snippets.Select(sn => new SnippetEntry { Key = sn.Key, Name = sn.Name, Content = sn.Content }).ToList(); // 알림 설정 저장 s.Reminder.Enabled = _reminderEnabled; s.Reminder.Corner = _reminderCorner; s.Reminder.IntervalMinutes = _reminderIntervalMinutes; s.Reminder.DisplaySeconds = _reminderDisplaySeconds; // 캡처 설정 저장 s.ScreenCapture.Prefix = string.IsNullOrWhiteSpace(_capPrefix) ? "cap" : _capPrefix.Trim(); s.ScreenCapture.GlobalHotkeyEnabled = _capGlobalHotkeyEnabled; s.ScreenCapture.GlobalHotkey = string.IsNullOrWhiteSpace(_capGlobalHotkey) ? "PrintScreen" : _capGlobalHotkey.Trim(); s.ScreenCapture.GlobalHotkeyMode = _capGlobalMode; s.ScreenCapture.ScrollDelayMs = Math.Max(50, _capScrollDelayMs); // 클립보드 히스토리 설정 저장 s.ClipboardHistory.Enabled = _clipboardEnabled; s.ClipboardHistory.MaxItems = _clipboardMaxItems; // 시스템 명령 설정 저장 var sc = s.SystemCommands; sc.ShowLock = _sysShowLock; sc.ShowSleep = _sysShowSleep; sc.ShowRestart = _sysShowRestart; sc.ShowShutdown = _sysShowShutdown; sc.ShowHibernate = _sysShowHibernate; sc.ShowLogout = _sysShowLogout; sc.ShowRecycleBin = _sysShowRecycleBin; // 시스템 명령 별칭 저장 var cmdAliases = new Dictionary>(); void SaveAlias(string key, string val) { var list = ParseAliases(val); if (list.Count > 0) cmdAliases[key] = list; } SaveAlias("lock", _aliasLock); SaveAlias("sleep", _aliasSleep); SaveAlias("restart", _aliasRestart); SaveAlias("shutdown", _aliasShutdown); SaveAlias("hibernate", _aliasHibernate); SaveAlias("logout", _aliasLogout); SaveAlias("recycle", _aliasRecycle); sc.CommandAliases = cmdAliases; _service.Save(); SaveCompleted?.Invoke(this, EventArgs.Empty); } public bool AddBatchCommand() { if (string.IsNullOrWhiteSpace(_newBatchKey) || string.IsNullOrWhiteSpace(_newBatchCommand)) return false; if (BatchCommands.Any(b => b.Key.Equals(_newBatchKey.Trim(), StringComparison.OrdinalIgnoreCase))) return false; BatchCommands.Add(new BatchCommandModel { Key = _newBatchKey.Trim(), Command = _newBatchCommand.Trim(), ShowWindow = _newBatchShowWindow }); NewBatchKey = ""; NewBatchCommand = ""; NewBatchShowWindow = false; return true; } public void RemoveBatchCommand(BatchCommandModel cmd) => BatchCommands.Remove(cmd); // ─── 전용 핫키 메서드 ───────────────────────────────────────────────────── public bool AddCustomHotkey() { if (string.IsNullOrWhiteSpace(_newHotkeyStr) || string.IsNullOrWhiteSpace(_newHotkeyTarget)) return false; var hotkey = _newHotkeyStr.Trim(); // 핫키 형식 검증 if (!Core.HotkeyParser.TryParse(hotkey, out _)) { CustomMessageBox.Show( $"핫키 형식이 올바르지 않습니다: '{hotkey}'\n예) Ctrl+Alt+1, Ctrl+Shift+F2", "AX Copilot — 핫키 오류", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning); return false; } // 중복 검사 if (CustomHotkeys.Any(h => h.Hotkey.Equals(hotkey, StringComparison.OrdinalIgnoreCase))) { CustomMessageBox.Show( $"이미 등록된 핫키입니다: '{hotkey}'", "AX Copilot — 중복 핫키", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning); return false; } CustomHotkeys.Add(new HotkeyRowModel { Hotkey = hotkey, Target = _newHotkeyTarget.Trim(), Label = string.IsNullOrWhiteSpace(_newHotkeyLabel) ? System.IO.Path.GetFileNameWithoutExtension(_newHotkeyTarget.Trim()) : _newHotkeyLabel.Trim(), Type = _newHotkeyType }); NewHotkeyStr = ""; NewHotkeyTarget = ""; NewHotkeyLabel = ""; NewHotkeyType = "app"; return true; } public void RemoveCustomHotkey(HotkeyRowModel row) => CustomHotkeys.Remove(row); // ─── 스니펫 메서드 ────────────────────────────────────────────────────── public bool AddSnippet() { if (string.IsNullOrWhiteSpace(_newSnippetKey) || string.IsNullOrWhiteSpace(_newSnippetContent)) return false; if (Snippets.Any(sn => sn.Key.Equals(_newSnippetKey.Trim(), StringComparison.OrdinalIgnoreCase))) return false; Snippets.Add(new SnippetRowModel { Key = _newSnippetKey.Trim(), Name = _newSnippetName.Trim(), Content = _newSnippetContent.Trim() }); NewSnippetKey = ""; NewSnippetName = ""; NewSnippetContent = ""; return true; } public void RemoveSnippet(SnippetRowModel row) => Snippets.Remove(row); // ─── 캡처 메서드 ──────────────────────────────────────────────────────── public void ResetCapPrefix() => CapPrefix = "cap"; public void ResetCapGlobalHotkey() { CapGlobalHotkey = "PrintScreen"; CapGlobalMode = "screen"; } // ─── 알림 메서드 ──────────────────────────────────────────────────────── public List GetReminderCategories() => _service.Settings.Reminder.EnabledCategories; // ─── 시스템 명령 메서드 ───────────────────────────────────────────────── public void ResetSystemCommandAliases() { AliasLock = ""; AliasSleep = ""; AliasRestart = ""; AliasShutdown = ""; AliasHibernate = ""; AliasLogout = ""; AliasRecycle = ""; } private static string FormatAliases(Dictionary> dict, string key) => dict.TryGetValue(key, out var list) ? string.Join(", ", list) : ""; private static List ParseAliases(string input) => input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(s => !string.IsNullOrWhiteSpace(s)) .ToList(); public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string? n = null) => PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(n)); }