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; private DsmConfig _loaded = new(); // 마지막으로 로드된 설정(SampleMarkers 등 UI에 없는 값 보관) private string? _appliedIni; // 이미 적용한 ini 경로 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("팁: 대상 폴더(또는 exe 폴더)에 dsm.config.ini 가 있으면 자동으로 읽어 적용합니다."); TryAutoLoad(RootBox.Text); } private async void BrowseRoot(object? s, Avalonia.Interactivity.RoutedEventArgs e) { var p = await PickFolder("분석할 대상 폴더 선택"); if (p != null) { RootBox.Text = p; TryAutoLoad(p); } } // ini 자동 탐색·로드 (대상 폴더 → exe 폴더). 이미 적용한 파일이면 다시 안 함(사용자 수정 보존). private void TryAutoLoad(string? root) { if (string.IsNullOrWhiteSpace(root)) return; var ini = Program.FindConfig(root.Trim().Trim('"')); if (ini != null && !string.Equals(ini, _appliedIni, StringComparison.OrdinalIgnoreCase)) LoadConfigFile(ini); } private async void LoadConfig(object? s, Avalonia.Interactivity.RoutedEventArgs e) { var top = GetTopLevel(this); if (top is null) return; var files = await top.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = "dsm.config.ini 선택", AllowMultiple = false, FileTypeFilter = new[] { new FilePickerFileType("설정 (*.ini)") { Patterns = new[] { "*.ini" } } } }); if (files.Count > 0) { var p = files[0].TryGetLocalPath(); if (p != null) LoadConfigFile(p); } } private void LoadConfigFile(string path) { try { var c = DsmConfig.Load(path); ExcludeBox.Text = string.Join(Environment.NewLine, c.ExcludePaths); SamplesBox.IsChecked = c.ExcludeSamples; AutoInternalBox.IsChecked = c.InternalAuto; FollowBox.IsChecked = c.FollowExternal; ExtDllBox.IsChecked = c.ExternalDlls; ComponentBox.Text = string.Join(Environment.NewLine, c.ManualMap.Select(kv => $"{kv.Key} = {kv.Value}")); foreach (var it in GroupCombo.Items) if (it is ContentControl cc && string.Equals(cc.Content?.ToString(), c.GroupMode, StringComparison.OrdinalIgnoreCase)) { GroupCombo.SelectedItem = it; break; } _loaded = c; _appliedIni = path; Log("설정 적용됨: " + path); } catch (Exception ex) { Log("설정 로드 실패: " + ex.Message); } } private async void BrowseOut(object? s, Avalonia.Interactivity.RoutedEventArgs e) { var p = await PickFolder("출력 폴더 선택"); if (p != null) OutBox.Text = p; } private async Task 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; // 대상 폴더에 dsm.config.ini 가 있으면(아직 미적용) 자동 로드 TryAutoLoad(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)), SampleMarkers = _loaded.SampleMarkers, // UI에 없는 값(ini의 samplemarker)은 로드된 설정에서 }; 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 ParseMap(IEnumerable lines) { var d = new Dictionary(); 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 SplitLines(string? text) { var list = new List(); 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); } }