[Phase 43] 4개 대형 파일 파셜 클래스 분할
SettingsWindow.AgentConfig (1,202줄): - AgentConfig.cs → 608줄 (등록모델·스킬·프롬프트·AI토글·사내외모드) - AgentHooks.cs → 605줄 (에이전트훅·MCP서버·감사로그·폴백설정) ChatWindow.Presets (1,280줄): - Presets.cs → 315줄 (대화 주제 버튼) - CustomPresets.cs → 978줄 (커스텀 프리셋 관리·하단바·포맷메뉴) ChatWindow.PreviewAndFiles (1,105줄): - PreviewAndFiles.cs → 709줄 (미리보기 패널) - FileBrowser.cs → 408줄 (에이전트 진행률 바·파일 탐색기) WorkflowAnalyzerWindow (929줄): - WorkflowAnalyzerWindow.xaml.cs → 274줄 (리사이즈·탭·데이터수집) - WorkflowAnalyzerWindow.Charts.cs → 667줄 (차트·타임라인·패널·유틸) 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -706,400 +706,4 @@ public partial class ChatWindow
|
||||
win.Content = content;
|
||||
win.Show();
|
||||
}
|
||||
|
||||
// ─── 에이전트 스티키 진행률 바 ──────────────────────────────────────────
|
||||
|
||||
private DateTime _progressStartTime;
|
||||
private DispatcherTimer? _progressElapsedTimer;
|
||||
|
||||
private void UpdateAgentProgressBar(AgentEvent evt)
|
||||
{
|
||||
switch (evt.Type)
|
||||
{
|
||||
case AgentEventType.Planning when evt.Steps is { Count: > 0 }:
|
||||
ShowStickyProgress(evt.Steps.Count);
|
||||
break;
|
||||
|
||||
case AgentEventType.StepStart when evt.StepTotal > 0:
|
||||
UpdateStickyProgress(evt.StepCurrent, evt.StepTotal, evt.Summary);
|
||||
break;
|
||||
|
||||
case AgentEventType.Complete:
|
||||
HideStickyProgress();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowStickyProgress(int totalSteps)
|
||||
{
|
||||
_progressStartTime = DateTime.Now;
|
||||
AgentProgressBar.Visibility = Visibility.Visible;
|
||||
ProgressIcon.Text = "\uE768"; // play
|
||||
ProgressStepLabel.Text = $"작업 준비 중... (0/{totalSteps})";
|
||||
ProgressPercent.Text = "0%";
|
||||
ProgressElapsed.Text = "0:00";
|
||||
ProgressFill.Width = 0;
|
||||
|
||||
// 경과 시간 타이머
|
||||
_progressElapsedTimer?.Stop();
|
||||
_progressElapsedTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
_progressElapsedTimer.Tick += (_, _) =>
|
||||
{
|
||||
var elapsed = DateTime.Now - _progressStartTime;
|
||||
ProgressElapsed.Text = elapsed.TotalHours >= 1
|
||||
? elapsed.ToString(@"h\:mm\:ss")
|
||||
: elapsed.ToString(@"m\:ss");
|
||||
};
|
||||
_progressElapsedTimer.Start();
|
||||
}
|
||||
|
||||
private void UpdateStickyProgress(int currentStep, int totalSteps, string stepDescription)
|
||||
{
|
||||
if (AgentProgressBar.Visibility != Visibility.Visible) return;
|
||||
|
||||
var pct = totalSteps > 0 ? (double)currentStep / totalSteps : 0;
|
||||
ProgressStepLabel.Text = $"{stepDescription} ({currentStep}/{totalSteps})";
|
||||
ProgressPercent.Text = $"{(int)(pct * 100)}%";
|
||||
|
||||
// 프로그레스 바 너비 애니메이션
|
||||
var parentBorder = ProgressFill.Parent as Border;
|
||||
if (parentBorder != null)
|
||||
{
|
||||
var targetWidth = parentBorder.ActualWidth * pct;
|
||||
var anim = new System.Windows.Media.Animation.DoubleAnimation(
|
||||
ProgressFill.Width, targetWidth, TimeSpan.FromMilliseconds(300))
|
||||
{
|
||||
EasingFunction = new System.Windows.Media.Animation.QuadraticEase(),
|
||||
};
|
||||
ProgressFill.BeginAnimation(WidthProperty, anim);
|
||||
}
|
||||
}
|
||||
|
||||
private void HideStickyProgress()
|
||||
{
|
||||
_progressElapsedTimer?.Stop();
|
||||
_progressElapsedTimer = null;
|
||||
|
||||
if (AgentProgressBar.Visibility != Visibility.Visible) return;
|
||||
|
||||
// 완료 표시 후 페이드아웃
|
||||
ProgressIcon.Text = "\uE930"; // check
|
||||
ProgressStepLabel.Text = "작업 완료";
|
||||
ProgressPercent.Text = "100%";
|
||||
|
||||
// 프로그레스 바 100%
|
||||
var parentBorder = ProgressFill.Parent as Border;
|
||||
if (parentBorder != null)
|
||||
{
|
||||
var anim = new System.Windows.Media.Animation.DoubleAnimation(
|
||||
ProgressFill.Width, parentBorder.ActualWidth, TimeSpan.FromMilliseconds(200));
|
||||
ProgressFill.BeginAnimation(WidthProperty, anim);
|
||||
}
|
||||
|
||||
// 3초 후 숨기기
|
||||
var hideTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) };
|
||||
hideTimer.Tick += (_, _) =>
|
||||
{
|
||||
hideTimer.Stop();
|
||||
var fadeOut = new System.Windows.Media.Animation.DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(300));
|
||||
fadeOut.Completed += (_, _) =>
|
||||
{
|
||||
AgentProgressBar.Visibility = Visibility.Collapsed;
|
||||
AgentProgressBar.Opacity = 1;
|
||||
ProgressFill.BeginAnimation(WidthProperty, null);
|
||||
ProgressFill.Width = 0;
|
||||
};
|
||||
AgentProgressBar.BeginAnimation(UIElement.OpacityProperty, fadeOut);
|
||||
};
|
||||
hideTimer.Start();
|
||||
}
|
||||
|
||||
// ─── 파일 탐색기 ──────────────────────────────────────────────────────
|
||||
|
||||
private static readonly HashSet<string> _ignoredDirs = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"bin", "obj", "node_modules", ".git", ".vs", ".idea", ".vscode",
|
||||
"__pycache__", ".mypy_cache", ".pytest_cache", "dist", "build",
|
||||
".cache", ".next", ".nuxt", "coverage", ".terraform",
|
||||
};
|
||||
|
||||
private DispatcherTimer? _fileBrowserRefreshTimer;
|
||||
|
||||
private void ToggleFileBrowser()
|
||||
{
|
||||
if (FileBrowserPanel.Visibility == Visibility.Visible)
|
||||
{
|
||||
FileBrowserPanel.Visibility = Visibility.Collapsed;
|
||||
Llm.ShowFileBrowser = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
FileBrowserPanel.Visibility = Visibility.Visible;
|
||||
Llm.ShowFileBrowser = true;
|
||||
BuildFileTree();
|
||||
}
|
||||
_settings.Save();
|
||||
}
|
||||
|
||||
private void BtnFileBrowserRefresh_Click(object sender, RoutedEventArgs e) => BuildFileTree();
|
||||
|
||||
private void BtnFileBrowserOpenFolder_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var folder = GetCurrentWorkFolder();
|
||||
if (string.IsNullOrEmpty(folder) || !System.IO.Directory.Exists(folder)) return;
|
||||
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = folder, UseShellExecute = true }); } catch (Exception) { /* 폴더 열기 실패 */ }
|
||||
}
|
||||
|
||||
private void BtnFileBrowserClose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FileBrowserPanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void BuildFileTree()
|
||||
{
|
||||
FileTreeView.Items.Clear();
|
||||
var folder = GetCurrentWorkFolder();
|
||||
if (string.IsNullOrEmpty(folder) || !System.IO.Directory.Exists(folder))
|
||||
{
|
||||
FileTreeView.Items.Add(new TreeViewItem { Header = "작업 폴더를 선택하세요", IsEnabled = false });
|
||||
return;
|
||||
}
|
||||
|
||||
FileBrowserTitle.Text = $"파일 탐색기 — {System.IO.Path.GetFileName(folder)}";
|
||||
var count = 0;
|
||||
PopulateDirectory(new System.IO.DirectoryInfo(folder), FileTreeView.Items, 0, ref count);
|
||||
}
|
||||
|
||||
private void PopulateDirectory(System.IO.DirectoryInfo dir, ItemCollection items, int depth, ref int count)
|
||||
{
|
||||
if (depth > 4 || count > 200) return;
|
||||
|
||||
// 디렉터리
|
||||
try
|
||||
{
|
||||
foreach (var subDir in dir.GetDirectories().OrderBy(d => d.Name))
|
||||
{
|
||||
if (count > 200) break;
|
||||
if (_ignoredDirs.Contains(subDir.Name) || subDir.Name.StartsWith('.')) continue;
|
||||
|
||||
count++;
|
||||
var dirItem = new TreeViewItem
|
||||
{
|
||||
Header = CreateFileTreeHeader("\uED25", subDir.Name, null),
|
||||
Tag = subDir.FullName,
|
||||
IsExpanded = depth < 1,
|
||||
};
|
||||
|
||||
// 지연 로딩: 더미 자식 → 펼칠 때 실제 로드
|
||||
if (depth < 3)
|
||||
{
|
||||
dirItem.Items.Add(new TreeViewItem { Header = "로딩 중..." }); // 더미
|
||||
var capturedDir = subDir;
|
||||
var capturedDepth = depth;
|
||||
dirItem.Expanded += (s, _) =>
|
||||
{
|
||||
if (s is TreeViewItem ti && ti.Items.Count == 1 && ti.Items[0] is TreeViewItem d && d.Header?.ToString() == "로딩 중...")
|
||||
{
|
||||
ti.Items.Clear();
|
||||
int c = 0;
|
||||
PopulateDirectory(capturedDir, ti.Items, capturedDepth + 1, ref c);
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
PopulateDirectory(subDir, dirItem.Items, depth + 1, ref count);
|
||||
}
|
||||
|
||||
items.Add(dirItem);
|
||||
}
|
||||
}
|
||||
catch (Exception) { /* 비핵심 작업 실패 — UI 차단 방지 */ }
|
||||
|
||||
// 파일
|
||||
try
|
||||
{
|
||||
foreach (var file in dir.GetFiles().OrderBy(f => f.Name))
|
||||
{
|
||||
if (count > 200) break;
|
||||
count++;
|
||||
|
||||
var ext = file.Extension.ToLowerInvariant();
|
||||
var icon = GetFileIcon(ext);
|
||||
var size = FormatFileSize(file.Length);
|
||||
|
||||
var fileItem = new TreeViewItem
|
||||
{
|
||||
Header = CreateFileTreeHeader(icon, file.Name, size),
|
||||
Tag = file.FullName,
|
||||
};
|
||||
|
||||
// 더블클릭 → 프리뷰
|
||||
var capturedPath = file.FullName;
|
||||
fileItem.MouseDoubleClick += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
TryShowPreview(capturedPath);
|
||||
};
|
||||
|
||||
// 우클릭 → 컨텍스트 메뉴 (MouseRightButtonUp에서 열어야 Popup이 바로 닫히지 않음)
|
||||
fileItem.MouseRightButtonUp += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
if (s is TreeViewItem ti) ti.IsSelected = true;
|
||||
ShowFileTreeContextMenu(capturedPath);
|
||||
};
|
||||
|
||||
items.Add(fileItem);
|
||||
}
|
||||
}
|
||||
catch (Exception) { /* 비핵심 작업 실패 — UI 차단 방지 */ }
|
||||
}
|
||||
|
||||
private static StackPanel CreateFileTreeHeader(string icon, string name, string? sizeText)
|
||||
{
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 11,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x9C, 0xA3, 0xAF)),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 5, 0),
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = name,
|
||||
FontSize = 11.5,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
if (sizeText != null)
|
||||
{
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $" {sizeText}",
|
||||
FontSize = 10,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x6B, 0x72, 0x80)),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
}
|
||||
return sp;
|
||||
}
|
||||
|
||||
private static string GetFileIcon(string ext) => ext switch
|
||||
{
|
||||
".html" or ".htm" => "\uEB41",
|
||||
".xlsx" or ".xls" => "\uE9F9",
|
||||
".docx" or ".doc" => "\uE8A5",
|
||||
".pdf" => "\uEA90",
|
||||
".csv" => "\uE80A",
|
||||
".md" => "\uE70B",
|
||||
".json" or ".xml" => "\uE943",
|
||||
".png" or ".jpg" or ".jpeg" or ".gif" or ".svg" or ".webp" => "\uEB9F",
|
||||
".cs" or ".py" or ".js" or ".ts" or ".java" or ".cpp" => "\uE943",
|
||||
".bat" or ".cmd" or ".ps1" or ".sh" => "\uE756",
|
||||
".txt" or ".log" => "\uE8A5",
|
||||
_ => "\uE7C3",
|
||||
};
|
||||
|
||||
private static string FormatFileSize(long bytes) => bytes switch
|
||||
{
|
||||
< 1024 => $"{bytes} B",
|
||||
< 1024 * 1024 => $"{bytes / 1024.0:F1} KB",
|
||||
_ => $"{bytes / (1024.0 * 1024.0):F1} MB",
|
||||
};
|
||||
|
||||
private void ShowFileTreeContextMenu(string filePath)
|
||||
{
|
||||
var primaryText = ThemeResourceHelper.Primary(this);
|
||||
var secondaryText = ThemeResourceHelper.Secondary(this);
|
||||
var hoverBg = ThemeResourceHelper.Hint(this);
|
||||
var dangerBrush = new SolidColorBrush(Color.FromRgb(0xE7, 0x4C, 0x3C));
|
||||
|
||||
var (popup, panel) = PopupMenuHelper.Create(this, this, PlacementMode.MousePoint, minWidth: 200);
|
||||
|
||||
void AddItem(string icon, string label, Action action, Brush? labelColor = null, Brush? iconColor = null)
|
||||
=> panel.Children.Add(PopupMenuHelper.MenuItem(label, labelColor ?? primaryText, hoverBg,
|
||||
() => { popup.IsOpen = false; action(); },
|
||||
icon: icon, iconColor: iconColor ?? secondaryText, fontSize: 12.5));
|
||||
|
||||
void AddSep() => panel.Children.Add(PopupMenuHelper.Separator());
|
||||
|
||||
var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant();
|
||||
if (_previewableExtensions.Contains(ext))
|
||||
AddItem("\uE8A1", "미리보기", () => ShowPreviewPanel(filePath));
|
||||
|
||||
AddItem("\uE8A7", "외부 프로그램으로 열기", () =>
|
||||
{
|
||||
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = filePath, UseShellExecute = true }); } catch (Exception) { /* 파일 열기 실패 */ }
|
||||
});
|
||||
AddItem("\uED25", "폴더에서 보기", () =>
|
||||
{
|
||||
try { System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\""); } catch (Exception) { /* 탐색기 열기 실패 */ }
|
||||
});
|
||||
AddItem("\uE8C8", "경로 복사", () =>
|
||||
{
|
||||
try { Clipboard.SetText(filePath); ShowToast("경로 복사됨"); } catch (Exception) { /* 클립보드 접근 실패 */ }
|
||||
});
|
||||
|
||||
AddSep();
|
||||
|
||||
// 이름 변경
|
||||
AddItem("\uE8AC", "이름 변경", () =>
|
||||
{
|
||||
var dir = System.IO.Path.GetDirectoryName(filePath) ?? "";
|
||||
var oldName = System.IO.Path.GetFileName(filePath);
|
||||
var dlg = new Views.InputDialog("이름 변경", "새 파일 이름:", oldName) { Owner = this };
|
||||
if (dlg.ShowDialog() == true && !string.IsNullOrWhiteSpace(dlg.ResponseText))
|
||||
{
|
||||
var newPath = System.IO.Path.Combine(dir, dlg.ResponseText.Trim());
|
||||
try
|
||||
{
|
||||
System.IO.File.Move(filePath, newPath);
|
||||
BuildFileTree();
|
||||
ShowToast($"이름 변경: {dlg.ResponseText.Trim()}");
|
||||
}
|
||||
catch (Exception ex) { ShowToast($"이름 변경 실패: {ex.Message}", "\uE783"); }
|
||||
}
|
||||
});
|
||||
|
||||
// 삭제
|
||||
AddItem("\uE74D", "삭제", () =>
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
$"파일을 삭제하시겠습니까?\n{System.IO.Path.GetFileName(filePath)}",
|
||||
"파일 삭제 확인", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.IO.File.Delete(filePath);
|
||||
BuildFileTree();
|
||||
ShowToast("파일 삭제됨");
|
||||
}
|
||||
catch (Exception ex) { ShowToast($"삭제 실패: {ex.Message}", "\uE783"); }
|
||||
}
|
||||
}, dangerBrush, dangerBrush);
|
||||
|
||||
// Dispatcher로 열어야 MouseRightButtonUp 후 바로 닫히지 않음
|
||||
Dispatcher.BeginInvoke(() => { popup.IsOpen = true; },
|
||||
System.Windows.Threading.DispatcherPriority.Input);
|
||||
}
|
||||
|
||||
/// <summary>에이전트가 파일 생성 시 파일 탐색기를 자동 새로고침합니다.</summary>
|
||||
private void RefreshFileTreeIfVisible()
|
||||
{
|
||||
if (FileBrowserPanel.Visibility != Visibility.Visible) return;
|
||||
|
||||
// 디바운스: 500ms 내 중복 호출 방지
|
||||
_fileBrowserRefreshTimer?.Stop();
|
||||
_fileBrowserRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
|
||||
_fileBrowserRefreshTimer.Tick += (_, _) =>
|
||||
{
|
||||
_fileBrowserRefreshTimer.Stop();
|
||||
BuildFileTree();
|
||||
};
|
||||
_fileBrowserRefreshTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user