Antigravity 초안

This commit is contained in:
Lectom C Han
2026-01-08 15:32:15 +09:00
commit 12262b4479
32 changed files with 3048 additions and 0 deletions

6
ExcelKv.Core/Class1.cs Normal file
View File

@@ -0,0 +1,6 @@
namespace ExcelKv.Core;
public class Class1
{
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MiniExcel" Version="1.42.0" />
<PackageReference Include="StackExchange.Redis" Version="2.10.1" />
</ItemGroup>
</Project>

209
ExcelKv.Core/ExcelLoader.cs Normal file
View File

@@ -0,0 +1,209 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MiniExcelLibs;
using StackExchange.Redis; // We'll just use IDatabase interface if possible, or dependency injection.
// For now, let's keep it simple and pass the db/batch or connection string.
// Better yet, pass an interface IStorageWrapper.
namespace ExcelKv.Core;
// Simple Config DTO for user-defined areas
public class RegionConfig
{
public int TopHeaderStartRow { get; set; } = 0;
public int TopHeaderDepth { get; set; } = 3;
public int LeftHeaderStartCol { get; set; } = 0;
public int LeftHeaderWidth { get; set; } = 4;
}
public interface IStorageWrapper
{
Task SetAsync(string key, string value);
Task IncrementAsync(string key, double value);
}
// Live Aggregator Helper (Moved here for shared access)
public class LiveAggregator
{
// Simple basic check for now. Can be expanded.
private static readonly System.Text.RegularExpressions.Regex MetricPattern =
new System.Text.RegularExpressions.Regex(@"(수량|합계|Volume|Weight|Total|Usage)", System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Compiled);
public static bool IsMetric(string colName) => MetricPattern.IsMatch(colName);
public static string GenerateGroupKey(string sheetName, string category, string spec, string metricName)
=> $"Stats:{Use_(category)}:{Use_(spec)}:{Use_(metricName)}";
private static string Use_(string s) => string.IsNullOrWhiteSpace(s) ? "Unknown" : s.Replace(" ", "_");
}
public class ExcelLoader
{
public static async Task<int> ProcessFileAsync(
string filePath,
string sheetName,
RegionConfig config,
IStorageWrapper storage,
SchemaRegistry registry)
{
Console.WriteLine($"[Core.Loader] Processing {filePath} Sheet: {sheetName}...");
Console.WriteLine($"[Config] Top: Row{config.TopHeaderStartRow} (+{config.TopHeaderDepth}), Left: Col{config.LeftHeaderStartCol} (+{config.LeftHeaderWidth})");
var rows = MiniExcel.Query(filePath, sheetName: sheetName, useHeaderRow: false).ToList();
// Validation
int dataStartRow = config.TopHeaderStartRow + config.TopHeaderDepth;
if (rows.Count <= dataStartRow) return 0;
// 1. Analyze Top Headers
// Extract the rows that form the Top Header
List<string[]> topHeaderRows = new();
for (int i = config.TopHeaderStartRow; i < dataStartRow; i++)
{
var rowDict = (IDictionary<string, object>)rows[i];
topHeaderRows.Add(FlattenDictionaryRow(rowDict));
}
// Flatten Top Headers (Horizontal Forward Fill)
// Offset: We effectively care about columns starting from LeftHeaderWidth to End
// but we pass the absolute index.
var topAxisKeys = FlattenTopHeaders(topHeaderRows, config.LeftHeaderStartCol + config.LeftHeaderWidth);
// Register Schema
string ns = System.IO.Path.GetFileName(filePath) + "_" + sheetName;
registry.RegisterSchema(ns, topAxisKeys);
// 2. Process Data Rows
// State for Vertical Forward Fill (Left Headers)
string[] lastLeftValues = new string[config.LeftHeaderWidth];
int processedCount = 0;
for (int i = dataStartRow; i < rows.Count; i++)
{
var rowDict = (IDictionary<string, object>)rows[i];
var rowVals = FlattenDictionaryRow(rowDict);
// A. Build Left Axis Key (Vertical Forward Fill)
var currentLeftParts = new List<string>();
bool rowHasContent = false;
for (int c = 0; c < config.LeftHeaderWidth; c++)
{
int absCol = config.LeftHeaderStartCol + c;
string val = (absCol < rowVals.Length) ? rowVals[absCol] : "";
// Logic: Unmerge & Fill (Same as Vertical Forward Fill)
if (string.IsNullOrWhiteSpace(val))
{
val = lastLeftValues[c];
}
else
{
lastLeftValues[c] = val; // New merge block starts
}
if(!string.IsNullOrWhiteSpace(val))
{
currentLeftParts.Add(val);
rowHasContent = true;
}
}
// Skip rows with no left context (e.g. empty separator lines)
if (!rowHasContent) continue;
string leftKey = string.Join(".", currentLeftParts);
// B. Map Values to Top Axis Keys
for (int c = config.LeftHeaderStartCol + config.LeftHeaderWidth; c < rowVals.Length; c++)
{
// Align physical column to Top Axis Index
// Top Axis Keys[0] corresponds to column (LeftStart + LeftWidth)
int topIndex = c - (config.LeftHeaderStartCol + config.LeftHeaderWidth);
if (topIndex < 0 || topIndex >= topAxisKeys.Count) continue;
string topKey = topAxisKeys[topIndex];
string val = rowVals[c];
if (string.IsNullOrWhiteSpace(val)) continue; // Sparse
// C. Construct Final Key
string fullKey = $"{sheetName}:{leftKey}*{topKey}";
await storage.SetAsync(fullKey, val);
// D. Live Compute
if (LiveAggregator.IsMetric(topKey) && double.TryParse(val, out double dVal))
{
await storage.IncrementAsync($"Stats:Global:{topKey}", dVal);
}
}
processedCount++;
}
return processedCount;
}
private static string[] FlattenDictionaryRow(IDictionary<string, object> rowDict)
{
// MiniExcel Dynamic Dictionary Keys are A, B... AA..
// We assume we want 0-indexed string array.
// We find the 'Max' column index to allocate array.
// This sorting + re-indexing is a bit naive for very wide sheets but OK for PoC.
// Better to use MiniExcel's column index if available, but dynamic object hides it.
// We'll trust the Key sorting for now.
var sortedKeys = rowDict.Keys.OrderBy(k => k.Length).ThenBy(k => k).ToList();
var result = new string[sortedKeys.Count];
// Note: Dictionary might define "A", "C" but skip "B".
// This simple list approach collapses them.
// TO BE EXACT: We should convert "A"->0, "B"->1.
// For PoC we keep this, but in production we need ColumnLetterToIndex logic.
for (int i = 0; i < sortedKeys.Count; i++)
{
var val = rowDict[sortedKeys[i]];
result[i] = val?.ToString() ?? "";
}
return result;
}
private static List<string> FlattenTopHeaders(List<string[]> headerRows, int startCol)
{
if (headerRows.Count == 0) return new List<string>();
int maxCol = headerRows.Max(r => r.Length);
var flatHeaders = new List<string>();
var lastValues = new string[headerRows.Count]; // Last value for each ROW level (Horizontal Fill)
// Iterate Columns
for (int c = startCol; c < maxCol; c++)
{
var parts = new List<string>();
// Iterate Rows (Depth)
for (int r = 0; r < headerRows.Count; r++)
{
string val = (c < headerRows[r].Length) ? headerRows[r][c] : "";
// Horizontal Forward Fill (Unmerge Logic)
if (string.IsNullOrWhiteSpace(val))
{
val = lastValues[r];
}
else
{
lastValues[r] = val;
}
if (!string.IsNullOrWhiteSpace(val)) parts.Add(val);
}
flatHeaders.Add(string.Join(".", parts));
}
return flatHeaders;
}
}

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace ExcelKv.Core;
// --- Schema Middleware ---
public class SchemaRegistry
{
private Dictionary<string, HashSet<string>> _knownSchemas = new();
public void RegisterSchema(string namespaceKey, List<string> newColumns)
{
if (!_knownSchemas.ContainsKey(namespaceKey))
{
_knownSchemas[namespaceKey] = new HashSet<string>();
foreach (var col in newColumns) _knownSchemas[namespaceKey].Add(col);
}
else
{
var existing = _knownSchemas[namespaceKey];
foreach (var col in newColumns) if (!existing.Contains(col)) existing.Add(col);
}
}
}

69
ExcelKv.Core/SiUnit.cs Normal file
View File

@@ -0,0 +1,69 @@
namespace ExcelKv.Core.Units;
public enum UnitCategory
{
None = 0,
Length,
Area,
Volume,
Weight,
Count, // ex: EA, Set
Time // ex: Hour, Day
}
public enum SiUnit
{
// Default
None = 0,
// Length (Base: Meter)
Meter = 100,
Millimeter = 101,
Centimeter = 102,
Kilometer = 103,
// Area (Base: SquareMeter)
SquareMeter = 200,
Hectare = 201,
// Volume (Base: CubicMeter)
CubicMeter = 300,
Liter = 301,
// Weight/Mass (Base: Kilogram)
Kilogram = 400,
Ton = 401,
Gram = 402,
// Count
Each = 900,
Set = 901
}
public static class SiUnitExtensions
{
public static UnitCategory GetCategory(this SiUnit unit)
{
int id = (int)unit;
if (id >= 100 && id < 200) return UnitCategory.Length;
if (id >= 200 && id < 300) return UnitCategory.Area;
if (id >= 300 && id < 400) return UnitCategory.Volume;
if (id >= 400 && id < 500) return UnitCategory.Weight;
if (id >= 900 && id < 1000) return UnitCategory.Count;
return UnitCategory.None;
}
public static string ToSymbol(this SiUnit unit) => unit switch
{
SiUnit.Meter => "m",
SiUnit.Millimeter => "mm",
SiUnit.Centimeter => "cm",
SiUnit.Kilometer => "km",
SiUnit.SquareMeter => "m2",
SiUnit.CubicMeter => "m3",
SiUnit.Kilogram => "kg",
SiUnit.Ton => "ton",
SiUnit.Each => "EA",
_ => ""
};
}