11차 A-4: 변경분을 폴더에 더해 ZIP 으로도

사용자 결정대로 뽑은 폴더 옆에 같은 이름 .zip 을 만든다. 안에 폴더와 안내 파일을 나란히 넣어 풀면 폴더로 뽑은 것과 같은 모양이고, 파일은 바이트 그대로 · 경로는 / · 한글 이름은 UTF-8 표시. .part 에 쓰고 옮겨 반쯤 된 ZIP 을 남기지 않고, 못 만들어도 추출은 성공으로 둔다. UniquePath 는 같은 이름 .zip 이 남아 있어도 그 이름을 쓴 것으로 본다.

하네스 ReportTest +13 (70건, 22벌 1,260건) · 빌드 경고 0 · 오류 0. 아홉 경우를 화면에서 다시 뽑아 ZIP 8/8 을 Windows tar 로 풀어 폴더와 경로 · 해시가 같음, 앱이 만든 ZIP 의 한글 이름 UTF-8 표시 확인. 웹 문서 버전 14. 기획안은 사용자 잠금 대기.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
김승준 2026-09-22 13:34:39 +09:00
parent d2709e3d4e
commit a99ca970e9
8 changed files with 148 additions and 24 deletions

View File

@ -124,8 +124,34 @@ A-1 에서 값으로 확인한 것 — 지운 파일은 "삭제되어 건너뜀
### 남은 것 ### 남은 것
- A-4(ZIP)는 아직 정하지 않았다. - A-4(ZIP)는 아래(09-22).
- 매뉴얼 · 웹 문서의 변경분 추출 설명(새 폴더 · 안내 파일)은 E-2 에서 함께 고친다. - 매뉴얼 · 웹 문서의 변경분 추출 설명(새 폴더 · 안내 파일)은 E-2 에서 함께 고쳤다.
## A-4. 변경분을 ZIP 으로도 (2026-09-22, 완료)
사용자 결정: **폴더에 더해 ZIP 도**(권장안). 장비에는 폴더를 넣고, 메일 · 메신저로 보낼 때 ZIP 을 쓴다.
### 만든 것
| 자리 | 한 일 |
|---|---|
| `Services/ExportZip.cs` (새) | `PathFor` — 뽑은 폴더 옆, 폴더 이름 + `.zip`. `Create` — 안에 **뽑은 폴더와 안내 파일을 나란히** 넣는다(풀면 폴더로 뽑은 것과 같은 모양, 안내가 소스 틈에 섞이지 않는 A-2 결정 그대로). 파일은 바이트 그대로 · 경로는 `/` · 수정 시각 그대로. `.part` 이름에 쓰고 다 되면 옮겨, 쓰다 막혀도 반쯤 된 ZIP 이 남지 않는다 |
| `Services/ExportPlan.cs` | `UniquePath` 가 같은 이름의 `.zip` 이 남아 있어도 그 이름을 쓴 것으로 본다 — 폴더만 지우고 ZIP 을 남긴 채 다시 뽑으면 옛 ZIP 을 덮을 뻔했다 |
| `ViewModels/GitViewModel.cs` | 폴더 · 안내를 쓴 뒤 ZIP 을 만든다. 못 만들어도 폴더는 이미 다 뽑혔으므로 추출은 성공(안내와 같은 규칙), 로그에 까닭. 알림 끝에 "· ZIP 도" / "· ZIP 은 못 만듦(로그)" |
| `MergeHub.csproj` | `System.IO.Compression` 참조 — .NET Framework 에 들어 있어 배포 묶음 파일이 늘지 않는다(빌드 출력에 새 dll 없음) |
| 하네스 `ReportTest` | **+13건(57 → 70)** — ZIP 자리 · 넣은 수 · 풀면 같은 이름 폴더 · 경로 · 해시 목록이 폴더와 같음 · 안내는 폴더 밖 · 안내 바이트 · 빈 파일 · `/` 경로 · 한글 경로 · 한글 이름 UTF-8 표시(bit 11) · 같은 이름 ZIP 이 있으면 `_2`(폴더가 없어도). 고치기 전에 넣어 "폴더 없이 ZIP 만 남은 경우" 가 실패하는 것을 보고 시작 |
### 확인 — 화면에서 아홉 경우 다시 (`e11-a1-setup.sh` · `e11-e1-a.ps1` · `e11-a4-verify.sh`)
- 모든 경우 알림 끝에 **"· ZIP 도"**. 새 폴더 8개마다 옆에 ZIP.
- ZIP 을 앱과 **다른 도구**(Windows `tar.exe`)로 풀어 뽑은 폴더와 견줌 — **8/8 경로 · SHA-256 목록 같음**, 안내 파일 바이트 같음 · 폴더 밖.
- 앱(.NET Framework 4.8)이 만든 ZIP 의 이름 칸 — `변경분_…/한글폴더/깊은/경로/아래/설명.cs` 까지 모두 **UTF-8 표시(bit 11) 켜짐**. 하네스는 .NET 8 에서 돌므로 이 표시는 실제 앱이 만든 파일에서 따로 확인했다.
- 하네스 22벌 **1,260건**(1,247 → +13) · 빌드 경고 0 · 오류 0 · 설정 해시 같음.
### 문서
- 웹 문서(버전 14): 매뉴얼 "변경분 추출" 에 ZIP 한 단계, 남은 일 A-4 완료, "기획안에서 아직 안 한 것" 에서 ZIP 뺌, 기획안 v2 표 변경분 추출 완료, 숫자 1,260건 · 하네스 막대 폭 다시 잼.
- 기획안: 변경분 파일 추출 "일부" → **완료**, Git 작업 버튼 줄 완료, 1,260건. 8쪽 A4 높이 그대로. **잠금은 사용자**(대기).
## B-1. 배포 묶음을 다시 만들고 꼭 필요한 파일을 가림 (2026-09-22, 완료) ## B-1. 배포 묶음을 다시 만들고 꼭 필요한 파일을 가림 (2026-09-22, 완료)

View File

@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 | | 날짜 | 항목 | 커밋 |
|---|---|---| |---|---|---|
| 2026-09-22 | **11차 A-4** — 변경분을 폴더에 더해 **ZIP 으로도** 뽑는다(사용자 결정). 뽑은 폴더 옆에 같은 이름 `.zip`, 안에 폴더와 안내 파일을 나란히(풀면 폴더로 뽑은 것과 같은 모양). 바이트 · 경로 그대로, 한글 이름은 UTF-8 표시, 반쯤 된 ZIP 은 남기지 않음, 못 만들어도 추출은 성공. 같은 이름 ZIP 이 남아 있으면 새 이름을 고름. 아홉 경우를 다시 뽑아 ZIP 8/8 을 Windows tar 로 풀어 폴더와 경로 · 해시가 같음. 하네스 ReportTest +13(70건). .NET Framework 기본 기능이라 배포 묶음 파일은 그대로 | 아래 |
| 2026-09-22 | **11차 D-1** — 시험 도구 둘(제품 아님). 줄끝 검사 스크립트가 부모 하나인 커밋(rebase · cherry-pick · revert)에서 없는 둘째 부모로 `git merge-base` 를 불러 오류를 찍던 것 → 첫 부모와만 견준다고 적고 부르지 않음. 파일 비교 탭 요약을 읽는 함수가 제품의 "차이 없음" 을 못 읽어 커밋 뒤 요약이 빈칸이던 것 → 읽음. rebase 한 길을 다시 돌려 오류 줄 0 · 요약 "차이 없음". 제품 코드 변경 없음 | 아래 | | 2026-09-22 | **11차 D-1** — 시험 도구 둘(제품 아님). 줄끝 검사 스크립트가 부모 하나인 커밋(rebase · cherry-pick · revert)에서 없는 둘째 부모로 `git merge-base` 를 불러 오류를 찍던 것 → 첫 부모와만 견준다고 적고 부르지 않음. 파일 비교 탭 요약을 읽는 함수가 제품의 "차이 없음" 을 못 읽어 커밋 뒤 요약이 빈칸이던 것 → 읽음. rebase 한 길을 다시 돌려 오류 줄 0 · 요약 "차이 없음". 제품 코드 변경 없음 | 아래 |
| 2026-09-22 | **11차 E-2** — 웹 문서(버전 13)와 기획안(버전 17)을 11차에 맞춤. 두 발행본 모두 로컬 파일과 같음을 먼저 확인. 매뉴얼에 "커밋 메시지 고치기"([커밋 수정] 과 우클릭 [메시지 바꾸기...] 를 한 표로) · "변경분 추출"(새 폴더 · 안내 파일) 두 절과 한계 셋, 남은 일 · 만든 것에 11차, 숫자 1,247건 · 커밋 129. 단추 이름 91개를 소스에서 찾아 09-18 에 적은 "맞추지 않음" 을 실제 글자 "C# 스타일 끔" 으로 바로잡음. 기획안은 커밋 메시지 수정 완료 · 4단계 준비 중 · 개정 09-22, 8쪽 A4 높이 그대로, 잠금은 사용자가 비밀번호로 | 아래 | | 2026-09-22 | **11차 E-2** — 웹 문서(버전 13)와 기획안(버전 17)을 11차에 맞춤. 두 발행본 모두 로컬 파일과 같음을 먼저 확인. 매뉴얼에 "커밋 메시지 고치기"([커밋 수정] 과 우클릭 [메시지 바꾸기...] 를 한 표로) · "변경분 추출"(새 폴더 · 안내 파일) 두 절과 한계 셋, 남은 일 · 만든 것에 11차, 숫자 1,247건 · 커밋 129. 단추 이름 91개를 소스에서 찾아 09-18 에 적은 "맞추지 않음" 을 실제 글자 "C# 스타일 끔" 으로 바로잡음. 기획안은 커밋 메시지 수정 완료 · 4단계 준비 중 · 개정 09-22, 8쪽 A4 높이 그대로, 잠금은 사용자가 비밀번호로 | 아래 |
| 2026-09-22 | **11차 E-1** — 11차에서 바꾼 길을 사람 순서로 다시 끝까지(배포한 플랫폼 위에서). A-1 아홉 경우: 지운 파일 · 이름 바뀐 옛 경로 · 범위가 안내에 남고, 같은 폴더에 두 번 뽑아도 새 폴더 둘로 섞이지 않고, 읽기 전용 · 잠긴 파일이 있어도 막힘 없이 끝남(모두 바이트 같음). C-1 여덟 경우: HEAD 메시지 · 작성자 보존 · 스테이징 · 올린 커밋 묻는 창 · 작업 중 꺼짐 · 병합 커밋 부모 둘. C-2 로 이력을 다시 쓴 저장소(트리 · 작성자 · 시각 4/4 같음)에서 병합 한 길. 다섯 길 + pull 모두 누른 수 = 로그, 줄끝 · BOM · 충돌 표식 · class 0, 두 탭 같은 커밋. **새로 걸린 것 없음.** 시험 도구가 창을 앞으로 부르며 최대화를 풀어 첫 누름이 빗나가던 것을 고침(제품 아님). 제품 코드 변경 없음 | 아래 | | 2026-09-22 | **11차 E-1** — 11차에서 바꾼 길을 사람 순서로 다시 끝까지(배포한 플랫폼 위에서). A-1 아홉 경우: 지운 파일 · 이름 바뀐 옛 경로 · 범위가 안내에 남고, 같은 폴더에 두 번 뽑아도 새 폴더 둘로 섞이지 않고, 읽기 전용 · 잠긴 파일이 있어도 막힘 없이 끝남(모두 바이트 같음). C-1 여덟 경우: HEAD 메시지 · 작성자 보존 · 스테이징 · 올린 커밋 묻는 창 · 작업 중 꺼짐 · 병합 커밋 부모 둘. C-2 로 이력을 다시 쓴 저장소(트리 · 작성자 · 시각 4/4 같음)에서 병합 한 길. 다섯 길 + pull 모두 누른 수 = 로그, 줄끝 · BOM · 충돌 표식 · class 0, 두 탭 같은 커밋. **새로 걸린 것 없음.** 시험 도구가 창을 앞으로 부르며 최대화를 풀어 첫 누름이 빗나가던 것을 고침(제품 아님). 제품 코드 변경 없음 | 아래 |

View File

@ -256,7 +256,7 @@ figcaption{font-size:.82rem;color:var(--ink-3);margin-top:6px}
</div> </div>
<div class="facts"> <div class="facts">
<div><b>22벌 · 1,247건</b><span>검사 하네스 전부 통과</span></div> <div><b>22벌 · 1,260건</b><span>검사 하네스 전부 통과</span></div>
<div><b>11차 · 베타 앞</b><span>변경분 안내 · 커밋 메시지 고치기 · 배포 묶음 43.5MB · D 드라이브 없는 PC. 다섯 길 다시 끝까지 줄끝 바뀐 파일 0</span></div> <div><b>11차 · 베타 앞</b><span>변경분 안내 · 커밋 메시지 고치기 · 배포 묶음 43.5MB · D 드라이브 없는 PC. 다섯 길 다시 끝까지 줄끝 바뀐 파일 0</span></div>
<div><b>129</b><span>MergeHub 로컬 커밋 (푸시 0)</span></div> <div><b>129</b><span>MergeHub 로컬 커밋 (푸시 0)</span></div>
<div><b>26 → 24</b><span>만든 저장소 병합 충돌 (주석만 다른 2건 자동 해결)</span></div> <div><b>26 → 24</b><span>만든 저장소 병합 충돌 (주석만 다른 2건 자동 해결)</span></div>
@ -488,6 +488,7 @@ UbisamBase.Shell.exe <span class="k">--merge --base</span> B.cs <span class="k">
<li><span class="btn-label">변경분 추출</span> → 폴더를 고른다.</li> <li><span class="btn-label">변경분 추출</span> → 폴더를 고른다.</li>
<li>고른 폴더 <b>아래에 새 폴더</b> <code>변경분_228995f..37a2ced_2026-09-22</code> 가 생기고, 바뀐 파일만 <b>바이트 그대로</b> 들어간다. 같은 이름이 있으면 <code>_2</code> · <code>_3</code> — 지난번에 뽑은 것이나 넣어 둔 파일과 섞이지 않는다.</li> <li>고른 폴더 <b>아래에 새 폴더</b> <code>변경분_228995f..37a2ced_2026-09-22</code> 가 생기고, 바뀐 파일만 <b>바이트 그대로</b> 들어간다. 같은 이름이 있으면 <code>_2</code> · <code>_3</code> — 지난번에 뽑은 것이나 넣어 둔 파일과 섞이지 않는다.</li>
<li>그 폴더 <b>옆에</b> 안내 파일 <code>…_변경분안내.txt</code> 가 생긴다 — 범위(커밋 · 제목 · 날짜), 뽑은 파일(추가 · 수정 · 이름 바뀜은 옛 이름), 그리고 <b>장비에서 지워야 할 파일</b>(지워진 파일 · 이름 바뀐 옛 경로). 폴더에는 없는 것이라 안내에만 있다.</li> <li>그 폴더 <b>옆에</b> 안내 파일 <code>…_변경분안내.txt</code> 가 생긴다 — 범위(커밋 · 제목 · 날짜), 뽑은 파일(추가 · 수정 · 이름 바뀜은 옛 이름), 그리고 <b>장비에서 지워야 할 파일</b>(지워진 파일 · 이름 바뀐 옛 경로). 폴더에는 없는 것이라 안내에만 있다.</li>
<li>같은 자리에 <b>ZIP</b> <code>변경분_228995f..37a2ced_2026-09-22.zip</code> 도 생긴다 — 안에 그 폴더와 안내 파일이 나란히 들어 있어, 풀면 폴더로 뽑은 것과 같은 모양이다(바이트 · 경로 같음). 장비에는 폴더를, 메일 · 메신저로 보낼 때는 ZIP 을 쓴다. 한글 경로도 탐색기에서 깨지지 않고 풀린다.</li>
</ol> </ol>
<p class="small muted">쓰기 전에 모두 쓸 수 있는지 먼저 보고, 하나라도 막히면 아무것도 쓰지 않는다. 병합 커밋 하나를 고르면 첫 부모와 견준다(안내 범위에 “(고른 커밋의 부모)”). 거꾸로 잡은 범위도 그 방향대로 뽑는다.</p> <p class="small muted">쓰기 전에 모두 쓸 수 있는지 먼저 보고, 하나라도 막히면 아무것도 쓰지 않는다. 병합 커밋 하나를 고르면 첫 부모와 견준다(안내 범위에 “(고른 커밋의 부모)”). 거꾸로 잡은 범위도 그 방향대로 뽑는다.</p>
@ -993,7 +994,7 @@ Log(<span class="s">"step 5"</span>);</pre>
<div class="tbl wide"><table> <div class="tbl wide"><table>
<tr><th>항목</th><th>상태</th><th>무엇</th></tr> <tr><th>항목</th><th>상태</th><th>무엇</th></tr>
<tr><td>A-1 · A-2 · A-3 변경분 추출</td><td><span class="chip c-done">완료</span></td><td>먼저 아홉 경우로 뽑아 보니 지운 파일 · 이름 바뀐 옛 경로 · 범위가 어디에도 안 남고, 쓰던 폴더에 섞이고, 막히면 반쯤 뽑힌 채 멈췄다. 이제 고른 폴더 아래 새 폴더에 뽑고 옆에 <b>안내 파일</b>(장비에서 지울 파일 포함), 쓰기 전에 먼저 본다</td></tr> <tr><td>A-1 · A-2 · A-3 변경분 추출</td><td><span class="chip c-done">완료</span></td><td>먼저 아홉 경우로 뽑아 보니 지운 파일 · 이름 바뀐 옛 경로 · 범위가 어디에도 안 남고, 쓰던 폴더에 섞이고, 막히면 반쯤 뽑힌 채 멈췄다. 이제 고른 폴더 아래 새 폴더에 뽑고 옆에 <b>안내 파일</b>(장비에서 지울 파일 포함), 쓰기 전에 먼저 본다</td></tr>
<tr><td>A-4 ZIP 으로 뽑기</td><td><span class="chip c-wait">대기</span></td><td>할지 먼저 정한다</td></tr> <tr><td>A-4 ZIP 으로 뽑기</td><td><span class="chip c-done">완료</span></td><td>사용자 결정: 폴더에 더해 ZIP 도. 폴더 옆에 같은 이름 <code>.zip</code>(폴더 · 안내 파일 나란히). 여덟 번 뽑아 모두 풀어 본 결과가 폴더와 경로 · 해시가 같음</td></tr>
<tr><td>B-1 배포 묶음</td><td><span class="chip c-done">완료</span></td><td>매뉴얼이 가리키던 Release 가 09-07 것이라 폴더 비교 탭이 없었다. 쓰지 않는 dll · 다른 OS 용을 빼 97MB → 43.5MB, <code>scripts\make-bundle.ps1</code> · <code>배포묶음.txt</code>. 빈 폴더에서 병합 한 길 끝까지</td></tr> <tr><td>B-1 배포 묶음</td><td><span class="chip c-done">완료</span></td><td>매뉴얼이 가리키던 Release 가 09-07 것이라 폴더 비교 탭이 없었다. 쓰지 않는 dll · 다른 OS 용을 빼 97MB → 43.5MB, <code>scripts\make-bundle.ps1</code> · <code>배포묶음.txt</code>. 빈 폴더에서 병합 한 길 끝까지</td></tr>
<tr><td>B-2 D 드라이브 없는 PC · 첫 실행</td><td><span class="chip c-done">완료</span></td><td>첫 실행은 오류 없고 기본값이 매뉴얼과 같다. D 를 못 쓰면 켜다 말고 <b>창 없는 프로세스만 남던 것</b> — 플랫폼에서 <code>%LOCALAPPDATA%\UbisamConfig</code> 로 가고, 그마저 안 되면 “켜지 못했습니다” 뒤 끝내게(배포함)</td></tr> <tr><td>B-2 D 드라이브 없는 PC · 첫 실행</td><td><span class="chip c-done">완료</span></td><td>첫 실행은 오류 없고 기본값이 매뉴얼과 같다. D 를 못 쓰면 켜다 말고 <b>창 없는 프로세스만 남던 것</b> — 플랫폼에서 <code>%LOCALAPPDATA%\UbisamConfig</code> 로 가고, 그마저 안 되면 “켜지 못했습니다” 뒤 끝내게(배포함)</td></tr>
<tr><td>C-1 [커밋 수정]</td><td><span class="chip c-done">완료</span></td><td>[마지막 메시지] 가 딴 브랜치 메시지를 가져오고 · 작성자가 바뀌고 · 작업 중에도 켜지던 것. 이제 HEAD 메시지 · 작성자 보존 · 작업 중 꺼짐 · 스테이징 파일 · 올린 커밋이면 먼저 묻기 · 알림에 옛 → 새 해시</td></tr> <tr><td>C-1 [커밋 수정]</td><td><span class="chip c-done">완료</span></td><td>[마지막 메시지] 가 딴 브랜치 메시지를 가져오고 · 작성자가 바뀌고 · 작업 중에도 켜지던 것. 이제 HEAD 메시지 · 작성자 보존 · 작업 중 꺼짐 · 스테이징 파일 · 올린 커밋이면 먼저 묻기 · 알림에 옛 → 새 해시</td></tr>
@ -1052,7 +1053,6 @@ Log(<span class="s">"step 5"</span>);</pre>
<li><b>Gitea PR · 리뷰 코멘트 인라인 표시</b> (기획 확장 범위) — 사내 Gitea 저장소가 아직 없다. 지금은 원격 주소 · 계정 설정과 Fetch · Pull · Push 까지.</li> <li><b>Gitea PR · 리뷰 코멘트 인라인 표시</b> (기획 확장 범위) — 사내 Gitea 저장소가 아직 없다. 지금은 원격 주소 · 계정 설정과 Fetch · Pull · Push 까지.</li>
<li><b>멀티 레포 대시보드 · 오프라인 캐싱</b> (확장 범위)</li> <li><b>멀티 레포 대시보드 · 오프라인 캐싱</b> (확장 범위)</li>
<li><b>Roslyn 실시간 구문 검사</b> (확장 범위) — 가벼운 코드 검사로 대신했다. Roslyn 은 붙이지 않았다.</li> <li><b>Roslyn 실시간 구문 검사</b> (확장 범위) — 가벼운 코드 검사로 대신했다. Roslyn 은 붙이지 않았다.</li>
<li><b>변경분 ZIP 내보내기</b> — 폴더로만 뽑는다.</li>
<li><b>기획 4단계</b>: 사내 베타 배포와 피드백, 단축키 가이드 포함 매뉴얼 (기획 일정 09.29 ~ 10.02).</li> <li><b>기획 4단계</b>: 사내 베타 배포와 피드백, 단축키 가이드 포함 매뉴얼 (기획 일정 09.29 ~ 10.02).</li>
<li><b>푸시</b> — 사내 Gitea 저장소가 만들어지면 그때 한 번에 올린다.</li> <li><b>푸시</b> — 사내 Gitea 저장소가 만들어지면 그때 한 번에 올린다.</li>
</ul> </ul>
@ -1069,7 +1069,7 @@ Log(<span class="s">"step 5"</span>);</pre>
<div class="doc"> <div class="doc">
<div class="facts" style="margin-top:12px"> <div class="facts" style="margin-top:12px">
<div><b>129</b><span>로컬 커밋 · 09-08 ~ 09-22</span></div> <div><b>129</b><span>로컬 커밋 · 09-08 ~ 09-22</span></div>
<div><b>1,247</b><span>검사 통과 (22벌)</span></div> <div><b>1,260</b><span>검사 통과 (22벌)</span></div>
<div><b>0 · 0</b><span>빌드 경고 · 오류</span></div> <div><b>0 · 0</b><span>빌드 경고 · 오류</span></div>
<div><b>0</b><span>푸시</span></div> <div><b>0</b><span>푸시</span></div>
</div> </div>
@ -1088,7 +1088,7 @@ Log(<span class="s">"step 5"</span>);</pre>
<li><time>09-16</time><div><b>8차 — 고른 파일에 고른 내용을</b>[3분할로 열기] 로 처음 열면 다른 파일 내용이 고른 파일에 저장되던 것 · 작업마다 요약 · 빈 cherry-pick · MERGED 결과 번호 · revert 끝까지 · 원래 작성자 · 첫 Win+↓ · 병합 커밋 본문에 해결 내역. 다섯 길 다시 끝까지. 22벌 1,094건.</div></li> <li><time>09-16</time><div><b>8차 — 고른 파일에 고른 내용을</b>[3분할로 열기] 로 처음 열면 다른 파일 내용이 고른 파일에 저장되던 것 · 작업마다 요약 · 빈 cherry-pick · MERGED 결과 번호 · revert 끝까지 · 원래 작성자 · 첫 Win+↓ · 병합 커밋 본문에 해결 내역. 다섯 길 다시 끝까지. 22벌 1,094건.</div></li>
<li><time>09-17</time><div><b>9차 — 두 칸이 무엇과 견주는지 사실대로</b>작업 중 충돌을 다 풀면 기준 HEAD · 새로고침 뒤 고른 커밋을 해시로 · 넣은 주석 줄 처음부터 무시 색 · 폴더 트리 시간 다시 재기 · 네 길을 마지막 파일까지 네 칸으로 다시 끝까지. 22벌 1,122건.</div></li> <li><time>09-17</time><div><b>9차 — 두 칸이 무엇과 견주는지 사실대로</b>작업 중 충돌을 다 풀면 기준 HEAD · 새로고침 뒤 고른 커밋을 해시로 · 넣은 주석 줄 처음부터 무시 색 · 폴더 트리 시간 다시 재기 · 네 길을 마지막 파일까지 네 칸으로 다시 끝까지. 22벌 1,122건.</div></li>
<li><time>09-17</time><div><b>10차 — 두 탭이 같은 커밋을 · 누름을 놓치는지</b>한 번 누른 것이 안 남던 것은 시험 도구 쪽으로 가림 · Git 탭 · 파일 비교 탭이 고른 커밋을 서로 바로 따라감 · 규칙 끈 채 반영 색 · 첫 폴더 비교 9.2초 다시 재기 · 폴더 비교 “같은 것만” 폴더 줄과 같음의 까닭 · “다름(코드는 같음)” · 다섯 길(+ pull) 누른 수 = 로그. 22벌 1,147건.</div></li> <li><time>09-17</time><div><b>10차 — 두 탭이 같은 커밋을 · 누름을 놓치는지</b>한 번 누른 것이 안 남던 것은 시험 도구 쪽으로 가림 · Git 탭 · 파일 비교 탭이 고른 커밋을 서로 바로 따라감 · 규칙 끈 채 반영 색 · 첫 폴더 비교 9.2초 다시 재기 · 폴더 비교 “같은 것만” 폴더 줄과 같음의 까닭 · “다름(코드는 같음)” · 다섯 길(+ pull) 누른 수 = 로그. 22벌 1,147건.</div></li>
<li><time>09-22</time><div><b>11차 — 사내 베타 앞</b>변경분 추출이 새 폴더 · 안내 파일(장비에서 지울 파일)로 · [커밋 수정] 바로잡기(HEAD 메시지 · 작성자 보존 · 먼저 묻기) · 이력 우클릭으로 앞 커밋 메시지 바꾸기 · 배포 묶음 97MB → 43.5MB · D 드라이브 없는 PC 에서도 켜짐(플랫폼) · C# 스타일 기본 AURORA · 바꾼 길 다시 끝까지. 22벌 1,247건.</div></li> <li><time>09-22</time><div><b>11차 — 사내 베타 앞</b>변경분 추출이 새 폴더 · 안내 파일(장비에서 지울 파일)로 · [커밋 수정] 바로잡기(HEAD 메시지 · 작성자 보존 · 먼저 묻기) · 이력 우클릭으로 앞 커밋 메시지 바꾸기 · 배포 묶음 97MB → 43.5MB · D 드라이브 없는 PC 에서도 켜짐(플랫폼) · C# 스타일 기본 AURORA · 바꾼 길 다시 끝까지. 22벌 1,260건.</div></li>
</ol> </ol>
<h2>6차에서 끝낸 것</h2> <h2>6차에서 끝낸 것</h2>
@ -1115,15 +1115,15 @@ Log(<span class="s">"step 5"</span>);</pre>
</table></div> </table></div>
<h2>검사 하네스</h2> <h2>검사 하네스</h2>
<div class="bars" role="img" aria-label="검사 하네스 통과 건수: 3차 514, 4차 571, 5차 698, 6차 948, 8차 1,094, 9차 1,122, 10차 1,147, 11차 1,247"> <div class="bars" role="img" aria-label="검사 하네스 통과 건수: 3차 514, 4차 571, 5차 698, 6차 948, 8차 1,094, 9차 1,122, 10차 1,147, 11차 1,260">
<div class="bar"><span class="lb">3차 · 16벌</span><span class="tr"><span class="fl" style="width:41.2%"></span></span><span class="v">514</span></div> <div class="bar"><span class="lb">3차 · 16벌</span><span class="tr"><span class="fl" style="width:40.8%"></span></span><span class="v">514</span></div>
<div class="bar"><span class="lb">4차 · 17벌</span><span class="tr"><span class="fl" style="width:45.8%"></span></span><span class="v">571</span></div> <div class="bar"><span class="lb">4차 · 17벌</span><span class="tr"><span class="fl" style="width:45.3%"></span></span><span class="v">571</span></div>
<div class="bar"><span class="lb">5차 · 20벌</span><span class="tr"><span class="fl" style="width:56.0%"></span></span><span class="v">698</span></div> <div class="bar"><span class="lb">5차 · 20벌</span><span class="tr"><span class="fl" style="width:55.4%"></span></span><span class="v">698</span></div>
<div class="bar"><span class="lb">6차 · 22벌</span><span class="tr"><span class="fl" style="width:76.0%"></span></span><span class="v">948</span></div> <div class="bar"><span class="lb">6차 · 22벌</span><span class="tr"><span class="fl" style="width:75.2%"></span></span><span class="v">948</span></div>
<div class="bar"><span class="lb">8차 · 22벌</span><span class="tr"><span class="fl" style="width:87.7%"></span></span><span class="v">1,094</span></div> <div class="bar"><span class="lb">8차 · 22벌</span><span class="tr"><span class="fl" style="width:86.8%"></span></span><span class="v">1,094</span></div>
<div class="bar"><span class="lb">9차 · 22벌</span><span class="tr"><span class="fl" style="width:90.0%"></span></span><span class="v">1,122</span></div> <div class="bar"><span class="lb">9차 · 22벌</span><span class="tr"><span class="fl" style="width:89.0%"></span></span><span class="v">1,122</span></div>
<div class="bar"><span class="lb">10차 · 22벌</span><span class="tr"><span class="fl" style="width:92.0%"></span></span><span class="v">1,147</span></div> <div class="bar"><span class="lb">10차 · 22벌</span><span class="tr"><span class="fl" style="width:91.0%"></span></span><span class="v">1,147</span></div>
<div class="bar"><span class="lb">11차 · 22벌</span><span class="tr"><span class="fl" style="width:100%"></span></span><span class="v">1,247</span></div> <div class="bar"><span class="lb">11차 · 22벌</span><span class="tr"><span class="fl" style="width:100%"></span></span><span class="v">1,260</span></div>
</div> </div>
<p class="small muted">차수 끝의 통과 건수(7차는 따로 적지 않았다). 로직을 건드리면 테스트를 먼저 늘린다.</p> <p class="small muted">차수 끝의 통과 건수(7차는 따로 적지 않았다). 로직을 건드리면 테스트를 먼저 늘린다.</p>
@ -1136,7 +1136,7 @@ Log(<span class="s">"step 5"</span>);</pre>
<tr><td>네 칸 병합</td><td>LOCAL · BASE · REMOTE · MERGED · Take Mine/Theirs · 자동 병합 · 충돌만 이동 · 완료 판정 · 한쪽 삭제 띠 · git mergetool</td><td><span class="chip c-done">완료</span></td></tr> <tr><td>네 칸 병합</td><td>LOCAL · BASE · REMOTE · MERGED · Take Mine/Theirs · 자동 병합 · 충돌만 이동 · 완료 판정 · 한쪽 삭제 띠 · git mergetool</td><td><span class="chip c-done">완료</span></td></tr>
<tr><td>Git</td><td>열기 · 클론 · 원격 · Fetch/Pull/Push · 체크아웃 · Merge · rebase · 충돌 목록 · 파일 단위 Accept · Save · 병합 커밋 · 스테이징 · 커밋 · amend · stash · 이력 그래프 · Revert · Cherry-pick · Reset · 커밋 간 비교 · 변경분 추출 · Blame · 태그 · 서브모듈 · 작업 취소</td><td><span class="chip c-done">완료</span></td></tr> <tr><td>Git</td><td>열기 · 클론 · 원격 · Fetch/Pull/Push · 체크아웃 · Merge · rebase · 충돌 목록 · 파일 단위 Accept · Save · 병합 커밋 · 스테이징 · 커밋 · amend · stash · 이력 그래프 · Revert · Cherry-pick · Reset · 커밋 간 비교 · 변경분 추출 · Blame · 태그 · 서브모듈 · 작업 취소</td><td><span class="chip c-done">완료</span></td></tr>
<tr><td>Git</td><td>충돌 여러 개 한 번에 Accept (6차 B-6) · 병합 커밋 본문에 충돌 해결 내역 (8차 F-1) · Git 탭과 파일 비교 탭이 같은 커밋을 봄 (10차 B-1)</td><td><span class="chip c-done">완료</span></td></tr> <tr><td>Git</td><td>충돌 여러 개 한 번에 Accept (6차 B-6) · 병합 커밋 본문에 충돌 해결 내역 (8차 F-1) · Git 탭과 파일 비교 탭이 같은 커밋을 봄 (10차 B-1)</td><td><span class="chip c-done">완료</span></td></tr>
<tr><td>Git</td><td>변경분 추출 새 폴더 · 안내 파일(지울 파일 · 범위) · 먼저 보고 쓰기 (11차 A) · [커밋 수정] HEAD 메시지 · 작성자 보존 · 먼저 묻기 (11차 C-1) · 이력 우클릭 메시지 바꾸기 (11차 C-2)</td><td><span class="chip c-done">완료</span></td></tr> <tr><td>Git</td><td>변경분 추출 새 폴더 · 안내 파일(지울 파일 · 범위) · ZIP · 먼저 보고 쓰기 (11차 A) · [커밋 수정] HEAD 메시지 · 작성자 보존 · 먼저 묻기 (11차 C-1) · 이력 우클릭 메시지 바꾸기 (11차 C-2)</td><td><span class="chip c-done">완료</span></td></tr>
<tr><td>배포</td><td><code>scripts\make-bundle.ps1</code> 배포 묶음(41개 · 43.5MB · <code>배포묶음.txt</code>) · D 드라이브 없는 PC 는 <code>%LOCALAPPDATA%\UbisamConfig</code>(플랫폼) (11차 B)</td><td><span class="chip c-done">완료</span></td></tr> <tr><td>배포</td><td><code>scripts\make-bundle.ps1</code> 배포 묶음(41개 · 43.5MB · <code>배포묶음.txt</code>) · D 드라이브 없는 PC 는 <code>%LOCALAPPDATA%\UbisamConfig</code>(플랫폼) (11차 B)</td><td><span class="chip c-done">완료</span></td></tr>
<tr><td>머지 주석</td><td>반영한 줄 위에 쌓기 · 작업자 이름 · 비교에서 무시 · 주석만 다른 충돌 자동 해결</td><td><span class="chip c-done">완료</span></td></tr> <tr><td>머지 주석</td><td>반영한 줄 위에 쌓기 · 작업자 이름 · 비교에서 무시 · 주석만 다른 충돌 자동 해결</td><td><span class="chip c-done">완료</span></td></tr>
<tr><td>C# 스타일</td><td>AURORA / Allman 맞춤 비교 · 저장 · 폴더 비교 · 설정</td><td><span class="chip c-done">완료</span></td></tr> <tr><td>C# 스타일</td><td>AURORA / Allman 맞춤 비교 · 저장 · 폴더 비교 · 설정</td><td><span class="chip c-done">완료</span></td></tr>
@ -1227,7 +1227,7 @@ Log(<span class="s">"step 5"</span>);</pre>
<tr><th>기능</th><th>상태</th><th>실제</th></tr> <tr><th>기능</th><th>상태</th><th>실제</th></tr>
<tr><td>브랜치 리셋</td><td><span class="chip c-done">완료</span></td><td>이력 탭 선택 커밋 → [Reset]</td></tr> <tr><td>브랜치 리셋</td><td><span class="chip c-done">완료</span></td><td>이력 탭 선택 커밋 → [Reset]</td></tr>
<tr><td>커밋 메시지 수정</td><td><span class="chip c-done">완료</span></td><td>마지막 커밋은 [커밋 수정](HEAD 메시지 · 작성자 보존 · 스테이징 · 올린 커밋이면 먼저 묻기), 그 앞 커밋은 이력 줄 우클릭 [메시지 바꾸기...] — 원격에 아직 없고 사이에 병합 · 태그가 없을 때(11차)</td></tr> <tr><td>커밋 메시지 수정</td><td><span class="chip c-done">완료</span></td><td>마지막 커밋은 [커밋 수정](HEAD 메시지 · 작성자 보존 · 스테이징 · 올린 커밋이면 먼저 묻기), 그 앞 커밋은 이력 줄 우클릭 [메시지 바꾸기...] — 원격에 아직 없고 사이에 병합 · 태그가 없을 때(11차)</td></tr>
<tr><td>변경분 파일 추출</td><td><span class="chip c-done">바뀜</span></td><td>[여기부터] 로 범위를 잡고 [변경분 추출] — 고른 폴더 아래 새 폴더로, 바이트 그대로. 옆에 안내 파일(범위 · 장비에서 지울 파일). ZIP 은 없음(11차 A-4 에서 정함)</td></tr> <tr><td>변경분 파일 추출</td><td><span class="chip c-done">완료</span></td><td>[여기부터] 로 범위를 잡고 [변경분 추출] — 고른 폴더 아래 새 폴더로, 바이트 그대로. 옆에 안내 파일(범위 · 장비에서 지울 파일)과 같은 것을 담은 ZIP(11차)</td></tr>
<tr><td>임시 저장 (Stash)</td><td><span class="chip c-done">완료</span></td><td>[stash 저장] · 목록에서 [적용] [삭제]</td></tr> <tr><td>임시 저장 (Stash)</td><td><span class="chip c-done">완료</span></td><td>[stash 저장] · 목록에서 [적용] [삭제]</td></tr>
<tr><td>파일 단위 되돌리기</td><td><span class="chip c-done">완료</span></td><td>변경 · 커밋 탭 [변경 취소]</td></tr> <tr><td>파일 단위 되돌리기</td><td><span class="chip c-done">완료</span></td><td>변경 · 커밋 탭 [변경 취소]</td></tr>
</table></div> </table></div>
@ -1459,7 +1459,7 @@ D:\MergeHubDemo\repo\ <span class="d">make-demo.ps1 이 만드는 Git
<li><b>반영 기록을 소스에 남기고, 그 기록 때문에 생기는 충돌은 스스로 푼다.</b> 우리 팀은 누가 언제 반영했는지를 <code>// [이름] 날짜</code> 로 남긴다. WinMerge 에는 이 규칙이 없어 손으로 적어야 하고, 다음 병합 때 주석끼리 충돌한다. 만든 저장소에서 git 충돌 26개 중 2개가 이런 충돌이었다.</li> <li><b>반영 기록을 소스에 남기고, 그 기록 때문에 생기는 충돌은 스스로 푼다.</b> 우리 팀은 누가 언제 반영했는지를 <code>// [이름] 날짜</code> 로 남긴다. WinMerge 에는 이 규칙이 없어 손으로 적어야 하고, 다음 병합 때 주석끼리 충돌한다. 만든 저장소에서 git 충돌 26개 중 2개가 이런 충돌이었다.</li>
<li><b>리뷰할 diff 를 지킨다.</b> 병합 결과를 내 쪽 파일의 줄끝 · 인코딩으로 쓴다. 3분할 저장이 LF 로 바꿔 쓰던 문제를 고친 뒤 3,367줄 파일의 diff 가 +8/−4 로 줄었다. CP949 한글 파일도 같은 규칙이다.</li> <li><b>리뷰할 diff 를 지킨다.</b> 병합 결과를 내 쪽 파일의 줄끝 · 인코딩으로 쓴다. 3분할 저장이 LF 로 바꿔 쓰던 문제를 고친 뒤 3,367줄 파일의 diff 가 +8/−4 로 줄었다. CP949 한글 파일도 같은 규칙이다.</li>
<li><b>스타일만 다른 diff 를 걷어 낸다.</b> 사람마다 다른 C# 중괄호 자리를 AURORA / Allman 로 맞춘 뒤 견준다. 시연 파일에서 남은 차이가 39줄에서 진짜 변경 1줄로 준다.</li> <li><b>스타일만 다른 diff 를 걷어 낸다.</b> 사람마다 다른 C# 중괄호 자리를 AURORA / Allman 로 맞춘 뒤 견준다. 시연 파일에서 남은 차이가 39줄에서 진짜 변경 1줄로 준다.</li>
<li><b>우리가 고친다.</b> 사내 요구가 생기면 그날 넣는다 — C# 스타일 맞춤은 요청 당일 들어갔다. 기록과 검사(22벌 1,247건)가 저장소에 함께 있다.</li> <li><b>우리가 고친다.</b> 사내 요구가 생기면 그날 넣는다 — C# 스타일 맞춤은 요청 당일 들어갔다. 기록과 검사(22벌 1,260건)가 저장소에 함께 있다.</li>
</ol> </ol>
<h2>WinMerge 가 더 나은 점</h2> <h2>WinMerge 가 더 나은 점</h2>

View File

@ -337,7 +337,7 @@
| **완료** | A-1 먼저 뽑아 보고 적는다 — 고치지 않음. 아홉 경우를 화면에서 뽑아 git 과 대조: 파일 고르기 · 바이트 보존(이진 · 한글 · 깊은 경로) · 범위 표시는 맞음. 확정된 문제 넷 — 지운 파일 · 이름 바뀐 옛 경로가 안 남음, 무엇과 무엇 사이인지 안 남음(병합은 첫 부모 · 거꾸로 범위도 말없이), 비어 있지 않은 폴더에 묻지 않고 덮어써 옛 파일이 섞임, 쓰다 막히면 반쯤 뽑힌 채 멈춤 | | **완료** | A-1 먼저 뽑아 보고 적는다 — 고치지 않음. 아홉 경우를 화면에서 뽑아 git 과 대조: 파일 고르기 · 바이트 보존(이진 · 한글 · 깊은 경로) · 범위 표시는 맞음. 확정된 문제 넷 — 지운 파일 · 이름 바뀐 옛 경로가 안 남음, 무엇과 무엇 사이인지 안 남음(병합은 첫 부모 · 거꾸로 범위도 말없이), 비어 있지 않은 폴더에 묻지 않고 덮어써 옛 파일이 섞임, 쓰다 막히면 반쯤 뽑힌 채 멈춤 |
| **완료** | A-2 뽑은 폴더 **옆**에 안내 파일(`…_변경분안내.txt`) — 사용자 결정대로 폴더 밖에 두고 지울 파일은 목록으로만. 범위(커밋 제목 · 날짜) · 뽑은 파일(추가 · 수정 · 이름 바뀜은 옛 이름) · 장비에서 지워야 할 파일(지워짐 · 옛 경로). 화면에서 뽑아 git 과 대조 · 하네스 ReportTest +14(45건) | | **완료** | A-2 뽑은 폴더 **옆**에 안내 파일(`…_변경분안내.txt`) — 사용자 결정대로 폴더 밖에 두고 지울 파일은 목록으로만. 범위(커밋 제목 · 날짜) · 뽑은 파일(추가 · 수정 · 이름 바뀜은 옛 이름) · 장비에서 지워야 할 파일(지워짐 · 옛 경로). 화면에서 뽑아 git 과 대조 · 하네스 ReportTest +14(45건) |
| **완료** | A-3 고른 폴더 아래 **새 폴더**(`변경분_4eb9ece..cbcad8a_2026-09-18`, 같은 이름이면 `_2`)를 만들어 뽑는다 · 뽑기 전에 쓸 수 있는지 **먼저 보고** 하나라도 막히면 아무것도 쓰지 않는다(사용자 결정). 화면에서 두 번 뽑아 기존 파일이 섞이지 않는 것 확인 · 하네스 ReportTest +12(57건) | | **완료** | A-3 고른 폴더 아래 **새 폴더**(`변경분_4eb9ece..cbcad8a_2026-09-18`, 같은 이름이면 `_2`)를 만들어 뽑는다 · 뽑기 전에 쓸 수 있는지 **먼저 보고** 하나라도 막히면 아무것도 쓰지 않는다(사용자 결정). 화면에서 두 번 뽑아 기존 파일이 섞이지 않는 것 확인 · 하네스 ReportTest +12(57건) |
| 대기 | A-4 ZIP 으로 뽑기 — 할지 먼저 정함 | | **완료** | A-4 ZIP 으로 뽑기 — 사용자 결정 "폴더에 더해 ZIP 도". 폴더 옆 같은 이름 `.zip`(폴더 · 안내 파일 나란히, 바이트 그대로, 한글 이름 UTF-8 표시). 아홉 경우 다시 — ZIP 8/8 을 tar 로 풀어 폴더와 경로 · 해시 같음. 하네스 ReportTest +13(70건) |
### B. 사내 베타 전에 깨지는 것 ### B. 사내 베타 전에 깨지는 것

View File

@ -31,6 +31,8 @@
<HintPath>$(UbisamPlatformDir)Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath> <HintPath>$(UbisamPlatformDir)Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
<Private>false</Private> <Private>false</Private>
</Reference> </Reference>
<!-- 변경분을 ZIP 으로도(11차 A-4). .NET Framework 에 들어 있는 것이라 배포 묶음에 파일이 늘지 않는다. -->
<Reference Include="System.IO.Compression" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -29,21 +29,26 @@ namespace MergeHub.Services
/// <summary> /// <summary>
/// 같은 이름이 이미 있으면 <c>_2</c> · <c>_3</c> 을 붙여 비어 있는 이름을 고른다. /// 같은 이름이 이미 있으면 <c>_2</c> · <c>_3</c> 을 붙여 비어 있는 이름을 고른다.
/// 하루에 같은 범위를 두 번 뽑아도 앞의 것을 건드리지 않는다. /// 하루에 같은 범위를 두 번 뽑아도 앞의 것을 건드리지 않는다.
/// 옆에 붙는 ZIP(<c>이름.zip</c>, 11차 A-4)이 남아 있어도 그 이름은 쓴 것으로 본다 - 폴더만 지우고
/// ZIP 을 남긴 채 다시 뽑으면 옛 ZIP 을 덮을 뻔했다.
/// </summary> /// </summary>
public static string UniquePath(string parent, string name) public static string UniquePath(string parent, string name)
{ {
var path = Path.Combine(parent, name); var path = Path.Combine(parent, name);
if (!Directory.Exists(path) && !File.Exists(path)) return path; if (Free(path)) return path;
for (var n = 2; n < 1000; n++) for (var n = 2; n < 1000; n++)
{ {
var next = Path.Combine(parent, $"{name}_{n}"); var next = Path.Combine(parent, $"{name}_{n}");
if (!Directory.Exists(next) && !File.Exists(next)) return next; if (Free(next)) return next;
} }
return Path.Combine(parent, $"{name}_{DateTime.Now:HHmmss}"); return Path.Combine(parent, $"{name}_{DateTime.Now:HHmmss}");
} }
private static bool Free(string path) =>
!Directory.Exists(path) && !File.Exists(path) && !File.Exists(path + ".zip");
/// <summary> /// <summary>
/// 뽑기 전에 <b>쓸 수 있는지</b> 본다. 못 쓰는 파일의 경로(저장소 안 경로)를 돌려준다. /// 뽑기 전에 <b>쓸 수 있는지</b> 본다. 못 쓰는 파일의 경로(저장소 안 경로)를 돌려준다.
/// ///

View File

@ -0,0 +1,74 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
namespace MergeHub.Services
{
/// <summary>
/// 뽑은 변경분을 ZIP 하나로도 묶는다(11차 A-4, 2026-09-22 사용자 결정: <b>폴더에 더해 ZIP 도</b>).
///
/// 장비에는 폴더를 넣고, 메일 · 메신저로 보낼 때는 ZIP 을 쓴다. ZIP 안에는 뽑은 폴더와 안내 파일을
/// <b>나란히</b> 넣는다 - 풀면 폴더로 뽑은 것과 같은 모양이 되고, 안내 파일이 소스 틈에 섞이지 않는다(A-2 결정).
/// 파일은 바이트 그대로 넣고, 안의 경로는 <c>/</c> 로 나눈다. 한글 이름은 UTF-8 로 적고 그렇다고 표시한다
/// (.NET 의 <see cref="ZipArchive"/> 가 ASCII 가 아닌 이름에 범용 플래그 bit 11 을 켠다).
/// </summary>
public static class ExportZip
{
/// <summary>뽑은 폴더 옆, 폴더 이름에 <c>.zip</c>.</summary>
public static string PathFor(string folder) =>
folder.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + ".zip";
/// <summary>
/// ZIP 을 만든다. 넣은 파일 수(안내 포함)를 돌려준다.
/// 이미 같은 이름이 있으면 덮지 않고 예외 - 자리는 <see cref="ExportPlan.UniquePath"/> 가 비워 둔다.
/// </summary>
/// <param name="folder">뽑은 폴더. ZIP 안에서 이 폴더 이름이 맨 위 폴더가 된다.</param>
/// <param name="notePath">안내 파일. 없거나 null 이면 넣지 않는다.</param>
public static int Create(string folder, string? notePath, string zipPath)
{
var root = folder.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var top = Path.GetFileName(root);
var count = 0;
// 쓰다 막히면 반쯤 된 ZIP 을 남기지 않는다 - 옆 이름에 쓰고 다 되면 옮긴다.
var temp = zipPath + ".part";
try
{
using (var stream = new FileStream(temp, FileMode.CreateNew, FileAccess.Write))
using (var zip = new ZipArchive(stream, ZipArchiveMode.Create))
{
foreach (var file in Directory.GetFiles(root, "*", SearchOption.AllDirectories).OrderBy(f => f, StringComparer.Ordinal))
{
var relative = file.Substring(root.Length + 1).Replace(Path.DirectorySeparatorChar, '/');
Add(zip, file, top + "/" + relative);
count++;
}
if (!string.IsNullOrEmpty(notePath) && File.Exists(notePath))
{
Add(zip, notePath!, Path.GetFileName(notePath));
count++;
}
}
File.Move(temp, zipPath);
return count;
}
catch
{
try { if (File.Exists(temp)) File.Delete(temp); } catch { /* 지우지 못해도 원래 오류를 알린다 */ }
throw;
}
}
private static void Add(ZipArchive zip, string file, string entryName)
{
var entry = zip.CreateEntry(entryName, CompressionLevel.Optimal);
entry.LastWriteTime = File.GetLastWriteTime(file);
using var source = File.OpenRead(file);
using var target = entry.Open();
source.CopyTo(target);
}
}
}

View File

@ -892,11 +892,27 @@ namespace MergeHub.ViewModels
if (noteWritten) _workspace.AppendLog($"변경분 안내: {notePath}"); if (noteWritten) _workspace.AppendLog($"변경분 안내: {notePath}");
// 폴더에 더해 같은 것을 ZIP 하나로도(11차 A-4, 사용자 결정) - 메일 · 메신저로 보낼 때 쓴다.
// ZIP 을 못 만들어도 폴더는 이미 다 뽑혔으므로 추출을 실패로 만들지 않는다(안내와 같은 규칙).
var zipPath = ExportZip.PathFor(target);
var zipMade = false;
try
{
var packed = ExportZip.Create(target, noteWritten ? notePath : null, zipPath);
zipMade = true;
_workspace.AppendLog($"변경분 ZIP: {zipPath} ({packed}개)");
}
catch (Exception ex)
{
_workspace.AppendLog($"변경분 ZIP 을 만들지 못했습니다: {ex.Message}");
}
var folderName = Path.GetFileName(target); var folderName = Path.GetFileName(target);
var zipNote = zipMade ? " · ZIP 도" : " · ZIP 은 못 만듦(로그)";
_workspace.OnUi(() => MessageUtil.ShowSuccessToast( _workspace.OnUi(() => MessageUtil.ShowSuccessToast(
skipped > 0 skipped > 0
? $"{folderName} 에 {written}개 (지워야 할 파일 {skipped}개는 안내에)" ? $"{folderName} 에 {written}개 (지워야 할 파일 {skipped}개는 안내에){zipNote}"
: $"{folderName} 에 {written}개 파일 추출")); : $"{folderName} 에 {written}개 파일 추출{zipNote}"));
}); });
} }