빌드 경고 해결 및 데이터 검증 진행중.
This commit is contained in:
@@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MiniExcelLibs;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Spreadsheet;
|
||||
|
||||
namespace ExcelKv.Core;
|
||||
|
||||
@@ -72,38 +74,46 @@ public class ExcelLoader
|
||||
Console.WriteLine($"[Core.Loader] Processing {filePath} Sheet: {sheetName}...");
|
||||
|
||||
var rows = MiniExcel.Query(filePath, sheetName: sheetName, useHeaderRow: false).ToList();
|
||||
var mergedRanges = GetMergedRanges(filePath, sheetName);
|
||||
|
||||
// Validation
|
||||
int dataStartRow = config.TopHeaderStartRow + config.TopHeaderDepth;
|
||||
if (rows.Count <= dataStartRow) return 0;
|
||||
|
||||
// 1. Analyze Top Headers (Data Columns)
|
||||
var topHeaderRows = new List<string[]>();
|
||||
for (int i = config.TopHeaderStartRow; i < dataStartRow; i++)
|
||||
{
|
||||
topHeaderRows.Add(FlattenDictionaryRow((IDictionary<string, object>)rows[i]));
|
||||
}
|
||||
|
||||
// Calculate Global Max Col from Data Rows to ensure we don't truncate data
|
||||
// Optimization: check a sample or assume logical limit. For correctness, check valid rows.
|
||||
int globalMaxCol = 0;
|
||||
int limitRow = config.DataEndRow.HasValue ? Math.Min(rows.Count, config.DataEndRow.Value + 1) : rows.Count;
|
||||
for(int i = dataStartRow; i < limitRow; i++)
|
||||
|
||||
// Flatten all rows up to limit for consistent indexing
|
||||
var flattenedRows = new List<string[]>();
|
||||
int globalMaxCol = 0;
|
||||
for (int i = 0; i < limitRow; i++)
|
||||
{
|
||||
var d = (IDictionary<string, object>)rows[i];
|
||||
if(d.Count > globalMaxCol) globalMaxCol = d.Count; // Approximate
|
||||
// FlattenDictionaryRow is cleaner but expensive to call just for count.
|
||||
// d.Keys.Count is effectively the column count for that row.
|
||||
var flat = FlattenDictionaryRow((IDictionary<string, object>)rows[i]);
|
||||
flattenedRows.Add(flat);
|
||||
if (flat.Length > globalMaxCol) globalMaxCol = flat.Length;
|
||||
}
|
||||
|
||||
int maxMergeCol = mergedRanges.Any() ? mergedRanges.Max(m => m.EndCol + 1) : 0;
|
||||
int normalizedMaxCol = Math.Max(globalMaxCol, maxMergeCol);
|
||||
|
||||
ApplyMergedValues(flattenedRows, mergedRanges, normalizedMaxCol);
|
||||
|
||||
// 1. Analyze Top Headers (Data Columns)
|
||||
var topHeaderRows = flattenedRows
|
||||
.Skip(config.TopHeaderStartRow)
|
||||
.Take(config.TopHeaderDepth)
|
||||
.ToList();
|
||||
|
||||
int headerMaxCol = topHeaderRows.Any() ? Math.Max(normalizedMaxCol, topHeaderRows.Max(r => r.Length)) : normalizedMaxCol;
|
||||
var filledHeaders = topHeaderRows;
|
||||
|
||||
// Flatten Data Headers (Right Side)
|
||||
var topAxisKeys = FlattenTopHeaders(topHeaderRows, config.LeftHeaderStartCol + config.LeftHeaderWidth, globalMaxCol);
|
||||
var topAxisKeys = FlattenTopHeaders(filledHeaders, config.LeftHeaderStartCol + config.LeftHeaderWidth, headerMaxCol);
|
||||
|
||||
// ... (Left Axis Header logic omitted for brevity, unchanged) ...
|
||||
// ** New: Extract Left Axis Headers (Corner Region) **
|
||||
// These are the headers *above* the Left Key columns.
|
||||
var leftAxisHeaders = new List<string>();
|
||||
var bottomHeaderRow = FlattenDictionaryRow((IDictionary<string, object>)rows[dataStartRow - 1]);
|
||||
var bottomHeaderRow = flattenedRows[dataStartRow - 1];
|
||||
|
||||
for (int c = 0; c < config.LeftHeaderWidth; c++)
|
||||
{
|
||||
@@ -122,8 +132,7 @@ public class ExcelLoader
|
||||
|
||||
for (int i = dataStartRow; i < limitRow; i++)
|
||||
{
|
||||
var rowDict = (IDictionary<string, object>)rows[i];
|
||||
var rowVals = FlattenDictionaryRow(rowDict);
|
||||
var rowVals = flattenedRows[i];
|
||||
|
||||
// ... (Left Key Logic Unchanged) ...
|
||||
var currentLeftParts = new List<string>();
|
||||
@@ -140,7 +149,7 @@ public class ExcelLoader
|
||||
}
|
||||
}
|
||||
if (!rowHasContent) continue;
|
||||
string leftKey = string.Join("----", currentLeftParts);
|
||||
string leftKey = string.Join("__", currentLeftParts);
|
||||
|
||||
// B. Map Values
|
||||
int limitCol = config.DataEndCol.HasValue ? Math.Min(rowVals.Length, config.DataEndCol.Value + 1) : rowVals.Length;
|
||||
@@ -168,7 +177,7 @@ public class ExcelLoader
|
||||
// Update: User said "소수점 6자리까지 표현하는걸로 통일". Could mean truncate or round. Safe bet is Round.
|
||||
}
|
||||
|
||||
string fullKey = $"{sheetName}:{leftKey}*{topKey}";
|
||||
string fullKey = $"{sheetName}:{leftKey}----{topKey}";
|
||||
|
||||
await storage.SetAsync(fullKey, val, i, c);
|
||||
|
||||
@@ -193,11 +202,43 @@ public class ExcelLoader
|
||||
for (int i = 0; i < sortedKeys.Count; i++)
|
||||
{
|
||||
var val = rowDict[sortedKeys[i]];
|
||||
result[i] = val?.ToString() ?? "";
|
||||
result[i] = ExtractCellValue(val);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Normalize cell value, preferring cached result for formulas.
|
||||
private static string ExtractCellValue(object val)
|
||||
{
|
||||
if (val == null) return "";
|
||||
|
||||
string ToStr(object? o) => o?.ToString() ?? "";
|
||||
|
||||
var type = val.GetType();
|
||||
var typeName = type.FullName ?? "";
|
||||
|
||||
// MiniExcel may return an internal ExcelFormula type; try to read cached value.
|
||||
if (typeName.Contains("Formula", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var cached = type.GetProperty("Value")?.GetValue(val)
|
||||
?? type.GetProperty("CachedValue")?.GetValue(val)
|
||||
?? type.GetProperty("Result")?.GetValue(val);
|
||||
if (cached != null) return ToStr(cached);
|
||||
}
|
||||
|
||||
var s = ToStr(val);
|
||||
if (s.StartsWith("="))
|
||||
{
|
||||
var cached = type.GetProperty("CachedValue")?.GetValue(val)
|
||||
?? type.GetProperty("Value")?.GetValue(val)
|
||||
?? type.GetProperty("Result")?.GetValue(val);
|
||||
if (cached != null) return ToStr(cached);
|
||||
// If no cached value, avoid persisting the formula string.
|
||||
return "";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static List<string> FlattenTopHeaders(List<string[]> headerRows, int startCol, int globalMaxCol)
|
||||
{
|
||||
if (headerRows.Count == 0) return new List<string>();
|
||||
@@ -206,8 +247,6 @@ public class ExcelLoader
|
||||
|
||||
var flatHeaders = new List<string>();
|
||||
|
||||
// Iterate Columns
|
||||
var lastValues = new string[headerRows.Count];
|
||||
for (int c = startCol; c < maxCol; c++)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
@@ -215,19 +254,102 @@ public class ExcelLoader
|
||||
for (int r = 0; r < headerRows.Count; r++)
|
||||
{
|
||||
string val = (c < headerRows[r].Length) ? headerRows[r][c] : "";
|
||||
|
||||
// Horizontal Forward Fill (Re-enabled for Merged Headers)
|
||||
if (string.IsNullOrWhiteSpace(val)) val = lastValues[r];
|
||||
else lastValues[r] = val;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val)) parts.Add(val);
|
||||
}
|
||||
// If empty, use "Col_Index" fallback or keep empty?
|
||||
// User schema usually requires keys. If empty, it's skipped in mapping.
|
||||
// Let's keep it empty, but if data exists, it won't map unless we have a key.
|
||||
// If parts is empty, let's leave valid empty string so mapping can decide.
|
||||
flatHeaders.Add(string.Join("----", parts));
|
||||
flatHeaders.Add(string.Join("__", parts));
|
||||
}
|
||||
return flatHeaders;
|
||||
}
|
||||
|
||||
private record MergeRange(int StartRow, int EndRow, int StartCol, int EndCol);
|
||||
|
||||
private static List<MergeRange> GetMergedRanges(string filePath, string sheetName)
|
||||
{
|
||||
var result = new List<MergeRange>();
|
||||
try
|
||||
{
|
||||
using var doc = SpreadsheetDocument.Open(filePath, false);
|
||||
var sheet = doc.WorkbookPart?.Workbook.Descendants<Sheet>().FirstOrDefault(s => s.Name == sheetName);
|
||||
if (sheet == null) return result;
|
||||
|
||||
var wsPart = doc.WorkbookPart?.GetPartById(sheet.Id!) as WorksheetPart;
|
||||
var mergeCells = wsPart?.Worksheet.Elements<MergeCells>().FirstOrDefault();
|
||||
if (mergeCells == null) return result;
|
||||
|
||||
foreach (var mc in mergeCells.Elements<MergeCell>())
|
||||
{
|
||||
var (sr, er, sc, ec) = ParseRange(mc.Reference?.Value ?? "");
|
||||
result.Add(new MergeRange(sr, er, sc, ec));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If merge metadata cannot be read, fall back to no-op.
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static (int startRow, int endRow, int startCol, int endCol) ParseRange(string reference)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reference)) return (0, 0, 0, 0);
|
||||
if (!reference.Contains(":"))
|
||||
{
|
||||
var (r, c) = ParseCell(reference);
|
||||
return (r, r, c, c);
|
||||
}
|
||||
var parts = reference.Split(':');
|
||||
var (r1, c1) = ParseCell(parts[0]);
|
||||
var (r2, c2) = ParseCell(parts[1]);
|
||||
return (Math.Min(r1, r2), Math.Max(r1, r2), Math.Min(c1, c2), Math.Max(c1, c2));
|
||||
}
|
||||
|
||||
private static (int row, int col) ParseCell(string cellRef)
|
||||
{
|
||||
int row = 0, col = 0;
|
||||
int i = 0;
|
||||
while (i < cellRef.Length && char.IsLetter(cellRef[i]))
|
||||
{
|
||||
col = col * 26 + (char.ToUpperInvariant(cellRef[i]) - 'A' + 1);
|
||||
i++;
|
||||
}
|
||||
string rowStr = cellRef[i..];
|
||||
int.TryParse(rowStr, out row);
|
||||
// Convert to zero-based indices
|
||||
return (Math.Max(0, row - 1), Math.Max(0, col - 1));
|
||||
}
|
||||
|
||||
private static void ApplyMergedValues(List<string[]> rows, List<MergeRange> merges, int maxCols)
|
||||
{
|
||||
NormalizeRows(rows, maxCols);
|
||||
foreach (var merge in merges)
|
||||
{
|
||||
if (merge.StartRow >= rows.Count) continue;
|
||||
string anchor = (merge.StartCol < rows[merge.StartRow].Length) ? rows[merge.StartRow][merge.StartCol] : "";
|
||||
for (int r = merge.StartRow; r <= merge.EndRow && r < rows.Count; r++)
|
||||
{
|
||||
var row = rows[r];
|
||||
for (int c = merge.StartCol; c <= merge.EndCol && c < row.Length; c++)
|
||||
{
|
||||
row[c] = anchor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeRows(List<string[]> rows, int maxCols)
|
||||
{
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
{
|
||||
if (rows[i].Length < maxCols)
|
||||
{
|
||||
var padded = new string[maxCols];
|
||||
Array.Copy(rows[i], padded, rows[i].Length);
|
||||
rows[i] = padded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user