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); } } }