[Phase 40] ChatWindow 2차 파셜 클래스 분할 (94.5% 감소)

4,767줄 ChatWindow.xaml.cs를 7개 파셜 파일로 추가 분할
메인 파일: 4,767줄 → 262줄 (94.5% 감소)
전체 ChatWindow 파셜 파일: 15개

- ChatWindow.Controls.cs (595줄): 사용자정보, 스크롤, 제목편집, 탭전환
- ChatWindow.WorkFolder.cs (359줄): 작업폴더, 폴더 설정
- ChatWindow.PermissionMenu.cs (498줄): 권한, 파일첨부, 사이드바
- ChatWindow.ConversationList.cs (747줄): 대화목록, 제목편집, 검색
- ChatWindow.Sending.cs (720줄): 전송, 편집모드, 타이머
- ChatWindow.HelpCommands.cs (157줄): /help 도움말
- ChatWindow.ResponseHandling.cs (1,494줄): 응답재생성, 스트리밍, 토스트
- 빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 19:21:52 +09:00
parent 0c997f0149
commit 6448451d78
9 changed files with 4592 additions and 4506 deletions

View File

@@ -0,0 +1,498 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Views;
public partial class ChatWindow
{
// ─── 권한 메뉴 ─────────────────────────────────────────────────────────
private void BtnPermission_Click(object sender, RoutedEventArgs e)
{
if (PermissionPopup == null) return;
PermissionItems.Children.Clear();
var levels = new (string Level, string Sym, string Desc, string Color)[] {
("Ask", "\uE8D7", "매번 확인 — 파일 접근 시 사용자에게 묻습니다", "#4B5EFC"),
("Auto", "\uE73E", "자동 허용 — 파일을 자동으로 읽고 씁니다", "#DD6B20"),
("Deny", "\uE711", "접근 차단 — 파일 접근을 허용하지 않습니다", "#C50F1F"),
};
var current = Llm.FilePermission;
foreach (var (level, sym, desc, color) in levels)
{
var isActive = level.Equals(current, StringComparison.OrdinalIgnoreCase);
// 라운드 코너 템플릿 (기본 Button 크롬 제거)
var template = new ControlTemplate(typeof(Button));
var bdFactory = new FrameworkElementFactory(typeof(Border));
bdFactory.SetValue(Border.BackgroundProperty, Brushes.Transparent);
bdFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
bdFactory.SetValue(Border.PaddingProperty, new Thickness(12, 8, 12, 8));
bdFactory.Name = "Bd";
var cpFactory = new FrameworkElementFactory(typeof(ContentPresenter));
cpFactory.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Left);
cpFactory.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
bdFactory.AppendChild(cpFactory);
template.VisualTree = bdFactory;
// 호버 효과
var hoverTrigger = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
hoverTrigger.Setters.Add(new Setter(Border.BackgroundProperty,
new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)), "Bd"));
template.Triggers.Add(hoverTrigger);
var btn = new Button
{
Template = template,
BorderThickness = new Thickness(0),
Cursor = Cursors.Hand,
HorizontalContentAlignment = HorizontalAlignment.Left,
Margin = new Thickness(0, 1, 0, 1),
};
ApplyHoverScaleAnimation(btn, 1.02);
var sp = new StackPanel { Orientation = Orientation.Horizontal };
// 커스텀 체크 아이콘
sp.Children.Add(CreateCheckIcon(isActive));
sp.Children.Add(new TextBlock
{
Text = sym, FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14,
Foreground = BrushFromHex(color),
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
});
var textStack = new StackPanel();
textStack.Children.Add(new TextBlock
{
Text = level, FontSize = 13, FontWeight = FontWeights.Bold,
Foreground = BrushFromHex(color),
});
textStack.Children.Add(new TextBlock
{
Text = desc, FontSize = 11,
Foreground = ThemeResourceHelper.Secondary(this),
TextWrapping = TextWrapping.Wrap,
MaxWidth = 220,
});
sp.Children.Add(textStack);
btn.Content = sp;
var capturedLevel = level;
btn.Click += (_, _) =>
{
Llm.FilePermission = capturedLevel;
UpdatePermissionUI();
SaveConversationSettings();
PermissionPopup.IsOpen = false;
};
PermissionItems.Children.Add(btn);
}
PermissionPopup.IsOpen = true;
}
private bool _autoWarningDismissed; // Auto 경고 배너 사용자가 닫았는지
private void BtnAutoWarningClose_Click(object sender, RoutedEventArgs e)
{
_autoWarningDismissed = true;
if (AutoPermissionWarning != null)
AutoPermissionWarning.Visibility = Visibility.Collapsed;
}
private void UpdatePermissionUI()
{
if (PermissionLabel == null || PermissionIcon == null) return;
var perm = Llm.FilePermission;
PermissionLabel.Text = perm;
PermissionIcon.Text = perm switch
{
"Auto" => "\uE73E",
"Deny" => "\uE711",
_ => "\uE8D7",
};
// Auto 모드일 때 경고 색상 + 배너 표시
if (perm == "Auto")
{
var warnColor = new SolidColorBrush(Color.FromRgb(0xDD, 0x6B, 0x20));
PermissionLabel.Foreground = warnColor;
PermissionIcon.Foreground = warnColor;
// Auto 전환 시 새 대화에서만 1회 필수 표시 (기존 대화에서 이미 Auto였으면 숨김)
ChatConversation? convForWarn;
lock (_convLock) convForWarn = _currentConversation;
var isExisting = convForWarn != null && convForWarn.Messages.Count > 0 && convForWarn.Permission == "Auto";
if (AutoPermissionWarning != null && !_autoWarningDismissed && !isExisting)
AutoPermissionWarning.Visibility = Visibility.Visible;
}
else
{
_autoWarningDismissed = false; // Auto가 아닌 모드로 전환하면 리셋
var defaultFg = ThemeResourceHelper.Secondary(this);
var iconFg = perm == "Deny" ? new SolidColorBrush(Color.FromRgb(0xC5, 0x0F, 0x1F))
: new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); // Ask = 파란색
PermissionLabel.Foreground = defaultFg;
PermissionIcon.Foreground = iconFg;
if (AutoPermissionWarning != null)
AutoPermissionWarning.Visibility = Visibility.Collapsed;
}
}
// ──── 데이터 활용 수준 메뉴 ────
private void BtnDataUsage_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (DataUsagePopup == null) return;
DataUsageItems.Children.Clear();
var options = new (string Key, string Sym, string Label, string Desc, string Color)[]
{
("active", "\uE9F5", "적극 활용", "폴더 내 문서를 자동 탐색하여 보고서 작성에 적극 활용합니다", "#107C10"),
("passive", "\uE8FD", "소극 활용", "사용자가 요청할 때만 폴더 데이터를 참조합니다", "#D97706"),
("none", "\uE8D8", "활용하지 않음", "폴더 내 문서를 읽거나 참조하지 않습니다", "#9CA3AF"),
};
foreach (var (key, sym, label, desc, color) in options)
{
var isActive = key.Equals(_folderDataUsage, StringComparison.OrdinalIgnoreCase);
var template = new ControlTemplate(typeof(Button));
var bdFactory = new FrameworkElementFactory(typeof(Border));
bdFactory.SetValue(Border.BackgroundProperty, Brushes.Transparent);
bdFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
bdFactory.SetValue(Border.PaddingProperty, new Thickness(12, 8, 12, 8));
bdFactory.Name = "Bd";
var cpFactory = new FrameworkElementFactory(typeof(ContentPresenter));
cpFactory.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Left);
cpFactory.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
bdFactory.AppendChild(cpFactory);
template.VisualTree = bdFactory;
var hoverTrigger = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
hoverTrigger.Setters.Add(new Setter(Border.BackgroundProperty,
new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)), "Bd"));
template.Triggers.Add(hoverTrigger);
var btn = new Button
{
Template = template,
BorderThickness = new Thickness(0),
Cursor = Cursors.Hand,
HorizontalContentAlignment = HorizontalAlignment.Left,
Margin = new Thickness(0, 1, 0, 1),
};
ApplyHoverScaleAnimation(btn, 1.02);
var sp = new StackPanel { Orientation = Orientation.Horizontal };
// 커스텀 체크 아이콘
sp.Children.Add(CreateCheckIcon(isActive));
sp.Children.Add(new TextBlock
{
Text = sym, FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14,
Foreground = BrushFromHex(color),
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
});
var textStack = new StackPanel();
textStack.Children.Add(new TextBlock
{
Text = label, FontSize = 13, FontWeight = FontWeights.Bold,
Foreground = BrushFromHex(color),
});
textStack.Children.Add(new TextBlock
{
Text = desc, FontSize = 11,
Foreground = ThemeResourceHelper.Secondary(this),
TextWrapping = TextWrapping.Wrap,
MaxWidth = 240,
});
sp.Children.Add(textStack);
btn.Content = sp;
var capturedKey = key;
btn.Click += (_, _) =>
{
_folderDataUsage = capturedKey;
UpdateDataUsageUI();
SaveConversationSettings();
DataUsagePopup.IsOpen = false;
};
DataUsageItems.Children.Add(btn);
}
DataUsagePopup.IsOpen = true;
}
private void UpdateDataUsageUI()
{
if (DataUsageLabel == null || DataUsageIcon == null) return;
var (label, icon, color) = _folderDataUsage switch
{
"passive" => ("소극", "\uE8FD", "#D97706"),
"none" => ("미사용", "\uE8D8", "#9CA3AF"),
_ => ("적극", "\uE9F5", "#107C10"),
};
DataUsageLabel.Text = label;
DataUsageIcon.Text = icon;
DataUsageIcon.Foreground = BrushFromHex(color);
}
/// <summary>Cowork/Code 탭 진입 시 설정의 기본 권한을 적용.</summary>
private void ApplyTabDefaultPermission()
{
if (_activeTab == "Chat")
{
// Chat 탭: 경고 배너 숨기고 기본 Ask 모드로 복원
Llm.FilePermission = "Ask";
UpdatePermissionUI();
return;
}
var defaultPerm = Llm.DefaultAgentPermission;
if (!string.IsNullOrEmpty(defaultPerm))
{
Llm.FilePermission = defaultPerm;
UpdatePermissionUI();
}
}
// ─── 파일 첨부 ─────────────────────────────────────────────────────────
private void BtnAttach_Click(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Multiselect = true,
Title = "첨부할 파일을 선택하세요",
Filter = "모든 파일 (*.*)|*.*|텍스트 (*.txt;*.md;*.csv)|*.txt;*.md;*.csv|코드 (*.cs;*.py;*.js;*.ts)|*.cs;*.py;*.js;*.ts",
};
// 작업 폴더가 있으면 초기 경로 설정
var workFolder = GetCurrentWorkFolder();
if (!string.IsNullOrEmpty(workFolder) && System.IO.Directory.Exists(workFolder))
dlg.InitialDirectory = workFolder;
if (dlg.ShowDialog() != true) return;
foreach (var file in dlg.FileNames)
AddAttachedFile(file);
}
private static readonly HashSet<string> ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"
};
private void AddAttachedFile(string filePath)
{
if (_attachedFiles.Contains(filePath)) return;
// 파일 크기 제한 (10MB)
try
{
var fi = new System.IO.FileInfo(filePath);
if (fi.Length > 10 * 1024 * 1024)
{
CustomMessageBox.Show($"파일이 너무 큽니다 (10MB 초과):\n{fi.Name}", "첨부 제한", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
// 이미지 파일 → Vision API용 base64 변환
var ext = fi.Extension.ToLowerInvariant();
if (ImageExtensions.Contains(ext) && Llm.EnableImageInput)
{
var maxKb = Llm.MaxImageSizeKb;
if (maxKb <= 0) maxKb = 5120;
if (fi.Length > maxKb * 1024)
{
CustomMessageBox.Show($"이미지가 너무 큽니다 ({fi.Length / 1024}KB, 최대 {maxKb}KB).",
"이미지 크기 초과", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var bytes = System.IO.File.ReadAllBytes(filePath);
var mimeType = ext switch
{
".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif",
".bmp" => "image/bmp",
".webp" => "image/webp",
_ => "image/png",
};
var attachment = new ImageAttachment
{
Base64 = Convert.ToBase64String(bytes),
MimeType = mimeType,
FileName = fi.Name,
};
// 중복 확인
if (_pendingImages.Any(i => i.FileName == attachment.FileName)) return;
_pendingImages.Add(attachment);
AddImagePreview(attachment);
return;
}
}
catch (Exception) { return; }
_attachedFiles.Add(filePath);
RefreshAttachedFilesUI();
}
private void RemoveAttachedFile(string filePath)
{
_attachedFiles.Remove(filePath);
RefreshAttachedFilesUI();
}
private void RefreshAttachedFilesUI()
{
AttachedFilesPanel.Items.Clear();
if (_attachedFiles.Count == 0)
{
AttachedFilesPanel.Visibility = Visibility.Collapsed;
return;
}
AttachedFilesPanel.Visibility = Visibility.Visible;
var secondaryBrush = ThemeResourceHelper.Secondary(this);
var hintBg = ThemeResourceHelper.Hint(this);
foreach (var file in _attachedFiles.ToList())
{
var fileName = System.IO.Path.GetFileName(file);
var capturedFile = file;
var chip = new Border
{
Background = hintBg,
CornerRadius = new CornerRadius(6),
Padding = new Thickness(8, 4, 4, 4),
Margin = new Thickness(0, 0, 4, 4),
Cursor = Cursors.Hand,
};
var sp = new StackPanel { Orientation = Orientation.Horizontal };
sp.Children.Add(new TextBlock
{
Text = "\uE8A5", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 10,
Foreground = secondaryBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0),
});
sp.Children.Add(new TextBlock
{
Text = fileName, FontSize = 11, Foreground = secondaryBrush,
VerticalAlignment = VerticalAlignment.Center, MaxWidth = 150, TextTrimming = TextTrimming.CharacterEllipsis,
ToolTip = file,
});
var removeBtn = new Button
{
Content = new TextBlock { Text = "\uE711", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 8, Foreground = secondaryBrush },
Background = Brushes.Transparent, BorderThickness = new Thickness(0),
Cursor = Cursors.Hand, Padding = new Thickness(4, 2, 4, 2), Margin = new Thickness(2, 0, 0, 0),
};
removeBtn.Click += (_, _) => RemoveAttachedFile(capturedFile);
sp.Children.Add(removeBtn);
chip.Child = sp;
AttachedFilesPanel.Items.Add(chip);
}
}
/// <summary>첨부 파일 내용을 시스템 메시지로 변환합니다.</summary>
private string BuildFileContextPrompt()
{
if (_attachedFiles.Count == 0) return "";
var sb = new System.Text.StringBuilder();
sb.AppendLine("\n[첨부 파일 컨텍스트]");
foreach (var file in _attachedFiles)
{
try
{
var ext = System.IO.Path.GetExtension(file).ToLowerInvariant();
var isBinary = ext is ".exe" or ".dll" or ".zip" or ".7z" or ".rar" or ".tar" or ".gz"
or ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".ico" or ".webp" or ".svg"
or ".pdf" or ".docx" or ".xlsx" or ".pptx" or ".doc" or ".xls" or ".ppt"
or ".mp3" or ".mp4" or ".avi" or ".mov" or ".mkv" or ".wav" or ".flac"
or ".psd" or ".ai" or ".sketch" or ".fig"
or ".msi" or ".iso" or ".img" or ".bin" or ".dat" or ".db" or ".sqlite";
if (isBinary)
{
sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} (바이너리 파일, 내용 생략) ---");
continue;
}
var content = System.IO.File.ReadAllText(file);
// 최대 8000자로 제한
if (content.Length > 8000)
content = content[..8000] + "\n... (이하 생략)";
sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} ---");
sb.AppendLine(content);
}
catch (Exception ex)
{
sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} (읽기 실패: {ex.Message}) ---");
}
}
return sb.ToString();
}
private void ResizeGrip_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
var newW = Width + e.HorizontalChange;
var newH = Height + e.VerticalChange;
if (newW >= MinWidth) Width = newW;
if (newH >= MinHeight) Height = newH;
}
// ─── 사이드바 토글 ───────────────────────────────────────────────────
private void BtnToggleSidebar_Click(object sender, RoutedEventArgs e)
{
_sidebarVisible = !_sidebarVisible;
if (_sidebarVisible)
{
// 사이드바 열기, 아이콘 바 숨기기
IconBarColumn.Width = new GridLength(0);
IconBarPanel.Visibility = Visibility.Collapsed;
SidebarPanel.Visibility = Visibility.Visible;
ToggleSidebarIcon.Text = "\uE76B";
AnimateSidebar(0, 270, () => SidebarColumn.MinWidth = 200);
}
else
{
// 사이드바 닫기, 아이콘 바 표시
SidebarColumn.MinWidth = 0;
ToggleSidebarIcon.Text = "\uE76C";
AnimateSidebar(270, 0, () =>
{
SidebarPanel.Visibility = Visibility.Collapsed;
IconBarColumn.Width = new GridLength(52);
IconBarPanel.Visibility = Visibility.Visible;
});
}
}
private void AnimateSidebar(double from, double to, Action? onComplete = null)
{
var duration = 200.0;
var start = DateTime.UtcNow;
var timer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) };
EventHandler tickHandler = null!;
tickHandler = (_, _) =>
{
var elapsed = (DateTime.UtcNow - start).TotalMilliseconds;
var t = Math.Min(elapsed / duration, 1.0);
t = 1 - (1 - t) * (1 - t);
SidebarColumn.Width = new GridLength(from + (to - from) * t);
if (elapsed >= duration)
{
timer.Stop();
timer.Tick -= tickHandler;
SidebarColumn.Width = new GridLength(to);
onComplete?.Invoke();
}
};
timer.Tick += tickHandler;
timer.Start();
}
}