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($"
";
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))}{tag}>");
}
else
{
var tag = rowIndex == 1 ? "th" : "td";
sb.Append($"<{tag}>{Escape(GetCellValue(cell, sharedStrings))}{tag}>");
}
}
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}
";
}
}