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>
374 lines
14 KiB
C#
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);
|
|
}
|
|
}
|