From 8083075ece29ca2cb83f4f1bb90ce8850b04dd31 Mon Sep 17 00:00:00 2001 From: ksj09406 Date: Thu, 10 Sep 2026 20:49:46 +0900 Subject: [PATCH] =?UTF-8?q?4=EC=B0=A8=20B-3:=20=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EB=B9=84=EA=B5=90=EC=97=90=EC=84=9C=20=EB=8B=A4=EC=9D=8C=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20=ED=8C=8C=EC=9D=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 훑는 중인 목록을 GitWorkspace 에 두고 파일 비교 화면에서 이전/다음으로 딛는다. 화면에 보이는 순서를 따르고, 찾은 줄 목록도 같은 방식으로 훑는다. 먼저 묻고 자리를 옮긴다. Ctrl+PageUp/PageDown. --- doc/history/2026-09-10-4차.md | 38 ++++++++++++++- doc/history/changelog.md | 2 + doc/진행상황.md | 2 +- src/MergeHub/Services/GitWorkspace.cs | 46 ++++++++++++++++++ src/MergeHub/Services/ShortcutMap.cs | 10 +++- src/MergeHub/ViewModels/FolderViewModel.cs | 41 ++++++++++++++++ src/MergeHub/ViewModels/HomeViewModel.cs | 55 ++++++++++++++++++++++ src/MergeHub/Views/HomeView.xaml | 11 +++++ src/MergeHub/Views/HomeView.xaml.cs | 3 ++ 9 files changed, 205 insertions(+), 3 deletions(-) diff --git a/doc/history/2026-09-10-4차.md b/doc/history/2026-09-10-4차.md index 47278d1..2c45a48 100644 --- a/doc/history/2026-09-10-4차.md +++ b/doc/history/2026-09-10-4차.md @@ -196,4 +196,40 @@ MergeHub 파일 비교 결과 | 짝의 두 파일을 지운 뒤 | 목록에 그대로 남고 **흐리게** 보였다 | | 그 죽은 짝을 고르기 | `두 파일 모두 찾을 수 없습니다` 토스트, 화면은 그대로 | -검사 33건 추가(`RecentTest`). 전체 **555건 통과** · 빌드 경고 0 · 오류 0. \ No newline at end of file +검사 33건 추가(`RecentTest`). 전체 **555건 통과** · 빌드 경고 0 · 오류 0. +--- + +## B-3. 폴더 비교에서 다음 다른 파일로 — **완료** (2026-09-10) + +**왜** - 파일 하나를 보고 **폴더 탭으로 돌아갔다가** 다음 파일을 다시 열어야 했다. +다른 파일이 스무 개면 이 왕복을 스무 번 한다. + +`GitWorkspace` 에 훑는 중인 목록(`Queue`)과 자리(`QueueIndex`)를 뒀다. +폴더 비교가 채우고 파일 비교가 딛는다 - 두 화면이 서로를 모르는 채로 같은 목록을 봐야 해서 +이미 통로 노릇을 하는 이 객체에 뒀다. + +**원본 결과가 아니라 지금 트리를 훑어 목록을 만든다.** 필터로 "코드가 다른 것만" 을 걸어 뒀으면 +그 안에서만 움직여야 한다 - 목록에 없는 파일로 넘어가면 왜 거기로 갔는지 알 수 없다. + +**찾은 줄 목록도 훑을 목록이 된다.** 같은 파일이 여러 줄 걸렸으면 한 번만 담는다 - +다음 파일을 눌렀는데 같은 파일이 다시 열리면 멈춘 것처럼 보인다. + +**최근 목록으로 연 파일은 목록을 비운다.** 남아 있던 목록을 그대로 두면 다음 파일이 엉뚱한 데로 간다. + +**먼저 묻고 그 다음에 자리를 옮긴다.** 확인 창에서 취소하면 자리도 그대로여야 하는데, +자리를 먼저 옮기면 취소했을 때 목록 위치만 앞서 나간다. 물은 뒤에는 두 번 묻지 않도록 막는다. + +단축키는 `Ctrl+PageUp` · `Ctrl+PageDown` 이다. `Alt+PageDown`(북마크)은 그대로 두었고 +검사로 빼앗지 않았음을 확인한다. + +**화면에서 확인한 것** (파일 네 개 · 하나는 하위 폴더) + +| 한 일 | 자리 | 열린 파일 | 이전/다음 | +|---|---|---|---| +| 검색 결과로 열기 | 1 / 4 | A.cpp | 끔 / 켬 | +| 다음 파일 ×3 | 2 → 3 → 4 | B.cpp → C.cpp → D.cpp | 마지막에서 다음이 꺼짐 | +| 이전 파일 ×2 | 3 → 2 | C.cpp → B.cpp | 둘 다 켬 | +| 고친 뒤 다음 파일 → 취소 | **2 / 4 그대로** | B.cpp 그대로 | 편집 내용도 그대로 | +| 다시 눌러 버리고 계속 | 3 / 4 | C.cpp | B.cpp 파일은 손대지 않음 | + +검사 5건 추가(`ShortcutTest` 63 → 68). 전체 **560건 통과** · 빌드 경고 0 · 오류 0. \ No newline at end of file diff --git a/doc/history/changelog.md b/doc/history/changelog.md index 3fa711f..03e8a74 100644 --- a/doc/history/changelog.md +++ b/doc/history/changelog.md @@ -8,6 +8,7 @@ | 날짜 | 항목 | 커밋 | |---|---|---| +| 2026-09-10 | **4차 B-3** — 폴더 비교 목록을 파일 비교 화면에서 이전/다음으로 딛는다 (Ctrl+PageUp/PageDown) | 아래 | | 2026-09-10 | **4차 B-1** — 최근에 견준 파일 짝·폴더 짝을 기억해 위 줄에서 바로 연다. 없는 경로는 흐리게만 하고 지우지 않는다 | 아래 | | 2026-09-10 | **4차 A-2** — 내보낸 보고서 머리글에 만든 사람을 넣음. 이름 판단을 머지 주석과 한 함수로 모았다 | 아래 | | 2026-09-10 | **4차 A-1** — 저장하지 않은 편집이 말없이 사라지던 길 여덟 군데를 막음. 플랫폼에 세 갈래 확인 창을 새로 넣었다 | 아래 | @@ -30,6 +31,7 @@ | 날짜 | 문서 | 내용 | |---|---|---| +| 2026-09-10 | **4차 B-3** — 폴더 비교 목록을 파일 비교 화면에서 이전/다음으로 딛는다 (Ctrl+PageUp/PageDown) | 아래 | | 2026-09-10 | **4차 B-1** — 최근에 견준 파일 짝·폴더 짝을 기억해 위 줄에서 바로 연다. 없는 경로는 흐리게만 하고 지우지 않는다 | 아래 | | 2026-09-10 | **4차 A-2** — 내보낸 보고서 머리글에 만든 사람을 넣음. 이름 판단을 머지 주석과 한 함수로 모았다 | 아래 | | 2026-09-10 | **4차 A-1** — 저장하지 않은 편집이 말없이 사라지던 길 여덟 군데를 막음. 플랫폼에 세 갈래 확인 창을 새로 넣었다 | 아래 | diff --git a/doc/진행상황.md b/doc/진행상황.md index b863029..571fd99 100644 --- a/doc/진행상황.md +++ b/doc/진행상황.md @@ -31,7 +31,7 @@ |---|---| | **완료** | B-1 최근 비교 목록 | | 대기 | B-2 같은 줄 접기 | -| 대기 | B-3 폴더 비교에서 다음 다른 파일로 | +| **완료** | B-3 폴더 비교에서 다음 다른 파일로 | | 대기 | B-4 외부 편집기로 열기 | ### C. 화면 구성 정리 diff --git a/src/MergeHub/Services/GitWorkspace.cs b/src/MergeHub/Services/GitWorkspace.cs index f4f9658..89f77f6 100644 --- a/src/MergeHub/Services/GitWorkspace.cs +++ b/src/MergeHub/Services/GitWorkspace.cs @@ -13,6 +13,24 @@ using UbisamBase.Core.SettingItems; namespace MergeHub.Services { + /// + /// 줄 단위로 견줄 파일 한 짝. 폴더 비교가 파일 비교로 넘기는 목록의 단위다. + /// + public sealed class ComparePair + { + public ComparePair(string left, string right, string name) + { + Left = left ?? string.Empty; + Right = right ?? string.Empty; + Name = name ?? string.Empty; + } + + public string Left { get; } + public string Right { get; } + + /// 화면에 적을 이름. 대개 상대 경로다. + public string Name { get; } + } /// /// 홈(머지툴) 탭과 Git 탭이 함께 쓰는 저장소 상태. /// IGitService 는 스레드 안전하지 않으므로 모든 호출을 여기서 한 줄로 직렬화한다. @@ -408,6 +426,34 @@ namespace MergeHub.Services OnUi(DeliverPendingPlainCompare); } + // ================= 폴더 비교에서 훑는 중인 파일 목록 ================= + + /// + /// 폴더 비교가 넘긴 "지금 훑는 중인 파일들". 파일 비교 화면의 이전/다음 파일이 이걸 딛는다. + /// + /// 화면에 보이는 순서 그대로 담는다 - 필터로 "코드가 다른 것만" 을 걸어 뒀으면 + /// 그 안에서만 움직여야 한다. 목록에 없는 파일로 넘어가면 왜 거기로 갔는지 알 수 없다. + /// + public IReadOnlyList Queue { get; private set; } = Array.Empty(); + + /// 지금 몇 번째를 보고 있는지. 목록이 없으면 -1. + public int QueueIndex { get; set; } = -1; + + /// 목록이나 자리가 바뀌었다. + public event EventHandler? QueueChanged; + + public void SetQueue(IReadOnlyList queue, int index) + { + Queue = queue ?? Array.Empty(); + QueueIndex = index; + OnUi(() => QueueChanged?.Invoke(this, EventArgs.Empty)); + } + + /// 목록을 비운다. 폴더를 거치지 않고 연 파일은 딛고 갈 자리가 없다. + public void ClearQueue() => SetQueue(Array.Empty(), -1); + + public void RaiseQueueChanged() => OnUi(() => QueueChanged?.Invoke(this, EventArgs.Empty)); + /// 넘긴 파일에서 짚어 줄 자리. 없으면 0. public int RequestedLine { get; private set; } diff --git a/src/MergeHub/Services/ShortcutMap.cs b/src/MergeHub/Services/ShortcutMap.cs index fae5dd4..c8232d5 100644 --- a/src/MergeHub/Services/ShortcutMap.cs +++ b/src/MergeHub/Services/ShortcutMap.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Windows.Input; @@ -20,6 +20,8 @@ namespace MergeHub.Services Recompare, GoToLine, NextBookmark, + PrevFile, + NextFile, ToggleBookmark, AcceptBlock, NextConflict, @@ -133,6 +135,12 @@ namespace MergeHub.Services new ShortcutEntry(ShortcutAction.NextBookmark, Key.F2, ModifierKeys.None, "이동·북마크", "다음 북마크 (대체 키)"), new ShortcutEntry(ShortcutAction.GoToLine, Key.G, ModifierKeys.Control, "이동·북마크", "줄 번호로 이동"), + new ShortcutEntry(ShortcutAction.PrevFile, Key.PageUp, ModifierKeys.Control, "이동·북마크", + "폴더 비교 목록에서 앞 파일로"), + new ShortcutEntry(ShortcutAction.NextFile, Key.PageDown, ModifierKeys.Control, "이동·북마크", + "폴더 비교 목록에서 다음 파일로", + "폴더 비교에서 파일을 열었을 때만 동작한다. 파일 하나를 보고 폴더 탭으로 돌아갔다 다시 여는 왕복을 없애려는 것이다."), + new ShortcutEntry(ShortcutAction.OpenFolderCompare, Key.D, ModifierKeys.Control, "파일·보기", "폴더 비교 열기", "WinMerge 는 새 비교 창을 띄우지만 여기서는 탭 하나로 돌아간다 - 창을 여러 개 띄우는 도구가 아니다."), new ShortcutEntry(ShortcutAction.Recompare, Key.F5, ModifierKeys.None, "파일·보기", "다시 비교(새로고침)"), diff --git a/src/MergeHub/ViewModels/FolderViewModel.cs b/src/MergeHub/ViewModels/FolderViewModel.cs index 13f8dd4..6730795 100644 --- a/src/MergeHub/ViewModels/FolderViewModel.cs +++ b/src/MergeHub/ViewModels/FolderViewModel.cs @@ -540,6 +540,19 @@ namespace MergeHub.ViewModels return; } + // 찾은 줄 목록도 훑을 목록이 된다. 같은 파일이 여러 줄 걸렸으면 한 번만 담는다 - + // 다음 파일을 눌렀는데 같은 파일이 다시 열리면 멈춘 것처럼 보인다. + var files = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var one in SearchHits) + { + if (seen.Add(one.RelativePath)) + files.Add(new ComparePair(one.LeftPath, one.RightPath, one.RelativePath)); + } + + _workspace.SetQueue(files, files.FindIndex(p => + string.Equals(p.Name, hit.RelativePath, StringComparison.OrdinalIgnoreCase))); + OpenInDiffRequested?.Invoke(this, (hit.LeftPath, hit.RightPath)); // 오른쪽에서만 나온 줄이면 오른쪽 파일 기준 번호다. @@ -629,6 +642,9 @@ namespace MergeHub.ViewModels return; } + // 지금 화면에 보이는 순서대로 목록을 넘긴다. 파일 비교 화면이 이걸로 이전/다음 파일을 딛는다. + SendQueue(node); + OpenInDiffRequested?.Invoke(this, (node.LeftPath, node.RightPath)); // 요청을 먼저 걸고 탭을 연다. 파일 비교 화면이 아직 없으면 그 화면이 만들어지면서 @@ -637,6 +653,31 @@ namespace MergeHub.ViewModels UbisamBase.Core.Modules.TabManager.Activate(ViewIds.Home); } + /// + /// 지금 목록에서 열 수 있는 파일을 보이는 차례대로 모아 넘긴다. + /// + /// 필터를 걸어 뒀으면 그 안에서만 움직여야 한다 - 목록에 없는 파일로 넘어가면 + /// 왜 거기로 갔는지 알 수 없다. 그래서 원본 결과가 아니라 지금 트리를 훑는다. + /// + private void SendQueue(FolderNode opened) + { + var list = new List(); + foreach (var root in Nodes) Collect(root, list); + + var at = list.FindIndex(p => + string.Equals(p.Left, opened.LeftPath, StringComparison.OrdinalIgnoreCase) && + string.Equals(p.Right, opened.RightPath, StringComparison.OrdinalIgnoreCase)); + + _workspace.SetQueue(list, at); + } + + private static void Collect(FolderNode node, List into) + { + if (node.CanOpenInDiff) into.Add(new ComparePair(node.LeftPath, node.RightPath, node.RelativePath)); + + foreach (var child in node.Children) Collect(child, into); + } + private bool CanCopyToRight() => SelectedNode is { } node && node.Item.LeftExists; [RelayCommand(CanExecute = nameof(CanCopyToRight))] diff --git a/src/MergeHub/ViewModels/HomeViewModel.cs b/src/MergeHub/ViewModels/HomeViewModel.cs index d4736d4..770bea9 100644 --- a/src/MergeHub/ViewModels/HomeViewModel.cs +++ b/src/MergeHub/ViewModels/HomeViewModel.cs @@ -38,6 +38,7 @@ namespace MergeHub.ViewModels _workspace.StateChanged += RaiseCanExecuteChanged; _workspace.PlainCompareRequested += OnPlainCompareRequested; _workspace.Recent.Changed += (_, _) => _workspace.OnUi(RefreshRecent); + _workspace.QueueChanged += (_, _) => RefreshQueue(); _reflowTimer.Tick += (_, _) => Reflow(); RefreshRecent(); @@ -190,6 +191,57 @@ namespace MergeHub.ViewModels /// 비교 대상 파일 목록(충돌 중이면 충돌 파일, 아니면 선택 커밋의 파일). public ObservableCollection Files { get; } = new ObservableCollection(); + // ================= 폴더에서 훑는 중인 파일 이동 ================= + + /// 훑을 목록이 있는지. 폴더를 거치지 않고 연 파일에는 없다. + public bool HasFileQueue => _workspace.Queue.Count > 0 && _workspace.QueueIndex >= 0; + + /// 지금 몇 번째인지. 예: 3 / 27. + public string FileQueuePosition => + HasFileQueue ? $"{_workspace.QueueIndex + 1} / {_workspace.Queue.Count}" : string.Empty; + + private bool CanPrevFile() => HasFileQueue && _workspace.QueueIndex > 0; + private bool CanNextFile() => HasFileQueue && _workspace.QueueIndex < _workspace.Queue.Count - 1; + + [RelayCommand(CanExecute = nameof(CanPrevFile))] + private void PrevFile() => MoveFile(-1); + + [RelayCommand(CanExecute = nameof(CanNextFile))] + private void NextFile() => MoveFile(1); + + /// + /// 폴더 비교 목록에서 앞뒤 파일로 옮긴다. + /// + /// 여기서 먼저 묻고 그 다음에 자리를 옮긴다. 확인 창에서 취소하면 자리도 그대로여야 하는데, + /// 자리를 먼저 옮기면 취소했을 때 목록 위치만 앞서 나간다. + /// 물은 뒤에는 _suspendGuard 로 막는다 - 여는 길(RequestPlainCompare)이 + /// 같은 것을 한 번 더 묻기 때문이다. + /// + private void MoveFile(int delta) + { + var next = _workspace.QueueIndex + delta; + if (next < 0 || next >= _workspace.Queue.Count) return; + + var pair = _workspace.Queue[next]; + if (!ConfirmDiscard($"다른 파일을 엽니다: {pair.Name}")) return; + + _workspace.QueueIndex = next; + + _suspendGuard = true; + try { _workspace.RequestPlainCompare(pair.Left, pair.Right); } + finally { _suspendGuard = false; } + + RefreshQueue(); + } + + private void RefreshQueue() + { + OnPropertyChanged(nameof(HasFileQueue)); + OnPropertyChanged(nameof(FileQueuePosition)); + PrevFileCommand.NotifyCanExecuteChanged(); + NextFileCommand.NotifyCanExecuteChanged(); + } + // ================= 최근 비교 목록 ================= /// 최근에 견준 파일 짝. 위 줄의 목록에 뿌린다. @@ -218,6 +270,9 @@ namespace MergeHub.ViewModels return; } + // 최근 목록으로 연 파일은 딛고 갈 목록이 없다. 남아 있던 목록을 그대로 두면 + // 다음 파일이 엉뚱한 데로 간다. + _workspace.ClearQueue(); _workspace.RequestPlainCompare(pair.Left, pair.Right); } diff --git a/src/MergeHub/Views/HomeView.xaml b/src/MergeHub/Views/HomeView.xaml index 8b93dbc..9441940 100644 --- a/src/MergeHub/Views/HomeView.xaml +++ b/src/MergeHub/Views/HomeView.xaml @@ -397,6 +397,17 @@ Background="Transparent" AllowDrop="True" DragOver="View_DragOver" Drop="RightFile_Drop"/> + +