[Phase 37-38] ChatWindow·SettingsWindow 파셜 클래스 분할 + 코드 품질 개선
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>
This commit is contained in:
40
CLAUDE.md
40
CLAUDE.md
@@ -4,6 +4,46 @@
|
||||
|
||||
---
|
||||
|
||||
## 0. 작업 완료 후 깃 푸시 규칙
|
||||
|
||||
### 매 작업 단위 완료 시 반드시 깃 푸시
|
||||
- Phase 작업(기능 개발, 리팩터링 등) 완료 → `dotnet build` 확인 → 소스 파일만 스테이징 → 커밋 → 푸시
|
||||
- **빌드 오류 없이 커밋** — `경고 0, 오류 0` 상태에서만 푸시
|
||||
- 커밋 메시지: `[PhaseXX] 작업 내용 요약 (1~2줄)`
|
||||
|
||||
### 오류 복구 불가 시 이전 버전 롤백
|
||||
작업 중 오류가 복구되지 않으면 깃에서 이전 버전을 받아 작업:
|
||||
```bash
|
||||
# 마지막 커밋으로 전체 복구
|
||||
git reset --hard HEAD
|
||||
|
||||
# 특정 커밋으로 복구 (git log로 커밋 해시 확인)
|
||||
git reset --hard <커밋해시>
|
||||
|
||||
# 원격 최신 버전으로 완전 복구
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
```
|
||||
- 복구 시도 2회 이상 실패 → 즉시 롤백, 사용자에게 알림
|
||||
- 롤백 후 원인 분석 → 더 작은 단위로 재작업
|
||||
|
||||
### 스테이징 규칙 (빌드 산출물 제외)
|
||||
```bash
|
||||
# 소스 코드만 스테이징 (bin/, obj/ 제외)
|
||||
git add src/AxCopilot/Views/
|
||||
git add src/AxCopilot/Services/
|
||||
git add src/AxCopilot/Models/
|
||||
git add src/AxCopilot/Handlers/
|
||||
git add src/AxCopilot/ViewModels/
|
||||
git add src/AxCopilot/Themes/
|
||||
git add src/AxCopilot/Core/
|
||||
git add docs/
|
||||
git add CLAUDE.md
|
||||
# 절대 추가 금지: bin/, obj/, *.dll, *.exe, *.pdb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. UI/UX 디자인 원칙
|
||||
|
||||
### 기본 컨트롤 사용 금지
|
||||
|
||||
@@ -4538,5 +4538,21 @@ Week 8: [23] AutoCompact + isEnabled + 최종 검증
|
||||
|
||||
---
|
||||
|
||||
최종 업데이트: 2026-04-03 (Phase 22~37 구현 완료 — CC 동등성 37/37 + 코드 품질 리팩터링 5차)
|
||||
## Phase 38 — SettingsWindow 파셜 클래스 분할 (v2.3) ✅ 완료
|
||||
|
||||
> **목표**: 3,216줄 SettingsWindow.xaml.cs를 3개 파셜 클래스 파일로 분할.
|
||||
|
||||
| 파일 | 줄 수 | 내용 |
|
||||
|------|-------|------|
|
||||
| `SettingsWindow.xaml.cs` (메인) | 373 | 생성자, 필드, 저장/닫기, 스니펫 이벤트 |
|
||||
| `SettingsWindow.UI.cs` | 802 | 섹션 헬퍼, 탭 전환, 독바, 스토리지, 핫키, 버전 |
|
||||
| `SettingsWindow.Tools.cs` | 875 | 도구/커넥터 카드 UI, AX Agent 탭, 도구 관리 |
|
||||
| `SettingsWindow.AgentConfig.cs` | 1,202 | 모델 등록, 스킬, 템플릿, AI토글, 네트워크모드, 훅, MCP |
|
||||
|
||||
- **메인 파일**: 3,216줄 → 373줄 (**88.4% 감소**)
|
||||
- **빌드**: 경고 0, 오류 0
|
||||
|
||||
---
|
||||
|
||||
최종 업데이트: 2026-04-03 (Phase 22~38 구현 완료 — CC 동등성 37/37 + 코드 품질 리팩터링 6차)
|
||||
|
||||
|
||||
1202
src/AxCopilot/Views/SettingsWindow.AgentConfig.cs
Normal file
1202
src/AxCopilot/Views/SettingsWindow.AgentConfig.cs
Normal file
File diff suppressed because it is too large
Load Diff
875
src/AxCopilot/Views/SettingsWindow.Tools.cs
Normal file
875
src/AxCopilot/Views/SettingsWindow.Tools.cs
Normal file
@@ -0,0 +1,875 @@
|
||||
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
|
||||
{
|
||||
// ─── 도구/커넥터 관리 UI (기타 탭) ─────────────────────────────────────────────
|
||||
|
||||
private void BuildToolRegistryPanel()
|
||||
{
|
||||
if (AgentEtcContent == null) return;
|
||||
|
||||
// 도구 목록 데이터 (카테고리별)
|
||||
var toolGroups = new (string Category, string Icon, string IconColor, (string Name, string Desc)[] Tools)[]
|
||||
{
|
||||
("파일/검색", "\uE8B7", "#F59E0B", new[]
|
||||
{
|
||||
("file_read", "파일 읽기 — 텍스트/바이너리 파일의 내용을 읽습니다"),
|
||||
("file_write", "파일 쓰기 — 새 파일을 생성하거나 기존 파일을 덮어씁니다"),
|
||||
("file_edit", "파일 편집 — 줄 번호 기반으로 파일의 특정 부분을 수정합니다"),
|
||||
("glob", "파일 패턴 검색 — 글로브 패턴으로 파일을 찾습니다 (예: **/*.cs)"),
|
||||
("grep_tool", "텍스트 검색 — 정규식으로 파일 내용을 검색합니다"),
|
||||
("folder_map", "폴더 구조 — 프로젝트의 디렉토리 트리를 조회합니다"),
|
||||
("document_read", "문서 읽기 — PDF, DOCX 등 문서 파일의 텍스트를 추출합니다"),
|
||||
}),
|
||||
("프로세스/빌드", "\uE756", "#06B6D4", new[]
|
||||
{
|
||||
("process", "프로세스 실행 — 외부 명령/프로그램을 실행합니다"),
|
||||
("build_run", "빌드/테스트 — 프로젝트 타입을 감지하여 빌드/테스트를 실행합니다"),
|
||||
("dev_env_detect", "개발 환경 감지 — IDE, 런타임, 빌드 도구를 자동으로 인식합니다"),
|
||||
}),
|
||||
("코드 분석", "\uE943", "#818CF8", new[]
|
||||
{
|
||||
("search_codebase", "코드 키워드 검색 — TF-IDF 기반으로 관련 코드를 찾습니다"),
|
||||
("code_review", "AI 코드 리뷰 — Git diff 분석, 파일 정적 검사, PR 요약"),
|
||||
("lsp", "LSP 인텔리전스 — 정의 이동, 참조 검색, 심볼 목록 (C#/TS/Python)"),
|
||||
("test_loop", "테스트 루프 — 테스트 생성/실행/분석 자동화"),
|
||||
("git_tool", "Git 작업 — status, log, diff, commit 등 Git 명령 실행"),
|
||||
("snippet_runner", "코드 실행 — C#/Python/JavaScript 스니펫 즉시 실행"),
|
||||
}),
|
||||
("문서 생성", "\uE8A5", "#34D399", new[]
|
||||
{
|
||||
("excel_create", "Excel 생성 — .xlsx 스프레드시트를 생성합니다"),
|
||||
("docx_create", "Word 생성 — .docx 문서를 생성합니다"),
|
||||
("csv_create", "CSV 생성 — CSV 파일을 생성합니다"),
|
||||
("markdown_create", "마크다운 생성 — .md 파일을 생성합니다"),
|
||||
("html_create", "HTML 생성 — HTML 파일을 생성합니다"),
|
||||
("chart_create", "차트 생성 — 데이터 시각화 차트를 생성합니다"),
|
||||
("batch_create", "배치 생성 — 스크립트 파일을 생성합니다"),
|
||||
("document_review", "문서 검증 — 문서 품질을 검사합니다"),
|
||||
("format_convert", "포맷 변환 — 문서 형식을 변환합니다"),
|
||||
}),
|
||||
("데이터 처리", "\uE9F5", "#F59E0B", new[]
|
||||
{
|
||||
("json_tool", "JSON 처리 — JSON 파싱, 변환, 검증, 포맷팅"),
|
||||
("regex_tool", "정규식 — 정규식 테스트, 추출, 치환"),
|
||||
("diff_tool", "텍스트 비교 — 두 파일/텍스트 비교, 통합 diff 출력"),
|
||||
("base64_tool", "인코딩 — Base64/URL 인코딩, 디코딩"),
|
||||
("hash_tool", "해시 계산 — MD5/SHA256 해시 계산 (파일/텍스트)"),
|
||||
("datetime_tool", "날짜/시간 — 날짜 변환, 타임존, 기간 계산"),
|
||||
}),
|
||||
("시스템/환경", "\uE770", "#06B6D4", new[]
|
||||
{
|
||||
("clipboard_tool", "클립보드 — Windows 클립보드 읽기/쓰기 (텍스트/이미지)"),
|
||||
("notify_tool", "알림 — Windows 시스템 알림 전송"),
|
||||
("env_tool", "환경변수 — 환경변수 읽기/설정 (프로세스 범위)"),
|
||||
("zip_tool", "압축 — 파일 압축(zip) / 해제"),
|
||||
("http_tool", "HTTP — 로컬/사내 HTTP API 호출 (GET/POST)"),
|
||||
("sql_tool", "SQLite — SQLite DB 쿼리 실행 (로컬 파일)"),
|
||||
}),
|
||||
("에이전트", "\uE99A", "#F472B6", new[]
|
||||
{
|
||||
("spawn_agent", "서브에이전트 — 하위 작업을 병렬로 실행하는 서브에이전트를 생성합니다"),
|
||||
("wait_agents", "에이전트 대기 — 실행 중인 서브에이전트의 결과를 수집합니다"),
|
||||
("memory", "에이전트 메모리 — 프로젝트 규칙, 선호도를 저장/검색합니다"),
|
||||
("skill_manager", "스킬 관리 — 스킬 목록 조회, 상세 정보, 재로드"),
|
||||
("project_rules", "프로젝트 지침 — AX.md 개발 지침을 읽고 씁니다"),
|
||||
}),
|
||||
};
|
||||
|
||||
// 도구 목록을 섹션으로 그룹화
|
||||
var toolCards = new List<UIElement>();
|
||||
|
||||
// 설명
|
||||
// 도구 헤더
|
||||
toolCards.Add(new TextBlock
|
||||
{
|
||||
Text = $"등록된 도구/커넥터 ({toolGroups.Sum(g => g.Tools.Length)})",
|
||||
FontSize = 13,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
Margin = new Thickness(2, 4, 0, 4),
|
||||
});
|
||||
toolCards.Add(new TextBlock
|
||||
{
|
||||
Text = "에이전트가 사용할 수 있는 도구 목록입니다. 코워크/코드 탭에서 LLM이 자동으로 호출합니다.",
|
||||
FontSize = 11,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#9999BB"),
|
||||
Margin = new Thickness(2, 0, 0, 10),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
|
||||
foreach (var group in toolGroups)
|
||||
{
|
||||
// 카테고리 카드
|
||||
var card = new Border
|
||||
{
|
||||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.White,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(14, 10, 14, 10),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
};
|
||||
|
||||
var cardPanel = new StackPanel();
|
||||
|
||||
// 도구 아이템 패널 (접기/열기 대상)
|
||||
var toolItemsPanel = new StackPanel { Visibility = Visibility.Collapsed };
|
||||
|
||||
// 접기/열기 화살표
|
||||
var arrow = new TextBlock
|
||||
{
|
||||
Text = "\uE70D",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 9,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||||
RenderTransform = new RotateTransform(0),
|
||||
};
|
||||
|
||||
// 카테고리 헤더 (클릭 가능)
|
||||
var catHeader = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var catHeaderPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
catHeaderPanel.Children.Add(arrow);
|
||||
catHeaderPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = group.Icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 14,
|
||||
Foreground = ThemeResourceHelper.HexBrush(group.IconColor),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
});
|
||||
var catTitle = new TextBlock
|
||||
{
|
||||
Text = $"{group.Category} ({group.Tools.Length})",
|
||||
FontSize = 12.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#1A1B2E"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
catHeaderPanel.Children.Add(catTitle);
|
||||
catHeader.Child = catHeaderPanel;
|
||||
|
||||
// 클릭 시 접기/열기 토글
|
||||
catHeader.MouseLeftButtonDown += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
bool isVisible = toolItemsPanel.Visibility == Visibility.Visible;
|
||||
toolItemsPanel.Visibility = isVisible ? Visibility.Collapsed : Visibility.Visible;
|
||||
arrow.RenderTransform = new RotateTransform(isVisible ? 0 : 90);
|
||||
};
|
||||
|
||||
cardPanel.Children.Add(catHeader);
|
||||
|
||||
// 구분선
|
||||
toolItemsPanel.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = TryFindResource("BorderColor") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#F0F0F4"),
|
||||
Margin = new Thickness(0, 6, 0, 4),
|
||||
});
|
||||
|
||||
// 도구 아이템
|
||||
foreach (var (name, toolDesc) in group.Tools)
|
||||
{
|
||||
var row = new Grid { Margin = new Thickness(0, 3, 0, 3) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
|
||||
var nameBlock = new TextBlock
|
||||
{
|
||||
Text = name,
|
||||
FontSize = 12,
|
||||
FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"),
|
||||
Foreground = TryFindResource("AccentColor") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#4B5EFC"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
MinWidth = 130,
|
||||
Margin = new Thickness(4, 0, 12, 0),
|
||||
};
|
||||
Grid.SetColumn(nameBlock, 0);
|
||||
row.Children.Add(nameBlock);
|
||||
|
||||
var descBlock = new TextBlock
|
||||
{
|
||||
Text = toolDesc,
|
||||
FontSize = 11.5,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#6666AA"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
Grid.SetColumn(descBlock, 1);
|
||||
row.Children.Add(descBlock);
|
||||
|
||||
toolItemsPanel.Children.Add(row);
|
||||
}
|
||||
|
||||
cardPanel.Children.Add(toolItemsPanel);
|
||||
card.Child = cardPanel;
|
||||
toolCards.Add(card);
|
||||
}
|
||||
|
||||
// ── 도구 목록을 직접 추가 (카테고리별 접기/열기) ──
|
||||
foreach (var card in toolCards)
|
||||
AgentEtcContent.Children.Insert(AgentEtcContent.Children.Count, card);
|
||||
|
||||
int insertIdx = AgentEtcContent.Children.Count;
|
||||
|
||||
// ── 로드된 스킬 목록 ──
|
||||
BuildSkillListSection(ref insertIdx);
|
||||
}
|
||||
|
||||
private void BuildSkillListSection(ref int insertIdx)
|
||||
{
|
||||
if (AgentEtcContent == null) return;
|
||||
|
||||
var skills = Services.Agent.SkillService.Skills;
|
||||
if (skills.Count == 0) return;
|
||||
|
||||
var accentColor = ThemeResourceHelper.HexColor("#4B5EFC");
|
||||
var subtleText = ThemeResourceHelper.HexColor("#6666AA");
|
||||
|
||||
// 스킬 콘텐츠를 모아서 접기/열기 섹션에 넣기
|
||||
var skillItems = new List<UIElement>();
|
||||
|
||||
// 설명
|
||||
skillItems.Add(new TextBlock
|
||||
{
|
||||
Text = "/ 명령으로 호출할 수 있는 스킬 목록입니다. 앱 내장 + 사용자 추가 스킬이 포함됩니다.\n" +
|
||||
"(스킬은 사용자가 직접 /명령어를 입력해야 실행됩니다. LLM이 자동 호출하지 않습니다.)",
|
||||
FontSize = 11,
|
||||
Foreground = new SolidColorBrush(subtleText),
|
||||
Margin = new Thickness(2, 0, 0, 10),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
|
||||
// 내장 스킬 / 고급 스킬 분류
|
||||
var builtIn = skills.Where(s => string.IsNullOrEmpty(s.Requires)).ToList();
|
||||
var advanced = skills.Where(s => !string.IsNullOrEmpty(s.Requires)).ToList();
|
||||
|
||||
// 내장 스킬 카드
|
||||
if (builtIn.Count > 0)
|
||||
{
|
||||
var card = CreateSkillGroupCard("내장 스킬", "\uE768", "#34D399", builtIn);
|
||||
skillItems.Add(card);
|
||||
}
|
||||
|
||||
// 고급 스킬 (런타임 의존) 카드
|
||||
if (advanced.Count > 0)
|
||||
{
|
||||
var card = CreateSkillGroupCard("고급 스킬 (런타임 필요)", "\uE9D9", "#A78BFA", advanced);
|
||||
skillItems.Add(card);
|
||||
}
|
||||
|
||||
// ── 스킬 가져오기/내보내기 버튼 ──
|
||||
var btnPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(2, 4, 0, 10),
|
||||
};
|
||||
|
||||
// 가져오기 버튼
|
||||
var importBtn = new Border
|
||||
{
|
||||
Background = ThemeResourceHelper.HexBrush("#4B5EFC"),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(14, 7, 14, 7),
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var importContent = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
importContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE8B5",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12,
|
||||
Foreground = Brushes.White,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
importContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "스킬 가져오기 (.zip)",
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = Brushes.White,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
importBtn.Child = importContent;
|
||||
importBtn.MouseLeftButtonUp += SkillImport_Click;
|
||||
importBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
||||
importBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
btnPanel.Children.Add(importBtn);
|
||||
|
||||
// 내보내기 버튼
|
||||
var exportBtn = new Border
|
||||
{
|
||||
Background = ThemeResourceHelper.HexBrush("#F0F1F5"),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(14, 7, 14, 7),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var exportContent = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
exportContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uEDE1",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12,
|
||||
Foreground = ThemeResourceHelper.HexBrush("#555"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
exportContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "스킬 내보내기",
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = ThemeResourceHelper.HexBrush("#444"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
exportBtn.Child = exportContent;
|
||||
exportBtn.MouseLeftButtonUp += SkillExport_Click;
|
||||
exportBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
||||
exportBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
btnPanel.Children.Add(exportBtn);
|
||||
|
||||
skillItems.Add(btnPanel);
|
||||
|
||||
// ── 갤러리/통계 링크 버튼 ──
|
||||
var linkPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Margin = new Thickness(2, 0, 0, 12),
|
||||
};
|
||||
|
||||
var galleryBtn = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(14, 7, 14, 7),
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x18, 0x4B, 0x5E, 0xFC)),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var galleryContent = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
galleryContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE768",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12,
|
||||
Foreground = ThemeResourceHelper.HexBrush("#4B5EFC"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
galleryContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "스킬 갤러리 열기",
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = ThemeResourceHelper.HexBrush("#4B5EFC"),
|
||||
});
|
||||
galleryBtn.Child = galleryContent;
|
||||
galleryBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
var win = new SkillGalleryWindow();
|
||||
win.Owner = Window.GetWindow(this);
|
||||
win.ShowDialog();
|
||||
};
|
||||
galleryBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.8;
|
||||
galleryBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
linkPanel.Children.Add(galleryBtn);
|
||||
|
||||
var statsBtn = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(14, 7, 14, 7),
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x18, 0xA7, 0x8B, 0xFA)),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var statsContent = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
statsContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE9D9",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12,
|
||||
Foreground = ThemeResourceHelper.HexBrush("#A78BFA"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
statsContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "실행 통계 보기",
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = ThemeResourceHelper.HexBrush("#A78BFA"),
|
||||
});
|
||||
statsBtn.Child = statsContent;
|
||||
statsBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
var win = new AgentStatsDashboardWindow();
|
||||
win.Owner = Window.GetWindow(this);
|
||||
win.ShowDialog();
|
||||
};
|
||||
statsBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.8;
|
||||
statsBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
linkPanel.Children.Add(statsBtn);
|
||||
|
||||
skillItems.Add(linkPanel);
|
||||
|
||||
// ── 스킬 섹션 직접 추가 ──
|
||||
// 스킬 헤더
|
||||
var skillHeader = new TextBlock
|
||||
{
|
||||
Text = $"슬래시 스킬 ({skills.Count})",
|
||||
FontSize = 13,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
Margin = new Thickness(2, 16, 0, 8),
|
||||
};
|
||||
AgentEtcContent.Children.Insert(insertIdx++, skillHeader);
|
||||
foreach (var item in skillItems)
|
||||
AgentEtcContent.Children.Insert(insertIdx++, item);
|
||||
}
|
||||
|
||||
private Border CreateSkillGroupCard(string title, string icon, string colorHex,
|
||||
List<Services.Agent.SkillDefinition> skills)
|
||||
{
|
||||
var color = ThemeResourceHelper.HexColor(colorHex);
|
||||
var card = new Border
|
||||
{
|
||||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.White,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(14, 10, 14, 10),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
};
|
||||
|
||||
var panel = new StackPanel();
|
||||
|
||||
// 스킬 아이템 패널 (접기/열기 대상)
|
||||
var skillItemsPanel = new StackPanel { Visibility = Visibility.Collapsed };
|
||||
|
||||
// 접기/열기 화살표
|
||||
var arrow = new TextBlock
|
||||
{
|
||||
Text = "\uE70D",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 9,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||||
RenderTransform = new RotateTransform(0),
|
||||
};
|
||||
|
||||
// 카테고리 헤더 (클릭 가능)
|
||||
var catHeader = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var catHeaderPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
catHeaderPanel.Children.Add(arrow);
|
||||
catHeaderPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 14,
|
||||
Foreground = new SolidColorBrush(color),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
});
|
||||
catHeaderPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"{title} ({skills.Count})",
|
||||
FontSize = 12.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#1A1B2E"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
catHeader.Child = catHeaderPanel;
|
||||
|
||||
// 클릭 시 접기/열기 토글
|
||||
catHeader.MouseLeftButtonDown += (s, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
bool isVisible = skillItemsPanel.Visibility == Visibility.Visible;
|
||||
skillItemsPanel.Visibility = isVisible ? Visibility.Collapsed : Visibility.Visible;
|
||||
arrow.RenderTransform = new RotateTransform(isVisible ? 0 : 90);
|
||||
};
|
||||
|
||||
panel.Children.Add(catHeader);
|
||||
|
||||
// 구분선
|
||||
skillItemsPanel.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = TryFindResource("BorderColor") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#F0F0F4"),
|
||||
Margin = new Thickness(0, 2, 0, 4),
|
||||
});
|
||||
|
||||
// 스킬 아이템
|
||||
foreach (var skill in skills)
|
||||
{
|
||||
var row = new StackPanel { Margin = new Thickness(0, 4, 0, 4) };
|
||||
|
||||
// 위 줄: 스킬 명칭 + 가용성 뱃지
|
||||
var namePanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
namePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"/{skill.Name}",
|
||||
FontSize = 12,
|
||||
FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"),
|
||||
Foreground = skill.IsAvailable
|
||||
? (TryFindResource("AccentColor") as Brush ?? ThemeResourceHelper.HexBrush("#4B5EFC"))
|
||||
: Brushes.Gray,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(4, 0, 8, 0),
|
||||
Opacity = skill.IsAvailable ? 1.0 : 0.5,
|
||||
});
|
||||
|
||||
if (!skill.IsAvailable && !string.IsNullOrEmpty(skill.Requires))
|
||||
{
|
||||
namePanel.Children.Add(new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x20, 0xF8, 0x71, 0x71)),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Padding = new Thickness(5, 1, 5, 1),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = skill.UnavailableHint,
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xF8, 0x71, 0x71)),
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
},
|
||||
});
|
||||
}
|
||||
else if (skill.IsAvailable && !string.IsNullOrEmpty(skill.Requires))
|
||||
{
|
||||
namePanel.Children.Add(new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x20, 0x34, 0xD3, 0x99)),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Padding = new Thickness(5, 1, 5, 1),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "✓ 사용 가능",
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x34, 0xD3, 0x99)),
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
row.Children.Add(namePanel);
|
||||
|
||||
// 아래 줄: 설명 (뱃지와 구분되도록 위 여백 추가)
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"{skill.Label} — {skill.Description}",
|
||||
FontSize = 11.5,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush
|
||||
?? ThemeResourceHelper.HexBrush("#6666AA"),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(4, 3, 0, 0),
|
||||
Opacity = skill.IsAvailable ? 1.0 : 0.5,
|
||||
});
|
||||
|
||||
skillItemsPanel.Children.Add(row);
|
||||
}
|
||||
|
||||
panel.Children.Add(skillItemsPanel);
|
||||
card.Child = panel;
|
||||
return card;
|
||||
}
|
||||
|
||||
// ─── AX Agent 서브탭 전환 ───────────────────────────────────────────
|
||||
|
||||
private void AgentSubTab_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (AgentPanelCommon == null) return; // 초기화 전 방어
|
||||
AgentPanelCommon.Visibility = AgentTabCommon.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
AgentPanelChat.Visibility = AgentTabChat.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
AgentPanelCoworkCode.Visibility = AgentTabCoworkCode.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
AgentPanelCowork.Visibility = AgentTabCowork.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
AgentPanelCode.Visibility = AgentTabCode.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (AgentPanelDev != null)
|
||||
AgentPanelDev.Visibility = AgentTabDev.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (AgentPanelEtc != null)
|
||||
AgentPanelEtc.Visibility = AgentTabEtc.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (AgentPanelTools != null)
|
||||
{
|
||||
var show = AgentTabTools.IsChecked == true;
|
||||
AgentPanelTools.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (show) LoadToolCards();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 도구 관리 카드 UI ──────────────────────────────────────────────
|
||||
|
||||
private bool _toolCardsLoaded;
|
||||
private HashSet<string> _disabledTools = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>도구 카드 UI를 카테고리별로 생성합니다.</summary>
|
||||
private void LoadToolCards()
|
||||
{
|
||||
if (_toolCardsLoaded || ToolCardsPanel == null) return;
|
||||
_toolCardsLoaded = true;
|
||||
|
||||
var app = CurrentApp;
|
||||
var settings = app?.SettingsService?.Settings.Llm;
|
||||
using var tools = Services.Agent.ToolRegistry.CreateDefault();
|
||||
_disabledTools = new HashSet<string>(settings?.DisabledTools ?? new(), StringComparer.OrdinalIgnoreCase);
|
||||
var disabled = _disabledTools;
|
||||
|
||||
// 카테고리 매핑
|
||||
var categories = new Dictionary<string, List<Services.Agent.IAgentTool>>
|
||||
{
|
||||
["파일/검색"] = new(),
|
||||
["문서 생성"] = new(),
|
||||
["문서 품질"] = new(),
|
||||
["코드/개발"] = new(),
|
||||
["데이터/유틸"] = new(),
|
||||
["시스템"] = new(),
|
||||
};
|
||||
|
||||
var toolCategoryMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// 파일/검색
|
||||
["file_read"] = "파일/검색", ["file_write"] = "파일/검색", ["file_edit"] = "파일/검색",
|
||||
["glob"] = "파일/검색", ["grep"] = "파일/검색", ["folder_map"] = "파일/검색",
|
||||
["document_read"] = "파일/검색", ["file_watch"] = "파일/검색",
|
||||
// 문서 생성
|
||||
["excel_skill"] = "문서 생성", ["docx_skill"] = "문서 생성", ["csv_skill"] = "문서 생성",
|
||||
["markdown_skill"] = "문서 생성", ["html_skill"] = "문서 생성", ["chart_skill"] = "문서 생성",
|
||||
["batch_skill"] = "문서 생성", ["pptx_skill"] = "문서 생성",
|
||||
["document_planner"] = "문서 생성", ["document_assembler"] = "문서 생성",
|
||||
// 문서 품질
|
||||
["document_review"] = "문서 품질", ["format_convert"] = "문서 품질",
|
||||
["template_render"] = "문서 품질", ["text_summarize"] = "문서 품질",
|
||||
// 코드/개발
|
||||
["dev_env_detect"] = "코드/개발", ["build_run"] = "코드/개발", ["git_tool"] = "코드/개발",
|
||||
["lsp"] = "코드/개발", ["sub_agent"] = "코드/개발", ["wait_agents"] = "코드/개발",
|
||||
["code_search"] = "코드/개발", ["test_loop"] = "코드/개발",
|
||||
["code_review"] = "코드/개발", ["project_rule"] = "코드/개발",
|
||||
// 시스템
|
||||
["process"] = "시스템", ["skill_manager"] = "시스템", ["memory"] = "시스템",
|
||||
["clipboard"] = "시스템", ["notify"] = "시스템", ["env"] = "시스템",
|
||||
["image_analyze"] = "시스템",
|
||||
};
|
||||
|
||||
foreach (var tool in tools.All)
|
||||
{
|
||||
var cat = toolCategoryMap.TryGetValue(tool.Name, out var c) ? c : "데이터/유틸";
|
||||
if (categories.ContainsKey(cat))
|
||||
categories[cat].Add(tool);
|
||||
else
|
||||
categories["데이터/유틸"].Add(tool);
|
||||
}
|
||||
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0xF5, 0xF5, 0xF8));
|
||||
|
||||
foreach (var kv in categories)
|
||||
{
|
||||
if (kv.Value.Count == 0) continue;
|
||||
|
||||
// 카테고리 헤더
|
||||
var header = new TextBlock
|
||||
{
|
||||
Text = $"{kv.Key} ({kv.Value.Count})",
|
||||
FontSize = 13, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
Margin = new Thickness(0, 8, 0, 6),
|
||||
};
|
||||
ToolCardsPanel.Children.Add(header);
|
||||
|
||||
// 카드 WrapPanel
|
||||
var wrap = new WrapPanel { Margin = new Thickness(0, 0, 0, 4) };
|
||||
foreach (var tool in kv.Value.OrderBy(t => t.Name))
|
||||
{
|
||||
var isEnabled = !disabled.Contains(tool.Name);
|
||||
var card = CreateToolCard(tool, isEnabled, disabled, accentBrush, secondaryText, itemBg);
|
||||
wrap.Children.Add(card);
|
||||
}
|
||||
ToolCardsPanel.Children.Add(wrap);
|
||||
}
|
||||
|
||||
// MCP 서버 상태
|
||||
LoadMcpStatus();
|
||||
}
|
||||
|
||||
/// <summary>개별 도구 카드를 생성합니다 (이름 + 설명 + 토글).</summary>
|
||||
private Border CreateToolCard(Services.Agent.IAgentTool tool, bool isEnabled,
|
||||
HashSet<string> disabled, Brush accentBrush, Brush secondaryText, Brush itemBg)
|
||||
{
|
||||
var card = new Border
|
||||
{
|
||||
Background = itemBg,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(10, 8, 10, 8),
|
||||
Margin = new Thickness(0, 0, 8, 8),
|
||||
Width = 240,
|
||||
BorderBrush = isEnabled ? Brushes.Transparent : new SolidColorBrush(Color.FromArgb(0x30, 0xDC, 0x26, 0x26)),
|
||||
BorderThickness = new Thickness(1),
|
||||
};
|
||||
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
// 이름 + 설명
|
||||
var textStack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
||||
var nameBlock = new TextBlock
|
||||
{
|
||||
Text = tool.Name,
|
||||
FontSize = 12, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
};
|
||||
textStack.Children.Add(nameBlock);
|
||||
|
||||
var desc = tool.Description;
|
||||
if (desc.Length > 50) desc = desc[..50] + "…";
|
||||
var descBlock = new TextBlock
|
||||
{
|
||||
Text = desc,
|
||||
FontSize = 10.5,
|
||||
Foreground = secondaryText,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
};
|
||||
textStack.Children.Add(descBlock);
|
||||
Grid.SetColumn(textStack, 0);
|
||||
grid.Children.Add(textStack);
|
||||
|
||||
// 토글 (CheckBox + ToggleSwitch 스타일)
|
||||
var toggle = new CheckBox
|
||||
{
|
||||
IsChecked = isEnabled,
|
||||
Style = TryFindResource("ToggleSwitch") as Style,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
};
|
||||
toggle.Checked += (_, _) =>
|
||||
{
|
||||
disabled.Remove(tool.Name);
|
||||
card.BorderBrush = Brushes.Transparent;
|
||||
};
|
||||
toggle.Unchecked += (_, _) =>
|
||||
{
|
||||
disabled.Add(tool.Name);
|
||||
card.BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0xDC, 0x26, 0x26));
|
||||
};
|
||||
Grid.SetColumn(toggle, 1);
|
||||
grid.Children.Add(toggle);
|
||||
|
||||
card.Child = grid;
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>MCP 서버 연결 상태 표시를 생성합니다.</summary>
|
||||
private void LoadMcpStatus()
|
||||
{
|
||||
if (McpStatusPanel == null) return;
|
||||
McpStatusPanel.Children.Clear();
|
||||
|
||||
var app = CurrentApp;
|
||||
var settings = app?.SettingsService?.Settings.Llm;
|
||||
var mcpServers = settings?.McpServers;
|
||||
|
||||
if (mcpServers == null || mcpServers.Count == 0)
|
||||
{
|
||||
McpStatusPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "등록된 MCP 서버가 없습니다.",
|
||||
FontSize = 12,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
Margin = new Thickness(0, 4, 0, 0),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0xF5, 0xF5, 0xF8));
|
||||
|
||||
foreach (var server in mcpServers)
|
||||
{
|
||||
var row = new Border
|
||||
{
|
||||
Background = itemBg,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(12, 8, 12, 8),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
};
|
||||
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
// 상태 아이콘
|
||||
var statusDot = new Border
|
||||
{
|
||||
Width = 8, Height = 8,
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Background = new SolidColorBrush(Color.FromRgb(0x34, 0xA8, 0x53)), // 녹색 (등록됨)
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
};
|
||||
Grid.SetColumn(statusDot, 0);
|
||||
grid.Children.Add(statusDot);
|
||||
|
||||
// 서버 이름 + 명령
|
||||
var infoStack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
||||
infoStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = server.Name ?? "(이름 없음)",
|
||||
FontSize = 12, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
});
|
||||
infoStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = server.Command ?? "",
|
||||
FontSize = 10.5,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
});
|
||||
Grid.SetColumn(infoStack, 1);
|
||||
grid.Children.Add(infoStack);
|
||||
|
||||
// 상태 텍스트
|
||||
var statusText = new TextBlock
|
||||
{
|
||||
Text = "등록됨",
|
||||
FontSize = 11,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x34, 0xA8, 0x53)),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(statusText, 2);
|
||||
grid.Children.Add(statusText);
|
||||
|
||||
row.Child = grid;
|
||||
McpStatusPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
802
src/AxCopilot/Views/SettingsWindow.UI.cs
Normal file
802
src/AxCopilot/Views/SettingsWindow.UI.cs
Normal file
@@ -0,0 +1,802 @@
|
||||
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
|
||||
{
|
||||
// ─── 접기/열기 가능 섹션 헬퍼 ──────────────────────────────────────────
|
||||
private Border CreateCollapsibleSection(string title, UIElement content, bool expanded = true)
|
||||
{
|
||||
var headerColor = ThemeResourceHelper.HexBrush("#AAAACC");
|
||||
var section = new StackPanel();
|
||||
var contentPanel = new StackPanel { Visibility = expanded ? Visibility.Visible : Visibility.Collapsed };
|
||||
if (content is Panel panel)
|
||||
{
|
||||
// 패널의 자식들을 contentPanel로 옮김 (StackPanel 등)
|
||||
contentPanel.Children.Add(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
contentPanel.Children.Add(content);
|
||||
}
|
||||
|
||||
var arrow = new TextBlock
|
||||
{
|
||||
Text = "\uE70D",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 9,
|
||||
Foreground = headerColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
RenderTransformOrigin = new Point(0.5, 0.5),
|
||||
RenderTransform = new RotateTransform(expanded ? 90 : 0),
|
||||
};
|
||||
var titleBlock = new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 10.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = headerColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
var headerPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
headerPanel.Children.Add(arrow);
|
||||
headerPanel.Children.Add(titleBlock);
|
||||
|
||||
var header = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
Cursor = Cursors.Hand,
|
||||
Padding = new Thickness(2, 10, 2, 6),
|
||||
Child = headerPanel,
|
||||
};
|
||||
header.MouseLeftButtonUp += (s, e) =>
|
||||
{
|
||||
bool isNowVisible = contentPanel.Visibility == Visibility.Visible;
|
||||
contentPanel.Visibility = isNowVisible ? Visibility.Collapsed : Visibility.Visible;
|
||||
arrow.RenderTransform = new RotateTransform(isNowVisible ? 0 : 90);
|
||||
};
|
||||
header.MouseEnter += (s, _) => titleBlock.Foreground = ThemeResourceHelper.HexBrush("#CCCCEE");
|
||||
header.MouseLeave += (s, _) => titleBlock.Foreground = headerColor;
|
||||
|
||||
section.Children.Add(header);
|
||||
section.Children.Add(contentPanel);
|
||||
|
||||
return new Border
|
||||
{
|
||||
Child = section,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
};
|
||||
}
|
||||
|
||||
private Border CreateCollapsibleSection(string title, IEnumerable<UIElement> children, bool expanded = true)
|
||||
{
|
||||
var contentPanel = new StackPanel();
|
||||
foreach (var child in children)
|
||||
contentPanel.Children.Add(child);
|
||||
return CreateCollapsibleSection(title, contentPanel, expanded);
|
||||
}
|
||||
|
||||
// ─── 선택 텍스트 AI 명령 설정 ────────────────────────────────────────────────
|
||||
|
||||
private void BuildTextActionCommandsPanel()
|
||||
{
|
||||
if (TextActionCommandsPanel == null) return;
|
||||
TextActionCommandsPanel.Children.Clear();
|
||||
|
||||
var svc = CurrentApp?.SettingsService;
|
||||
if (svc == null) return;
|
||||
var enabled = svc.Settings.Launcher.TextActionCommands;
|
||||
var toggleStyle = TryFindResource("ToggleSwitch") as Style;
|
||||
|
||||
foreach (var (key, label) in TextActionPopup.AvailableCommands)
|
||||
{
|
||||
var row = new Grid { Margin = new Thickness(0, 3, 0, 3) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var labelBlock = new TextBlock
|
||||
{
|
||||
Text = label, FontSize = 12.5,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black,
|
||||
};
|
||||
Grid.SetColumn(labelBlock, 0);
|
||||
row.Children.Add(labelBlock);
|
||||
|
||||
var capturedKey = key;
|
||||
var cb = new CheckBox
|
||||
{
|
||||
IsChecked = enabled.Contains(key, StringComparer.OrdinalIgnoreCase),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
if (toggleStyle != null) cb.Style = toggleStyle;
|
||||
cb.Checked += (_, _) =>
|
||||
{
|
||||
if (!enabled.Contains(capturedKey)) enabled.Add(capturedKey);
|
||||
svc.Save();
|
||||
};
|
||||
cb.Unchecked += (_, _) =>
|
||||
{
|
||||
// 최소 1개 유지
|
||||
if (enabled.Count <= 1) { cb.IsChecked = true; return; }
|
||||
enabled.RemoveAll(x => x == capturedKey);
|
||||
svc.Save();
|
||||
};
|
||||
Grid.SetColumn(cb, 1);
|
||||
row.Children.Add(cb);
|
||||
|
||||
TextActionCommandsPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 테마 하위 탭 전환 ──────────────────────────────────────────────────────
|
||||
|
||||
private void ThemeSubTab_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ThemeSelectPanel == null || ThemeColorsPanel == null) return;
|
||||
if (ThemeSubTabSelect?.IsChecked == true)
|
||||
{
|
||||
ThemeSelectPanel.Visibility = Visibility.Visible;
|
||||
ThemeColorsPanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
ThemeSelectPanel.Visibility = Visibility.Collapsed;
|
||||
ThemeColorsPanel.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 일반 하위 탭 전환 (일반 + 알림) ──────────────────────────────────────
|
||||
|
||||
private bool _notifyMoved;
|
||||
|
||||
private void GeneralSubTab_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (GeneralMainPanel == null || GeneralNotifyPanel == null) return;
|
||||
|
||||
// 알림 탭 내용을 최초 1회 NotifyContent로 이동
|
||||
if (!_notifyMoved && NotifyContent != null)
|
||||
{
|
||||
MoveNotifyTabContent();
|
||||
_notifyMoved = true;
|
||||
}
|
||||
|
||||
GeneralMainPanel.Visibility = GeneralSubTabMain?.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
GeneralNotifyPanel.Visibility = GeneralSubTabNotify?.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (GeneralStoragePanel != null)
|
||||
{
|
||||
GeneralStoragePanel.Visibility = GeneralSubTabStorage?.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (GeneralSubTabStorage?.IsChecked == true) RefreshStorageInfo2();
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveNotifyTabContent()
|
||||
{
|
||||
// XAML의 알림 TabItem에서 ScrollViewer → StackPanel 내용을 NotifyContent로 이동
|
||||
// 사이드 네비의 "알림" 탭을 찾아서 숨기기
|
||||
var mainTab = Content as System.Windows.Controls.Grid;
|
||||
var tabControl = mainTab?.Children.OfType<TabControl>().FirstOrDefault()
|
||||
?? FindVisualChild<TabControl>(this);
|
||||
if (tabControl == null) return;
|
||||
|
||||
TabItem? notifyTab = null;
|
||||
foreach (TabItem tab in tabControl.Items)
|
||||
{
|
||||
if (tab.Header?.ToString() == "알림") { notifyTab = tab; break; }
|
||||
}
|
||||
|
||||
if (notifyTab?.Content is ScrollViewer sv && sv.Content is StackPanel sp)
|
||||
{
|
||||
// 내용물을 NotifyContent로 복제 이동
|
||||
var children = sp.Children.Cast<UIElement>().ToList();
|
||||
sp.Children.Clear();
|
||||
foreach (var child in children)
|
||||
NotifyContent.Children.Add(child);
|
||||
}
|
||||
|
||||
// 알림 탭 숨기기
|
||||
if (notifyTab != null) notifyTab.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private static T? FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
|
||||
{
|
||||
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is T t) return t;
|
||||
var result = FindVisualChild<T>(child);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── 독 바 설정 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildDockBarSettings()
|
||||
{
|
||||
var svc = CurrentApp?.SettingsService;
|
||||
if (svc == null) return;
|
||||
var launcher = svc.Settings.Launcher;
|
||||
|
||||
// 토글 바인딩
|
||||
ChkDockAutoShow.IsChecked = launcher.DockBarAutoShow;
|
||||
ChkDockAutoShow.Checked += (_, _) =>
|
||||
{
|
||||
launcher.DockBarAutoShow = true; svc.Save();
|
||||
CurrentApp?.ToggleDockBar();
|
||||
};
|
||||
ChkDockAutoShow.Unchecked += (_, _) =>
|
||||
{
|
||||
launcher.DockBarAutoShow = false; svc.Save();
|
||||
// 끄기 시에는 설정만 저장 — 독 바를 토글하지 않음 (이미 표시 중이면 유지, 다음 재시작 시 안 뜸)
|
||||
};
|
||||
|
||||
ChkDockRainbowGlow.IsChecked = launcher.DockBarRainbowGlow;
|
||||
ChkDockRainbowGlow.Checked += (_, _) => { launcher.DockBarRainbowGlow = true; svc.Save(); RefreshDock(); };
|
||||
ChkDockRainbowGlow.Unchecked += (_, _) => { launcher.DockBarRainbowGlow = false; svc.Save(); RefreshDock(); };
|
||||
|
||||
SliderDockOpacity.Value = launcher.DockBarOpacity;
|
||||
SliderDockOpacity.ValueChanged += (_, e) => { launcher.DockBarOpacity = e.NewValue; svc.Save(); RefreshDock(); };
|
||||
|
||||
// 표시 항목 토글 리스트
|
||||
if (DockItemsPanel == null) return;
|
||||
DockItemsPanel.Children.Clear();
|
||||
var toggleStyle = TryFindResource("ToggleSwitch") as System.Windows.Style;
|
||||
var enabled = launcher.DockBarItems;
|
||||
|
||||
foreach (var (key, icon, tooltip) in DockBarWindow.AvailableItems)
|
||||
{
|
||||
var row = new System.Windows.Controls.Grid { Margin = new Thickness(0, 3, 0, 3) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = $"{tooltip}",
|
||||
FontSize = 12.5,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = TryFindResource("PrimaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Black,
|
||||
};
|
||||
System.Windows.Controls.Grid.SetColumn(label, 0);
|
||||
row.Children.Add(label);
|
||||
|
||||
var capturedKey = key;
|
||||
var cb = new CheckBox
|
||||
{
|
||||
IsChecked = enabled.Contains(key),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
if (toggleStyle != null) cb.Style = toggleStyle;
|
||||
cb.Checked += (_, _) => { if (!enabled.Contains(capturedKey)) enabled.Add(capturedKey); svc.Save(); RefreshDock(); };
|
||||
cb.Unchecked += (_, _) => { enabled.RemoveAll(x => x == capturedKey); svc.Save(); RefreshDock(); };
|
||||
System.Windows.Controls.Grid.SetColumn(cb, 1);
|
||||
row.Children.Add(cb);
|
||||
|
||||
DockItemsPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private static SolidColorBrush BrushFromHex(string hex)
|
||||
{
|
||||
try { return ThemeResourceHelper.HexBrush(hex); }
|
||||
catch (Exception) { return new SolidColorBrush(Colors.Gray); }
|
||||
}
|
||||
|
||||
private static void RefreshDock()
|
||||
{
|
||||
CurrentApp?.RefreshDockBar();
|
||||
}
|
||||
|
||||
private void BtnDockResetPosition_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var result = CustomMessageBox.Show(
|
||||
"독 바를 화면 하단 중앙으로 이동하시겠습니까?",
|
||||
"독 바 위치 초기화",
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
|
||||
var svc = CurrentApp?.SettingsService;
|
||||
if (svc == null) return;
|
||||
svc.Settings.Launcher.DockBarLeft = -1;
|
||||
svc.Settings.Launcher.DockBarTop = -1;
|
||||
svc.Save();
|
||||
|
||||
// 즉시 독 바 위치 이동
|
||||
CurrentApp?.RefreshDockBar();
|
||||
}
|
||||
|
||||
// ─── 저장 공간 관리 ──────────────────────────────────────────────────────
|
||||
|
||||
private void RefreshStorageInfo()
|
||||
{
|
||||
if (StorageSummaryText == null) return;
|
||||
var report = StorageAnalyzer.Analyze();
|
||||
|
||||
StorageSummaryText.Text = $"앱 전체 사용량: {StorageAnalyzer.FormatSize(report.TotalAppUsage)}";
|
||||
StorageDriveText.Text = $"드라이브 {report.DriveLabel} 여유: {StorageAnalyzer.FormatSize(report.DriveFreeSpace)} / {StorageAnalyzer.FormatSize(report.DriveTotalSpace)}";
|
||||
|
||||
if (StorageDetailPanel == null) return;
|
||||
StorageDetailPanel.Children.Clear();
|
||||
|
||||
var items = new (string Label, long Size)[]
|
||||
{
|
||||
("대화 기록", report.Conversations),
|
||||
("감사 로그", report.AuditLogs),
|
||||
("앱 로그", report.Logs),
|
||||
("코드 인덱스", report.CodeIndex),
|
||||
("임베딩 DB", report.EmbeddingDb),
|
||||
("클립보드 히스토리", report.ClipboardHistory),
|
||||
("플러그인", report.Plugins),
|
||||
("JSON 스킬", report.Skills),
|
||||
};
|
||||
|
||||
foreach (var (label, size) in items)
|
||||
{
|
||||
if (size == 0) continue;
|
||||
var row = new Grid { Margin = new Thickness(0, 2, 0, 2) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var labelTb = new TextBlock { Text = label, FontSize = 12, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, VerticalAlignment = VerticalAlignment.Center };
|
||||
Grid.SetColumn(labelTb, 0);
|
||||
row.Children.Add(labelTb);
|
||||
|
||||
var sizeTb = new TextBlock { Text = StorageAnalyzer.FormatSize(size), FontSize = 12, FontFamily = new FontFamily("Consolas"), Foreground = Brushes.Gray, VerticalAlignment = VerticalAlignment.Center };
|
||||
Grid.SetColumn(sizeTb, 1);
|
||||
row.Children.Add(sizeTb);
|
||||
|
||||
StorageDetailPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnStorageRefresh_Click(object sender, RoutedEventArgs e) => RefreshStorageInfo();
|
||||
|
||||
private void RefreshStorageInfo2()
|
||||
{
|
||||
if (StorageSummaryText2 == null) return;
|
||||
var report = StorageAnalyzer.Analyze();
|
||||
StorageSummaryText2.Text = $"앱 전체 사용량: {StorageAnalyzer.FormatSize(report.TotalAppUsage)}";
|
||||
StorageDriveText2.Text = $"드라이브 {report.DriveLabel} 여유: {StorageAnalyzer.FormatSize(report.DriveFreeSpace)} / {StorageAnalyzer.FormatSize(report.DriveTotalSpace)}";
|
||||
|
||||
if (StorageDetailPanel2 == null) return;
|
||||
StorageDetailPanel2.Children.Clear();
|
||||
var items = new (string Label, long Size)[]
|
||||
{
|
||||
("대화 기록", report.Conversations), ("감사 로그", report.AuditLogs),
|
||||
("앱 로그", report.Logs), ("코드 인덱스", report.CodeIndex),
|
||||
("임베딩 DB", report.EmbeddingDb),
|
||||
("클립보드 히스토리", report.ClipboardHistory),
|
||||
("플러그인", report.Plugins), ("JSON 스킬", report.Skills),
|
||||
};
|
||||
foreach (var (label, size) in items)
|
||||
{
|
||||
if (size == 0) continue;
|
||||
var row = new Grid { Margin = new Thickness(0, 2, 0, 2) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
var labelTb = new TextBlock { Text = label, FontSize = 12, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black };
|
||||
Grid.SetColumn(labelTb, 0); row.Children.Add(labelTb);
|
||||
var sizeTb = new TextBlock { Text = StorageAnalyzer.FormatSize(size), FontSize = 12, FontFamily = new FontFamily("Consolas"), Foreground = Brushes.Gray };
|
||||
Grid.SetColumn(sizeTb, 1); row.Children.Add(sizeTb);
|
||||
StorageDetailPanel2.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnStorageRefresh2_Click(object sender, RoutedEventArgs e) => RefreshStorageInfo2();
|
||||
|
||||
private void BtnStorageCleanup_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 테마 리소스 조회
|
||||
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
|
||||
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF));
|
||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x22, 0xFF, 0xFF, 0xFF));
|
||||
var shadowColor = TryFindResource("ShadowColor") is Color sc ? sc : Colors.Black;
|
||||
|
||||
// 보관 기간 선택 팝업 — 커스텀 버튼으로 날짜 선택
|
||||
var popup = new Window
|
||||
{
|
||||
WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent,
|
||||
Width = 360, SizeToContent = SizeToContent.Height,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
Owner = this, ShowInTaskbar = false, Topmost = true,
|
||||
};
|
||||
|
||||
int selectedDays = -1;
|
||||
|
||||
var outerBorder = new Border
|
||||
{
|
||||
Background = bgBrush, CornerRadius = new CornerRadius(14), BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1), Margin = new Thickness(16),
|
||||
Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 20, ShadowDepth = 4, Opacity = 0.3, Color = shadowColor },
|
||||
};
|
||||
|
||||
var stack = new StackPanel { Margin = new Thickness(24, 20, 24, 20) };
|
||||
stack.Children.Add(new TextBlock { Text = "보관 기간 선택", FontSize = 15, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 6) });
|
||||
stack.Children.Add(new TextBlock { Text = "선택한 기간 이전의 데이터를 삭제합니다.\n※ 통계/대화 기록은 삭제되지 않습니다.", FontSize = 12, Foreground = subFgBrush, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 16) });
|
||||
|
||||
var btnDays = new (int Days, string Label)[] { (7, "최근 7일만 보관"), (14, "최근 14일만 보관"), (30, "최근 30일만 보관"), (0, "전체 삭제") };
|
||||
foreach (var (days, label) in btnDays)
|
||||
{
|
||||
var d = days;
|
||||
var isDelete = d == 0;
|
||||
var deleteBg = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0x44, 0x44));
|
||||
var deleteBorder = new SolidColorBrush(Color.FromArgb(0x40, 0xFF, 0x44, 0x44));
|
||||
var deleteText = new SolidColorBrush(Color.FromRgb(0xFF, 0x66, 0x66));
|
||||
var btn = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(10), Cursor = Cursors.Hand,
|
||||
Padding = new Thickness(14, 10, 14, 10), Margin = new Thickness(0, 0, 0, 6),
|
||||
Background = isDelete ? deleteBg : itemBg,
|
||||
BorderBrush = isDelete ? deleteBorder : borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
};
|
||||
btn.Child = new TextBlock { Text = label, FontSize = 13, Foreground = isDelete ? deleteText : fgBrush };
|
||||
var normalBg = isDelete ? deleteBg : itemBg;
|
||||
btn.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||||
btn.MouseLeave += (s, _) => { if (s is Border b) b.Background = normalBg; };
|
||||
btn.MouseLeftButtonUp += (_, _) => { selectedDays = d; popup.Close(); };
|
||||
stack.Children.Add(btn);
|
||||
}
|
||||
|
||||
// 취소
|
||||
var cancelBtn = new Border { CornerRadius = new CornerRadius(10), Cursor = Cursors.Hand, Padding = new Thickness(14, 8, 14, 8), Margin = new Thickness(0, 6, 0, 0), Background = Brushes.Transparent };
|
||||
cancelBtn.Child = new TextBlock { Text = "취소", FontSize = 12, Foreground = subFgBrush, HorizontalAlignment = HorizontalAlignment.Center };
|
||||
cancelBtn.MouseLeftButtonUp += (_, _) => popup.Close();
|
||||
stack.Children.Add(cancelBtn);
|
||||
|
||||
outerBorder.Child = stack;
|
||||
popup.Content = outerBorder;
|
||||
popup.ShowDialog();
|
||||
|
||||
if (selectedDays < 0) return;
|
||||
|
||||
// 삭제 전 확인
|
||||
var confirmMsg = selectedDays == 0
|
||||
? "전체 데이터를 삭제합니다. 정말 진행하시겠습니까?"
|
||||
: $"최근 {selectedDays}일 이전의 데이터를 삭제합니다. 정말 진행하시겠습니까?";
|
||||
var confirm = CustomMessageBox.Show(confirmMsg, "삭제 확인", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||
if (confirm != MessageBoxResult.Yes) return;
|
||||
|
||||
var freed = StorageAnalyzer.Cleanup(
|
||||
retainDays: selectedDays,
|
||||
cleanConversations: false,
|
||||
cleanAuditLogs: true,
|
||||
cleanLogs: true,
|
||||
cleanCodeIndex: true,
|
||||
cleanClipboard: selectedDays == 0
|
||||
);
|
||||
|
||||
CustomMessageBox.Show(
|
||||
$"{StorageAnalyzer.FormatSize(freed)}를 확보했습니다.",
|
||||
"정리 완료", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
|
||||
RefreshStorageInfo();
|
||||
}
|
||||
|
||||
// ─── 알림 카테고리 체크박스 ───────────────────────────────────────────────
|
||||
|
||||
private void BuildQuoteCategoryCheckboxes()
|
||||
{
|
||||
if (QuoteCategoryPanel == null) return;
|
||||
QuoteCategoryPanel.Children.Clear();
|
||||
|
||||
var enabled = _vm.GetReminderCategories();
|
||||
var toggleStyle = TryFindResource("ToggleSwitch") as Style;
|
||||
|
||||
foreach (var (key, label, countFunc) in Services.QuoteService.Categories)
|
||||
{
|
||||
var count = countFunc();
|
||||
var displayLabel = key == "today_events"
|
||||
? $"{label} ({count}개, 오늘 {Services.QuoteService.GetTodayMatchCount()}개)"
|
||||
: $"{label} ({count}개)";
|
||||
|
||||
// 좌: 라벨, 우: 토글 스위치
|
||||
var row = new Grid { Margin = new Thickness(0, 3, 0, 3) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var labelBlock = new TextBlock
|
||||
{
|
||||
Text = displayLabel,
|
||||
FontSize = 12.5,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = TryFindResource("PrimaryText") as System.Windows.Media.Brush
|
||||
?? System.Windows.Media.Brushes.White,
|
||||
};
|
||||
Grid.SetColumn(labelBlock, 0);
|
||||
row.Children.Add(labelBlock);
|
||||
|
||||
var cb = new CheckBox
|
||||
{
|
||||
IsChecked = enabled.Contains(key, StringComparer.OrdinalIgnoreCase),
|
||||
Tag = key,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
if (toggleStyle != null) cb.Style = toggleStyle;
|
||||
cb.Checked += (s, _) =>
|
||||
{
|
||||
if (s is CheckBox c && c.Tag is string k && !enabled.Contains(k))
|
||||
enabled.Add(k);
|
||||
};
|
||||
cb.Unchecked += (s, _) =>
|
||||
{
|
||||
if (s is CheckBox c && c.Tag is string k)
|
||||
enabled.RemoveAll(x => x.Equals(k, StringComparison.OrdinalIgnoreCase));
|
||||
};
|
||||
Grid.SetColumn(cb, 1);
|
||||
row.Children.Add(cb);
|
||||
|
||||
QuoteCategoryPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 버전 표시 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 하단 버전 텍스트를 AxCopilot.csproj <Version> 값에서 동적으로 읽어 설정합니다.
|
||||
/// 버전을 올릴 때는 AxCopilot.csproj → <Version> 하나만 수정하면 됩니다.
|
||||
/// 이 함수와 SettingsWindow.xaml 의 VersionInfoText 는 항상 함께 유지됩니다.
|
||||
/// </summary>
|
||||
private void SetVersionText()
|
||||
{
|
||||
try
|
||||
{
|
||||
var asm = System.Reflection.Assembly.GetExecutingAssembly();
|
||||
// FileVersionInfo 에서 읽어야 csproj <Version> 이 반영됩니다.
|
||||
var fvi = System.Diagnostics.FileVersionInfo.GetVersionInfo(asm.Location);
|
||||
var ver = fvi.ProductVersion ?? fvi.FileVersion ?? "?";
|
||||
// 빌드 메타데이터 제거 (예: "1.0.3+gitabcdef" → "1.0.3")
|
||||
var plusIdx = ver.IndexOf('+');
|
||||
if (plusIdx > 0) ver = ver[..plusIdx];
|
||||
VersionInfoText.Text = $"AX Copilot · v{ver}";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
VersionInfoText.Text = "AX Copilot";
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 핫키 (콤보박스 선택 방식) ──────────────────────────────────────────
|
||||
|
||||
/// <summary>이전 녹화기에서 호출되던 초기화 — 콤보박스 전환 후 무연산 (호환용)</summary>
|
||||
private void RefreshHotkeyBadges() { /* 콤보박스 SelectedValue 바인딩으로 대체 */ }
|
||||
|
||||
/// <summary>현재 핫키가 콤보박스 목록에 없으면 항목으로 추가합니다.</summary>
|
||||
private void EnsureHotkeyInCombo()
|
||||
{
|
||||
if (HotkeyCombo == null) return;
|
||||
var hotkey = _vm.Hotkey;
|
||||
if (string.IsNullOrWhiteSpace(hotkey)) return;
|
||||
|
||||
// 이미 목록에 있는지 확인
|
||||
foreach (System.Windows.Controls.ComboBoxItem item in HotkeyCombo.Items)
|
||||
{
|
||||
if (item.Tag is string tag && tag == hotkey) return;
|
||||
}
|
||||
|
||||
// 목록에 없으면 현재 값을 추가
|
||||
var display = hotkey.Replace("+", " + ");
|
||||
var newItem = new System.Windows.Controls.ComboBoxItem
|
||||
{
|
||||
Content = $"{display} (사용자 정의)",
|
||||
Tag = hotkey
|
||||
};
|
||||
HotkeyCombo.Items.Insert(0, newItem);
|
||||
HotkeyCombo.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>Window-level PreviewKeyDown — 핫키 녹화 제거 후 잔여 호출 보호</summary>
|
||||
private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { }
|
||||
|
||||
/// <summary>WPF Key → HotkeyParser가 인식하는 문자열 이름.</summary>
|
||||
private static string GetKeyName(Key key) => key switch
|
||||
{
|
||||
Key.Space => "Space",
|
||||
Key.Enter or Key.Return => "Enter",
|
||||
Key.Tab => "Tab",
|
||||
Key.Back => "Backspace",
|
||||
Key.Delete => "Delete",
|
||||
Key.Escape => "Escape",
|
||||
Key.Home => "Home",
|
||||
Key.End => "End",
|
||||
Key.PageUp => "PageUp",
|
||||
Key.PageDown => "PageDown",
|
||||
Key.Left => "Left",
|
||||
Key.Right => "Right",
|
||||
Key.Up => "Up",
|
||||
Key.Down => "Down",
|
||||
Key.Insert => "Insert",
|
||||
// A–Z
|
||||
>= Key.A and <= Key.Z => key.ToString(),
|
||||
// 0–9 (메인 키보드)
|
||||
>= Key.D0 and <= Key.D9 => ((int)(key - Key.D0)).ToString(),
|
||||
// F1–F12
|
||||
>= Key.F1 and <= Key.F12 => key.ToString(),
|
||||
// 기호
|
||||
Key.OemTilde => "`",
|
||||
Key.OemMinus => "-",
|
||||
Key.OemPlus => "=",
|
||||
Key.OemOpenBrackets => "[",
|
||||
Key.OemCloseBrackets => "]",
|
||||
Key.OemPipe or Key.OemBackslash => "\\",
|
||||
Key.OemSemicolon => ";",
|
||||
Key.OemQuotes => "'",
|
||||
Key.OemComma => ",",
|
||||
Key.OemPeriod => ".",
|
||||
Key.OemQuestion => "/",
|
||||
_ => key.ToString()
|
||||
};
|
||||
|
||||
private void HotkeyCombo_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
|
||||
{
|
||||
// 콤보박스 선택이 바뀌면 ViewModel의 Hotkey를 업데이트
|
||||
// (바인딩이 SelectedValue에 연결되어 자동 처리되지만,
|
||||
// 기존 RefreshHotkeyBadges 호출은 콤보박스 도입으로 불필요)
|
||||
}
|
||||
|
||||
// ─── 기존 이벤트 핸들러 ──────────────────────────────────────────────────
|
||||
|
||||
private async void BtnTestConnection_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var btn = sender as Button;
|
||||
if (btn != null) btn.Content = "테스트 중...";
|
||||
try
|
||||
{
|
||||
// 현재 UI 값으로 임시 LLM 서비스 생성하여 테스트 (설정 저장/창 닫기 없이)
|
||||
var llm = new Services.LlmService(_vm.Service);
|
||||
var (ok, msg) = await llm.TestConnectionAsync();
|
||||
llm.Dispose();
|
||||
CustomMessageBox.Show(msg, ok ? "연결 성공" : "연결 실패",
|
||||
MessageBoxButton.OK,
|
||||
ok ? MessageBoxImage.Information : MessageBoxImage.Warning);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CustomMessageBox.Show(ex.Message, "오류", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (btn != null) btn.Content = "테스트";
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 기능 설정 서브탭 전환 ──────────────────────────────────────────
|
||||
private void FuncSubTab_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (FuncPanel_AI == null) return; // 초기화 전 방어
|
||||
|
||||
FuncPanel_AI.Visibility = FuncSubTab_AI.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
FuncPanel_Launcher.Visibility = FuncSubTab_Launcher.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
FuncPanel_Design.Visibility = FuncSubTab_Design.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// ─── 접기/열기 섹션 토글 ───────────────────────────────────────────
|
||||
private void CollapsibleSection_Toggle(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Border border && border.Tag is System.Windows.Controls.Expander expander)
|
||||
expander.IsExpanded = !expander.IsExpanded;
|
||||
}
|
||||
|
||||
private void ServiceSubTab_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SvcPanelOllama == null) return; // 초기화 전 방어
|
||||
SvcPanelOllama.Visibility = SvcTabOllama.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
SvcPanelVllm.Visibility = SvcTabVllm.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
SvcPanelGemini.Visibility = SvcTabGemini.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
SvcPanelClaude.Visibility = SvcTabClaude.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void ThemeCard_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button btn && btn.Tag is string key)
|
||||
_vm.SelectTheme(key);
|
||||
}
|
||||
|
||||
private void ColorSwatch_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button btn && btn.Tag is ColorRowModel row)
|
||||
_vm.PickColor(row);
|
||||
}
|
||||
|
||||
private void DevModeCheckBox_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not CheckBox cb || !cb.IsChecked.GetValueOrDefault()) return;
|
||||
// 설정 창 로드 중 바인딩에 의한 자동 Checked 이벤트 무시 (이미 활성화된 상태 복원)
|
||||
if (!IsLoaded) return;
|
||||
|
||||
// 테마 리소스 조회
|
||||
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
|
||||
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60));
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40));
|
||||
|
||||
// 비밀번호 확인 다이얼로그
|
||||
var dlg = new Window
|
||||
{
|
||||
Title = "개발자 모드 — 비밀번호 확인",
|
||||
Width = 340, SizeToContent = SizeToContent.Height,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
Owner = this, ResizeMode = ResizeMode.NoResize,
|
||||
WindowStyle = WindowStyle.None, AllowsTransparency = true,
|
||||
Background = Brushes.Transparent,
|
||||
};
|
||||
var border = new Border
|
||||
{
|
||||
Background = bgBrush, CornerRadius = new CornerRadius(12),
|
||||
BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20),
|
||||
};
|
||||
var stack = new StackPanel();
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\U0001f512 개발자 모드 활성화",
|
||||
FontSize = 15, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12),
|
||||
});
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "비밀번호를 입력하세요:",
|
||||
FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 6),
|
||||
});
|
||||
var pwBox = new PasswordBox
|
||||
{
|
||||
FontSize = 14, Padding = new Thickness(8, 6, 8, 6),
|
||||
Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*',
|
||||
};
|
||||
stack.Children.Add(pwBox);
|
||||
|
||||
var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) };
|
||||
var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) };
|
||||
cancelBtn.Click += (_, _) => { dlg.DialogResult = false; };
|
||||
btnRow.Children.Add(cancelBtn);
|
||||
var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true };
|
||||
okBtn.Click += (_, _) =>
|
||||
{
|
||||
if (pwBox.Password == "mouse12#")
|
||||
dlg.DialogResult = true;
|
||||
else
|
||||
{
|
||||
pwBox.Clear();
|
||||
pwBox.Focus();
|
||||
}
|
||||
};
|
||||
btnRow.Children.Add(okBtn);
|
||||
stack.Children.Add(btnRow);
|
||||
border.Child = stack;
|
||||
dlg.Content = border;
|
||||
dlg.Loaded += (_, _) => pwBox.Focus();
|
||||
|
||||
if (dlg.ShowDialog() != true)
|
||||
{
|
||||
// 비밀번호 실패/취소 — 체크 해제 + DevMode 강제 false
|
||||
_vm.DevMode = false;
|
||||
cb.IsChecked = false;
|
||||
}
|
||||
UpdateDevModeContentVisibility();
|
||||
}
|
||||
|
||||
private void DevModeCheckBox_Unchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateDevModeContentVisibility();
|
||||
}
|
||||
|
||||
/// <summary>개발자 모드 활성화 상태에 따라 개발자 탭 내용 표시/숨김.</summary>
|
||||
private void UpdateDevModeContentVisibility()
|
||||
{
|
||||
if (DevModeContent != null)
|
||||
DevModeContent.Visibility = _vm.DevMode ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user