1
0
Fork 0

feat(tools): add first-run external tool installer wizard and catalog

첫 실행 시 외부 도구 설치 마법사를 띄우고 7종(FFmpeg·LibreOffice·Pandoc·ImageMagick·H2Orestart·agy·Codex)을 기본 전체 선택 상태로 순차 설치한다.
- Core: ExternalToolCatalog(정적 카탈로그, winget ID 실검증), ExternalToolInstaller(winget 무인 실행·재감지 판정, IExternalCommandRunner 시드로 테스트 가능)
- Core: ExternalToolDetector에 winget FFmpeg 스캔 + Pandoc/ImageMagick 감지 추가
- App: ToolSetupWindow + ToolSetupViewModel, 첫 실행 플래그(setup.tools.done), 설정 창 '외부 도구 설치/검증' 재진입 버튼
- Tests: ExternalToolInstallerTests 5건
- 기존 미커밋 작업(광고 Ads 지원: AdBannerControl/AdLargeCardControl/AdService, AI 백엔드 agy·switchboard·codex 채팅 클라이언트, 관련 테스트) 포함
This commit is contained in:
Yun Chan 2026-09-07 21:16:49 +09:00
parent f00f6567f4
commit 6a41c4a0dc
37 changed files with 2545 additions and 69 deletions

View file

@ -0,0 +1,105 @@
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Xunit;
namespace Everything2Everything.Tests;
public class AdDesignAuditTests
{
private static readonly string SolutionRoot = FindSolutionRoot();
private static readonly string ViewsDir = Path.Combine(SolutionRoot, "src", "Everything2Everything.App", "Views");
private static string FindSolutionRoot()
{
var dir = Directory.GetCurrentDirectory();
while (dir != null && !File.Exists(Path.Combine(dir, "Everything2Everything.slnx")))
{
dir = Directory.GetParent(dir)?.FullName;
}
return dir ?? throw new DirectoryNotFoundException("솔루션 루트를 찾을 수 없습니다.");
}
[Fact]
public void AdControls_MustExistAndBeWellFormed()
{
var bannerPath = Path.Combine(ViewsDir, "AdBannerControl.xaml");
var largeCardPath = Path.Combine(ViewsDir, "AdLargeCardControl.xaml");
Assert.True(File.Exists(bannerPath), "AdBannerControl.xaml 이 존재해야 합니다.");
Assert.True(File.Exists(largeCardPath), "AdLargeCardControl.xaml 이 존재해야 합니다.");
var bannerDoc = XDocument.Parse(File.ReadAllText(bannerPath));
var largeCardDoc = XDocument.Parse(File.ReadAllText(largeCardPath));
Assert.NotNull(bannerDoc.Root);
Assert.NotNull(largeCardDoc.Root);
}
[Fact]
public void AdControls_MustComplyWithStrictDesignAudit()
{
var targetFiles = new[]
{
Path.Combine(ViewsDir, "AdBannerControl.xaml"),
Path.Combine(ViewsDir, "AdLargeCardControl.xaml")
};
var rawEmojis = new[] { "⚙", "📁", "🗑", "✖", "🔧", "✨", "🚀", "⚡" };
var violations = new List<string>();
foreach (var file in targetFiles)
{
if (!File.Exists(file)) continue;
var doc = XDocument.Parse(File.ReadAllText(file));
var fileName = Path.GetFileName(file);
// 1. 유니코드 이모지 금지 검증
var buttons = doc.Descendants().Where(e => e.Name.LocalName is "Button" or "ToggleButton");
foreach (var btn in buttons)
{
var content = btn.Attribute("Content")?.Value;
if (!string.IsNullOrEmpty(content) && rawEmojis.Any(emoji => content.Contains(emoji)))
{
violations.Add($"[{fileName}] 버튼에 이모지 사용 감지: {content}");
}
}
// 2. 가로 StackPanel 내 아이콘/텍스트 VerticalAlignment="Center" 검증
var stackPanels = doc.Descendants().Where(e => e.Name.LocalName == "StackPanel" &&
e.Attribute("Orientation")?.Value == "Horizontal");
foreach (var sp in stackPanels)
{
foreach (var child in sp.Elements())
{
var va = child.Attribute("VerticalAlignment")?.Value;
if (va != "Center")
{
violations.Add($"[{fileName}] 가로 StackPanel의 자식 <{child.Name.LocalName}>에 VerticalAlignment=\"Center\" 누락");
}
}
}
// 3. TextBlock 오버플로 방지 (TextTrimming 또는 TextWrapping) 검증
var textBlocks = doc.Descendants().Where(e => e.Name.LocalName == "TextBlock");
foreach (var tb in textBlocks)
{
var text = tb.Attribute("Text")?.Value ?? "";
// 바인딩된 동적 텍스트는 Trimming이나 Wrapping 필수
if (text.Contains("{Binding") || text.Contains("{x:Bind"))
{
var trimming = tb.Attribute("TextTrimming")?.Value;
var wrapping = tb.Attribute("TextWrapping")?.Value;
if (string.IsNullOrEmpty(trimming) && string.IsNullOrEmpty(wrapping))
{
violations.Add($"[{fileName}] 바인딩 TextBlock에 TextTrimming 또는 TextWrapping 누락: {text}");
}
}
}
}
Assert.True(violations.Count == 0,
"광고 컨트롤 디자인 감사 위반이 발견되었습니다:\n" + string.Join("\n", violations));
}
}