Files
AX-Copilot/src/AxCopilot/Views/SettingsWindow.xaml.cs
lacvet 08524466d2 [Phase 37-38] ChatWindow·SettingsWindow 파셜 클래스 분할 + 코드 품질 개선
Phase 37 — ChatWindow God Class 파셜 분할 (10,184 → 4,767줄, -53%)
  - ChatWindow.MessageRendering.cs (522줄): 메시지 렌더링, 체크 아이콘
  - ChatWindow.SlashCommands.cs (579줄): 슬래시 명령, 드래그앤드롭
  - ChatWindow.AgentSupport.cs (475줄): 에이전트 루프, 시스템 프롬프트
  - ChatWindow.TaskDecomposition.cs (1,170줄): Plan UI, Diff, 이벤트 배너
  - ChatWindow.Presets.cs (1,280줄): 프리셋, 하단바, 설정 토글
  - ChatWindow.ModelSelector.cs (395줄): 모델 선택, 대화 관리
  - ChatWindow.PreviewAndFiles.cs (1,105줄): 미리보기, 파일 탐색기

Phase 38 — SettingsWindow 파셜 분할 (3,216 → 373줄, -88%)
  - SettingsWindow.UI.cs (802줄): 탭 전환, 독바, 스토리지, 핫키
  - SettingsWindow.Tools.cs (875줄): 도구 카드 UI, AX Agent 탭
  - SettingsWindow.AgentConfig.cs (1,202줄): 모델, 스킬, 훅, MCP

Phase 35-36 — 코드 품질 심층 정리
  - bare catch 전량 → catch (Exception) (109개 파일)
  - ColorConverter → ThemeResourceHelper.HexBrush() (81건)
  - Application.Current as App → CurrentApp 프로퍼티 (15개 파일)
  - AgentContext.Settings DI 주입 (11개 에이전트 도구)
  - PopupMenuHelper 실제 적용 (4개 팝업)

CLAUDE.md: 작업 후 깃 푸시 + 오류 시 롤백 지침 추가
docs: TECHNOLOGY_OVERVIEW.md 신규 작성 (762줄 기술 문서)

빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 18:37:54 +09:00

374 lines
14 KiB
C#

using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using AxCopilot.Services;
using AxCopilot.ViewModels;
namespace AxCopilot.Views;
public partial class SettingsWindow : Window
{
private readonly SettingsViewModel _vm;
private readonly Action<string> _previewCallback;
private readonly Action _revertCallback;
private bool _saved;
/// <summary>App 인스턴스 캐시 — <c>Application.Current as App</c> 반복 호출 제거.</summary>
private static App? CurrentApp => System.Windows.Application.Current as App;
/// <summary>
/// 핫키 녹화 시작/종료를 외부(App.xaml.cs)에 알리는 콜백.
/// true = 녹화 시작(핫키 일시 정지), false = 녹화 종료(핫키 재개)
/// </summary>
public Action<bool>? SuspendHotkeyCallback { get; set; }
/// <param name="previewCallback">테마 키를 받아 런처에 즉시 미리보기 적용</param>
/// <param name="revertCallback">취소/X 닫기 시 원래 설정으로 복원</param>
public SettingsWindow(SettingsViewModel vm, Action<string> previewCallback, Action revertCallback)
{
InitializeComponent();
_vm = vm;
_previewCallback = previewCallback;
_revertCallback = revertCallback;
DataContext = vm;
vm.ThemePreviewRequested += (_, _) => _previewCallback(vm.SelectedThemeKey);
vm.SaveCompleted += (_, _) =>
{
_saved = true;
Close();
// 인덱스 재빌드를 백그라운드에서 조용히 실행 (UI 차단 없음)
_ = Task.Run(async () =>
{
try
{
var indexService = CurrentApp?.IndexService;
if (indexService != null)
await indexService.BuildAsync();
}
catch (Exception) { /* 인덱싱 실패해도 설정 저장은 완료 */ }
});
};
Loaded += async (_, _) =>
{
RefreshHotkeyBadges();
SetVersionText();
EnsureHotkeyInCombo();
BuildQuoteCategoryCheckboxes();
BuildDockBarSettings();
BuildTextActionCommandsPanel();
MoveBlockSectionToEtc();
// 스킬이 아직 로드되지 않았으면 백그라운드에서 로드 후 UI 구성
var app = CurrentApp;
var skillsLoaded = Services.Agent.SkillService.Skills.Count > 0;
if (!skillsLoaded && (app?.SettingsService?.Settings.Llm.EnableSkillSystem ?? false))
{
// 스킬 로드 (RuntimeDetector 포함)를 백그라운드에서 실행
await Task.Run(() =>
{
Services.Agent.SkillService.EnsureSkillFolder();
Services.Agent.SkillService.LoadSkills(app?.SettingsService?.Settings.Llm.SkillsFolderPath);
});
}
BuildToolRegistryPanel();
LoadAdvancedSettings();
RefreshStorageInfo();
// 개발자 모드는 저장된 설정 유지 (끄면 하위 기능 모두 비활성)
UpdateDevModeContentVisibility();
// AI 기능 토글 초기화
ApplyAiEnabledState(app?.SettingsService?.Settings.AiEnabled ?? false, init: true);
// 네트워크 모드 토글 초기화 (사내 모드 = true = 차단)
if (InternalModeToggle != null)
InternalModeToggle.IsChecked = app?.SettingsService?.Settings.InternalModeEnabled ?? true;
};
}
// ─── 에이전트 차단 섹션 → 기타 탭 이동 ──────────────────────────────────────
private void MoveBlockSectionToEtc()
{
if (AgentBlockSection == null || AgentEtcContent == null) return;
var parent = AgentBlockSection.Parent as Panel;
if (parent != null)
{
parent.Children.Remove(AgentBlockSection);
AgentBlockSection.Margin = new Thickness(0, 0, 0, 16);
AgentEtcContent.Children.Add(AgentBlockSection);
}
}
private void AddSnippet_Click(object sender, RoutedEventArgs e)
{
if (!_vm.AddSnippet())
CustomMessageBox.Show(
"키워드와 내용은 필수 항목입니다.\n동일한 키워드가 이미 존재하면 추가할 수 없습니다.",
"입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
}
private void DeleteSnippet_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is ViewModels.SnippetRowModel row)
_vm.RemoveSnippet(row);
}
private void Browse_Click(object sender, RoutedEventArgs e) => _vm.BrowseTarget();
private void AddShortcut_Click(object sender, RoutedEventArgs e)
{
if (!_vm.AddShortcut())
CustomMessageBox.Show(
"키워드와 실행 대상은 필수 항목입니다.\n이미 동일한 키워드가 존재하면 추가할 수 없습니다.",
"입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
}
private void DeleteShortcut_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is ViewModels.AppShortcutModel shortcut)
_vm.RemoveShortcut(shortcut);
}
private void AddBatchCommand_Click(object sender, RoutedEventArgs e)
{
if (!_vm.AddBatchCommand())
CustomMessageBox.Show(
"키워드와 명령어는 필수 항목입니다.\n동일한 키워드가 이미 존재하면 추가할 수 없습니다.",
"입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
}
private void DeleteBatchCommand_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is ViewModels.BatchCommandModel cmd)
_vm.RemoveBatchCommand(cmd);
}
private void ResetCommandAliases_Click(object sender, RoutedEventArgs e)
{
_vm.ResetSystemCommandAliases();
}
private void AddIndexPath_Click(object sender, RoutedEventArgs e)
{
var text = NewIndexPathBox?.Text?.Trim() ?? "";
if (!string.IsNullOrWhiteSpace(text))
{
_vm.AddIndexPath(text);
if (NewIndexPathBox != null) NewIndexPathBox.Text = "";
}
}
private void BrowseIndexPath_Click(object sender, RoutedEventArgs e) => _vm.BrowseIndexPath();
private void ResetCapPrefix_Click(object sender, RoutedEventArgs e) => _vm.ResetCapPrefix();
private void ResetCapGlobalHotkey_Click(object sender, RoutedEventArgs e) => _vm.ResetCapGlobalHotkey();
private void CapHotkeyRecorder_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
e.Handled = true;
var key = e.Key == System.Windows.Input.Key.System ? e.SystemKey : e.Key;
// 수정자만 입력된 경우 무시
if (key is System.Windows.Input.Key.LeftCtrl or System.Windows.Input.Key.RightCtrl
or System.Windows.Input.Key.LeftAlt or System.Windows.Input.Key.RightAlt
or System.Windows.Input.Key.LeftShift or System.Windows.Input.Key.RightShift
or System.Windows.Input.Key.LWin or System.Windows.Input.Key.RWin)
return;
if (key == System.Windows.Input.Key.Escape) return;
var parts = new List<string>();
if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Control)) parts.Add("Ctrl");
if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Alt)) parts.Add("Alt");
if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Shift)) parts.Add("Shift");
if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Windows)) parts.Add("Win");
var keyName = key switch
{
System.Windows.Input.Key.Snapshot => "PrintScreen",
System.Windows.Input.Key.Scroll => "ScrollLock",
System.Windows.Input.Key.Pause => "Pause",
_ => key.ToString()
};
parts.Add(keyName);
var hotkey = string.Join("+", parts);
if (AxCopilot.Core.HotkeyParser.TryParse(hotkey, out _))
_vm.CapGlobalHotkey = hotkey;
}
private void ReminderCorner_Click(object sender, RoutedEventArgs e)
{
if (sender is System.Windows.Controls.RadioButton rb && rb.Tag is string corner)
_vm.ReminderCorner = corner;
}
private void ReminderPreview_Click(object sender, RoutedEventArgs e)
{
try
{
var svc = _vm.Service;
var (text, author) = Services.QuoteService.GetRandom(svc.Settings.Reminder.EnabledCategories);
var usage = TimeSpan.FromMinutes(42); // 샘플 값
// 현재 편집 중인 값을 임시로 적용
var cfg = svc.Settings.Reminder;
var origCorner = cfg.Corner;
var origSeconds = cfg.DisplaySeconds;
cfg.Corner = _vm.ReminderCorner;
cfg.DisplaySeconds = _vm.ReminderDisplaySeconds;
var popup = new ReminderPopupWindow(text, author, usage, svc);
popup.Show();
cfg.Corner = origCorner;
cfg.DisplaySeconds = origSeconds;
}
catch (Exception ex)
{
Services.LogService.Warn($"알림 미리보기 실패: {ex.Message}");
}
}
private void RemoveIndexPath_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is string path)
_vm.RemoveIndexPath(path);
}
private void AddExtension_Click(object sender, RoutedEventArgs e)
{
var text = NewExtensionBox?.Text?.Trim() ?? "";
if (!string.IsNullOrWhiteSpace(text))
{
_vm.AddExtension(text);
if (NewExtensionBox != null) NewExtensionBox.Text = "";
}
}
private void RemoveExtension_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is string ext)
_vm.RemoveExtension(ext);
}
private void Save_Click(object sender, RoutedEventArgs e)
{
SaveAdvancedSettings(); // 폴백/MCP 텍스트 박스 → settings 저장
_vm.Save();
CustomMessageBox.Show("설정이 저장되었습니다.", "AX Copilot", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
Close();
}
private void ExportSettings_Click(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.SaveFileDialog
{
Title = "설정 내보내기",
Filter = "AX Copilot 설정|*.axsettings",
FileName = $"AxCopilot_Settings_{DateTime.Now:yyyyMMdd}",
DefaultExt = ".axsettings"
};
if (dlg.ShowDialog() != true) return;
try
{
var srcPath = SettingsService.SettingsPath;
if (System.IO.File.Exists(srcPath))
{
System.IO.File.Copy(srcPath, dlg.FileName, overwrite: true);
CustomMessageBox.Show(
$"설정이 내보내졌습니다:\n{dlg.FileName}",
"AX Copilot", MessageBoxButton.OK,
MessageBoxImage.Information);
}
}
catch (Exception ex)
{
CustomMessageBox.Show($"내보내기 실패: {ex.Message}", "오류");
}
}
private void ImportSettings_Click(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Title = "설정 불러오기",
Filter = "AX Copilot 설정|*.axsettings|모든 파일|*.*"
};
if (dlg.ShowDialog() != true) return;
var result = CustomMessageBox.Show(
"설정을 불러오면 현재 설정이 덮어씌워집니다.\n계속하시겠습니까?",
"AX Copilot — 설정 불러오기",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != System.Windows.MessageBoxResult.Yes) return;
try
{
var fileContent = System.IO.File.ReadAllText(dlg.FileName);
// 암호화된 파일인지 판별: PortableDecrypt 시도
var json = CryptoService.PortableDecrypt(fileContent);
if (string.IsNullOrEmpty(json))
{
// 평문 JSON일 수 있음 — 직접 파싱 시도
try
{
var test = System.Text.Json.JsonSerializer.Deserialize<Models.AppSettings>(fileContent);
if (test != null) json = fileContent;
}
catch (Exception) { }
}
if (string.IsNullOrEmpty(json))
{
CustomMessageBox.Show("유효하지 않은 설정 파일입니다.", "오류");
return;
}
// 유효한 설정인지 최종 확인
var settings = System.Text.Json.JsonSerializer.Deserialize<Models.AppSettings>(json);
if (settings == null)
{
CustomMessageBox.Show("설정 파일을 파싱할 수 없습니다.", "오류");
return;
}
// 암호화하여 저장
var encrypted = CryptoService.PortableEncrypt(json);
System.IO.File.WriteAllText(SettingsService.SettingsPath, encrypted);
CustomMessageBox.Show(
"설정이 불러와졌습니다.\n변경 사항을 적용하려면 앱을 재시작하세요.",
"AX Copilot", MessageBoxButton.OK,
MessageBoxImage.Information);
Close();
}
catch (Exception ex)
{
CustomMessageBox.Show($"불러오기 실패: {ex.Message}", "오류");
}
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
// 저장하지 않고 닫아도 확인 없이 바로 닫힘 (revert는 OnClosed에서 처리)
base.OnClosing(e);
}
protected override void OnClosed(EventArgs e)
{
if (!_saved) _revertCallback();
base.OnClosed(e);
}
}