Files
AX-Copilot/src/AxCopilot/ViewModels/SettingsViewModel.cs
lacvet e33f8ac620 [Phase UX] 창 위치 기억·파일 아이콘·퍼지 랭킹·미리보기 패널 4종 UX 개선
창 위치 기억 (Feature 1):
- AppSettings.cs: LauncherSettings에 RememberPosition, LastLeft, LastTop 프로퍼티 추가
- SettingsViewModel.cs/.Properties.cs/.Methods.cs: RememberPosition 바인딩 프로퍼티 연동
- LauncherWindow.Animations.cs: CenterOnScreen() — RememberPosition ON 시 저장 좌표 복원
- LauncherWindow.Shell.cs: Window_Deactivated — 비활성화 시 현재 위치 비동기 저장
- SettingsWindow.xaml: 런처 탭 › "마지막 위치 기억" 토글 추가

파일 아이콘 표시 (Feature 2):
- Services/IconCacheService.cs (신규, 192줄): Shell32 SHGetFileInfo로 아이콘 추출,
  %LOCALAPPDATA%\AxCopilot\IconCache\에 PNG 캐시, WarmUp()으로 앱 시작 시 미리 준비
- Core/CommandResolver.cs: 퍼지 검색 결과에 IconCacheService.GetIconPath() 연결
- Handlers/FileBrowserHandler.cs: 상위폴더·폴더·파일 항목에 IconCacheService 연결
- App.xaml.cs: SystemIdle 시점에 IconCacheService.WarmUp() 호출

퍼지 검색 랭킹 개선 (Feature 3):
- Services/UsageRankingService.cs 전면 개선: 기존 int 횟수 → UsageRecord{Count, LastUsedMs}
- GetScore() 반환형 int → double, 30일 반감기 지수 감쇠(decay=exp(-days/43.3)) 적용
- 구형 usage.json 자동 마이그레이션 (count만 있는 형식 → 신규 형식)
- GetTopItems() / SortByUsage() 점수 기준 정렬로 업데이트

미리보기 패널 (Feature 4):
- ViewModels/LauncherViewModel.cs: PreviewText, HasPreview 프로퍼티 + UpdatePreviewAsync()
  클립보드 텍스트(최대 400자) 및 텍스트 파일(최초 6줄) 미리보기, 80ms 디바운스
- Views/LauncherWindow.xaml: RowDefinitions 7→8개, Row5에 PreviewPanel Border 삽입,
  IndexStatusText Row5→6, WidgetBar Row6→7, ToastOverlay RowSpan 3→4

빌드: 경고 0, 오류 0
2026-04-04 17:38:12 +09:00

327 lines
15 KiB
C#

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Forms;
using System.Windows.Media;
using AxCopilot.Models;
using AxCopilot.Services;
using AxCopilot.Themes;
using AxCopilot.Views;
namespace AxCopilot.ViewModels;
public partial class SettingsViewModel : INotifyPropertyChanged
{
private readonly SettingsService _service;
internal SettingsService Service => _service;
// ─── 작업 복사본 ───────────────────────────────────────────────────────
private string _hotkey;
private int _maxResults;
private double _opacity;
private string _selectedThemeKey;
private string _launcherPosition;
private string _webSearchEngine;
private bool _snippetAutoExpand;
private string _language;
private string _indexSpeed;
// 기능 토글
private bool _showNumberBadges;
private bool _enableFavorites;
private bool _enableRecent;
private bool _enableActionMode;
private bool _closeOnFocusLost;
private bool _rememberPosition;
private bool _showPrefixBadge;
private bool _enableIconAnimation;
private bool _enableRainbowGlow;
private bool _enableSelectionGlow;
private bool _enableRandomPlaceholder;
private bool _showLauncherBorder;
private bool _shortcutHelpUseThemeColor;
// LLM 공통 설정
private string _llmService;
private bool _llmStreaming;
private int _llmMaxContextTokens;
private int _llmRetentionDays;
private double _llmTemperature;
// 서비스별 독립 설정
private string _ollamaEndpoint = "http://localhost:11434";
private string _ollamaApiKey = "";
private string _ollamaModel = "";
private string _vllmEndpoint = "";
private string _vllmApiKey = "";
private string _vllmModel = "";
private string _geminiApiKey = "";
private string _geminiModel = "gemini-2.5-flash";
private string _claudeApiKey = "";
private string _claudeModel = "claude-sonnet-4-6";
// ─── 이벤트 ───────────────────────────────────────────────────────────
public event EventHandler? ThemePreviewRequested;
public event EventHandler? SaveCompleted;
public SettingsViewModel(SettingsService service)
{
_service = service;
var s = service.Settings;
_hotkey = s.Hotkey;
_maxResults = s.Launcher.MaxResults;
_opacity = s.Launcher.Opacity;
_selectedThemeKey = (s.Launcher.Theme ?? "system").ToLowerInvariant();
_launcherPosition = s.Launcher.Position;
_webSearchEngine = s.Launcher.WebSearchEngine;
_snippetAutoExpand = s.Launcher.SnippetAutoExpand;
_language = s.Launcher.Language;
_indexSpeed = s.IndexSpeed ?? "normal";
// 기능 토글 로드
_showNumberBadges = s.Launcher.ShowNumberBadges;
_enableFavorites = s.Launcher.EnableFavorites;
_enableRecent = s.Launcher.EnableRecent;
_enableActionMode = s.Launcher.EnableActionMode;
_closeOnFocusLost = s.Launcher.CloseOnFocusLost;
_rememberPosition = s.Launcher.RememberPosition;
_showPrefixBadge = s.Launcher.ShowPrefixBadge;
_enableIconAnimation = s.Launcher.EnableIconAnimation;
_enableRainbowGlow = s.Launcher.EnableRainbowGlow;
_enableSelectionGlow = s.Launcher.EnableSelectionGlow;
_enableRandomPlaceholder = s.Launcher.EnableRandomPlaceholder;
_showLauncherBorder = s.Launcher.ShowLauncherBorder;
_shortcutHelpUseThemeColor = s.Launcher.ShortcutHelpUseThemeColor;
_enableTextAction = s.Launcher.EnableTextAction;
// v1.7.1: 파일 대화상자 통합 기본값을 false로 변경 (브라우저 업로드 오작동 방지)
// 기존 저장값이 true이면 false로 재설정
_enableFileDialogIntegration = false;
s.Launcher.EnableFileDialogIntegration = false;
_enableClipboardAutoCategory = s.Launcher.EnableClipboardAutoCategory;
_maxPinnedClipboardItems = s.Launcher.MaxPinnedClipboardItems;
_textActionTranslateLanguage = s.Launcher.TextActionTranslateLanguage;
_maxSubAgents = s.Llm.MaxSubAgents;
_pdfExportPath = s.Llm.PdfExportPath;
_tipDurationSeconds = s.Llm.TipDurationSeconds;
// LLM 설정 로드
var llm = s.Llm;
_llmService = llm.Service;
_llmStreaming = llm.Streaming;
_llmMaxContextTokens = llm.MaxContextTokens;
_llmRetentionDays = llm.RetentionDays;
_llmTemperature = llm.Temperature;
_defaultAgentPermission = llm.DefaultAgentPermission;
_defaultOutputFormat = llm.DefaultOutputFormat;
_defaultMood = string.IsNullOrEmpty(llm.DefaultMood) ? "modern" : llm.DefaultMood;
_autoPreview = llm.AutoPreview;
_maxAgentIterations = llm.MaxAgentIterations > 0 ? llm.MaxAgentIterations : 25;
_maxRetryOnError = llm.MaxRetryOnError > 0 ? llm.MaxRetryOnError : 3;
_agentLogLevel = llm.AgentLogLevel;
_agentDecisionLevel = llm.AgentDecisionLevel;
_planMode = string.IsNullOrEmpty(llm.PlanMode) ? "off" : llm.PlanMode;
_enableMultiPassDocument = llm.EnableMultiPassDocument;
_enableCoworkVerification = llm.EnableCoworkVerification;
_enableFilePathHighlight = llm.EnableFilePathHighlight;
_folderDataUsage = string.IsNullOrEmpty(llm.FolderDataUsage) ? "active" : llm.FolderDataUsage;
_enableAuditLog = llm.EnableAuditLog;
_enableAgentMemory = llm.EnableAgentMemory;
_enableProjectRules = llm.EnableProjectRules;
_maxMemoryEntries = llm.MaxMemoryEntries;
_enableImageInput = llm.EnableImageInput;
_maxImageSizeKb = llm.MaxImageSizeKb > 0 ? llm.MaxImageSizeKb : 5120;
_enableToolHooks = llm.EnableToolHooks;
_toolHookTimeoutMs = llm.ToolHookTimeoutMs > 0 ? llm.ToolHookTimeoutMs : 10000;
_enableSkillSystem = llm.EnableSkillSystem;
_skillsFolderPath = llm.SkillsFolderPath;
_slashPopupPageSize = llm.SlashPopupPageSize > 0 ? Math.Clamp(llm.SlashPopupPageSize, 3, 10) : 6;
_enableDragDropAiActions = llm.EnableDragDropAiActions;
_dragDropAutoSend = llm.DragDropAutoSend;
_enableCodeReview = llm.Code.EnableCodeReview;
_enableAutoRouter = llm.EnableAutoRouter;
_autoRouterConfidence = llm.AutoRouterConfidence;
_enableChatRainbowGlow = llm.EnableChatRainbowGlow;
_notifyOnComplete = llm.NotifyOnComplete;
_showTips = llm.ShowTips;
_devMode = llm.DevMode; // 저장된 설정 유지 (한 번 켜면 유지, 끄면 하위 기능 비활성)
_devModeStepApproval = llm.DevModeStepApproval;
_workflowVisualizer = llm.WorkflowVisualizer;
_freeTierMode = llm.FreeTierMode;
_freeTierDelaySeconds = llm.FreeTierDelaySeconds > 0 ? llm.FreeTierDelaySeconds : 4;
_showTotalCallStats = llm.ShowTotalCallStats;
// 서비스별 독립 설정 로드
_ollamaEndpoint = llm.OllamaEndpoint;
_ollamaModel = llm.OllamaModel;
_vllmEndpoint = llm.VllmEndpoint;
_vllmModel = llm.VllmModel;
_geminiModel = string.IsNullOrEmpty(llm.GeminiModel) ? "gemini-2.5-flash" : llm.GeminiModel;
_claudeModel = string.IsNullOrEmpty(llm.ClaudeModel) ? "claude-sonnet-4-6" : llm.ClaudeModel;
// API 키 로드: 서비스별 독립 저장
_geminiApiKey = llm.GeminiApiKey;
_claudeApiKey = llm.ClaudeApiKey;
if (llm.EncryptionEnabled)
{
_ollamaApiKey = string.IsNullOrEmpty(llm.OllamaApiKey) ? "" : "(저장됨)";
_vllmApiKey = string.IsNullOrEmpty(llm.VllmApiKey) ? "" : "(저장됨)";
}
else
{
_ollamaApiKey = llm.OllamaApiKey;
_vllmApiKey = llm.VllmApiKey;
}
// 기존 단일 설정에서 마이그레이션 (최초 1회)
if (string.IsNullOrEmpty(llm.OllamaEndpoint) && !string.IsNullOrEmpty(llm.Endpoint) && llm.Service == "ollama")
{
_ollamaEndpoint = llm.Endpoint;
_ollamaModel = llm.Model;
if (!llm.EncryptionEnabled) _ollamaApiKey = llm.EncryptedApiKey;
}
if (string.IsNullOrEmpty(llm.VllmEndpoint) && !string.IsNullOrEmpty(llm.Endpoint) && llm.Service == "vllm")
{
_vllmEndpoint = llm.Endpoint;
_vllmModel = llm.Model;
if (!llm.EncryptionEnabled) _vllmApiKey = llm.EncryptedApiKey;
}
if (string.IsNullOrEmpty(llm.GeminiApiKey) && !string.IsNullOrEmpty(llm.ApiKey) && llm.Service == "gemini")
{
_geminiApiKey = llm.ApiKey;
_geminiModel = llm.Model;
}
if (string.IsNullOrEmpty(llm.ClaudeApiKey) && !string.IsNullOrEmpty(llm.ApiKey) && llm.Service == "claude")
{
_claudeApiKey = llm.ApiKey;
_claudeModel = llm.Model;
}
// 차단 경로/확장자 로드
foreach (var p in llm.BlockedPaths) BlockedPaths.Add(p);
foreach (var e in llm.BlockedExtensions) BlockedExtensions.Add(e);
// 등록 모델 로드
foreach (var rm in llm.RegisteredModels)
RegisteredModels.Add(new RegisteredModelRow
{
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 ?? "",
});
// 프롬프트 템플릿 로드
foreach (var pt in llm.PromptTemplates)
PromptTemplates.Add(new PromptTemplateRow { Name = pt.Name, Content = pt.Content, Icon = pt.Icon });
foreach (var card in ThemeCards)
card.IsSelected = card.Key == _selectedThemeKey;
// 알림 설정 로드
_reminderEnabled = s.Reminder.Enabled;
_reminderCorner = s.Reminder.Corner;
_reminderIntervalMinutes = s.Reminder.IntervalMinutes;
_reminderDisplaySeconds = s.Reminder.DisplaySeconds;
// 캡처 설정 로드
_capPrefix = string.IsNullOrWhiteSpace(s.ScreenCapture.Prefix) ? "cap" : s.ScreenCapture.Prefix;
_capGlobalHotkeyEnabled = s.ScreenCapture.GlobalHotkeyEnabled;
_capGlobalHotkey = string.IsNullOrWhiteSpace(s.ScreenCapture.GlobalHotkey) ? "PrintScreen" : s.ScreenCapture.GlobalHotkey;
_capGlobalMode = string.IsNullOrWhiteSpace(s.ScreenCapture.GlobalHotkeyMode) ? "screen" : s.ScreenCapture.GlobalHotkeyMode;
_capScrollDelayMs = s.ScreenCapture.ScrollDelayMs > 0 ? s.ScreenCapture.ScrollDelayMs : 120;
// 클립보드 히스토리 설정 로드
_clipboardEnabled = s.ClipboardHistory.Enabled;
_clipboardMaxItems = s.ClipboardHistory.MaxItems;
// 시스템 명령 설정 로드
var sys = s.SystemCommands;
_sysShowLock = sys.ShowLock;
_sysShowSleep = sys.ShowSleep;
_sysShowRestart = sys.ShowRestart;
_sysShowShutdown = sys.ShowShutdown;
_sysShowHibernate = sys.ShowHibernate;
_sysShowLogout = sys.ShowLogout;
_sysShowRecycleBin = sys.ShowRecycleBin;
// 시스템 명령 별칭 로드
var ca = sys.CommandAliases;
_aliasLock = FormatAliases(ca, "lock");
_aliasSleep = FormatAliases(ca, "sleep");
_aliasRestart = FormatAliases(ca, "restart");
_aliasShutdown = FormatAliases(ca, "shutdown");
_aliasHibernate = FormatAliases(ca, "hibernate");
_aliasLogout = FormatAliases(ca, "logout");
_aliasRecycle = FormatAliases(ca, "recycle");
// 인덱스 경로 로드
foreach (var path in s.IndexPaths)
IndexPaths.Add(path);
// 인덱스 확장자 로드
foreach (var ext in s.IndexExtensions)
IndexExtensions.Add(ext);
// 전용 핫키 로드
foreach (var h in s.CustomHotkeys)
CustomHotkeys.Add(new HotkeyRowModel { Hotkey = h.Hotkey, Target = h.Target, Label = h.Label, Type = h.Type });
// 스니펫 로드
foreach (var sn in s.Snippets)
Snippets.Add(new SnippetRowModel { Key = sn.Key, Name = sn.Name, Content = sn.Content });
// 배치 명령 로드
foreach (var alias in s.Aliases.Where(a => a.Type == "batch"))
{
BatchCommands.Add(new BatchCommandModel
{
Key = alias.Key,
Command = alias.Target,
ShowWindow = alias.ShowWindow
});
}
// 기존 aliases 로드 (app/url/folder type만)
foreach (var alias in s.Aliases.Where(a => a.Type is "app" or "url" or "folder"))
{
AppShortcuts.Add(new AppShortcutModel
{
Key = alias.Key,
Description = alias.Description ?? "",
Target = alias.Target,
Type = alias.Type
});
}
var c = s.Launcher.CustomTheme ?? new CustomThemeColors();
ColorRows = new List<ColorRowModel>
{
new("런처 배경", nameof(CustomThemeColors.LauncherBackground), c.LauncherBackground),
new("항목 배경", nameof(CustomThemeColors.ItemBackground), c.ItemBackground),
new("선택 항목 배경", nameof(CustomThemeColors.ItemSelectedBackground), c.ItemSelectedBackground),
new("호버 배경", nameof(CustomThemeColors.ItemHoverBackground), c.ItemHoverBackground),
new("기본 텍스트", nameof(CustomThemeColors.PrimaryText), c.PrimaryText),
new("보조 텍스트", nameof(CustomThemeColors.SecondaryText), c.SecondaryText),
new("플레이스홀더", nameof(CustomThemeColors.PlaceholderText), c.PlaceholderText),
new("강조색", nameof(CustomThemeColors.AccentColor), c.AccentColor),
new("구분선", nameof(CustomThemeColors.SeparatorColor), c.SeparatorColor),
new("힌트 배경", nameof(CustomThemeColors.HintBackground), c.HintBackground),
new("힌트 텍스트", nameof(CustomThemeColors.HintText), c.HintText),
new("테두리", nameof(CustomThemeColors.BorderColor), c.BorderColor),
new("스크롤바", nameof(CustomThemeColors.ScrollbarThumb), c.ScrollbarThumb),
new("그림자", nameof(CustomThemeColors.ShadowColor), c.ShadowColor),
};
_customWindowCornerRadius = c.WindowCornerRadius;
_customItemCornerRadius = c.ItemCornerRadius;
}
}