문제: 선택 폴더의 하위가 아닌 다른 폴더에 있는 ProjectReference/DLL 이 분석되지 않음 (예: BoxZainer → ..\..\Common\*, ..\..\DLL\*). - follow: 루트 밖 ProjectReference 를 전이적으로 따라가 노드로 포함 - externaldll: HintPath 로 참조하는 외부 DLL 을 (외부 DLL) 싱크 노드로 포함 - 그룹/노드명 기준을 포함된 파일들의 공통 상위 폴더(scanBase)로 자동 조정 - GUI 체크박스(루트 밖 따라가기 / 외부 DLL 포함) 및 ini(follow, externaldll) 추가 검증: BoxZainer → 189 노드(프로젝트 117 + 외부DLL 72) · 819 간선 · 순환 0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
156 lines
5.4 KiB
C#
156 lines
5.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Markup.Xaml;
|
|
using Avalonia.Platform.Storage;
|
|
|
|
namespace DsmTool;
|
|
|
|
public partial class MainWindow : Window
|
|
{
|
|
private string? _lastHtml;
|
|
private string? _lastOut;
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
|
|
// 범용 기본값 — 빌드 산출물만 제외, 내부 NuGet은 자동 감지
|
|
ExcludeBox.Text = string.Join(Environment.NewLine, DsmConfig.DefaultExcludePaths);
|
|
ComponentBox.Text = "";
|
|
|
|
// 실행 시 대상 폴더에 현재 폴더 자동 삽입
|
|
try
|
|
{
|
|
var cur = Directory.GetCurrentDirectory();
|
|
if (Directory.Exists(cur)) RootBox.Text = cur;
|
|
}
|
|
catch { }
|
|
|
|
Log("대상 폴더가 현재 폴더로 지정되었습니다. 필요 시 변경 후 [생성] 을 누르세요.");
|
|
Log("팁: 대상 폴더에 dsm.config.ini 를 두면 팀별 설정이 자동 적용됩니다.");
|
|
}
|
|
|
|
private async void BrowseRoot(object? s, Avalonia.Interactivity.RoutedEventArgs e)
|
|
{
|
|
var p = await PickFolder("분석할 대상 폴더 선택");
|
|
if (p != null) RootBox.Text = p;
|
|
}
|
|
|
|
private async void BrowseOut(object? s, Avalonia.Interactivity.RoutedEventArgs e)
|
|
{
|
|
var p = await PickFolder("출력 폴더 선택");
|
|
if (p != null) OutBox.Text = p;
|
|
}
|
|
|
|
private async Task<string?> PickFolder(string title)
|
|
{
|
|
var top = GetTopLevel(this);
|
|
if (top is null) return null;
|
|
var res = await top.StorageProvider.OpenFolderPickerAsync(
|
|
new FolderPickerOpenOptions { Title = title, AllowMultiple = false });
|
|
return res.Count > 0 ? res[0].TryGetLocalPath() : null;
|
|
}
|
|
|
|
private async void Generate(object? s, Avalonia.Interactivity.RoutedEventArgs e)
|
|
{
|
|
var root = (RootBox.Text ?? "").Trim().Trim('"');
|
|
if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root))
|
|
{
|
|
Log("⚠ 유효한 대상 폴더가 아닙니다: " + root);
|
|
return;
|
|
}
|
|
var outDir = (OutBox.Text ?? "").Trim().Trim('"');
|
|
if (string.IsNullOrWhiteSpace(outDir)) outDir = root;
|
|
|
|
var cfg = new DsmConfig
|
|
{
|
|
ExcludeSamples = SamplesBox.IsChecked == true,
|
|
InternalAuto = AutoInternalBox.IsChecked == true,
|
|
FollowExternal = FollowBox.IsChecked == true,
|
|
ExternalDlls = ExtDllBox.IsChecked == true,
|
|
GroupMode = (GroupCombo.SelectedItem as ContentControl)?.Content?.ToString() ?? "auto",
|
|
ExcludePaths = SplitLines(ExcludeBox.Text).Select(x => x.ToLowerInvariant()).ToList(),
|
|
ManualMap = ParseMap(SplitLines(ComponentBox.Text)),
|
|
};
|
|
if (cfg.ExcludePaths.Count == 0) cfg.ExcludePaths = DsmConfig.DefaultExcludePaths.ToList();
|
|
|
|
GenBtn.IsEnabled = false;
|
|
LogBox.Text = "";
|
|
Log("분석 시작… " + root);
|
|
try
|
|
{
|
|
var r = await Task.Run(() => DsmEngine.Run(root, outDir, cfg, Log));
|
|
_lastHtml = r.HtmlPath;
|
|
_lastOut = outDir;
|
|
OpenHtmlBtn.IsEnabled = true;
|
|
OpenFolderBtn.IsEnabled = true;
|
|
Log("");
|
|
Log($"✔ 완료 노드 {r.Nodes} · 내부 간선 {r.Edges} · 순환(상삼각) 위반 {r.Upper}");
|
|
Log($" 최다 Fan-in : {r.MaxFanIn} ({r.MaxFanInV})");
|
|
Log($" 최다 Fan-out: {r.MaxFanOut} ({r.MaxFanOutV})");
|
|
Log($" → {r.HtmlPath}");
|
|
Log($" → {r.CsvPath}");
|
|
if (r.Upper > 0) Log(" ⚠ 상삼각 위반 > 0 : 순환 의존이 존재합니다.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log("✘ 오류: " + ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
GenBtn.IsEnabled = true;
|
|
}
|
|
}
|
|
|
|
private void OpenHtml(object? s, Avalonia.Interactivity.RoutedEventArgs e)
|
|
{
|
|
if (_lastHtml != null && File.Exists(_lastHtml))
|
|
Process.Start(new ProcessStartInfo { FileName = _lastHtml, UseShellExecute = true });
|
|
}
|
|
|
|
private void OpenFolder(object? s, Avalonia.Interactivity.RoutedEventArgs e)
|
|
{
|
|
if (_lastOut != null && Directory.Exists(_lastOut))
|
|
Process.Start(new ProcessStartInfo { FileName = _lastOut, UseShellExecute = true });
|
|
}
|
|
|
|
private static Dictionary<string, string> ParseMap(IEnumerable<string> lines)
|
|
{
|
|
var d = new Dictionary<string, string>();
|
|
foreach (var line in lines)
|
|
{
|
|
var i = line.IndexOf('=');
|
|
if (i <= 0) continue;
|
|
var k = line[..i].Trim().ToLowerInvariant();
|
|
var v = line[(i + 1)..].Trim();
|
|
if (k.Length > 0 && v.Length > 0) d[k] = v;
|
|
}
|
|
return d;
|
|
}
|
|
|
|
private static List<string> SplitLines(string? text)
|
|
{
|
|
var list = new List<string>();
|
|
if (string.IsNullOrEmpty(text)) return list;
|
|
foreach (var raw in text.Replace("\r", "").Split('\n'))
|
|
{
|
|
var t = raw.Trim();
|
|
if (t.Length > 0) list.Add(t);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private void Log(string msg)
|
|
{
|
|
if (Avalonia.Threading.Dispatcher.UIThread.CheckAccess())
|
|
LogBox.Text += msg + Environment.NewLine;
|
|
else
|
|
Avalonia.Threading.Dispatcher.UIThread.Post(() => LogBox.Text += msg + Environment.NewLine);
|
|
}
|
|
}
|