using System.Text; namespace SharedKernel.Helpers; /// /// RFC 4180 호환 CSV 직렬화/역직렬화. 외부 라이브러리 미사용 (bitforum 코드 컨벤션). /// 직렬화는 UTF-8 BOM 포함 (Excel 호환). /// public static class CsvHelper { /// /// 행 시퀀스를 RFC 4180 CSV byte[] 로 직렬화. UTF-8 BOM 포함. /// public static byte[] WriteUtf8Bom(IEnumerable rows) { var sb = new StringBuilder(); foreach (var row in rows) { for (var i = 0; i < row.Length; i++) { if (i > 0) { sb.Append(','); } sb.Append(EscapeCell(row[i])); } sb.Append("\r\n"); } var data = Encoding.UTF8.GetBytes(sb.ToString()); var bom = new byte[] { 0xEF, 0xBB, 0xBF }; var result = new byte[bom.Length + data.Length]; Buffer.BlockCopy(bom, 0, result, 0, bom.Length); Buffer.BlockCopy(data, 0, result, bom.Length, data.Length); return result; } /// RFC 4180 escape: 쉼표/따옴표/개행 포함 시 "..." 로 감싸고 내부 따옴표는 "" 로. public static string EscapeCell(string? cell) { cell ??= ""; if (cell.Contains(',') || cell.Contains('"') || cell.Contains('\n') || cell.Contains('\r')) { return "\"" + cell.Replace("\"", "\"\"") + "\""; } return cell; } /// /// TextReader 에서 RFC 4180 CSV 행을 lazy 하게 읽는다. /// UTF-8 BOM 이 앞에 있으면 자동으로 건너뜀 (StreamReader 가 처리해 주거나, 여기서 fallback). /// public static IEnumerable Read(TextReader reader) { var currentRow = new List(); var currentField = new StringBuilder(); var inQuotes = false; var hasContent = false; var atStart = true; int ch; while ((ch = reader.Read()) >= 0) { var c = (char)ch; // 파일 첫머리 BOM (U+FEFF) 건너뜀 (StreamReader 가 못 처리한 경우 대비) if (atStart) { atStart = false; if (c == '') { continue; } } if (inQuotes) { if (c == '"') { var next = reader.Peek(); if (next == '"') { currentField.Append('"'); reader.Read(); } else { inQuotes = false; } } else { currentField.Append(c); } hasContent = true; } else { if (c == '"') { inQuotes = true; hasContent = true; } else if (c == ',') { currentRow.Add(currentField.ToString()); currentField.Clear(); hasContent = true; } else if (c == '\r') { // \r\n 정상 처리: \n 이 뒤에 오면 그때 행 종료 } else if (c == '\n') { if (hasContent || currentRow.Count > 0) { currentRow.Add(currentField.ToString()); currentField.Clear(); yield return currentRow.ToArray(); currentRow = new List(); hasContent = false; } } else { currentField.Append(c); hasContent = true; } } } // 마지막 행: 개행 없이 끝난 경우 if (hasContent || currentRow.Count > 0) { currentRow.Add(currentField.ToString()); yield return currentRow.ToArray(); } } }