using ClosedXML.Excel; namespace Shared.Excel; public sealed class ClosedXmlWorkbookService { public IReadOnlyList ReadHeaders(Stream stream) { using var workbook = new XLWorkbook(stream); var sheet = workbook.Worksheets.First(); var last = sheet.Row(1).LastCellUsed()?.Address.ColumnNumber ?? 0; if (last == 0) return []; return Enumerable.Range(1, last) .Select(i => sheet.Cell(1, i).GetFormattedString().Trim()) .Where(x => !string.IsNullOrWhiteSpace(x)) .ToArray(); } public IEnumerable ReadRows(Stream stream, int maxRows) { using var workbook = new XLWorkbook(stream); var sheet = workbook.Worksheets.First(); var lastColumn = sheet.Row(1).LastCellUsed()?.Address.ColumnNumber ?? 0; var lastRow = sheet.LastRowUsed()?.RowNumber() ?? 1; var headers = Enumerable.Range(1, lastColumn) .Select(i => sheet.Cell(1, i).GetFormattedString().Trim()) .ToArray(); if (lastRow - 1 > maxRows) throw new InvalidOperationException($"최대 {maxRows:N0}행까지 업로드할 수 있습니다."); for (var rowNo = 2; rowNo <= lastRow; rowNo++) { var values = new Dictionary(StringComparer.OrdinalIgnoreCase); var hasValue = false; for (var column = 1; column <= lastColumn; column++) { var header = headers[column - 1]; if (string.IsNullOrWhiteSpace(header)) continue; var value = sheet.Cell(rowNo, column).GetFormattedString().Trim(); if (!string.IsNullOrWhiteSpace(value)) hasValue = true; values[header] = string.IsNullOrWhiteSpace(value) ? null : value; } if (hasValue) yield return new ImportRawRow(rowNo, values); } } public MemoryStream CreateTemplate(ImportDefinition definition) { var workbook = new XLWorkbook(); var sheet = workbook.AddWorksheet("업로드"); var fields = definition.Fields.Where(x => x.Importable).ToArray(); for (var i = 0; i < fields.Length; i++) { var cell = sheet.Cell(1, i + 1); cell.Value = fields[i].Label; cell.Style.Font.Bold = true; cell.Style.Fill.BackgroundColor = XLColor.LightGray; cell.Comment.AddText(fields[i].Required ? "필수 입력" : "선택 입력"); } sheet.SheetView.FreezeRows(1); sheet.Columns(1, fields.Length).Width = 18; sheet.Row(1).Height = 22; sheet.Range(1, 1, 1, fields.Length).SetAutoFilter(); var help = workbook.AddWorksheet("필드설명"); help.Cell("A1").Value = "필드"; help.Cell("B1").Value = "필수"; help.Cell("C1").Value = "형식"; help.Cell("D1").Value = "허용 별칭"; help.Range("A1:D1").Style.Font.Bold = true; for (var i = 0; i < fields.Length; i++) { var row = i + 2; help.Cell(row, 1).Value = fields[i].Label; help.Cell(row, 2).Value = fields[i].Required ? "Y" : "N"; help.Cell(row, 3).Value = fields[i].DataType; help.Cell(row, 4).Value = string.Join(", ", fields[i].Aliases); } help.Columns().AdjustToContents(8, 35); var output = new MemoryStream(); workbook.SaveAs(output); output.Position = 0; workbook.Dispose(); return output; } public MemoryStream CreateErrorWorkbook( Stream source, IReadOnlyDictionary> issues) { using var workbook = new XLWorkbook(source); var sheet = workbook.Worksheets.First(); var errorColumn = (sheet.Row(1).LastCellUsed()?.Address.ColumnNumber ?? 0) + 1; sheet.Cell(1, errorColumn).Value = "오류사유"; sheet.Cell(1, errorColumn).Style.Font.Bold = true; foreach (var (rowNo, rowIssues) in issues) { sheet.Cell(rowNo, errorColumn).Value = string.Join(" | ", rowIssues.Select(x => x.Message)); } sheet.Column(errorColumn).Width = 45; var output = new MemoryStream(); workbook.SaveAs(output); output.Position = 0; return output; } }