diff --git a/doc/history/2026-09-14-6차.md b/doc/history/2026-09-14-6차.md
index e38a7f4..384179f 100644
--- a/doc/history/2026-09-14-6차.md
+++ b/doc/history/2026-09-14-6차.md
@@ -654,3 +654,40 @@ MERGED 는 git 이 충돌 표시를 넣어 둔 파일이라, 두 칸으로는
- 자동 풀기는 Git 탭 [Merge] 에만 걸었다. pull · rebase · cherry-pick 이 낸 충돌에는 아직 걸지 않았다.
- MERGED 칸에서 반영하며 새로 넣은 주석 줄은, 다시 정렬하면(타이핑 멈춤 · 규칙 변경) "무시" 색이 된다. 차이 수에는 원래도 세지 않던 줄이다.
+
+## B-6. 충돌 탭에서 하나씩 누를 때마다 오래 기다린다 (2026-09-14, 완료)
+
+### 고치기 전에 다시 났다
+
+새로 만든 저장소를 병합(충돌 24)하고, 같은 줄 충돌 15개(`Vision/Unit10.cs` ~ `Motion/Unit24.cs`)를 충돌 탭에서 **하나씩 골라 Accept Mine**.
+
+| 본 것 | 값 |
+|---|---|
+| 15개 합계 | **22.0초** |
+| 한 개 평균 | **1.47초** (자동 조작의 고르기 0.3초 포함) |
+| 여러 개 고르기 | 안 됨 - 목록이 한 개만 고른다 |
+
+### 무엇이 원인이었나
+
+- 목록이 `SelectionMode` 기본값(한 개)이었다.
+- Accept 한 번이 작업 하나(`RunAndRefreshAsync`)라, 끝날 때마다 저장소 전체를 다시 읽었다.
+
+### 고친 것
+
+| 파일 | 무엇 |
+|---|---|
+| `Views/GitView.xaml` | 충돌 목록 `SelectionMode="Extended"`(Ctrl · Shift · Ctrl+A). 단추 옆에 "N개 고름 - Accept 는 고른 파일 모두에 한 번에 적용됩니다". 안내 글에 여러 개 고르기 |
+| `Views/GitView.xaml.cs` | `SelectionChanged` 에서 고른 목록을 넘긴다(SelectedItems 는 바인딩이 안 된다) |
+| `ViewModels/GitViewModel.cs` | `SetSelectedConflicts` · `ConflictTargets`. Accept Mine/Theirs/Both 가 고른 파일 모두를 **작업 하나**로 풀고 다시 읽기는 끝에 한 번. 한 파일이 실패해도 나머지는 풀고, 실패는 로그 · 알림. 로그 "충돌 N개를 Mine 로 한 번에 풀었습니다 (… ms)". Accept Both 확인 창은 "N개 파일 (첫 파일 외 N-1개)" |
+
+### 확인한 것 (같은 저장소 · 같은 15개)
+
+| 확인 | 결과 |
+|---|---|
+| 15개를 골라 Accept Mine 한 번 | "15개 고름" 표시. git 반영 **0.8초**, 목록이 9개로 다시 설 때까지 **1.2초** (고치기 전 22.0초) |
+| 로그 | "충돌 15개를 Mine 로 한 번에 풀었습니다 (224ms · 저장소 다시 읽기는 끝에 한 번)" |
+| 결과 | 충돌 24 → 9. 15개 모두 내 쪽(HEAD) 글과 같음 |
+
+(자동 조작은 목록이 화면에 보이는 항목만 만들어 두는 탓에 처음에는 10개만 골랐다. 목록 항목을 모두 불러오게 고쳐 15개로 다시 쟀다.)
+
+빌드 경고 0 · 오류 0.
diff --git a/doc/history/changelog.md b/doc/history/changelog.md
index ee8381e..1ae29ea 100644
--- a/doc/history/changelog.md
+++ b/doc/history/changelog.md
@@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 |
|---|---|---|
+| 2026-09-14 | **6차 B-6** — 충돌 탭에서 여러 파일을 골라(Ctrl · Shift) Accept 를 한 번에. 작업 하나로 풀고 저장소 다시 읽기는 끝에 한 번. 같은 줄 충돌 15개 22.0초 → 1.2초 | 아래 |
| 2026-09-14 | **6차 B-5** — 머지 주석을 비교에서 무시(주석 줄은 거르고 줄 끝 주석은 떼고 견줌). 주석만 다른 git 충돌은 양쪽 주석을 남겨 자동으로 풂. 정렬이 주석 줄과 코드 줄을 짝지으면 떼어 놓음. 폴더 비교도. 설정 [머지 주석] 에서 끔 | 아래 |
| 2026-09-14 | **6차 B-4 확인 중 발견 두 건** — "마지막 차이" 로 멀리 건너뛰면 표시층 보정이 화면 밖 행으로 재져 네 칸이 9줄 위로 밀리던 것을 고침. `git mergetool` 이 넘긴 세 벌을 두 칸이 아닌 네 칸으로 열고 결과를 MERGED 파일에 쓴다 | 아래 |
| 2026-09-14 | **6차 B-4** — 3분할 MERGED 칸 줄 번호 자리가 20px 라 2949 가 "294" 로 잘리던 것을 고침. 네 칸 여백을 54 → 66px 로 맞춰 넓혀 다섯 자리까지 보인다 | 아래 |
diff --git a/doc/진행상황.md b/doc/진행상황.md
index 8992d68..ce10bae 100644
--- a/doc/진행상황.md
+++ b/doc/진행상황.md
@@ -109,7 +109,7 @@
| **완료** | B-4 MERGED 칸 줄 번호가 네 자리에서 잘린다 — 네 칸 여백을 66px 로, MERGED 번호 자리 20 → 36px(다섯 자리) |
| **완료** | B-4 확인 중 발견 두 건 — 멀리 건너뛰면 네 칸이 위로 밀려 아래가 빔 · `git mergetool` 세 벌이 두 칸으로 열림 |
| **완료** | B-5 머지 주석은 소스에 넣고, 병합 뒤에는 그 줄을 무시한다 — 비교(두 칸 · 네 칸 · 폴더)에서 가리고, 주석만 다른 git 충돌은 양쪽 주석을 남겨 자동으로 푼다. 설정에서 끔 |
-| 대기 | B-6 충돌 탭에서 하나씩 누를 때마다 오래 기다린다 |
+| **완료** | B-6 충돌 탭에서 하나씩 누를 때마다 오래 기다린다 — 여러 개 골라 Accept 한 번, 다시 읽기는 끝에 한 번. 15개 22.0초 → 1.2초 |
| 대기 | B-7 상태바가 병합 중에 믿을 수 없는 값을 보인다 |
| 대기 | B-8 병합 커밋 뒤 파일 비교 탭이 어정쩡하게 남는다 |
diff --git a/src/MergeHub/ViewModels/GitViewModel.cs b/src/MergeHub/ViewModels/GitViewModel.cs
index 90c6fb7..438e3d0 100644
--- a/src/MergeHub/ViewModels/GitViewModel.cs
+++ b/src/MergeHub/ViewModels/GitViewModel.cs
@@ -276,7 +276,29 @@ namespace MergeHub.ViewModels
});
});
- private bool HasConflict() => SelectedConflict is not null;
+ ///
+ /// 충돌 목록에서 고른 파일들(6차 B-6). Ctrl · Shift 로 여러 개를 고를 수 있다.
+ /// ListBox.SelectedItems 는 바인딩이 안 돼 화면이 고를 때마다 넘겨준다.
+ ///
+ private IReadOnlyList _selectedConflicts = Array.Empty();
+
+ public void SetSelectedConflicts(IReadOnlyList conflicts)
+ {
+ _selectedConflicts = conflicts ?? Array.Empty();
+ OnPropertyChanged(nameof(ConflictSelectionText));
+ RaiseCanExecuteChanged();
+ }
+
+ /// Accept 가 적용될 파일들. 여러 개를 골랐으면 그것들, 아니면 고른 하나.
+ private IReadOnlyList ConflictTargets =>
+ _selectedConflicts.Count > 0 ? _selectedConflicts
+ : SelectedConflict is { } one ? new[] { one } : Array.Empty();
+
+ /// 여러 개를 골랐을 때 단추 옆에 띄우는 한 줄.
+ public string ConflictSelectionText =>
+ ConflictTargets.Count > 1 ? $"{ConflictTargets.Count}개 고름 - Accept 는 고른 파일 모두에 한 번에 적용됩니다" : string.Empty;
+
+ private bool HasConflict() => ConflictTargets.Count > 0;
[RelayCommand(CanExecute = nameof(HasConflict))]
private Task AcceptMineAsync() => ResolveAsync(ConflictResolution.Mine);
@@ -289,7 +311,10 @@ namespace MergeHub.ViewModels
{
// 파일 단위 "둘 다" 는 파일 두 벌을 통째로 이어 붙인다. 양쪽이 같은 이름으로 새로 만든 파일이면
// 같은 형식이 두 번 나와 빌드가 안 된다(5차 C-1 에서 그랬다). 누르기 전에 무엇이 일어나는지 알린다(6차 A-5).
- var path = SelectedConflict?.Path ?? string.Empty;
+ var targets = ConflictTargets;
+ var path = targets.Count <= 1
+ ? targets.FirstOrDefault()?.Path ?? string.Empty
+ : $"{targets.Count}개 파일 ({targets[0].Path} 외 {targets.Count - 1}개)";
var go = MessageUtil.ShowYesNo(
"파일 두 벌을 이어 붙입니다",
$"{path}\n\n내 것 다음에 상대 것을 한 파일로 이어 붙입니다. 내 파일의 인코딩과 줄끝으로 씁니다.\n\n" +
@@ -317,9 +342,50 @@ namespace MergeHub.ViewModels
_workspace.RequestOpenConflict(conflict.Path);
}
- private Task ResolveAsync(ConflictResolution resolution) =>
- _workspace.RunAndRefreshAsync($"충돌 해결({resolution})",
- () => _workspace.Git.ResolveConflict(SelectedConflict!.Path, resolution));
+ ///
+ /// 고른 충돌 파일들을 한 작업으로 푼다(6차 B-6).
+ ///
+ /// 예전에는 파일 하나마다 작업 하나였고, 작업이 끝날 때마다 저장소 전체를 다시 읽었다(한 개 약 1.5초).
+ /// 이제는 고른 파일을 모두 풀고 다시 읽기는 끝에 한 번이다.
+ /// 한 파일이 실패해도 나머지는 계속 풀고, 실패한 것은 로그와 알림으로 알린다.
+ ///
+ private Task ResolveAsync(ConflictResolution resolution)
+ {
+ var targets = ConflictTargets.ToList();
+ if (targets.Count == 0) return Task.CompletedTask;
+
+ var label = targets.Count == 1 ? $"충돌 해결({resolution})" : $"충돌 해결({resolution}) {targets.Count}개";
+
+ return _workspace.RunAndRefreshAsync(label, () =>
+ {
+ var failed = new List();
+ var watch = Stopwatch.StartNew();
+
+ foreach (var conflict in targets)
+ {
+ try
+ {
+ _workspace.Git.ResolveConflict(conflict.Path, resolution);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ failed.Add($"{conflict.Path}: {ex.Message}");
+ }
+ }
+
+ watch.Stop();
+
+ _workspace.OnUi(() =>
+ {
+ if (targets.Count > 1)
+ _workspace.AppendLog($"충돌 {targets.Count - failed.Count}개를 {resolution} 로 한 번에 풀었습니다 " +
+ $"({watch.ElapsedMilliseconds}ms · 저장소 다시 읽기는 끝에 한 번)");
+
+ foreach (var line in failed) _workspace.AppendLog("충돌 해결 실패: " + line);
+ if (failed.Count > 0) MessageUtil.ShowWarningToast($"{failed.Count}개는 풀지 못했습니다 - 로그를 보세요");
+ });
+ });
+ }
// ================= 이력 조작 =================
diff --git a/src/MergeHub/Views/GitView.xaml b/src/MergeHub/Views/GitView.xaml
index 998d6ed..431659a 100644
--- a/src/MergeHub/Views/GitView.xaml
+++ b/src/MergeHub/Views/GitView.xaml
@@ -240,13 +240,15 @@
+ Text="파일 전체를 한쪽으로 채택하려면 아래 버튼을 쓰고, 줄마다 고르려면 목록을 두 번 누르거나 [3분할로 열기] 를 누르세요. 파일 비교 탭에서 세 칸으로 열립니다. Ctrl · Shift 로 여러 파일을 고르면 Accept 가 한 번에 적용됩니다(Ctrl+A 전부)."/>
+
@@ -291,6 +293,8 @@
Background="{DynamicResource Ubisam.Brush.Warning}"
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"/>
+
diff --git a/src/MergeHub/Views/GitView.xaml.cs b/src/MergeHub/Views/GitView.xaml.cs
index a0744b1..b28f02b 100644
--- a/src/MergeHub/Views/GitView.xaml.cs
+++ b/src/MergeHub/Views/GitView.xaml.cs
@@ -1,4 +1,5 @@
-using System.Windows;
+using System.Linq;
+using System.Windows;
using System.Windows.Controls;
using MergeHub.ViewModels;
@@ -44,6 +45,13 @@ namespace MergeHub.Views
if (DataContext is MergeHub.ViewModels.GitViewModel vm && vm.OpenConflictInCompareCommand.CanExecute(null))
vm.OpenConflictInCompareCommand.Execute(null);
}
+ /// 고른 충돌 파일들을 넘긴다(6차 B-6). ListBox.SelectedItems 는 바인딩할 수 없다.
+ private void Conflicts_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (sender is not ListBox list || ViewModel is not { } vm) return;
+ vm.SetSelectedConflicts(list.SelectedItems.OfType().ToList());
+ }
+
private void Password_Changed(object sender, RoutedEventArgs e)
{
if (sender is PasswordBox box && ViewModel is { } vm) vm.Workspace.Password = box.Password;