diff --git a/doc/진행상황.md b/doc/진행상황.md index 42c8af0..59e92d5 100644 --- a/doc/진행상황.md +++ b/doc/진행상황.md @@ -131,7 +131,7 @@ CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘 | 대기 | D-3 폴더 안 전체 검색 | | ~~취소~~ | ~~D-4 비교 창 여러 개~~ (2026-09-09 사용자 판단으로 뺌) | | **완료** | D-5 덩어리 단위 반영 (3-Way 에서 BASE 를 가져오던 것을 REMOTE 로) | -| 대기 | D-6 비교 결과 텍스트 내보내기 | +| **완료** | D-6 비교 결과 텍스트 내보내기 | | 대기 | D-7 드래그 앤 드롭으로 열기 | ### E. 추가 요청 (2026-09-09 오후) @@ -160,6 +160,45 @@ BASE 머리글에 그렇게 적었다 - `BASE (merge base) · 이 파일 없음 조상에 파일이 있는 경우는 지금까지처럼 파일 이름이 그대로 나온다. --- +### D-6. 비교 결과 텍스트 내보내기 — **완료** (2026-09-09) + +**왜 필요했나** - 결과를 남에게 보내거나 결재에 붙일 때 화면을 캡처하는 것 말고는 방법이 없었다. +캡처는 검색도 안 되고 줄 번호를 짚어 말하기도 어렵다. + +`Services/CompareReport.cs` 신설. 파일 비교와 폴더 비교 양쪽에서 쓴다. +글을 만드는 일만 여기서 하고, 어디에 저장할지 묻는 것은 화면 쪽 일이다 - 그래야 화면 없이 검사한다. + +파일 비교 결과는 이렇게 나온다. + +``` +MergeHub 파일 비교 결과 +만든 때: 2026-09-09 14:46:23 +왼쪽 : ...\WeldController_기준.cs +오른쪽: ...\WeldController_내작업.cs +---------------------------------------------------------------- +요약: 총 43 줄 · 다름 5 · 오른쪽에만 5 · 왼쪽에만 0 + +다른 줄 +---------------------------------------------------------------- +[8/8] 다름 + - private const int PowerWatt = 1800; + + private const int PowerWatt = 2000; +[-/28] 오른쪽에만 + + { +``` + +**줄 번호를 양쪽 다 적는다.** 한쪽에만 있는 줄이 있어 번호가 어긋나므로, 하나만 적으면 +파일에서 그 줄을 찾을 수 없다. 없는 쪽은 `-` 로 둔다. + +폴더 비교 결과는 상태와 경로를 줄마다 적는다. 하위 폴더 파일도 상대 경로로 들어가고, +폴더 자체는 줄로 적지 않는다 - 그 안의 파일이 이미 다 적혀 있다. + +**곁들여** 요약에 주석만 다른 수를 덧붙였다 - `다름 3(주석만 1)`. +D-1 로 새로 생긴 갈래가 요약에는 안 보였다. + +**확인**: 두 화면에서 각각 뽑아 메모장으로 열었다. `ReportTest` 23건 신설. 전체 **451건**. +--- + ### D-1. 주석만 다른 파일을 다른 색으로 — **완료** (2026-09-09) 폴더 비교 결과에 `주석만 다름` 상태를 새로 두었다. 코드는 같고 주석·공백만 다른 파일이다. diff --git a/src/MergeHub/Services/CompareReport.cs b/src/MergeHub/Services/CompareReport.cs new file mode 100644 index 0000000..a26f82b --- /dev/null +++ b/src/MergeHub/Services/CompareReport.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MergeHub.Models; + +namespace MergeHub.Services +{ + /// + /// 비교 결과를 메모장으로 열어 읽을 수 있는 글로 만든다. + /// + /// 왜 글로 뽑나 - 결과를 남에게 보내거나 결재에 붙일 때 화면을 캡처하는 것 말고는 방법이 없었다. + /// 캡처는 검색도 안 되고 줄 번호를 짚어 말하기도 어렵다. + /// + /// 화면 없이 검사할 수 있어야 하므로 글을 만드는 일만 여기서 한다. + /// 어디에 저장할지 묻는 것은 화면 쪽 일이다. + /// + public static class CompareReport + { + private const string Rule = "----------------------------------------------------------------"; + + /// 파일 비교 결과. + public static string ForFile(string leftLabel, string rightLabel, + IReadOnlyList lines, DateTime when) + { + var text = new StringBuilder(); + + Head(text, "파일 비교 결과", leftLabel, rightLabel, when); + + var changed = 0; + var added = 0; + var removed = 0; + + foreach (var line in lines) + { + if (line.IsMergeNote || line.IsIgnored) continue; + + switch (line.State) + { + case MergeLineState.Changed: changed++; break; + case MergeLineState.LocalOnly: added++; break; + case MergeLineState.BaseOnly: removed++; break; + } + } + + text.AppendLine($"요약: 총 {lines.Count} 줄 · 다름 {changed} · 오른쪽에만 {added} · 왼쪽에만 {removed}"); + text.AppendLine(); + + if (changed + added + removed == 0) + { + text.AppendLine("다른 줄이 없습니다."); + return text.ToString(); + } + + text.AppendLine("다른 줄"); + text.AppendLine(Rule); + + foreach (var line in lines) + { + if (line.IsMergeNote || line.IsIgnored) continue; + if (line.State == MergeLineState.Same) continue; + + // 줄 번호는 양쪽을 다 적는다. 한쪽에만 있는 줄이 있어 번호가 어긋나므로, + // 하나만 적으면 파일에서 그 줄을 찾을 수 없다. + var number = $"{Number(line.BaseNumber)}/{Number(line.LocalNumber)}"; + + text.AppendLine($"[{number}] {StateName(line.State)}"); + if (line.HasBase) text.AppendLine(" - " + line.BaseText); + if (line.HasLocal) text.AppendLine(" + " + line.LocalText); + } + + return text.ToString(); + } + + /// 폴더 비교 결과. + public static string ForFolder(string leftFolder, string rightFolder, + FolderCompareResult result, DateTime when) + { + var text = new StringBuilder(); + + Head(text, "폴더 비교 결과", leftFolder, rightFolder, when); + text.AppendLine("요약: " + result.Summary); + if (result.Truncated) text.AppendLine("(항목이 너무 많아 도중에 멈춘 결과입니다)"); + text.AppendLine(); + + var rows = new List<(string Status, string Path)>(); + Collect(result.Items, rows); + + if (rows.Count == 0) + { + text.AppendLine("다른 항목이 없습니다."); + return text.ToString(); + } + + text.AppendLine("다른 항목"); + text.AppendLine(Rule); + + foreach (var row in rows) text.AppendLine($"{row.Status,-12}{row.Path}"); + + return text.ToString(); + } + + /// 폴더 트리를 훑어 "같음"이 아닌 파일만 모은다. + private static void Collect(IEnumerable items, List<(string, string)> rows) + { + foreach (var item in items) + { + if (item.IsDirectory) + { + Collect(item.Children, rows); + continue; + } + + if (item.Status == FolderEntryStatus.Same) continue; + rows.Add((StatusName(item.Status), item.RelativePath)); + } + } + + private static void Head(StringBuilder text, string title, string left, string right, DateTime when) + { + text.AppendLine($"MergeHub {title}"); + text.AppendLine($"만든 때: {when:yyyy-MM-dd HH:mm:ss}"); + text.AppendLine($"왼쪽 : {left}"); + text.AppendLine($"오른쪽: {right}"); + text.AppendLine(Rule); + } + + private static string Number(int? value) => value is int n ? n.ToString() : "-"; + + private static string StateName(MergeLineState state) => state switch + { + MergeLineState.Changed => "다름", + MergeLineState.LocalOnly => "오른쪽에만", + MergeLineState.BaseOnly => "왼쪽에만", + _ => "같음" + }; + + private static string StatusName(FolderEntryStatus status) => status switch + { + FolderEntryStatus.Different => "다름", + FolderEntryStatus.CommentOnly => "주석만 다름", + FolderEntryStatus.Binary => "다름(이진)", + FolderEntryStatus.LeftOnly => "왼쪽에만", + FolderEntryStatus.RightOnly => "오른쪽에만", + _ => "같음" + }; + } +} \ No newline at end of file diff --git a/src/MergeHub/Services/FolderComparer.cs b/src/MergeHub/Services/FolderComparer.cs index 7651f77..7b5a0af 100644 --- a/src/MergeHub/Services/FolderComparer.cs +++ b/src/MergeHub/Services/FolderComparer.cs @@ -101,11 +101,16 @@ namespace MergeHub.Services public int LeftOnlyCount { get; set; } public int RightOnlyCount { get; set; } + /// 그중 코드는 같고 주석만 다른 것. "다름" 안에 든 수다. + public int CommentOnlyCount { get; set; } + /// 항목 상한에 걸려 도중에 멈췄는지. public bool Truncated { get; set; } public string Summary => - $"전체 {TotalFiles}개 · 다름 {DifferentCount} · 왼쪽만 {LeftOnlyCount} · 오른쪽만 {RightOnlyCount} · 같음 {SameCount}" + $"전체 {TotalFiles}개 · 다름 {DifferentCount}" + + (CommentOnlyCount > 0 ? $"(주석만 {CommentOnlyCount})" : string.Empty) + + $" · 왼쪽만 {LeftOnlyCount} · 오른쪽만 {RightOnlyCount} · 같음 {SameCount}" + (Truncated ? " (너무 많아 일부만 봄)" : string.Empty); } @@ -198,6 +203,11 @@ namespace MergeHub.Services switch (item.Status) { case FolderEntryStatus.Same: result.SameCount++; break; + // "주석만 다름"도 다름의 한 갈래다. 둘 다 센다 - 요약이 "다름 3(주석만 1)" 로 읽힌다. + case FolderEntryStatus.CommentOnly: + result.DifferentCount++; + result.CommentOnlyCount++; + break; case FolderEntryStatus.LeftOnly: result.LeftOnlyCount++; break; case FolderEntryStatus.RightOnly: result.RightOnlyCount++; break; default: result.DifferentCount++; break; diff --git a/src/MergeHub/ViewModels/FolderViewModel.cs b/src/MergeHub/ViewModels/FolderViewModel.cs index d1701c6..1175ffe 100644 --- a/src/MergeHub/ViewModels/FolderViewModel.cs +++ b/src/MergeHub/ViewModels/FolderViewModel.cs @@ -208,6 +208,7 @@ namespace MergeHub.ViewModels CopyToLeftCommand.NotifyCanExecuteChanged(); DeleteLeftCommand.NotifyCanExecuteChanged(); DeleteRightCommand.NotifyCanExecuteChanged(); + ExportReportCommand.NotifyCanExecuteChanged(); } [RelayCommand] @@ -222,6 +223,57 @@ namespace MergeHub.ViewModels if (AskForFolder("오른쪽(비교 대상) 폴더 고르기", RightFolder) is { } picked) RightFolder = picked; } + // ================= 결과 내보내기 ================= + + private bool CanExportReport() => _lastResult != null; + + /// + /// 비교 결과를 글로 뽑아 파일에 쓴다. + /// + /// 화면을 캡처하는 것 말고는 결과를 남에게 보낼 방법이 없었다. 캡처는 검색도 안 되고 + /// 줄 번호를 짚어 말하기도 어렵다. 글이면 메일에 붙이든 결재에 넣든 그대로 쓴다. + /// + [RelayCommand(CanExecute = nameof(CanExportReport))] + private void ExportReport() + { + if (_lastResult is not { } result) return; + + var dialog = new Microsoft.Win32.SaveFileDialog + { + Title = "폴더 비교 결과 저장", + Filter = "텍스트 파일 (*.txt)|*.txt|모든 파일 (*.*)|*.*", + DefaultExt = ".txt", + FileName = $"폴더비교_{DateTime.Now:yyyyMMdd_HHmm}.txt" + }; + + var start = PathUtil.FullFolderPath(LeftFolder); + if (start.Length > 0) dialog.InitialDirectory = start; + + bool ok; + try + { + ok = dialog.ShowDialog() == true; + } + catch (Exception) + { + dialog.InitialDirectory = string.Empty; + ok = dialog.ShowDialog() == true; + } + + if (!ok) return; + + try + { + var report = CompareReport.ForFolder(LeftFolder, RightFolder, result, DateTime.Now); + TextFile.Write(dialog.FileName, report); + MessageUtil.ShowSuccessToast($"저장했습니다 · {Path.GetFileName(dialog.FileName)}"); + } + catch (Exception ex) + { + MessageUtil.ShowErrorToast($"저장하지 못했습니다: {ex.Message}"); + } + } + private static string? AskForFolder(string title, string current) => FolderPicker.Pick(title, current); private bool CanCompare() => !IsBusy; diff --git a/src/MergeHub/ViewModels/HomeViewModel.cs b/src/MergeHub/ViewModels/HomeViewModel.cs index 40f55f0..d4c394b 100644 --- a/src/MergeHub/ViewModels/HomeViewModel.cs +++ b/src/MergeHub/ViewModels/HomeViewModel.cs @@ -471,6 +471,7 @@ namespace MergeHub.ViewModels ReloadCommand.NotifyCanExecuteChanged(); SaveAndCommitCommand.NotifyCanExecuteChanged(); SavePlainCommand.NotifyCanExecuteChanged(); + ExportReportCommand.NotifyCanExecuteChanged(); AutoMergeCommand.NotifyCanExecuteChanged(); AcceptBlockCommand.NotifyCanExecuteChanged(); @@ -1443,6 +1444,64 @@ namespace MergeHub.ViewModels } } + // ================= 결과 내보내기 ================= + + private bool CanExportReport() => Lines.Count > 0; + + /// + /// 비교 결과를 글로 뽑아 파일에 쓴다. + /// + /// 화면을 캡처하는 것 말고는 결과를 남에게 보낼 방법이 없었다. 캡처는 검색도 안 되고 + /// 줄 번호를 짚어 말하기도 어렵다. + /// + [RelayCommand(CanExecute = nameof(CanExportReport))] + private void ExportReport() + { + var dialog = new Microsoft.Win32.SaveFileDialog + { + Title = "파일 비교 결과 저장", + Filter = "텍스트 파일 (*.txt)|*.txt|모든 파일 (*.*)|*.*", + DefaultExt = ".txt", + FileName = $"파일비교_{DateTime.Now:yyyyMMdd_HHmm}.txt" + }; + + var start = PathUtil.FolderOfFile(RightFile.Length > 0 ? RightFile : LeftFile); + if (start.Length > 0) dialog.InitialDirectory = start; + + bool ok; + try + { + ok = dialog.ShowDialog() == true; + } + catch (Exception) + { + dialog.InitialDirectory = string.Empty; + ok = dialog.ShowDialog() == true; + } + + if (!ok) return; + + try + { + var report = CompareReport.ForFile(ReportLabel(BaseHeader, LeftFile), + ReportLabel(LocalHeader, RightFile), + Lines, DateTime.Now); + TextFile.Write(dialog.FileName, report); + MessageUtil.ShowSuccessToast($"저장했습니다 · {Path.GetFileName(dialog.FileName)}"); + } + catch (Exception ex) + { + MessageUtil.ShowErrorToast($"저장하지 못했습니다: {ex.Message}"); + } + } + + /// + /// 보고서에 적을 쪽 이름. Git 모드면 머리글(브랜치·커밋)이 더 쓸모 있고, + /// 파일 비교 모드면 경로가 있어야 어느 파일인지 알 수 있다. + /// + private string ReportLabel(string header, string path) => + IsGitMode || path.Length == 0 ? header : path; + private static string? AskForFile(string title, string current) { var dialog = new Microsoft.Win32.OpenFileDialog diff --git a/src/MergeHub/Views/FolderView.xaml b/src/MergeHub/Views/FolderView.xaml index 26d355e..b76ea51 100644 --- a/src/MergeHub/Views/FolderView.xaml +++ b/src/MergeHub/Views/FolderView.xaml @@ -255,6 +255,9 @@ +