From a99ca970e961ee869e4b3b2897191e5861163da5 Mon Sep 17 00:00:00 2001 From: ksj09406 Date: Tue, 22 Sep 2026 13:34:39 +0900 Subject: [PATCH] =?UTF-8?q?11=EC=B0=A8=20A-4:=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EB=B6=84=EC=9D=84=20=ED=8F=B4=EB=8D=94=EC=97=90=20=EB=8D=94?= =?UTF-8?q?=ED=95=B4=20ZIP=20=EC=9C=BC=EB=A1=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 결정대로 뽑은 폴더 옆에 같은 이름 .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 --- doc/history/2026-09-18-11차.md | 30 +++++++++- doc/history/changelog.md | 1 + doc/web/index.html | 34 ++++++------ doc/진행상황.md | 2 +- src/MergeHub/MergeHub.csproj | 2 + src/MergeHub/Services/ExportPlan.cs | 9 ++- src/MergeHub/Services/ExportZip.cs | 74 +++++++++++++++++++++++++ src/MergeHub/ViewModels/GitViewModel.cs | 20 ++++++- 8 files changed, 148 insertions(+), 24 deletions(-) create mode 100644 src/MergeHub/Services/ExportZip.cs diff --git a/doc/history/2026-09-18-11차.md b/doc/history/2026-09-18-11차.md index c592422..7ff750f 100644 --- a/doc/history/2026-09-18-11차.md +++ b/doc/history/2026-09-18-11차.md @@ -124,8 +124,34 @@ A-1 에서 값으로 확인한 것 — 지운 파일은 "삭제되어 건너뜀 ### 남은 것 -- A-4(ZIP)는 아직 정하지 않았다. -- 매뉴얼 · 웹 문서의 변경분 추출 설명(새 폴더 · 안내 파일)은 E-2 에서 함께 고친다. +- A-4(ZIP)는 아래(09-22). +- 매뉴얼 · 웹 문서의 변경분 추출 설명(새 폴더 · 안내 파일)은 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, 완료) diff --git a/doc/history/changelog.md b/doc/history/changelog.md index db6e1ad..5f45e8d 100644 --- a/doc/history/changelog.md +++ b/doc/history/changelog.md @@ -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차 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, 두 탭 같은 커밋. **새로 걸린 것 없음.** 시험 도구가 창을 앞으로 부르며 최대화를 풀어 첫 누름이 빗나가던 것을 고침(제품 아님). 제품 코드 변경 없음 | 아래 | diff --git a/doc/web/index.html b/doc/web/index.html index 7d31ecd..b665bda 100644 --- a/doc/web/index.html +++ b/doc/web/index.html @@ -256,7 +256,7 @@ figcaption{font-size:.82rem;color:var(--ink-3);margin-top:6px}
-
22벌 · 1,247건검사 하네스 전부 통과
+
22벌 · 1,260건검사 하네스 전부 통과
11차 · 베타 앞변경분 안내 · 커밋 메시지 고치기 · 배포 묶음 43.5MB · D 드라이브 없는 PC. 다섯 길 다시 끝까지 줄끝 바뀐 파일 0
129MergeHub 로컬 커밋 (푸시 0)
26 → 24만든 저장소 병합 충돌 (주석만 다른 2건 자동 해결)
@@ -488,6 +488,7 @@ UbisamBase.Shell.exe --merge --base B.cs
  • 변경분 추출 → 폴더를 고른다.
  • 고른 폴더 아래에 새 폴더 변경분_228995f..37a2ced_2026-09-22 가 생기고, 바뀐 파일만 바이트 그대로 들어간다. 같은 이름이 있으면 _2 · _3 — 지난번에 뽑은 것이나 넣어 둔 파일과 섞이지 않는다.
  • 그 폴더 옆에 안내 파일 …_변경분안내.txt 가 생긴다 — 범위(커밋 · 제목 · 날짜), 뽑은 파일(추가 · 수정 · 이름 바뀜은 옛 이름), 그리고 장비에서 지워야 할 파일(지워진 파일 · 이름 바뀐 옛 경로). 폴더에는 없는 것이라 안내에만 있다.
  • +
  • 같은 자리에 ZIP 변경분_228995f..37a2ced_2026-09-22.zip 도 생긴다 — 안에 그 폴더와 안내 파일이 나란히 들어 있어, 풀면 폴더로 뽑은 것과 같은 모양이다(바이트 · 경로 같음). 장비에는 폴더를, 메일 · 메신저로 보낼 때는 ZIP 을 쓴다. 한글 경로도 탐색기에서 깨지지 않고 풀린다.
  • 쓰기 전에 모두 쓸 수 있는지 먼저 보고, 하나라도 막히면 아무것도 쓰지 않는다. 병합 커밋 하나를 고르면 첫 부모와 견준다(안내 범위에 “(고른 커밋의 부모)”). 거꾸로 잡은 범위도 그 방향대로 뽑는다.

    @@ -993,7 +994,7 @@ Log("step 5");
    - + @@ -1052,7 +1053,6 @@ Log("step 5");
  • Gitea PR · 리뷰 코멘트 인라인 표시 (기획 확장 범위) — 사내 Gitea 저장소가 아직 없다. 지금은 원격 주소 · 계정 설정과 Fetch · Pull · Push 까지.
  • 멀티 레포 대시보드 · 오프라인 캐싱 (확장 범위)
  • Roslyn 실시간 구문 검사 (확장 범위) — 가벼운 코드 검사로 대신했다. Roslyn 은 붙이지 않았다.
  • -
  • 변경분 ZIP 내보내기 — 폴더로만 뽑는다.
  • 기획 4단계: 사내 베타 배포와 피드백, 단축키 가이드 포함 매뉴얼 (기획 일정 09.29 ~ 10.02).
  • 푸시 — 사내 Gitea 저장소가 만들어지면 그때 한 번에 올린다.
  • @@ -1069,7 +1069,7 @@ Log("step 5");
    129로컬 커밋 · 09-08 ~ 09-22
    -
    1,247검사 통과 (22벌)
    +
    1,260검사 통과 (22벌)
    0 · 0빌드 경고 · 오류
    0푸시
    @@ -1088,7 +1088,7 @@ Log("step 5");
  • 8차 — 고른 파일에 고른 내용을[3분할로 열기] 로 처음 열면 다른 파일 내용이 고른 파일에 저장되던 것 · 작업마다 요약 · 빈 cherry-pick · MERGED 결과 번호 · revert 끝까지 · 원래 작성자 · 첫 Win+↓ · 병합 커밋 본문에 해결 내역. 다섯 길 다시 끝까지. 22벌 1,094건.
  • 9차 — 두 칸이 무엇과 견주는지 사실대로작업 중 충돌을 다 풀면 기준 HEAD · 새로고침 뒤 고른 커밋을 해시로 · 넣은 주석 줄 처음부터 무시 색 · 폴더 트리 시간 다시 재기 · 네 길을 마지막 파일까지 네 칸으로 다시 끝까지. 22벌 1,122건.
  • 10차 — 두 탭이 같은 커밋을 · 누름을 놓치는지한 번 누른 것이 안 남던 것은 시험 도구 쪽으로 가림 · Git 탭 · 파일 비교 탭이 고른 커밋을 서로 바로 따라감 · 규칙 끈 채 반영 색 · 첫 폴더 비교 9.2초 다시 재기 · 폴더 비교 “같은 것만” 폴더 줄과 같음의 까닭 · “다름(코드는 같음)” · 다섯 길(+ pull) 누른 수 = 로그. 22벌 1,147건.
  • -
  • 11차 — 사내 베타 앞변경분 추출이 새 폴더 · 안내 파일(장비에서 지울 파일)로 · [커밋 수정] 바로잡기(HEAD 메시지 · 작성자 보존 · 먼저 묻기) · 이력 우클릭으로 앞 커밋 메시지 바꾸기 · 배포 묶음 97MB → 43.5MB · D 드라이브 없는 PC 에서도 켜짐(플랫폼) · C# 스타일 기본 AURORA · 바꾼 길 다시 끝까지. 22벌 1,247건.
  • +
  • 11차 — 사내 베타 앞변경분 추출이 새 폴더 · 안내 파일(장비에서 지울 파일)로 · [커밋 수정] 바로잡기(HEAD 메시지 · 작성자 보존 · 먼저 묻기) · 이력 우클릭으로 앞 커밋 메시지 바꾸기 · 배포 묶음 97MB → 43.5MB · D 드라이브 없는 PC 에서도 켜짐(플랫폼) · C# 스타일 기본 AURORA · 바꾼 길 다시 끝까지. 22벌 1,260건.
  • 6차에서 끝낸 것

    @@ -1115,15 +1115,15 @@ Log("step 5");
    항목상태무엇
    A-1 · A-2 · A-3 변경분 추출완료먼저 아홉 경우로 뽑아 보니 지운 파일 · 이름 바뀐 옛 경로 · 범위가 어디에도 안 남고, 쓰던 폴더에 섞이고, 막히면 반쯤 뽑힌 채 멈췄다. 이제 고른 폴더 아래 새 폴더에 뽑고 옆에 안내 파일(장비에서 지울 파일 포함), 쓰기 전에 먼저 본다
    A-4 ZIP 으로 뽑기대기할지 먼저 정한다
    A-4 ZIP 으로 뽑기완료사용자 결정: 폴더에 더해 ZIP 도. 폴더 옆에 같은 이름 .zip(폴더 · 안내 파일 나란히). 여덟 번 뽑아 모두 풀어 본 결과가 폴더와 경로 · 해시가 같음
    B-1 배포 묶음완료매뉴얼이 가리키던 Release 가 09-07 것이라 폴더 비교 탭이 없었다. 쓰지 않는 dll · 다른 OS 용을 빼 97MB → 43.5MB, scripts\make-bundle.ps1 · 배포묶음.txt. 빈 폴더에서 병합 한 길 끝까지
    B-2 D 드라이브 없는 PC · 첫 실행완료첫 실행은 오류 없고 기본값이 매뉴얼과 같다. D 를 못 쓰면 켜다 말고 창 없는 프로세스만 남던 것 — 플랫폼에서 %LOCALAPPDATA%\UbisamConfig 로 가고, 그마저 안 되면 “켜지 못했습니다” 뒤 끝내게(배포함)
    C-1 [커밋 수정]완료[마지막 메시지] 가 딴 브랜치 메시지를 가져오고 · 작성자가 바뀌고 · 작업 중에도 켜지던 것. 이제 HEAD 메시지 · 작성자 보존 · 작업 중 꺼짐 · 스테이징 파일 · 올린 커밋이면 먼저 묻기 · 알림에 옛 → 새 해시

    검사 하네스

    - @@ -1459,7 +1459,7 @@ D:\MergeHubDemo\repo\ make-demo.ps1 이 만드는 Git
  • 반영 기록을 소스에 남기고, 그 기록 때문에 생기는 충돌은 스스로 푼다. 우리 팀은 누가 언제 반영했는지를 // [이름] 날짜 로 남긴다. WinMerge 에는 이 규칙이 없어 손으로 적어야 하고, 다음 병합 때 주석끼리 충돌한다. 만든 저장소에서 git 충돌 26개 중 2개가 이런 충돌이었다.
  • 리뷰할 diff 를 지킨다. 병합 결과를 내 쪽 파일의 줄끝 · 인코딩으로 쓴다. 3분할 저장이 LF 로 바꿔 쓰던 문제를 고친 뒤 3,367줄 파일의 diff 가 +8/−4 로 줄었다. CP949 한글 파일도 같은 규칙이다.
  • 스타일만 다른 diff 를 걷어 낸다. 사람마다 다른 C# 중괄호 자리를 AURORA / Allman 로 맞춘 뒤 견준다. 시연 파일에서 남은 차이가 39줄에서 진짜 변경 1줄로 준다.
  • -
  • 우리가 고친다. 사내 요구가 생기면 그날 넣는다 — C# 스타일 맞춤은 요청 당일 들어갔다. 기록과 검사(22벌 1,247건)가 저장소에 함께 있다.
  • +
  • 우리가 고친다. 사내 요구가 생기면 그날 넣는다 — C# 스타일 맞춤은 요청 당일 들어갔다. 기록과 검사(22벌 1,260건)가 저장소에 함께 있다.
  • WinMerge 가 더 나은 점

    diff --git a/doc/진행상황.md b/doc/진행상황.md index 7bd0f2f..4078fc0 100644 --- a/doc/진행상황.md +++ b/doc/진행상황.md @@ -337,7 +337,7 @@ | **완료** | A-1 먼저 뽑아 보고 적는다 — 고치지 않음. 아홉 경우를 화면에서 뽑아 git 과 대조: 파일 고르기 · 바이트 보존(이진 · 한글 · 깊은 경로) · 범위 표시는 맞음. 확정된 문제 넷 — 지운 파일 · 이름 바뀐 옛 경로가 안 남음, 무엇과 무엇 사이인지 안 남음(병합은 첫 부모 · 거꾸로 범위도 말없이), 비어 있지 않은 폴더에 묻지 않고 덮어써 옛 파일이 섞임, 쓰다 막히면 반쯤 뽑힌 채 멈춤 | | **완료** | A-2 뽑은 폴더 **옆**에 안내 파일(`…_변경분안내.txt`) — 사용자 결정대로 폴더 밖에 두고 지울 파일은 목록으로만. 범위(커밋 제목 · 날짜) · 뽑은 파일(추가 · 수정 · 이름 바뀜은 옛 이름) · 장비에서 지워야 할 파일(지워짐 · 옛 경로). 화면에서 뽑아 git 과 대조 · 하네스 ReportTest +14(45건) | | **완료** | 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. 사내 베타 전에 깨지는 것 diff --git a/src/MergeHub/MergeHub.csproj b/src/MergeHub/MergeHub.csproj index 9a858c9..bfc986b 100644 --- a/src/MergeHub/MergeHub.csproj +++ b/src/MergeHub/MergeHub.csproj @@ -31,6 +31,8 @@ $(UbisamPlatformDir)Microsoft.Extensions.DependencyInjection.Abstractions.dll false + + diff --git a/src/MergeHub/Services/ExportPlan.cs b/src/MergeHub/Services/ExportPlan.cs index 6a1cd2b..2a5196d 100644 --- a/src/MergeHub/Services/ExportPlan.cs +++ b/src/MergeHub/Services/ExportPlan.cs @@ -29,21 +29,26 @@ namespace MergeHub.Services /// /// 같은 이름이 이미 있으면 _2 · _3 을 붙여 비어 있는 이름을 고른다. /// 하루에 같은 범위를 두 번 뽑아도 앞의 것을 건드리지 않는다. + /// 옆에 붙는 ZIP(이름.zip, 11차 A-4)이 남아 있어도 그 이름은 쓴 것으로 본다 - 폴더만 지우고 + /// ZIP 을 남긴 채 다시 뽑으면 옛 ZIP 을 덮을 뻔했다. /// public static string UniquePath(string parent, string 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++) { 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}"); } + private static bool Free(string path) => + !Directory.Exists(path) && !File.Exists(path) && !File.Exists(path + ".zip"); + /// /// 뽑기 전에 쓸 수 있는지 본다. 못 쓰는 파일의 경로(저장소 안 경로)를 돌려준다. /// diff --git a/src/MergeHub/Services/ExportZip.cs b/src/MergeHub/Services/ExportZip.cs new file mode 100644 index 0000000..c43f633 --- /dev/null +++ b/src/MergeHub/Services/ExportZip.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; + +namespace MergeHub.Services +{ + /// + /// 뽑은 변경분을 ZIP 하나로도 묶는다(11차 A-4, 2026-09-22 사용자 결정: 폴더에 더해 ZIP 도). + /// + /// 장비에는 폴더를 넣고, 메일 · 메신저로 보낼 때는 ZIP 을 쓴다. ZIP 안에는 뽑은 폴더와 안내 파일을 + /// 나란히 넣는다 - 풀면 폴더로 뽑은 것과 같은 모양이 되고, 안내 파일이 소스 틈에 섞이지 않는다(A-2 결정). + /// 파일은 바이트 그대로 넣고, 안의 경로는 / 로 나눈다. 한글 이름은 UTF-8 로 적고 그렇다고 표시한다 + /// (.NET 의 가 ASCII 가 아닌 이름에 범용 플래그 bit 11 을 켠다). + /// + public static class ExportZip + { + /// 뽑은 폴더 옆, 폴더 이름에 .zip. + public static string PathFor(string folder) => + folder.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + ".zip"; + + /// + /// ZIP 을 만든다. 넣은 파일 수(안내 포함)를 돌려준다. + /// 이미 같은 이름이 있으면 덮지 않고 예외 - 자리는 가 비워 둔다. + /// + /// 뽑은 폴더. ZIP 안에서 이 폴더 이름이 맨 위 폴더가 된다. + /// 안내 파일. 없거나 null 이면 넣지 않는다. + 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); + } + } +} diff --git a/src/MergeHub/ViewModels/GitViewModel.cs b/src/MergeHub/ViewModels/GitViewModel.cs index 2a02b1d..e578820 100644 --- a/src/MergeHub/ViewModels/GitViewModel.cs +++ b/src/MergeHub/ViewModels/GitViewModel.cs @@ -892,11 +892,27 @@ namespace MergeHub.ViewModels 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 zipNote = zipMade ? " · ZIP 도" : " · ZIP 은 못 만듦(로그)"; _workspace.OnUi(() => MessageUtil.ShowSuccessToast( skipped > 0 - ? $"{folderName} 에 {written}개 (지워야 할 파일 {skipped}개는 안내에)" - : $"{folderName} 에 {written}개 파일 추출")); + ? $"{folderName} 에 {written}개 (지워야 할 파일 {skipped}개는 안내에){zipNote}" + : $"{folderName} 에 {written}개 파일 추출{zipNote}")); }); }