[Phase46] 대형 파일 분할 리팩터링 2차 — 19개 신규 파셜 파일 생성

## 분할 대상 및 결과

### AgentLoopService.cs (1,334줄 → 846줄)
- AgentLoopService.HtmlReport.cs (151줄): AutoSaveAsHtml, ConvertTextToHtml, EscapeHtml
- AgentLoopService.Verification.cs (349줄): 도구 분류 판별 + RunPostToolVerificationAsync + EmitEvent + CheckDecisionRequired + FormatToolCallSummary

### ChatWindow 분할 (8개 신규 파셜 파일)
- ChatWindow.PlanViewer.cs (474줄): 계획 뷰어 — AddPlanningCard, AddDecisionButtons, CollapseDecisionButtons, ShowPlanButton 등 8개 메서드
- ChatWindow.EventBanner.cs (411줄): AddAgentEventBanner, BuildFileQuickActions
- ChatWindow.TaskDecomposition.cs (1,170줄 → 307줄): RenderSuggestActionChips, BuildFeedbackContext, UpdateProgressBar, BuildDiffView 잔류
- ChatWindow.BottomBar.cs (345줄): BuildBottomBar, BuildCodeBottomBar, ShowLogLevelMenu, ShowLanguageMenu 등
- ChatWindow.MoodMenu.cs (456줄): ShowFormatMenu, ShowMoodMenu, ShowCustomMoodDialog 등
- ChatWindow.CustomPresets.cs (978줄 → 203줄): ShowCustomPresetDialog, SelectTopic 잔류
- ChatWindow.ConversationMenu.cs (255줄): ShowConversationMenu (카테고리/삭제/즐겨찾기 팝업)
- ChatWindow.ConversationTitleEdit.cs (108줄): EnterTitleEditMode

### SettingsViewModel 분할
- SettingsViewModel.LlmProperties.cs (417줄): LLM·에이전트 관련 바인딩 프로퍼티
- SettingsViewModel.Properties.cs (837줄 → 427줄): 기능 토글·테마·스니펫 등 앱 수준 프로퍼티

### TemplateService 분할
- TemplateService.Css.cs (559줄): 11종 CSS 테마 문자열 상수
- TemplateService.cs (734줄 → 179줄): 메서드 로직만 잔류

### PlanViewerWindow 분할
- PlanViewerWindow.StepRenderer.cs (616줄): RenderSteps + SwapSteps + EditStep + 버튼 빌더 9개
- PlanViewerWindow.cs (931줄 → 324줄): Win32/생성자/공개 API 잔류

### App.xaml.cs 분할 (776줄 → 452줄)
- App.Settings.cs (252줄): SetupTrayIcon, OpenSettings, ToggleDockBar, RefreshDockBar, OpenAiChat
- App.Helpers.cs (92줄): LoadAppIcon, IsAutoStartEnabled, SetAutoStart, OnExit

### LlmService.ToolUse.cs 분할 (719줄 → 115줄)
- LlmService.ClaudeTools.cs (180줄): SendClaudeWithToolsAsync, BuildClaudeToolBody
- LlmService.GeminiTools.cs (175줄): SendGeminiWithToolsAsync, BuildGeminiToolBody
- LlmService.OpenAiTools.cs (215줄): SendOpenAiWithToolsAsync, BuildOpenAiToolBody

### SettingsWindow.UI.cs 분할 (802줄 → 310줄)
- SettingsWindow.Storage.cs (167줄): RefreshStorageInfo, BtnStorageCleanup_Click 등
- SettingsWindow.HotkeyUI.cs (127줄): RefreshHotkeyBadges, EnsureHotkeyInCombo, GetKeyName 등
- SettingsWindow.DevMode.cs (90줄): DevModeCheckBox_Checked, UpdateDevModeContentVisibility

## 빌드 결과: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 20:51:26 +09:00
parent f5a1ba999c
commit aa907d7b79
29 changed files with 5540 additions and 5365 deletions

View File

@@ -200,779 +200,4 @@ public partial class ChatWindow
if (_activeTab == "Cowork")
BuildBottomBar();
}
/// <summary>선택된 디자인 무드 키 (HtmlSkill에서 사용).</summary>
private string _selectedMood = null!; // Loaded 이벤트에서 초기화
private string _selectedLanguage = "auto"; // Code 탭 개발 언어
private string _folderDataUsage = null!; // Loaded 이벤트에서 초기화
/// <summary>하단 바를 구성합니다 (포맷 + 디자인 드롭다운 버튼).</summary>
private void BuildBottomBar()
{
MoodIconPanel.Children.Clear();
var secondaryText = ThemeResourceHelper.Secondary(this);
// ── 포맷 버튼 ──
var currentFormat = Llm.DefaultOutputFormat ?? "auto";
var formatLabel = GetFormatLabel(currentFormat);
var formatBtn = CreateFolderBarButton("\uE9F9", formatLabel, "보고서 형태 선택", "#8B5CF6");
formatBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowFormatMenu(); };
// Name 등록 (Popup PlacementTarget용)
try { RegisterName("BtnFormatMenu", formatBtn); } catch (Exception) { try { UnregisterName("BtnFormatMenu"); RegisterName("BtnFormatMenu", formatBtn); } catch (Exception) { /* 이름 등록 실패 */ } }
MoodIconPanel.Children.Add(formatBtn);
// 구분선
MoodIconPanel.Children.Add(new Border
{
Width = 1, Height = 18,
Background = ThemeResourceHelper.Separator(this),
Margin = new Thickness(4, 0, 4, 0),
VerticalAlignment = VerticalAlignment.Center,
});
// ── 디자인 버튼 (소극 스타일) ──
var currentMood = TemplateService.AllMoods.FirstOrDefault(m => m.Key == _selectedMood);
var moodLabel = currentMood?.Label ?? "모던";
var moodIcon = currentMood?.Icon ?? "🔷";
var moodBtn = CreateFolderBarButton(null, $"{moodIcon} {moodLabel}", "디자인 무드 선택");
moodBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowMoodMenu(); };
try { RegisterName("BtnMoodMenu", moodBtn); } catch (Exception) { try { UnregisterName("BtnMoodMenu"); RegisterName("BtnMoodMenu", moodBtn); } catch (Exception) { /* 이름 등록 실패 */ } }
MoodIconPanel.Children.Add(moodBtn);
// 구분선
MoodIconPanel.Children.Add(new Border
{
Width = 1, Height = 18,
Background = ThemeResourceHelper.Separator(this),
Margin = new Thickness(4, 0, 4, 0),
VerticalAlignment = VerticalAlignment.Center,
});
// ── 파일 탐색기 토글 버튼 ──
var fileBrowserBtn = CreateFolderBarButton("\uED25", "파일", "파일 탐색기 열기/닫기", "#D97706");
fileBrowserBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ToggleFileBrowser(); };
MoodIconPanel.Children.Add(fileBrowserBtn);
// ── 실행 이력 상세도 버튼 ──
AppendLogLevelButton();
// 구분선 표시
if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Visible;
}
/// <summary>Code 탭 하단 바: 개발 언어 선택 + 파일 탐색기 토글.</summary>
private void BuildCodeBottomBar()
{
MoodIconPanel.Children.Clear();
var secondaryText = ThemeResourceHelper.Secondary(this);
// 개발 언어 선택 버튼
var langLabel = _selectedLanguage switch
{
"python" => "🐍 Python",
"java" => "☕ Java",
"csharp" => "🔷 C#",
"cpp" => "⚙ C++",
"javascript" => "🌐 JavaScript",
_ => "🔧 자동 감지",
};
var langBtn = CreateFolderBarButton(null, langLabel, "개발 언어 선택");
langBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowLanguageMenu(); };
try { RegisterName("BtnLangMenu", langBtn); } catch (Exception) { try { UnregisterName("BtnLangMenu"); RegisterName("BtnLangMenu", langBtn); } catch (Exception) { /* 이름 등록 실패 */ } }
MoodIconPanel.Children.Add(langBtn);
// 구분선
MoodIconPanel.Children.Add(new Border
{
Width = 1, Height = 18,
Background = ThemeResourceHelper.Separator(this),
Margin = new Thickness(4, 0, 4, 0),
VerticalAlignment = VerticalAlignment.Center,
});
// 파일 탐색기 토글
var fileBrowserBtn = CreateFolderBarButton("\uED25", "파일", "파일 탐색기 열기/닫기", "#D97706");
fileBrowserBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ToggleFileBrowser(); };
MoodIconPanel.Children.Add(fileBrowserBtn);
// ── 실행 이력 상세도 버튼 ──
AppendLogLevelButton();
if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Visible;
}
/// <summary>하단 바에 실행 이력 상세도 선택 버튼을 추가합니다.</summary>
private void AppendLogLevelButton()
{
// 구분선
MoodIconPanel.Children.Add(new Border
{
Width = 1, Height = 18,
Background = ThemeResourceHelper.Separator(this),
Margin = new Thickness(4, 0, 4, 0),
VerticalAlignment = VerticalAlignment.Center,
});
var currentLevel = Llm.AgentLogLevel ?? "simple";
var levelLabel = currentLevel switch
{
"debug" => "디버그",
"detailed" => "상세",
_ => "간략",
};
var logBtn = CreateFolderBarButton("\uE946", levelLabel, "실행 이력 상세도", "#059669");
logBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; ShowLogLevelMenu(); };
try { RegisterName("BtnLogLevelMenu", logBtn); } catch (Exception) { try { UnregisterName("BtnLogLevelMenu"); RegisterName("BtnLogLevelMenu", logBtn); } catch (Exception) { /* 이름 등록 실패 */ } }
MoodIconPanel.Children.Add(logBtn);
}
/// <summary>실행 이력 상세도 팝업 메뉴를 표시합니다.</summary>
private void ShowLogLevelMenu()
{
FormatMenuItems.Children.Clear();
var primaryText = ThemeResourceHelper.Primary(this);
var accentBrush = ThemeResourceHelper.Accent(this);
var secondaryText = ThemeResourceHelper.Secondary(this);
var levels = new (string Key, string Label, string Desc)[]
{
("simple", "Simple (간략)", "도구 결과만 한 줄로 표시"),
("detailed", "Detailed (상세)", "도구 호출/결과 + 접이식 상세"),
("debug", "Debug (디버그)", "모든 정보 + 파라미터 표시"),
};
var current = Llm.AgentLogLevel ?? "simple";
foreach (var (key, label, desc) in levels)
{
var isActive = current == key;
var sp = new StackPanel { Orientation = Orientation.Horizontal };
sp.Children.Add(CreateCheckIcon(isActive, accentBrush));
sp.Children.Add(new TextBlock
{
Text = label,
FontSize = 13,
Foreground = isActive ? accentBrush : primaryText,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
});
sp.Children.Add(new TextBlock
{
Text = desc,
FontSize = 10,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
});
var item = new Border
{
Child = sp,
Padding = new Thickness(12, 8, 12, 8),
CornerRadius = new CornerRadius(6),
Background = Brushes.Transparent,
Cursor = Cursors.Hand,
};
var hoverBg = ThemeResourceHelper.HoverBg(this);
item.MouseEnter += (s, _) => ((Border)s!).Background = hoverBg;
item.MouseLeave += (s, _) => ((Border)s!).Background = Brushes.Transparent;
item.MouseLeftButtonUp += (_, _) =>
{
Llm.AgentLogLevel = key;
_settings.Save();
FormatMenuPopup.IsOpen = false;
if (_activeTab == "Cowork") BuildBottomBar();
else if (_activeTab == "Code") BuildCodeBottomBar();
};
FormatMenuItems.Children.Add(item);
}
try
{
var target = FindName("BtnLogLevelMenu") as UIElement;
if (target != null) FormatMenuPopup.PlacementTarget = target;
}
catch (Exception) { /* 비핵심 작업 실패 — UI 차단 방지 */ }
FormatMenuPopup.IsOpen = true;
}
private void ShowLanguageMenu()
{
FormatMenuItems.Children.Clear();
var primaryText = ThemeResourceHelper.Primary(this);
var accentBrush = ThemeResourceHelper.Accent(this);
var languages = new (string Key, string Label, string Icon)[]
{
("auto", "자동 감지", "🔧"),
("python", "Python", "🐍"),
("java", "Java", "☕"),
("csharp", "C# (.NET)", "🔷"),
("cpp", "C/C++", "⚙"),
("javascript", "JavaScript / Vue", "🌐"),
};
foreach (var (key, label, icon) in languages)
{
var isActive = _selectedLanguage == key;
var sp = new StackPanel { Orientation = Orientation.Horizontal };
sp.Children.Add(CreateCheckIcon(isActive, accentBrush));
sp.Children.Add(new TextBlock { Text = icon, FontSize = 13, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0) });
sp.Children.Add(new TextBlock { Text = label, FontSize = 13, Foreground = isActive ? accentBrush : primaryText, FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal });
var itemBorder = new Border
{
Child = sp, Background = Brushes.Transparent,
CornerRadius = new CornerRadius(8), Cursor = Cursors.Hand,
Padding = new Thickness(8, 7, 12, 7),
};
ApplyMenuItemHover(itemBorder);
var capturedKey = key;
itemBorder.MouseLeftButtonUp += (_, _) =>
{
FormatMenuPopup.IsOpen = false;
_selectedLanguage = capturedKey;
BuildCodeBottomBar();
};
FormatMenuItems.Children.Add(itemBorder);
}
if (FindName("BtnLangMenu") is UIElement langTarget)
FormatMenuPopup.PlacementTarget = langTarget;
FormatMenuPopup.IsOpen = true;
}
/// <summary>폴더바 내 드롭다운 버튼 (소극/적극 스타일과 동일)</summary>
private Border CreateFolderBarButton(string? mdlIcon, string label, string tooltip, string? iconColorHex = null)
{
var secondaryText = ThemeResourceHelper.Secondary(this);
var iconColor = iconColorHex != null ? BrushFromHex(iconColorHex) : secondaryText;
var sp = new StackPanel { Orientation = Orientation.Horizontal };
if (mdlIcon != null)
{
sp.Children.Add(new TextBlock
{
Text = mdlIcon,
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 12,
Foreground = iconColor,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 4, 0),
});
}
sp.Children.Add(new TextBlock
{
Text = label,
FontSize = 12,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
});
return new Border
{
Child = sp,
Background = Brushes.Transparent,
Padding = new Thickness(6, 4, 6, 4),
Cursor = Cursors.Hand,
ToolTip = tooltip,
};
}
private static string GetFormatLabel(string key) => key switch
{
"xlsx" => "Excel",
"html" => "HTML 보고서",
"docx" => "Word",
"md" => "Markdown",
"csv" => "CSV",
_ => "AI 자동",
};
/// <summary>현재 프리셋/카테고리에 맞는 에이전트 이름, 심볼, 색상을 반환합니다.</summary>
private (string Name, string Symbol, string Color) GetAgentIdentity()
{
string? category = null;
lock (_convLock)
{
category = _currentConversation?.Category;
}
return category switch
{
// Cowork 프리셋 카테고리
"보고서" => ("보고서 에이전트", "◆", "#3B82F6"),
"데이터" => ("데이터 분석 에이전트", "◆", "#10B981"),
"문서" => ("문서 작성 에이전트", "◆", "#6366F1"),
"논문" => ("논문 분석 에이전트", "◆", "#6366F1"),
"파일" => ("파일 관리 에이전트", "◆", "#8B5CF6"),
"자동화" => ("자동화 에이전트", "◆", "#EF4444"),
// Code 프리셋 카테고리
"코드개발" => ("코드 개발 에이전트", "◆", "#3B82F6"),
"리팩터링" => ("리팩터링 에이전트", "◆", "#6366F1"),
"코드리뷰" => ("코드 리뷰 에이전트", "◆", "#10B981"),
"보안점검" => ("보안 점검 에이전트", "◆", "#EF4444"),
"테스트" => ("테스트 에이전트", "◆", "#F59E0B"),
// Chat 카테고리
"연구개발" => ("연구개발 에이전트", "◆", "#0EA5E9"),
"시스템" => ("시스템 에이전트", "◆", "#64748B"),
"수율분석" => ("수율분석 에이전트", "◆", "#F59E0B"),
"제품분석" => ("제품분석 에이전트", "◆", "#EC4899"),
"경영" => ("경영 분석 에이전트", "◆", "#8B5CF6"),
"인사" => ("인사 관리 에이전트", "◆", "#14B8A6"),
"제조기술" => ("제조기술 에이전트", "◆", "#F97316"),
"재무" => ("재무 분석 에이전트", "◆", "#6366F1"),
_ when _activeTab == "Code" => ("코드 에이전트", "◆", "#3B82F6"),
_ when _activeTab == "Cowork" => ("코워크 에이전트", "◆", "#4B5EFC"),
_ => ("AX 에이전트", "◆", "#4B5EFC"),
};
}
/// <summary>포맷 선택 팝업 메뉴를 표시합니다.</summary>
private void ShowFormatMenu()
{
FormatMenuItems.Children.Clear();
var primaryText = ThemeResourceHelper.Primary(this);
var secondaryText = ThemeResourceHelper.Secondary(this);
var accentBrush = ThemeResourceHelper.Accent(this);
var currentFormat = Llm.DefaultOutputFormat ?? "auto";
var formats = new (string Key, string Label, string Icon, string Color)[]
{
("auto", "AI 자동 선택", "\uE8BD", "#8B5CF6"),
("xlsx", "Excel", "\uE9F9", "#217346"),
("html", "HTML 보고서", "\uE12B", "#E44D26"),
("docx", "Word", "\uE8A5", "#2B579A"),
("md", "Markdown", "\uE943", "#6B7280"),
("csv", "CSV", "\uE9D9", "#10B981"),
};
foreach (var (key, label, icon, color) in formats)
{
var isActive = key == currentFormat;
var sp = new StackPanel { Orientation = Orientation.Horizontal };
// 커스텀 체크 아이콘
sp.Children.Add(CreateCheckIcon(isActive, accentBrush));
sp.Children.Add(new TextBlock
{
Text = icon,
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 13,
Foreground = BrushFromHex(color),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
});
sp.Children.Add(new TextBlock
{
Text = label, FontSize = 13,
Foreground = primaryText,
VerticalAlignment = VerticalAlignment.Center,
});
var itemBorder = new Border
{
Child = sp,
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(8),
Cursor = Cursors.Hand,
Padding = new Thickness(8, 7, 12, 7),
};
ApplyMenuItemHover(itemBorder);
var capturedKey = key;
itemBorder.MouseLeftButtonUp += (_, _) =>
{
FormatMenuPopup.IsOpen = false;
Llm.DefaultOutputFormat = capturedKey;
_settings.Save();
BuildBottomBar();
};
FormatMenuItems.Children.Add(itemBorder);
}
// PlacementTarget을 동적 등록된 버튼으로 설정
if (FindName("BtnFormatMenu") is UIElement formatTarget)
FormatMenuPopup.PlacementTarget = formatTarget;
FormatMenuPopup.IsOpen = true;
}
/// <summary>디자인 무드 선택 팝업 메뉴를 표시합니다.</summary>
private void ShowMoodMenu()
{
MoodMenuItems.Children.Clear();
var primaryText = ThemeResourceHelper.Primary(this);
var secondaryText = ThemeResourceHelper.Secondary(this);
var accentBrush = ThemeResourceHelper.Accent(this);
var borderBrush = ThemeResourceHelper.Border(this);
// 2열 갤러리 그리드
var grid = new System.Windows.Controls.Primitives.UniformGrid { Columns = 2 };
foreach (var mood in TemplateService.AllMoods)
{
var isActive = _selectedMood == mood.Key;
var isCustom = Llm.CustomMoods.Any(cm => cm.Key == mood.Key);
var colors = TemplateService.GetMoodColors(mood.Key);
// 미니 프리뷰 카드
var previewCard = new Border
{
Width = 160, Height = 80,
CornerRadius = new CornerRadius(6),
Background = ThemeResourceHelper.HexBrush(colors.Background),
BorderBrush = isActive ? accentBrush : ThemeResourceHelper.HexBrush(colors.Border),
BorderThickness = new Thickness(isActive ? 2 : 1),
Padding = new Thickness(8, 6, 8, 6),
Margin = new Thickness(2),
};
var previewContent = new StackPanel();
// 헤딩 라인
previewContent.Children.Add(new Border
{
Width = 60, Height = 6, CornerRadius = new CornerRadius(2),
Background = ThemeResourceHelper.HexBrush(colors.PrimaryText),
HorizontalAlignment = HorizontalAlignment.Left,
Margin = new Thickness(0, 0, 0, 4),
});
// 악센트 라인
previewContent.Children.Add(new Border
{
Width = 40, Height = 3, CornerRadius = new CornerRadius(1),
Background = ThemeResourceHelper.HexBrush(colors.Accent),
HorizontalAlignment = HorizontalAlignment.Left,
Margin = new Thickness(0, 0, 0, 6),
});
// 텍스트 라인들
for (int i = 0; i < 3; i++)
{
previewContent.Children.Add(new Border
{
Width = 120 - i * 20, Height = 3, CornerRadius = new CornerRadius(1),
Background = new SolidColorBrush(ThemeResourceHelper.HexColor(colors.SecondaryText)) { Opacity = 0.5 },
HorizontalAlignment = HorizontalAlignment.Left,
Margin = new Thickness(0, 0, 0, 3),
});
}
// 미니 카드 영역
var cardRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 0) };
for (int i = 0; i < 2; i++)
{
cardRow.Children.Add(new Border
{
Width = 28, Height = 14, CornerRadius = new CornerRadius(2),
Background = ThemeResourceHelper.HexBrush(colors.CardBg),
BorderBrush = ThemeResourceHelper.HexBrush(colors.Border),
BorderThickness = new Thickness(0.5),
Margin = new Thickness(0, 0, 4, 0),
});
}
previewContent.Children.Add(cardRow);
previewCard.Child = previewContent;
// 무드 라벨
var labelPanel = new StackPanel { Margin = new Thickness(4, 2, 4, 4) };
var labelRow = new StackPanel { Orientation = Orientation.Horizontal };
labelRow.Children.Add(new TextBlock
{
Text = mood.Icon, FontSize = 12,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 4, 0),
});
labelRow.Children.Add(new TextBlock
{
Text = mood.Label, FontSize = 11.5,
Foreground = primaryText,
FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal,
VerticalAlignment = VerticalAlignment.Center,
});
if (isActive)
{
labelRow.Children.Add(new TextBlock
{
Text = " ✓", FontSize = 11,
Foreground = accentBrush,
VerticalAlignment = VerticalAlignment.Center,
});
}
labelPanel.Children.Add(labelRow);
// 전체 카드 래퍼
var cardWrapper = new Border
{
CornerRadius = new CornerRadius(8),
Background = Brushes.Transparent,
Cursor = Cursors.Hand,
Padding = new Thickness(4),
Margin = new Thickness(2),
};
var wrapperContent = new StackPanel();
wrapperContent.Children.Add(previewCard);
wrapperContent.Children.Add(labelPanel);
cardWrapper.Child = wrapperContent;
// 호버
cardWrapper.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x12, 0xFF, 0xFF, 0xFF)); };
cardWrapper.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
var capturedMood = mood;
cardWrapper.MouseLeftButtonUp += (_, _) =>
{
MoodMenuPopup.IsOpen = false;
_selectedMood = capturedMood.Key;
Llm.DefaultMood = capturedMood.Key;
_settings.Save();
BuildBottomBar();
};
// 커스텀 무드: 우클릭
if (isCustom)
{
cardWrapper.MouseRightButtonUp += (s, e) =>
{
e.Handled = true;
MoodMenuPopup.IsOpen = false;
ShowCustomMoodContextMenu(s as Border, capturedMood.Key);
};
}
grid.Children.Add(cardWrapper);
}
MoodMenuItems.Children.Add(grid);
// ── 구분선 + 추가 버튼 ──
MoodMenuItems.Children.Add(new System.Windows.Shapes.Rectangle
{
Height = 1,
Fill = borderBrush,
Margin = new Thickness(8, 4, 8, 4),
Opacity = 0.4,
});
var addSp = new StackPanel { Orientation = Orientation.Horizontal };
addSp.Children.Add(new TextBlock
{
Text = "\uE710",
FontFamily = ThemeResourceHelper.SegoeMdl2,
FontSize = 13,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(4, 0, 8, 0),
});
addSp.Children.Add(new TextBlock
{
Text = "커스텀 무드 추가",
FontSize = 13,
Foreground = secondaryText,
VerticalAlignment = VerticalAlignment.Center,
});
var addBorder = new Border
{
Child = addSp,
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(8),
Cursor = Cursors.Hand,
Padding = new Thickness(8, 6, 12, 6),
};
ApplyMenuItemHover(addBorder);
addBorder.MouseLeftButtonUp += (_, _) =>
{
MoodMenuPopup.IsOpen = false;
ShowCustomMoodDialog();
};
MoodMenuItems.Children.Add(addBorder);
if (FindName("BtnMoodMenu") is UIElement moodTarget)
MoodMenuPopup.PlacementTarget = moodTarget;
MoodMenuPopup.IsOpen = true;
}
/// <summary>커스텀 무드 추가/편집 다이얼로그를 표시합니다.</summary>
private void ShowCustomMoodDialog(Models.CustomMoodEntry? existing = null)
{
bool isEdit = existing != null;
var dlg = new CustomMoodDialog(
existingKey: existing?.Key ?? "",
existingLabel: existing?.Label ?? "",
existingIcon: existing?.Icon ?? "🎯",
existingDesc: existing?.Description ?? "",
existingCss: existing?.Css ?? "")
{
Owner = this,
};
if (dlg.ShowDialog() == true)
{
if (isEdit)
{
existing!.Label = dlg.MoodLabel;
existing.Icon = dlg.MoodIcon;
existing.Description = dlg.MoodDescription;
existing.Css = dlg.MoodCss;
}
else
{
Llm.CustomMoods.Add(new Models.CustomMoodEntry
{
Key = dlg.MoodKey,
Label = dlg.MoodLabel,
Icon = dlg.MoodIcon,
Description = dlg.MoodDescription,
Css = dlg.MoodCss,
});
}
_settings.Save();
TemplateService.LoadCustomMoods(Llm.CustomMoods);
BuildBottomBar();
}
}
/// <summary>커스텀 무드 우클릭 컨텍스트 메뉴.</summary>
private void ShowCustomMoodContextMenu(Border? anchor, string moodKey)
{
if (anchor == null) return;
var popup = new System.Windows.Controls.Primitives.Popup
{
PlacementTarget = anchor,
Placement = System.Windows.Controls.Primitives.PlacementMode.Right,
StaysOpen = false, AllowsTransparency = true,
};
var menuBg = ThemeResourceHelper.Background(this);
var primaryText = ThemeResourceHelper.Primary(this);
var secondaryText = ThemeResourceHelper.Secondary(this);
var borderBrush = ThemeResourceHelper.Border(this);
var menuBorder = new Border
{
Background = menuBg,
CornerRadius = new CornerRadius(10),
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
Padding = new Thickness(4),
MinWidth = 120,
Effect = new System.Windows.Media.Effects.DropShadowEffect
{
BlurRadius = 12, ShadowDepth = 2, Opacity = 0.3, Color = Colors.Black,
},
};
var stack = new StackPanel();
var editItem = CreateContextMenuItem("\uE70F", "편집", primaryText, secondaryText);
editItem.MouseLeftButtonDown += (_, _) =>
{
popup.IsOpen = false;
var entry = Llm.CustomMoods.FirstOrDefault(c => c.Key == moodKey);
if (entry != null) ShowCustomMoodDialog(entry);
};
stack.Children.Add(editItem);
var deleteItem = CreateContextMenuItem("\uE74D", "삭제", new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)), secondaryText);
deleteItem.MouseLeftButtonDown += (_, _) =>
{
popup.IsOpen = false;
var result = CustomMessageBox.Show(
$"이 디자인 무드를 삭제하시겠습니까?",
"무드 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
Llm.CustomMoods.RemoveAll(c => c.Key == moodKey);
if (_selectedMood == moodKey) _selectedMood = "modern";
_settings.Save();
TemplateService.LoadCustomMoods(Llm.CustomMoods);
BuildBottomBar();
}
};
stack.Children.Add(deleteItem);
menuBorder.Child = stack;
popup.Child = menuBorder;
popup.IsOpen = true;
}
private string? _promptCardPlaceholder;
private void ShowPlaceholder()
{
if (string.IsNullOrEmpty(_promptCardPlaceholder)) return;
InputWatermark.Text = _promptCardPlaceholder;
InputWatermark.Visibility = Visibility.Visible;
InputBox.Text = "";
InputBox.Focus();
}
private void UpdateWatermarkVisibility()
{
// 슬래시 칩이 활성화되어 있으면 워터마크 숨기기 (겹침 방지)
if (_activeSlashCmd != null)
{
InputWatermark.Visibility = Visibility.Collapsed;
return;
}
if (_promptCardPlaceholder != null && string.IsNullOrEmpty(InputBox.Text))
InputWatermark.Visibility = Visibility.Visible;
else
InputWatermark.Visibility = Visibility.Collapsed;
}
private void ClearPromptCardPlaceholder()
{
_promptCardPlaceholder = null;
InputWatermark.Visibility = Visibility.Collapsed;
}
private void BtnSettings_Click(object sender, RoutedEventArgs e)
{
// Phase 32: Shift+클릭 → 인라인 설정 패널 토글, 일반 클릭 → SettingsWindow
if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Shift))
{
ToggleSettingsPanel();
return;
}
if (System.Windows.Application.Current is App app)
app.OpenSettingsFromChat();
}
/// <summary>Phase 32-E: 우측 설정 패널 슬라이드인/아웃 토글.</summary>
private void ToggleSettingsPanel()
{
if (SettingsPanel.IsOpen)
{
SettingsPanel.IsOpen = false;
}
else
{
var activeTab = "Chat";
if (TabCowork?.IsChecked == true) activeTab = "Cowork";
else if (TabCode?.IsChecked == true) activeTab = "Code";
SettingsPanel.LoadFromSettings(_settings, activeTab);
SettingsPanel.CloseRequested -= OnSettingsPanelClose;
SettingsPanel.CloseRequested += OnSettingsPanelClose;
SettingsPanel.IsOpen = true;
}
}
private void OnSettingsPanelClose(object? sender, EventArgs e)
{
SettingsPanel.IsOpen = false;
}
}