D-6: 비교 결과 텍스트 내보내기
결과를 남에게 보내거나 결재에 붙일 때 화면을 캡처하는 것 말고는 방법이 없었다. 캡처는 검색도 안 되고 줄 번호를 짚어 말하기도 어렵다. Services/CompareReport.cs 신설. 파일 비교와 폴더 비교 양쪽에서 쓴다. 글을 만드는 일만 여기서 하고, 어디에 저장할지 묻는 것은 화면 쪽 일이다. 그래야 화면 없이 검사한다. 파일 쪽은 요약과 다른 줄 목록을 적는다. 줄 번호는 양쪽을 다 적는다. 한쪽에만 있는 줄이 있어 번호가 어긋나므로 하나만 적으면 파일에서 그 줄을 찾을 수 없다. 폴더 쪽은 상태와 상대 경로를 줄마다 적는다. 폴더 자체는 적지 않는다. 그 안의 파일이 이미 다 적혀 있다. 곁들여 요약에 주석만 다른 수를 덧붙였다. "다름 3(주석만 1)". D-1 로 새로 생긴 갈래가 요약에는 안 보였다. 확인: 두 화면에서 각각 뽑아 메모장으로 열었다. ReportTest 23건 신설. 전체 451건 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
32c0146b48
commit
ee5d34c2a6
41
doc/진행상황.md
41
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)
|
||||
|
||||
**왜 필요했나** - 결과를 남에게 보내거나 결재에 붙일 때 <b>화면을 캡처하는 것 말고는 방법이 없었다.</b>
|
||||
캡처는 검색도 안 되고 줄 번호를 짚어 말하기도 어렵다.
|
||||
|
||||
`Services/CompareReport.cs` 신설. 파일 비교와 폴더 비교 양쪽에서 쓴다.
|
||||
<b>글을 만드는 일만</b> 여기서 하고, 어디에 저장할지 묻는 것은 화면 쪽 일이다 - 그래야 화면 없이 검사한다.
|
||||
|
||||
파일 비교 결과는 이렇게 나온다.
|
||||
|
||||
```
|
||||
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)
|
||||
|
||||
폴더 비교 결과에 `주석만 다름` 상태를 새로 두었다. 코드는 같고 주석·공백만 다른 파일이다.
|
||||
|
||||
147
src/MergeHub/Services/CompareReport.cs
Normal file
147
src/MergeHub/Services/CompareReport.cs
Normal file
@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using MergeHub.Models;
|
||||
|
||||
namespace MergeHub.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 비교 결과를 <b>메모장으로 열어 읽을 수 있는 글</b>로 만든다.
|
||||
///
|
||||
/// 왜 글로 뽑나 - 결과를 남에게 보내거나 결재에 붙일 때 화면을 캡처하는 것 말고는 방법이 없었다.
|
||||
/// 캡처는 검색도 안 되고 줄 번호를 짚어 말하기도 어렵다.
|
||||
///
|
||||
/// 화면 없이 검사할 수 있어야 하므로 <b>글을 만드는 일만</b> 여기서 한다.
|
||||
/// 어디에 저장할지 묻는 것은 화면 쪽 일이다.
|
||||
/// </summary>
|
||||
public static class CompareReport
|
||||
{
|
||||
private const string Rule = "----------------------------------------------------------------";
|
||||
|
||||
/// <summary>파일 비교 결과.</summary>
|
||||
public static string ForFile(string leftLabel, string rightLabel,
|
||||
IReadOnlyList<MergeLine> 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;
|
||||
|
||||
// 줄 번호는 <b>양쪽을 다 적는다.</b> 한쪽에만 있는 줄이 있어 번호가 어긋나므로,
|
||||
// 하나만 적으면 파일에서 그 줄을 찾을 수 없다.
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>폴더 비교 결과.</summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>폴더 트리를 훑어 "같음"이 아닌 <b>파일만</b> 모은다.</summary>
|
||||
private static void Collect(IEnumerable<FolderCompareItem> 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 => "오른쪽에만",
|
||||
_ => "같음"
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -101,11 +101,16 @@ namespace MergeHub.Services
|
||||
public int LeftOnlyCount { get; set; }
|
||||
public int RightOnlyCount { get; set; }
|
||||
|
||||
/// <summary>그중 코드는 같고 주석만 다른 것. "다름" 안에 든 수다.</summary>
|
||||
public int CommentOnlyCount { get; set; }
|
||||
|
||||
/// <summary>항목 상한에 걸려 도중에 멈췄는지.</summary>
|
||||
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;
|
||||
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 비교 결과를 글로 뽑아 파일에 쓴다.
|
||||
///
|
||||
/// 화면을 캡처하는 것 말고는 결과를 남에게 보낼 방법이 없었다. 캡처는 검색도 안 되고
|
||||
/// 줄 번호를 짚어 말하기도 어렵다. 글이면 메일에 붙이든 결재에 넣든 그대로 쓴다.
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 비교 결과를 글로 뽑아 파일에 쓴다.
|
||||
///
|
||||
/// 화면을 캡처하는 것 말고는 결과를 남에게 보낼 방법이 없었다. 캡처는 검색도 안 되고
|
||||
/// 줄 번호를 짚어 말하기도 어렵다.
|
||||
/// </summary>
|
||||
[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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보고서에 적을 쪽 이름. Git 모드면 머리글(브랜치·커밋)이 더 쓸모 있고,
|
||||
/// 파일 비교 모드면 경로가 있어야 어느 파일인지 알 수 있다.
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
@ -255,6 +255,9 @@
|
||||
</StackPanel>
|
||||
|
||||
<WrapPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button Content="결과 내보내기" Command="{Binding ExportReportCommand}"
|
||||
Height="28" Padding="10,2" FontSize="11" Margin="0,0,14,0"
|
||||
ToolTip="비교 결과를 글로 뽑아 파일에 저장합니다. 메일이나 결재에 그대로 붙일 수 있습니다"/>
|
||||
<Button Content="줄 단위 비교" Command="{Binding OpenInDiffCommand}"
|
||||
Height="28" Padding="10,2" FontSize="11" Margin="0,0,6,0"/>
|
||||
<Button Content="→ 오른쪽으로 복사" Command="{Binding CopyToRightCommand}"
|
||||
|
||||
@ -354,6 +354,11 @@
|
||||
Height="26" MinHeight="26" Padding="10,2" FontSize="11" Margin="10,0,0,0"
|
||||
VerticalAlignment="Top"
|
||||
ToolTip="단축키 목록을 봅니다 (F1)"/>
|
||||
<Button DockPanel.Dock="Right" Content="결과 내보내기" Command="{Binding ExportReportCommand}"
|
||||
Height="26" MinHeight="26" Padding="10,2" FontSize="11" Margin="10,0,0,0"
|
||||
VerticalAlignment="Top"
|
||||
ToolTip="비교 결과를 글로 뽑아 파일에 저장합니다. 메일이나 결재에 그대로 붙일 수 있습니다"/>
|
||||
|
||||
|
||||
<WrapPanel Orientation="Horizontal">
|
||||
<!-- 지금 어느 모드인지 늘 보이게 한다 - 커밋 이력이 없는 이유를 알 수 있어야 한다. -->
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user