using System; using System.Collections.Generic; using System.Linq; using System.Text; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Spreadsheet; namespace AxCopilot.Services.Agent; /// XLSX 파일을 HTML 테이블로 변환하여 WebView2에서 미리보기합니다. internal static class XlsxToHtmlConverter { private const int MaxRows = 500; private const int MaxCols = 50; public static string Convert(string xlsxPath) { try { using var doc = SpreadsheetDocument.Open(xlsxPath, false); var workbookPart = doc.WorkbookPart; if (workbookPart == null) return WrapHtml("

스프레드시트 내용이 없습니다.

"); var sheets = workbookPart.Workbook?.Sheets?.Elements().ToList(); if (sheets == null || sheets.Count == 0) return WrapHtml("

시트가 없습니다.

"); var sharedStrings = workbookPart.SharedStringTablePart?.SharedStringTable; var sb = new StringBuilder(); // 시트 탭 UI if (sheets.Count > 1) { sb.AppendLine("
"); for (int i = 0; i < sheets.Count; i++) { var name = sheets[i].Name?.Value ?? $"Sheet{i + 1}"; var activeClass = i == 0 ? " active" : ""; sb.AppendLine($""); } sb.AppendLine("
"); } // 시트별 테이블 for (int si = 0; si < sheets.Count; si++) { var sheet = sheets[si]; var partId = sheet.Id?.Value; if (string.IsNullOrEmpty(partId)) continue; WorksheetPart sheetPart; try { sheetPart = (WorksheetPart)workbookPart.GetPartById(partId); } catch { continue; } var display = si == 0 ? "block" : "none"; sb.AppendLine($"
"); sb.AppendLine(RenderSheet(sheetPart, sharedStrings)); sb.AppendLine("
"); } return WrapHtml(sb.ToString()); } catch (Exception ex) { return WrapHtml($"

XLSX 파싱 오류: {Escape(ex.Message)}

"); } } private static string RenderSheet(WorksheetPart sheetPart, SharedStringTable? sharedStrings) { var sheetData = sheetPart.Worksheet?.GetFirstChild(); if (sheetData == null) return "

시트 데이터가 없습니다.

"; var rows = sheetData.Elements().Take(MaxRows + 1).ToList(); if (rows.Count == 0) return "

데이터가 없습니다.

"; // 병합 셀 정보 var mergeCells = sheetPart.Worksheet?.GetFirstChild() ?.Elements().ToList() ?? new List(); var mergeMap = BuildMergeMap(mergeCells); var sb = new StringBuilder(); sb.AppendLine("
"); var rowIndex = 0; foreach (var row in rows.Take(MaxRows)) { rowIndex++; sb.Append(rowIndex == 1 ? "" : ""); var cells = row.Elements().Take(MaxCols).ToList(); foreach (var cell in cells) { var cellRef = cell.CellReference?.Value ?? ""; if (mergeMap.TryGetValue(cellRef, out var merge)) { if (merge.IsHidden) continue; // 병합된 셀 건너뛰기 var spanAttrs = ""; if (merge.ColSpan > 1) spanAttrs += $" colspan='{merge.ColSpan}'"; if (merge.RowSpan > 1) spanAttrs += $" rowspan='{merge.RowSpan}'"; var tag = rowIndex == 1 ? "th" : "td"; sb.Append($"<{tag}{spanAttrs}>{Escape(GetCellValue(cell, sharedStrings))}"); } else { var tag = rowIndex == 1 ? "th" : "td"; sb.Append($"<{tag}>{Escape(GetCellValue(cell, sharedStrings))}"); } } sb.AppendLine(""); } sb.AppendLine("
"); if (rows.Count > MaxRows) sb.AppendLine($"

... {MaxRows}행까지 표시 (전체 데이터는 Excel에서 확인)

"); return sb.ToString(); } private static string GetCellValue(Cell cell, SharedStringTable? sharedStrings) { var value = cell.CellValue?.Text ?? cell.InnerText ?? ""; if (string.IsNullOrEmpty(value)) return ""; if (cell.DataType?.Value == CellValues.SharedString && sharedStrings != null) { if (int.TryParse(value, out var idx) && idx >= 0 && idx < sharedStrings.Count()) return sharedStrings.ElementAt(idx).InnerText; } return value; } private sealed record MergeCellInfo(int ColSpan, int RowSpan, bool IsHidden); private static Dictionary BuildMergeMap(List mergeCells) { var map = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var mc in mergeCells) { var range = mc.Reference?.Value; if (string.IsNullOrEmpty(range)) continue; var parts = range.Split(':'); if (parts.Length != 2) continue; var (startCol, startRow) = ParseCellRef(parts[0]); var (endCol, endRow) = ParseCellRef(parts[1]); var colSpan = endCol - startCol + 1; var rowSpan = endRow - startRow + 1; // 시작 셀: span 정보 포함 map[parts[0]] = new MergeCellInfo(colSpan, rowSpan, false); // 나머지 셀: 숨김 for (int r = startRow; r <= endRow; r++) for (int c = startCol; c <= endCol; c++) { var cellRef = ColumnIndexToLetter(c) + r; if (!string.Equals(cellRef, parts[0], StringComparison.OrdinalIgnoreCase)) map[cellRef] = new MergeCellInfo(1, 1, true); } } return map; } private static (int Col, int Row) ParseCellRef(string cellRef) { int col = 0, i = 0; while (i < cellRef.Length && char.IsLetter(cellRef[i])) { col = col * 26 + (char.ToUpper(cellRef[i]) - 'A' + 1); i++; } int.TryParse(cellRef[i..], out var row); return (col, row); } private static string ColumnIndexToLetter(int col) { var sb = new StringBuilder(); while (col > 0) { col--; sb.Insert(0, (char)('A' + col % 26)); col /= 26; } return sb.ToString(); } private static string Escape(string text) => System.Net.WebUtility.HtmlEncode(text); private static string WrapHtml(string body) { return $@" {body} "; } }