## 분할 대상 및 결과 ### ChatWindow.ResponseHandling.cs (741줄 → 269줄) - ChatWindow.StreamingUI.cs (303줄, 신규): CreateStreamingContainer, FinalizeStreamingContainer, ParseSuggestionChips, FormatTokenCount, EstimateTokenCount, StopGeneration - ChatWindow.ConversationExport.cs (188줄, 신규): ForkConversation, OpenCommandPalette, ExecuteCommand, ExportConversation, ExportToHtml ### ChatWindow.PreviewAndFiles.cs (709줄 → ~340줄) - ChatWindow.PreviewPopup.cs (~230줄, 신규): ShowPreviewTabContextMenu, OpenPreviewPopupWindow, _previewTabPopup 필드 ### HelpDetailWindow.xaml.cs (673줄 → 254줄) - HelpDetailWindow.Shortcuts.cs (168줄, 신규): BuildShortcutItems() 정적 메서드 (단축키 항목 160개+ 생성) - HelpDetailWindow.Navigation.cs (266줄, 신규): 테마 프로퍼티, BuildTopMenu/SwitchTopMenu, BuildCategoryBar, NavigateToPage, 이벤트 핸들러 - partial class 전환: `public partial class HelpDetailWindow : Window` ### SkillService.cs (661줄 → 386줄) - SkillService.Import.cs (203줄, 신규): ExportSkill, ImportSkills, MapToolNames — 가져오기/내보내기 섹션 - SkillDefinition.cs (81줄, 신규): SkillDefinition 클래스 독립 파일로 분리 (별도 최상위 클래스) - partial class 전환: `public static partial class SkillService` ## NEXT_ROADMAP.md Phase 46 완료 항목 추가 ## 빌드 결과: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
463 lines
17 KiB
C#
463 lines
17 KiB
C#
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
using System.Windows.Threading;
|
|
using AxCopilot.Services.Agent;
|
|
|
|
namespace AxCopilot.Views;
|
|
|
|
public partial class ChatWindow
|
|
{
|
|
// ─── 미리보기 패널 (탭 기반) ─────────────────────────────────────────────
|
|
|
|
private static readonly HashSet<string> _previewableExtensions = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
".html", ".htm", ".md", ".txt", ".csv", ".json", ".xml", ".log",
|
|
};
|
|
|
|
/// <summary>열려 있는 프리뷰 탭 목록 (파일 경로 기준).</summary>
|
|
private readonly List<string> _previewTabs = new();
|
|
private string? _activePreviewTab;
|
|
|
|
private void TryShowPreview(string filePath)
|
|
{
|
|
if (string.IsNullOrEmpty(filePath) || !System.IO.File.Exists(filePath))
|
|
return;
|
|
|
|
// 별도 커스텀 창으로 미리보기 (WebView2 HWND airspace 문제 근본 해결)
|
|
PreviewWindow.ShowPreview(filePath, _selectedMood);
|
|
}
|
|
|
|
private void ShowPreviewPanel(string filePath)
|
|
{
|
|
// 탭에 없으면 추가
|
|
if (!_previewTabs.Contains(filePath, StringComparer.OrdinalIgnoreCase))
|
|
_previewTabs.Add(filePath);
|
|
|
|
_activePreviewTab = filePath;
|
|
|
|
// 패널 열기
|
|
if (PreviewColumn.Width.Value < 100)
|
|
{
|
|
PreviewColumn.Width = new GridLength(420);
|
|
SplitterColumn.Width = new GridLength(5);
|
|
}
|
|
PreviewPanel.Visibility = Visibility.Visible;
|
|
PreviewSplitter.Visibility = Visibility.Visible;
|
|
BtnPreviewToggle.Visibility = Visibility.Visible;
|
|
|
|
RebuildPreviewTabs();
|
|
LoadPreviewContent(filePath);
|
|
}
|
|
|
|
/// <summary>탭 바 UI를 다시 구성합니다.</summary>
|
|
private void RebuildPreviewTabs()
|
|
{
|
|
PreviewTabPanel.Children.Clear();
|
|
|
|
var accentBrush = ThemeResourceHelper.Accent(this);
|
|
var secondaryText = ThemeResourceHelper.Secondary(this);
|
|
var primaryText = ThemeResourceHelper.Primary(this);
|
|
var borderBrush = ThemeResourceHelper.Border(this);
|
|
|
|
foreach (var tabPath in _previewTabs)
|
|
{
|
|
var fileName = System.IO.Path.GetFileName(tabPath);
|
|
var isActive = string.Equals(tabPath, _activePreviewTab, StringComparison.OrdinalIgnoreCase);
|
|
|
|
var tabBorder = new Border
|
|
{
|
|
Background = isActive
|
|
? new SolidColorBrush(Color.FromArgb(0x15, 0xFF, 0xFF, 0xFF))
|
|
: Brushes.Transparent,
|
|
BorderBrush = isActive ? accentBrush : Brushes.Transparent,
|
|
BorderThickness = new Thickness(0, 0, 0, isActive ? 2 : 0),
|
|
Padding = new Thickness(8, 6, 4, 6),
|
|
Cursor = Cursors.Hand,
|
|
MaxWidth = _previewTabs.Count <= 3 ? 200 : (_previewTabs.Count <= 5 ? 140 : 100),
|
|
};
|
|
|
|
var tabContent = new StackPanel { Orientation = Orientation.Horizontal };
|
|
|
|
// 파일명
|
|
tabContent.Children.Add(new TextBlock
|
|
{
|
|
Text = fileName,
|
|
FontSize = 11,
|
|
Foreground = isActive ? primaryText : secondaryText,
|
|
FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
TextTrimming = TextTrimming.CharacterEllipsis,
|
|
MaxWidth = tabBorder.MaxWidth - 30,
|
|
ToolTip = tabPath,
|
|
});
|
|
|
|
// 닫기 버튼 (x) — 활성 탭은 항상 표시, 비활성 탭은 호버 시에만 표시
|
|
var closeFg = isActive ? primaryText : secondaryText;
|
|
var closeBtnText = new TextBlock
|
|
{
|
|
Text = "\uE711",
|
|
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
|
FontSize = 10,
|
|
Foreground = closeFg,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
};
|
|
var closeBtn = new Border
|
|
{
|
|
Background = Brushes.Transparent,
|
|
CornerRadius = new CornerRadius(3),
|
|
Padding = new Thickness(3, 2, 3, 2),
|
|
Margin = new Thickness(5, 0, 0, 0),
|
|
Cursor = Cursors.Hand,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
// 비활성 탭은 초기에 숨김, 활성 탭은 항상 표시
|
|
Visibility = isActive ? Visibility.Visible : Visibility.Hidden,
|
|
Child = closeBtnText,
|
|
};
|
|
|
|
var closePath = tabPath;
|
|
closeBtn.MouseEnter += (s, _) =>
|
|
{
|
|
if (s is Border b)
|
|
{
|
|
b.Background = new SolidColorBrush(Color.FromArgb(0x40, 0xFF, 0x50, 0x50));
|
|
if (b.Child is TextBlock tb) tb.Foreground = new SolidColorBrush(Color.FromRgb(0xFF, 0x60, 0x60));
|
|
}
|
|
};
|
|
closeBtn.MouseLeave += (s, _) =>
|
|
{
|
|
if (s is Border b)
|
|
{
|
|
b.Background = Brushes.Transparent;
|
|
if (b.Child is TextBlock tb) tb.Foreground = closeFg;
|
|
}
|
|
};
|
|
closeBtn.Tag = "close"; // 닫기 버튼 식별용
|
|
closeBtn.MouseLeftButtonUp += (_, e) =>
|
|
{
|
|
e.Handled = true; // 부모 탭 클릭 이벤트 차단
|
|
ClosePreviewTab(closePath);
|
|
};
|
|
|
|
tabContent.Children.Add(closeBtn);
|
|
tabBorder.Child = tabContent;
|
|
|
|
// 탭 클릭 → 활성화 (MouseLeftButtonUp 사용: 닫기 버튼의 PreviewMouseLeftButtonDown보다 늦게 실행되어 충돌 방지)
|
|
var clickPath = tabPath;
|
|
tabBorder.MouseLeftButtonUp += (_, e) =>
|
|
{
|
|
if (e.Handled) return;
|
|
e.Handled = true;
|
|
_activePreviewTab = clickPath;
|
|
RebuildPreviewTabs();
|
|
LoadPreviewContent(clickPath);
|
|
};
|
|
|
|
// 우클릭 → 컨텍스트 메뉴
|
|
var ctxPath = tabPath;
|
|
tabBorder.MouseRightButtonUp += (_, e) =>
|
|
{
|
|
e.Handled = true;
|
|
ShowPreviewTabContextMenu(ctxPath);
|
|
};
|
|
|
|
// 더블클릭 → 별도 창에서 보기
|
|
var dblPath = tabPath;
|
|
tabBorder.MouseLeftButtonDown += (_, e) =>
|
|
{
|
|
if (e.Handled) return;
|
|
if (e.ClickCount == 2)
|
|
{
|
|
e.Handled = true;
|
|
OpenPreviewPopupWindow(dblPath);
|
|
}
|
|
};
|
|
|
|
// 호버 효과 — 비활성 탭에서 배경 강조 + 닫기 버튼 표시
|
|
var capturedIsActive = isActive;
|
|
var capturedCloseBtn = closeBtn;
|
|
tabBorder.MouseEnter += (s, _) =>
|
|
{
|
|
if (s is Border b && !capturedIsActive)
|
|
b.Background = new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF));
|
|
// 비활성 탭도 호버 시 닫기 버튼 표시
|
|
if (!capturedIsActive)
|
|
capturedCloseBtn.Visibility = Visibility.Visible;
|
|
};
|
|
tabBorder.MouseLeave += (s, _) =>
|
|
{
|
|
if (s is Border b && !capturedIsActive)
|
|
b.Background = Brushes.Transparent;
|
|
// 비활성 탭 호버 해제 시 닫기 버튼 숨김
|
|
if (!capturedIsActive)
|
|
capturedCloseBtn.Visibility = Visibility.Hidden;
|
|
};
|
|
|
|
PreviewTabPanel.Children.Add(tabBorder);
|
|
|
|
// 탭 사이 구분선
|
|
if (tabPath != _previewTabs[^1])
|
|
{
|
|
PreviewTabPanel.Children.Add(new Border
|
|
{
|
|
Width = 1, Height = 14,
|
|
Background = borderBrush,
|
|
Margin = new Thickness(0, 4, 0, 4),
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ClosePreviewTab(string filePath)
|
|
{
|
|
_previewTabs.Remove(filePath);
|
|
|
|
if (_previewTabs.Count == 0)
|
|
{
|
|
HidePreviewPanel();
|
|
return;
|
|
}
|
|
|
|
// 닫힌 탭이 활성 탭이면 마지막 탭으로 전환
|
|
if (string.Equals(filePath, _activePreviewTab, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_activePreviewTab = _previewTabs[^1];
|
|
LoadPreviewContent(_activePreviewTab);
|
|
}
|
|
|
|
RebuildPreviewTabs();
|
|
}
|
|
|
|
private async void LoadPreviewContent(string filePath)
|
|
{
|
|
var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant();
|
|
|
|
// 모든 콘텐츠 숨기기
|
|
PreviewWebView.Visibility = Visibility.Collapsed;
|
|
PreviewTextScroll.Visibility = Visibility.Collapsed;
|
|
PreviewDataGrid.Visibility = Visibility.Collapsed;
|
|
PreviewEmpty.Visibility = Visibility.Collapsed;
|
|
|
|
if (!System.IO.File.Exists(filePath))
|
|
{
|
|
PreviewEmpty.Text = "파일을 찾을 수 없습니다";
|
|
PreviewEmpty.Visibility = Visibility.Visible;
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
switch (ext)
|
|
{
|
|
case ".html":
|
|
case ".htm":
|
|
await EnsureWebViewInitializedAsync();
|
|
PreviewWebView.Source = new Uri(filePath);
|
|
PreviewWebView.Visibility = Visibility.Visible;
|
|
break;
|
|
|
|
case ".csv":
|
|
LoadCsvPreview(filePath);
|
|
PreviewDataGrid.Visibility = Visibility.Visible;
|
|
break;
|
|
|
|
case ".md":
|
|
await EnsureWebViewInitializedAsync();
|
|
var mdText = System.IO.File.ReadAllText(filePath);
|
|
if (mdText.Length > 50000) mdText = mdText[..50000];
|
|
var mdHtml = Services.Agent.TemplateService.RenderMarkdownToHtml(mdText, _selectedMood);
|
|
PreviewWebView.NavigateToString(mdHtml);
|
|
PreviewWebView.Visibility = Visibility.Visible;
|
|
break;
|
|
|
|
case ".txt":
|
|
case ".json":
|
|
case ".xml":
|
|
case ".log":
|
|
var text = System.IO.File.ReadAllText(filePath);
|
|
if (text.Length > 50000) text = text[..50000] + "\n\n... (이후 생략)";
|
|
PreviewTextBlock.Text = text;
|
|
PreviewTextScroll.Visibility = Visibility.Visible;
|
|
break;
|
|
|
|
default:
|
|
PreviewEmpty.Text = "미리보기할 수 없는 파일 형식입니다";
|
|
PreviewEmpty.Visibility = Visibility.Visible;
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
PreviewTextBlock.Text = $"미리보기 오류: {ex.Message}";
|
|
PreviewTextScroll.Visibility = Visibility.Visible;
|
|
}
|
|
}
|
|
|
|
private bool _webViewInitialized;
|
|
private static readonly string WebView2DataFolder =
|
|
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
"AxCopilot", "WebView2");
|
|
|
|
private async Task EnsureWebViewInitializedAsync()
|
|
{
|
|
if (_webViewInitialized) return;
|
|
try
|
|
{
|
|
var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(
|
|
userDataFolder: WebView2DataFolder);
|
|
await PreviewWebView.EnsureCoreWebView2Async(env);
|
|
_webViewInitialized = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Services.LogService.Warn($"WebView2 초기화 실패: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void LoadCsvPreview(string filePath)
|
|
{
|
|
try
|
|
{
|
|
var lines = System.IO.File.ReadAllLines(filePath);
|
|
if (lines.Length == 0) return;
|
|
|
|
var dt = new System.Data.DataTable();
|
|
var headers = ParseCsvLine(lines[0]);
|
|
foreach (var h in headers)
|
|
dt.Columns.Add(h);
|
|
|
|
var maxRows = Math.Min(lines.Length, 501);
|
|
for (int i = 1; i < maxRows; i++)
|
|
{
|
|
var vals = ParseCsvLine(lines[i]);
|
|
var row = dt.NewRow();
|
|
for (int j = 0; j < Math.Min(vals.Length, headers.Length); j++)
|
|
row[j] = vals[j];
|
|
dt.Rows.Add(row);
|
|
}
|
|
|
|
PreviewDataGrid.ItemsSource = dt.DefaultView;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
PreviewTextBlock.Text = $"CSV 로드 오류: {ex.Message}";
|
|
PreviewTextScroll.Visibility = Visibility.Visible;
|
|
PreviewDataGrid.Visibility = Visibility.Collapsed;
|
|
}
|
|
}
|
|
|
|
private static string[] ParseCsvLine(string line)
|
|
{
|
|
var fields = new System.Collections.Generic.List<string>();
|
|
var current = new System.Text.StringBuilder();
|
|
bool inQuotes = false;
|
|
|
|
for (int i = 0; i < line.Length; i++)
|
|
{
|
|
char c = line[i];
|
|
if (inQuotes)
|
|
{
|
|
if (c == '"' && i + 1 < line.Length && line[i + 1] == '"')
|
|
{
|
|
current.Append('"');
|
|
i++;
|
|
}
|
|
else if (c == '"')
|
|
inQuotes = false;
|
|
else
|
|
current.Append(c);
|
|
}
|
|
else
|
|
{
|
|
if (c == '"')
|
|
inQuotes = true;
|
|
else if (c == ',')
|
|
{
|
|
fields.Add(current.ToString());
|
|
current.Clear();
|
|
}
|
|
else
|
|
current.Append(c);
|
|
}
|
|
}
|
|
fields.Add(current.ToString());
|
|
return fields.ToArray();
|
|
}
|
|
|
|
private void HidePreviewPanel()
|
|
{
|
|
_previewTabs.Clear();
|
|
_activePreviewTab = null;
|
|
PreviewColumn.Width = new GridLength(0);
|
|
SplitterColumn.Width = new GridLength(0);
|
|
PreviewPanel.Visibility = Visibility.Collapsed;
|
|
PreviewSplitter.Visibility = Visibility.Collapsed;
|
|
PreviewWebView.Visibility = Visibility.Collapsed;
|
|
PreviewTextScroll.Visibility = Visibility.Collapsed;
|
|
PreviewDataGrid.Visibility = Visibility.Collapsed;
|
|
try { if (_webViewInitialized) PreviewWebView.CoreWebView2?.NavigateToString("<html></html>"); } catch (Exception) { /* WebView 초기화 실패 */ }
|
|
}
|
|
|
|
/// <summary>프리뷰 탭 바 클릭 시 WebView2에서 포커스를 회수 (HWND airspace 문제 방지).</summary>
|
|
private void PreviewTabBar_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
{
|
|
// WebView2가 포커스를 잡고 있으면 WPF 버튼 클릭이 무시될 수 있으므로 포커스를 강제 이동
|
|
if (PreviewWebView.IsFocused || PreviewWebView.IsKeyboardFocusWithin)
|
|
{
|
|
var border = sender as Border;
|
|
border?.Focus();
|
|
}
|
|
}
|
|
|
|
private void BtnClosePreview_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
HidePreviewPanel();
|
|
BtnPreviewToggle.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
private void BtnPreviewToggle_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (PreviewPanel.Visibility == Visibility.Visible)
|
|
{
|
|
// 숨기기 (탭은 유지)
|
|
PreviewPanel.Visibility = Visibility.Collapsed;
|
|
PreviewSplitter.Visibility = Visibility.Collapsed;
|
|
PreviewColumn.Width = new GridLength(0);
|
|
SplitterColumn.Width = new GridLength(0);
|
|
}
|
|
else if (_previewTabs.Count > 0)
|
|
{
|
|
// 다시 열기
|
|
PreviewPanel.Visibility = Visibility.Visible;
|
|
PreviewSplitter.Visibility = Visibility.Visible;
|
|
PreviewColumn.Width = new GridLength(420);
|
|
SplitterColumn.Width = new GridLength(5);
|
|
RebuildPreviewTabs();
|
|
if (_activePreviewTab != null) LoadPreviewContent(_activePreviewTab);
|
|
}
|
|
}
|
|
|
|
private void BtnOpenExternal_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (string.IsNullOrEmpty(_activePreviewTab) || !System.IO.File.Exists(_activePreviewTab))
|
|
return;
|
|
|
|
try
|
|
{
|
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
|
{
|
|
FileName = _activePreviewTab,
|
|
UseShellExecute = true,
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"외부 프로그램 실행 오류: {ex.Message}");
|
|
}
|
|
}
|
|
}
|