deukpack 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.ko.md +138 -0
- package/README.md +182 -0
- package/RELEASING.md +71 -0
- package/bin/deukpack.js +9 -0
- package/dist/ast/DeukPackASTBuilder.d.ts +153 -0
- package/dist/ast/DeukPackASTBuilder.d.ts.map +1 -0
- package/dist/ast/DeukPackASTBuilder.js +931 -0
- package/dist/ast/DeukPackASTBuilder.js.map +1 -0
- package/dist/codegen/CSharpGenerator.d.ts +136 -0
- package/dist/codegen/CSharpGenerator.d.ts.map +1 -0
- package/dist/codegen/CSharpGenerator.js +2303 -0
- package/dist/codegen/CSharpGenerator.js.map +1 -0
- package/dist/codegen/CodeGenerator.d.ts +11 -0
- package/dist/codegen/CodeGenerator.d.ts.map +1 -0
- package/dist/codegen/CodeGenerator.js +11 -0
- package/dist/codegen/CodeGenerator.js.map +1 -0
- package/dist/codegen/CppGenerator.d.ts +23 -0
- package/dist/codegen/CppGenerator.d.ts.map +1 -0
- package/dist/codegen/CppGenerator.js +215 -0
- package/dist/codegen/CppGenerator.js.map +1 -0
- package/dist/codegen/HighPerformanceCSharpGenerator.d.ts +29 -0
- package/dist/codegen/HighPerformanceCSharpGenerator.d.ts.map +1 -0
- package/dist/codegen/HighPerformanceCSharpGenerator.js +486 -0
- package/dist/codegen/HighPerformanceCSharpGenerator.js.map +1 -0
- package/dist/core/DeukPackEngine.d.ts +69 -0
- package/dist/core/DeukPackEngine.d.ts.map +1 -0
- package/dist/core/DeukPackEngine.js +379 -0
- package/dist/core/DeukPackEngine.js.map +1 -0
- package/dist/core/DeukPackGenerator.d.ts +9 -0
- package/dist/core/DeukPackGenerator.d.ts.map +1 -0
- package/dist/core/DeukPackGenerator.js +15 -0
- package/dist/core/DeukPackGenerator.js.map +1 -0
- package/dist/core/DeukParser.d.ts +12 -0
- package/dist/core/DeukParser.d.ts.map +1 -0
- package/dist/core/DeukParser.js +27 -0
- package/dist/core/DeukParser.js.map +1 -0
- package/dist/core/IdlParser.d.ts +27 -0
- package/dist/core/IdlParser.d.ts.map +1 -0
- package/dist/core/IdlParser.js +157 -0
- package/dist/core/IdlParser.js.map +1 -0
- package/dist/core/ProtoParser.d.ts +12 -0
- package/dist/core/ProtoParser.d.ts.map +1 -0
- package/dist/core/ProtoParser.js +27 -0
- package/dist/core/ProtoParser.js.map +1 -0
- package/dist/csharp/DpExcelProtocol.cs +3005 -0
- package/dist/csharp/DpProtocolLibrary.cs +13 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer/DeukLexer.d.ts +31 -0
- package/dist/lexer/DeukLexer.d.ts.map +1 -0
- package/dist/lexer/DeukLexer.js +292 -0
- package/dist/lexer/DeukLexer.js.map +1 -0
- package/dist/lexer/IdlLexer.d.ts +33 -0
- package/dist/lexer/IdlLexer.d.ts.map +1 -0
- package/dist/lexer/IdlLexer.js +286 -0
- package/dist/lexer/IdlLexer.js.map +1 -0
- package/dist/native/NativeDeukPackEngine.d.ts +30 -0
- package/dist/native/NativeDeukPackEngine.d.ts.map +1 -0
- package/dist/native/NativeDeukPackEngine.js +99 -0
- package/dist/native/NativeDeukPackEngine.js.map +1 -0
- package/dist/proto/ProtoASTBuilder.d.ts +29 -0
- package/dist/proto/ProtoASTBuilder.d.ts.map +1 -0
- package/dist/proto/ProtoASTBuilder.js +239 -0
- package/dist/proto/ProtoASTBuilder.js.map +1 -0
- package/dist/proto/ProtoLexer.d.ts +29 -0
- package/dist/proto/ProtoLexer.d.ts.map +1 -0
- package/dist/proto/ProtoLexer.js +264 -0
- package/dist/proto/ProtoLexer.js.map +1 -0
- package/dist/proto/ProtoTypes.d.ts +40 -0
- package/dist/proto/ProtoTypes.d.ts.map +1 -0
- package/dist/proto/ProtoTypes.js +37 -0
- package/dist/proto/ProtoTypes.js.map +1 -0
- package/dist/protocols/BinaryProtocol.d.ts +7 -0
- package/dist/protocols/BinaryProtocol.d.ts.map +1 -0
- package/dist/protocols/BinaryProtocol.js +11 -0
- package/dist/protocols/BinaryProtocol.js.map +1 -0
- package/dist/protocols/BinaryWriter.d.ts +22 -0
- package/dist/protocols/BinaryWriter.d.ts.map +1 -0
- package/dist/protocols/BinaryWriter.js +104 -0
- package/dist/protocols/BinaryWriter.js.map +1 -0
- package/dist/protocols/CompactProtocol.d.ts +7 -0
- package/dist/protocols/CompactProtocol.d.ts.map +1 -0
- package/dist/protocols/CompactProtocol.js +11 -0
- package/dist/protocols/CompactProtocol.js.map +1 -0
- package/dist/protocols/ExcelProtocol.d.ts +98 -0
- package/dist/protocols/ExcelProtocol.d.ts.map +1 -0
- package/dist/protocols/ExcelProtocol.js +639 -0
- package/dist/protocols/ExcelProtocol.js.map +1 -0
- package/dist/protocols/JsonProtocol.d.ts +68 -0
- package/dist/protocols/JsonProtocol.d.ts.map +1 -0
- package/dist/protocols/JsonProtocol.js +422 -0
- package/dist/protocols/JsonProtocol.js.map +1 -0
- package/dist/protocols/WireProtocol.d.ts +348 -0
- package/dist/protocols/WireProtocol.d.ts.map +1 -0
- package/dist/protocols/WireProtocol.js +912 -0
- package/dist/protocols/WireProtocol.js.map +1 -0
- package/dist/serialization/WireDeserializer.d.ts +8 -0
- package/dist/serialization/WireDeserializer.d.ts.map +1 -0
- package/dist/serialization/WireDeserializer.js +13 -0
- package/dist/serialization/WireDeserializer.js.map +1 -0
- package/dist/serialization/WireSerializer.d.ts +20 -0
- package/dist/serialization/WireSerializer.d.ts.map +1 -0
- package/dist/serialization/WireSerializer.js +100 -0
- package/dist/serialization/WireSerializer.js.map +1 -0
- package/dist/types/DeukPackTypes.d.ts +291 -0
- package/dist/types/DeukPackTypes.d.ts.map +1 -0
- package/dist/types/DeukPackTypes.js +76 -0
- package/dist/types/DeukPackTypes.js.map +1 -0
- package/dist/utils/EndianUtils.d.ts +11 -0
- package/dist/utils/EndianUtils.d.ts.map +1 -0
- package/dist/utils/EndianUtils.js +32 -0
- package/dist/utils/EndianUtils.js.map +1 -0
- package/dist/utils/PerformanceMonitor.d.ts +26 -0
- package/dist/utils/PerformanceMonitor.d.ts.map +1 -0
- package/dist/utils/PerformanceMonitor.js +57 -0
- package/dist/utils/PerformanceMonitor.js.map +1 -0
- package/package.json +77 -0
- package/scripts/build_deukpack.js +669 -0
|
@@ -0,0 +1,3005 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeukPack DpExcelProtocol
|
|
3
|
+
* DpProtocol implementation that reads/writes Excel data using numeric field-ID hierarchy headers.
|
|
4
|
+
*
|
|
5
|
+
* Excel layout (신버전 3행 헤더):
|
|
6
|
+
* Row 1: HIERARCHY_ID / FIELD_ID (numeric field IDs, dot-separated: "1", "20", "20.1", "20.2.1")
|
|
7
|
+
* Row 2: DATATYPE — 득팩 표준 타입명 (int32, list<T>, record, enum<T> 등). 레거시(i32, lst, rec) 입력 수용.
|
|
8
|
+
* Row 3: COLUMN_NAME (human-readable)
|
|
9
|
+
* Row 4+: DATA
|
|
10
|
+
*
|
|
11
|
+
* Structure patterns:
|
|
12
|
+
* Primitive: col "1" (int64) → one cell per row
|
|
13
|
+
* Struct: col "40" (record), "40.1" (int32), "40.22.1" (string) → implicit parent inference
|
|
14
|
+
* List: col "20" (list<T>), "20.1" (int32 elem) → root col has list index (0,1,2,...), multi-row
|
|
15
|
+
*
|
|
16
|
+
* Type handling: 모든 타입 판별은 DpTypeNames.FromProtocolName(dt) 사용. 레거시(i32/lst/rec)와 표준(int32/list/record) 동일 처리.
|
|
17
|
+
* 분리 시트: ContainerSheetPolicy 단일 통로. IsContainerType / GetKindForWireType / GetSheetNamesForLookup 사용.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
using System;
|
|
21
|
+
using System.Collections;
|
|
22
|
+
using System.Collections.Generic;
|
|
23
|
+
using System.Linq;
|
|
24
|
+
using System.Reflection;
|
|
25
|
+
using System.Text;
|
|
26
|
+
|
|
27
|
+
namespace DeukPack.Protocol
|
|
28
|
+
{
|
|
29
|
+
/// <summary>Header-only tree node from Excel rows 1–3. Usable without csDeukDefine DLL.</summary>
|
|
30
|
+
public class ExcelHeaderEntry
|
|
31
|
+
{
|
|
32
|
+
public string HierarchyId { get; set; }
|
|
33
|
+
public string DataType { get; set; }
|
|
34
|
+
public string ColumnName { get; set; }
|
|
35
|
+
/// <summary>1-based column index in the sheet, or -1 for implicit (no physical column).</summary>
|
|
36
|
+
public int Col { get; set; }
|
|
37
|
+
public List<ExcelHeaderEntry> Children { get; set; } = new List<ExcelHeaderEntry>();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// <summary>DpSchema/Excel 헤더 공통 flat 엔트리. FlattenSchema()와 GetFlatHeaders() 모두 이 타입 반환.</summary>
|
|
41
|
+
public class FlatHeaderField
|
|
42
|
+
{
|
|
43
|
+
public string HierarchyId { get; set; }
|
|
44
|
+
public string DataType { get; set; }
|
|
45
|
+
public string ColumnName { get; set; }
|
|
46
|
+
public string DocComment { get; set; }
|
|
47
|
+
/// <summary>Excel 시트 컬럼 위치 (1-based). DLL 스키마 유래 시 0.</summary>
|
|
48
|
+
public int Col { get; set; }
|
|
49
|
+
/// <summary>True if this entry is a struct grouping marker (no data, visual separator only).</summary>
|
|
50
|
+
public bool IsStructMarker { get; set; }
|
|
51
|
+
/// <summary>Immediate parent struct type name (e.g. "Position" for x/y/z). Null/empty for top-level fields.</summary>
|
|
52
|
+
public string ParentStructName { get; set; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public interface IExcelSheet
|
|
56
|
+
{
|
|
57
|
+
string CellValue(int row, int col);
|
|
58
|
+
bool IsCellEmpty(int row, int col);
|
|
59
|
+
int LastColumn { get; }
|
|
60
|
+
int LastRow { get; }
|
|
61
|
+
/// <summary>워크북 이름(확장자 제외). 헤더 버전(v1/v2) 판별용. 구현체가 모르면 빈 문자열.</summary>
|
|
62
|
+
string WorkbookName { get; }
|
|
63
|
+
/// <summary>시트(탭) 이름. 버전 판별용. 구현체가 모르면 빈 문자열.</summary>
|
|
64
|
+
string SheetName { get; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public interface IWritableExcelSheet : IExcelSheet
|
|
68
|
+
{
|
|
69
|
+
void SetCellValue(int row, int col, string value);
|
|
70
|
+
void SetCellFormula(int row, int col, string formula);
|
|
71
|
+
void SetColumnNumberFormat(int col, string format);
|
|
72
|
+
void SetRangeInteriorColor(int rowStart, int rowEnd, int colStart, int colEnd, int colorOle);
|
|
73
|
+
void SetRangeFont(int rowStart, int rowEnd, int colStart, int colEnd, int fontColorOle, bool bold, bool italic, double fontSize);
|
|
74
|
+
void SetBottomBorder(int row, int colStart, int colEnd, int colorOle);
|
|
75
|
+
void AutoFitColumns(int colStart, int colEnd);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// <summary>
|
|
79
|
+
/// Provides separated container sheets (list/set/map) by sheet name.
|
|
80
|
+
/// Return null if the sheet does not exist → fallback to embedded (main sheet) mode.
|
|
81
|
+
/// For write: also provides a writable sheet for appending rows.
|
|
82
|
+
/// </summary>
|
|
83
|
+
public interface IContainerSheetResolver
|
|
84
|
+
{
|
|
85
|
+
/// <summary>Returns the read-only view of a container sheet, or null if not present.</summary>
|
|
86
|
+
IExcelSheet GetSheet(string sheetName);
|
|
87
|
+
/// <summary>Returns the writable view of a container sheet, creating it if needed.
|
|
88
|
+
/// Return null to fall back to embedded write.</summary>
|
|
89
|
+
IWritableExcelSheet GetOrCreateSheet(string sheetName, string[] headerHierarchyIds, string[] headerDataTypes, string[] headerColumnNames, string[] headerStructNames = null);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// <summary>
|
|
93
|
+
/// 분리 시트 정책 단일 통로. 스키마에서 "표시 위치가 분리되는" 타입(List/Set/Map)과 시트 kind/이름 규칙을 한 곳에서 정의.
|
|
94
|
+
/// 시트 코드 변경 시 이 정책만 수정하면 됨.
|
|
95
|
+
/// </summary>
|
|
96
|
+
public static class ContainerSheetPolicy
|
|
97
|
+
{
|
|
98
|
+
/// <summary>분리 시트를 갖는 타입. 이 타입만 별도 시트로 표시.</summary>
|
|
99
|
+
public static bool IsContainerType(DpWireType wt)
|
|
100
|
+
{
|
|
101
|
+
return wt == DpWireType.List || wt == DpWireType.Set || wt == DpWireType.Map;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// <summary>쓰기/신규 시트 생성 시 사용할 canonical kind (표준 표기).</summary>
|
|
105
|
+
public static string GetKindForWireType(DpWireType wt)
|
|
106
|
+
{
|
|
107
|
+
if (wt == DpWireType.List) return "list";
|
|
108
|
+
if (wt == DpWireType.Set) return "set";
|
|
109
|
+
if (wt == DpWireType.Map) return "map";
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// <summary>시트 조회 시 시도할 kind 목록 (canonical 먼저, 레거시 호환 후).</summary>
|
|
114
|
+
public static string[] GetKindVariantsForLookup(DpWireType wt)
|
|
115
|
+
{
|
|
116
|
+
if (wt == DpWireType.List) return new[] { "list", "lst" };
|
|
117
|
+
if (wt == DpWireType.Set) return new[] { "set" };
|
|
118
|
+
if (wt == DpWireType.Map) return new[] { "map" };
|
|
119
|
+
return System.Array.Empty<string>();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// <summary>파싱 시 허용할 kind 문자열. 여기만 수정하면 TryParse 동작 일괄 변경.</summary>
|
|
123
|
+
public static readonly string[] ValidKindsForParsing = { "lst", "list", "set", "map" };
|
|
124
|
+
|
|
125
|
+
public static bool IsValidKind(string kind)
|
|
126
|
+
{
|
|
127
|
+
if (string.IsNullOrEmpty(kind)) return false;
|
|
128
|
+
string k = kind.Trim().ToLowerInvariant();
|
|
129
|
+
foreach (var v in ValidKindsForParsing)
|
|
130
|
+
if (k == v) return true;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// <summary>
|
|
136
|
+
/// Helpers for container sheet naming and parsing. 시트 이름/파일 이름 형식은 여기서만 정의.
|
|
137
|
+
/// Sheet name format: "{fieldName}:{fieldId}" e.g. "spawners:30.4.1" — 공간 절약, 31자 잘림 시 이름만 축약해 필드ID 유지.
|
|
138
|
+
/// </summary>
|
|
139
|
+
public static class ContainerSheetNaming
|
|
140
|
+
{
|
|
141
|
+
const int EXCEL_SHEET_NAME_MAX = 31;
|
|
142
|
+
|
|
143
|
+
/// <summary>Build the container sheet name: "{fieldName}:{fieldId}". Excel 31자 제한 시 fieldName만 축약해 필드ID는 항상 보존.</summary>
|
|
144
|
+
public static string FormatContainerSheetName(string fieldId, string kind, string fieldName)
|
|
145
|
+
{
|
|
146
|
+
string name = (fieldName ?? "").Trim();
|
|
147
|
+
string id = (fieldId ?? "").Trim();
|
|
148
|
+
string suffix = (id.Length > 0) ? ":" + id : "";
|
|
149
|
+
int maxNameLen = EXCEL_SHEET_NAME_MAX - suffix.Length;
|
|
150
|
+
if (maxNameLen <= 0) return suffix.Length > EXCEL_SHEET_NAME_MAX ? suffix.Substring(0, EXCEL_SHEET_NAME_MAX) : suffix;
|
|
151
|
+
if (name.Length > maxNameLen) name = name.Substring(0, maxNameLen);
|
|
152
|
+
return name + suffix;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// <summary>분리 시트 조회 시 사용할 후보 시트 이름. "{fieldName}:{fieldId}" 먼저, 이어서 레거시 "fieldId kind fieldName" 순.</summary>
|
|
156
|
+
public static string[] GetSheetNamesForLookup(string fieldId, DpWireType wireType, string fieldName)
|
|
157
|
+
{
|
|
158
|
+
var canonical = FormatContainerSheetName(fieldId, null, fieldName ?? "");
|
|
159
|
+
var kinds = ContainerSheetPolicy.GetKindVariantsForLookup(wireType);
|
|
160
|
+
var names = new List<string> { canonical };
|
|
161
|
+
foreach (string k in kinds)
|
|
162
|
+
{
|
|
163
|
+
string legacy = $"{fieldId} {k} {fieldName ?? ""}".Trim();
|
|
164
|
+
if (legacy.Length <= EXCEL_SHEET_NAME_MAX && !names.Contains(legacy, StringComparer.OrdinalIgnoreCase))
|
|
165
|
+
names.Add(legacy);
|
|
166
|
+
}
|
|
167
|
+
return names.ToArray();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static bool LooksLikeFieldId(string value)
|
|
171
|
+
{
|
|
172
|
+
if (string.IsNullOrEmpty(value)) return false;
|
|
173
|
+
foreach (char c in value) if (c != '.' && !char.IsDigit(c)) return false;
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// <summary>Parse container sheet name. "{fieldName}:{fieldId}" 또는 레거시 "{fieldId} {kind} {fieldName}".</summary>
|
|
178
|
+
public static bool TryParseContainerSheetName(string sheetName,
|
|
179
|
+
out string fieldId, out string kind, out string fieldName)
|
|
180
|
+
{
|
|
181
|
+
fieldId = kind = fieldName = null;
|
|
182
|
+
if (string.IsNullOrWhiteSpace(sheetName)) return false;
|
|
183
|
+
string s = sheetName.Trim();
|
|
184
|
+
// 신규 형식: "spawners:30.4.1" — 마지막 ':' 기준으로 우측이 숫자·점이면 fieldId
|
|
185
|
+
int lastColon = s.LastIndexOf(':');
|
|
186
|
+
if (lastColon >= 0 && lastColon < s.Length - 1)
|
|
187
|
+
{
|
|
188
|
+
string right = s.Substring(lastColon + 1).Trim();
|
|
189
|
+
if (LooksLikeFieldId(right))
|
|
190
|
+
{
|
|
191
|
+
fieldName = s.Substring(0, lastColon).Trim();
|
|
192
|
+
fieldId = right;
|
|
193
|
+
kind = "list";
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// 레거시: "30 list spawners" 또는 이전 괄호 형식 "spawners (30.4.1)"
|
|
198
|
+
int lastOpen = s.LastIndexOf(" (", StringComparison.Ordinal);
|
|
199
|
+
if (lastOpen >= 0 && s.Length > lastOpen + 2 && s[s.Length - 1] == ')')
|
|
200
|
+
{
|
|
201
|
+
fieldName = s.Substring(0, lastOpen).Trim();
|
|
202
|
+
fieldId = s.Substring(lastOpen + 2, s.Length - lastOpen - 3).Trim();
|
|
203
|
+
if (LooksLikeFieldId(fieldId)) { kind = "list"; return true; }
|
|
204
|
+
}
|
|
205
|
+
if (s.Length > 2 && s[0] == '(' && s[s.Length - 1] == ')')
|
|
206
|
+
{
|
|
207
|
+
fieldId = s.Substring(1, s.Length - 2).Trim();
|
|
208
|
+
if (LooksLikeFieldId(fieldId)) { fieldName = ""; kind = "list"; return true; }
|
|
209
|
+
}
|
|
210
|
+
var parts = s.Split(new[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
|
|
211
|
+
if (parts.Length < 3) return false;
|
|
212
|
+
string k = parts[1].ToLowerInvariant();
|
|
213
|
+
if (!ContainerSheetPolicy.IsValidKind(k)) return false;
|
|
214
|
+
fieldId = parts[0];
|
|
215
|
+
kind = k;
|
|
216
|
+
fieldName = parts[2];
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// <summary>Build container file name: "{exportBase}.{fieldId}.{kind}.{fieldName}.{ext}"</summary>
|
|
221
|
+
public static string FormatContainerFileName(string exportBase, string fieldId, string kind, string fieldName, string ext)
|
|
222
|
+
=> $"{exportBase}.{fieldId}.{kind}.{fieldName}{ext}";
|
|
223
|
+
|
|
224
|
+
/// <summary>
|
|
225
|
+
/// Extract exportBase from a workbook filename: remove the trailing "@deuk" or "@gplat" (compatible) suffix.
|
|
226
|
+
/// e.g. "mo_map@deuk.xlsx" → "mo_map", "npc@entity@gplat.xlsx" → "npc@entity"
|
|
227
|
+
/// </summary>
|
|
228
|
+
public static string GetExportBase(string workbookPath)
|
|
229
|
+
{
|
|
230
|
+
if (string.IsNullOrEmpty(workbookPath)) return "";
|
|
231
|
+
string name = System.IO.Path.GetFileNameWithoutExtension(workbookPath);
|
|
232
|
+
const string primarySuffix = "@deuk";
|
|
233
|
+
const string compatibleSuffix = "@gplat";
|
|
234
|
+
if (name.EndsWith(primarySuffix, StringComparison.OrdinalIgnoreCase))
|
|
235
|
+
name = name.Substring(0, name.Length - primarySuffix.Length);
|
|
236
|
+
else if (name.EndsWith(compatibleSuffix, StringComparison.OrdinalIgnoreCase))
|
|
237
|
+
name = name.Substring(0, name.Length - compatibleSuffix.Length);
|
|
238
|
+
return name;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// <summary>Column 1 of every separated sheet: nav-back button (value = meta_id, click to return to main sheet).</summary>
|
|
242
|
+
public const string NAV_BACK_COLUMN = "_nav";
|
|
243
|
+
/// <summary>Column 2 of every separated sheet: meta_id (links back to the parent record).</summary>
|
|
244
|
+
public const string META_ID_COLUMN = "meta_id";
|
|
245
|
+
/// <summary>Column 3 of every separated sheet: name (human-readable, copied from parent).</summary>
|
|
246
|
+
public const string META_NAME_COLUMN = "name";
|
|
247
|
+
|
|
248
|
+
private const string BACKUP_MARKER = "_bak_";
|
|
249
|
+
|
|
250
|
+
/// <summary>백업 시트 명명 통일: b_원래이름_날자 (날자 = yyMMdd_HHmm). Excel 시트명 31자 제한.</summary>
|
|
251
|
+
public const string BACKUP_PREFIX = "b_";
|
|
252
|
+
/// <summary>백업 날자 형식. MakeBackupSheetName와 IsBackupSheet 판별에 동일 사용.</summary>
|
|
253
|
+
public const string BACKUP_DATE_FORMAT = "yyMMdd_HHmm";
|
|
254
|
+
|
|
255
|
+
/// <summary>
|
|
256
|
+
/// True if the sheet name looks like a backup: contains "_bak_" (legacy) or matches b_원래이름_yyMMdd_HHmm.
|
|
257
|
+
/// </summary>
|
|
258
|
+
public static bool IsBackupSheet(string sheetName)
|
|
259
|
+
{
|
|
260
|
+
if (string.IsNullOrEmpty(sheetName)) return false;
|
|
261
|
+
if (sheetName.IndexOf(BACKUP_MARKER, StringComparison.OrdinalIgnoreCase) >= 0)
|
|
262
|
+
return true;
|
|
263
|
+
if (sheetName.StartsWith(BACKUP_PREFIX, StringComparison.OrdinalIgnoreCase) && sheetName.Length >= 15)
|
|
264
|
+
{
|
|
265
|
+
string suffix = sheetName.Substring(sheetName.Length - 12);
|
|
266
|
+
if (suffix.Length == 12 && suffix[0] == '_' && suffix[7] == '_')
|
|
267
|
+
{
|
|
268
|
+
bool digits = true;
|
|
269
|
+
for (int i = 1; i <= 6 && digits; i++) digits = char.IsDigit(suffix[i]);
|
|
270
|
+
for (int i = 8; i <= 11 && digits; i++) digits = char.IsDigit(suffix[i]);
|
|
271
|
+
if (digits) return true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// <summary>백업 시트 이름 생성 (통일 규칙: b_원래이름_yyMMdd_HHmm). Excel 31자 제한. existingSheetNames에 없으면 그대로, 있으면 _2, _3 붙임.</summary>
|
|
278
|
+
public static string MakeBackupSheetName(string originalSheetName, System.Collections.Generic.IEnumerable<string> existingSheetNames)
|
|
279
|
+
{
|
|
280
|
+
const int maxLen = 31;
|
|
281
|
+
string ts = System.DateTime.Now.ToString(BACKUP_DATE_FORMAT);
|
|
282
|
+
int suffixLen = BACKUP_PREFIX.Length + 1 + ts.Length;
|
|
283
|
+
int maxBase = Math.Max(1, maxLen - suffixLen);
|
|
284
|
+
string basePart = (originalSheetName ?? "").Trim();
|
|
285
|
+
if (basePart.Length > maxBase) basePart = basePart.Substring(0, maxBase);
|
|
286
|
+
string candidate = BACKUP_PREFIX + basePart + "_" + ts;
|
|
287
|
+
if (candidate.Length > maxLen) candidate = candidate.Substring(0, maxLen);
|
|
288
|
+
|
|
289
|
+
var existing = new System.Collections.Generic.HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
|
|
290
|
+
if (existingSheetNames != null)
|
|
291
|
+
{
|
|
292
|
+
foreach (string n in existingSheetNames)
|
|
293
|
+
if (!string.IsNullOrEmpty(n)) existing.Add(n);
|
|
294
|
+
}
|
|
295
|
+
if (existing.Count > 0 && existing.Contains(candidate))
|
|
296
|
+
{
|
|
297
|
+
for (int i = 2; i < 100; i++)
|
|
298
|
+
{
|
|
299
|
+
string alt = candidate.Length + 2 <= maxLen ? $"{candidate}_{i}" : $"{candidate.Substring(0, maxLen - 2)}_{i}";
|
|
300
|
+
if (alt.Length > maxLen) alt = alt.Substring(0, maxLen);
|
|
301
|
+
if (!existing.Contains(alt)) { candidate = alt; break; }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return candidate;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// <summary>
|
|
308
|
+
/// True if the sheet name is a separated container sheet (lst/set/map).
|
|
309
|
+
/// </summary>
|
|
310
|
+
public static bool IsContainerSheet(string sheetName)
|
|
311
|
+
{
|
|
312
|
+
return TryParseContainerSheetName(sheetName, out _, out _, out _);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// <summary>
|
|
316
|
+
/// True if the sheet is a "main content" sheet: not a backup, not a container (lst/set/map).
|
|
317
|
+
/// </summary>
|
|
318
|
+
public static bool IsMainContentSheet(string sheetName)
|
|
319
|
+
{
|
|
320
|
+
if (string.IsNullOrEmpty(sheetName)) return false;
|
|
321
|
+
if (IsBackupSheet(sheetName)) return false;
|
|
322
|
+
if (IsContainerSheet(sheetName)) return false;
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
public class DpExcelProtocol : DpProtocol
|
|
328
|
+
{
|
|
329
|
+
private readonly IExcelSheet _sheet;
|
|
330
|
+
private int _dataRow;
|
|
331
|
+
private int _maxDataRow;
|
|
332
|
+
private readonly int _sheetFirstDataRow;
|
|
333
|
+
|
|
334
|
+
// Optional resolver for separated container sheets (list/set/map)
|
|
335
|
+
private readonly IContainerSheetResolver _resolver;
|
|
336
|
+
|
|
337
|
+
// Column mapping built from Row 1 (HIERARCHY_ID)
|
|
338
|
+
private readonly Dictionary<string, int> _fieldIdToCol = new Dictionary<string, int>();
|
|
339
|
+
private readonly Dictionary<int, string> _colDataType = new Dictionary<int, string>();
|
|
340
|
+
private readonly Dictionary<int, string> _colColumnName = new Dictionary<int, string>();
|
|
341
|
+
|
|
342
|
+
// Struct navigation: path stack + pending fields stack
|
|
343
|
+
private readonly Stack<string> _pathStack = new Stack<string>();
|
|
344
|
+
private string _currentPath = "";
|
|
345
|
+
private readonly Stack<StructState> _structStack = new Stack<StructState>();
|
|
346
|
+
private List<FieldEntry> _pendingFields;
|
|
347
|
+
private int _pendingIdx;
|
|
348
|
+
|
|
349
|
+
// List navigation
|
|
350
|
+
private readonly Stack<ListState> _listStack = new Stack<ListState>();
|
|
351
|
+
|
|
352
|
+
// Last field returned by ReadFieldBegin (for ReadCurrentCellValue)
|
|
353
|
+
private FieldEntry _lastField;
|
|
354
|
+
|
|
355
|
+
// Row stack: save/restore _dataRow across nested structs within lists
|
|
356
|
+
private readonly Stack<int> _dataRowStack = new Stack<int>();
|
|
357
|
+
|
|
358
|
+
// Separated sheet context: when reading a struct element from a container sheet
|
|
359
|
+
private IExcelSheet _currentSeparatedSheet;
|
|
360
|
+
private int _currentSeparatedRow;
|
|
361
|
+
|
|
362
|
+
private struct FieldEntry
|
|
363
|
+
{
|
|
364
|
+
public short Id;
|
|
365
|
+
public DpWireType Type;
|
|
366
|
+
public int Col; // -1 for implicit parents
|
|
367
|
+
public string Path; // full field-ID path (e.g., "40.22")
|
|
368
|
+
public string ColumnName; // human-readable name (for container sheet naming)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private struct StructState
|
|
372
|
+
{
|
|
373
|
+
public List<FieldEntry> Fields;
|
|
374
|
+
public int Index;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private class ListState
|
|
378
|
+
{
|
|
379
|
+
public int StartRow;
|
|
380
|
+
public int CurrentElement;
|
|
381
|
+
public int Count;
|
|
382
|
+
public DpWireType ElementType;
|
|
383
|
+
public int RootCol;
|
|
384
|
+
public string ListPath; // field-ID path of the list root (e.g., "20")
|
|
385
|
+
public int SavedDataRow; // _dataRow before list started
|
|
386
|
+
public int StructDepth; // 0 = at list level; >0 = inside struct element(s)
|
|
387
|
+
public int PrimitiveElemCol; // for primitive lists: the child column holding the value
|
|
388
|
+
|
|
389
|
+
// Separated sheet state (non-null when reading from a container sheet)
|
|
390
|
+
public IExcelSheet SeparatedSheet; // the container sheet (read)
|
|
391
|
+
public int[] SeparatedDataRows; // rows in container sheet for this meta_id
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
public const int HIERARCHY_ID_ROW = 1;
|
|
395
|
+
public const int DATATYPE_ROW = 2;
|
|
396
|
+
public const int COLUMN_NAME_ROW = 3;
|
|
397
|
+
/// <summary>신버전 헤더 행 수(3). 헤더 고정 시 1~3행 고정.</summary>
|
|
398
|
+
public const int HEADER_ROW_COUNT = 3;
|
|
399
|
+
/// <summary>신버전 데이터 시작 행(4).</summary>
|
|
400
|
+
public const int FIRST_DATA_ROW = 4;
|
|
401
|
+
/// <summary>이전 스키마 헤더 행 수(4).</summary>
|
|
402
|
+
public const int LEGACY_HEADER_ROW_COUNT = 4;
|
|
403
|
+
/// <summary>이전 스키마 데이터 시작 행(5).</summary>
|
|
404
|
+
public const int LEGACY_FIRST_DATA_ROW = 5;
|
|
405
|
+
|
|
406
|
+
/// <summary>메인 시트 고정 열 이름 (1~3열). tuid 기반 테이블 기본값. keyed 테이블은 GetMainSheetFixedColumnNamesForCategory 사용.</summary>
|
|
407
|
+
public static readonly string[] MainSheetFixedColumnNames = { "tuid", "tid", "name", "note" };
|
|
408
|
+
|
|
409
|
+
/// <summary>카테고리별 메인 시트 고정 열. getKeyFieldNames가 null이면 MainSheetFixedColumnNames. keyed(키≠tuid)면 tuid 제외, 키→name→note 순.</summary>
|
|
410
|
+
public static IReadOnlyList<string> GetMainSheetFixedColumnNamesForCategory(string category, Func<string, IReadOnlyList<string>> getKeyFieldNames = null)
|
|
411
|
+
{
|
|
412
|
+
if (string.IsNullOrEmpty(category) || getKeyFieldNames == null)
|
|
413
|
+
return MainSheetFixedColumnNames;
|
|
414
|
+
var keys = getKeyFieldNames(category);
|
|
415
|
+
if (keys == null || keys.Count == 0)
|
|
416
|
+
return MainSheetFixedColumnNames;
|
|
417
|
+
bool isKeyed = !string.Equals(keys[0], "tuid", StringComparison.OrdinalIgnoreCase);
|
|
418
|
+
if (!isKeyed)
|
|
419
|
+
return MainSheetFixedColumnNames;
|
|
420
|
+
var list = new List<string>(keys);
|
|
421
|
+
if (!list.Contains("name")) list.Add("name");
|
|
422
|
+
if (!list.Contains("note")) list.Add("note");
|
|
423
|
+
return list;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/// <summary>워크북·시트 이름으로 v1/v2 판별 후 (데이터 시작 행, 헤더 행 수) 반환. 애드인에서 프로토콜 없이 행 정보가 필요할 때 사용.</summary>
|
|
427
|
+
/// <param name="defaultMainSheetName">프로젝트 기본 메인 시트 이름(deukpack.config). 이 이름과 일치하면 v2로 간주. null이면 기존 동작(시트명 "meta"만 v2).</param>
|
|
428
|
+
public static (int firstDataRow, int headerRowCount) GetLayout(string workbookNameOrPath, string sheetName = null, string defaultMainSheetName = null)
|
|
429
|
+
{
|
|
430
|
+
bool newVer = IsNewVersionWorkbook(workbookNameOrPath, sheetName, defaultMainSheetName);
|
|
431
|
+
return (firstDataRow: GetFirstDataRow(newVer), headerRowCount: GetHeaderRowCount(newVer));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/// <summary>컨테이너(리스트) 시트의 헤더 정보: 1열이 _nav이면 (true, 2, 3), 아니면 (false, 1, 2). 메인/리스트 공통 모듈에서 한 번만 조회해 사용.</summary>
|
|
435
|
+
public static (bool hasNavCol, int metaIdCol, int metaNameCol) GetContainerLayout(IExcelSheet sheet)
|
|
436
|
+
{
|
|
437
|
+
if (sheet == null) return (false, 1, 2);
|
|
438
|
+
string col1Hier = sheet.CellValue(HIERARCHY_ID_ROW, 1)?.Trim() ?? "";
|
|
439
|
+
bool hasNav = string.Equals(col1Hier, ContainerSheetNaming.NAV_BACK_COLUMN, StringComparison.OrdinalIgnoreCase);
|
|
440
|
+
return hasNav ? (true, 2, 3) : (false, 1, 2);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/// <summary>v2(신규)면 true. 시트명이 defaultMainSheetName 또는 "meta"이면 v2. 워크북 파일명이 "meta"여도 v2.</summary>
|
|
444
|
+
public static bool IsNewVersionWorkbook(string workbookNameOrPath, string mainSheetName = null, string defaultMainSheetName = null)
|
|
445
|
+
{
|
|
446
|
+
if (!string.IsNullOrWhiteSpace(mainSheetName))
|
|
447
|
+
{
|
|
448
|
+
var sn = mainSheetName.Trim();
|
|
449
|
+
if (string.Equals(sn, "meta", StringComparison.OrdinalIgnoreCase)) return true;
|
|
450
|
+
if (!string.IsNullOrWhiteSpace(defaultMainSheetName) && string.Equals(sn, defaultMainSheetName.Trim(), StringComparison.OrdinalIgnoreCase)) return true;
|
|
451
|
+
}
|
|
452
|
+
if (string.IsNullOrWhiteSpace(workbookNameOrPath)) return false;
|
|
453
|
+
string name = System.IO.Path.GetFileNameWithoutExtension(workbookNameOrPath.Trim());
|
|
454
|
+
if (string.IsNullOrEmpty(name)) return false;
|
|
455
|
+
name = name.Trim();
|
|
456
|
+
if (string.Equals(name, "meta", StringComparison.OrdinalIgnoreCase)) return true;
|
|
457
|
+
if (!string.IsNullOrWhiteSpace(defaultMainSheetName) && string.Equals(name, defaultMainSheetName.Trim(), StringComparison.OrdinalIgnoreCase)) return true;
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
internal static int GetFirstDataRow(bool isNewVersion) => isNewVersion ? FIRST_DATA_ROW : LEGACY_FIRST_DATA_ROW;
|
|
462
|
+
internal static int GetHeaderRowCount(bool isNewVersion) => isNewVersion ? HEADER_ROW_COUNT : LEGACY_HEADER_ROW_COUNT;
|
|
463
|
+
|
|
464
|
+
private bool _useCompactHeader;
|
|
465
|
+
private readonly int _firstDataRow;
|
|
466
|
+
private readonly int _headerRowCount;
|
|
467
|
+
|
|
468
|
+
/// <summary>현재 시트의 데이터 시작 행(v1=5, v2=4). 프로토콜 내부 버전 정보 기준.</summary>
|
|
469
|
+
public int FirstDataRow => _firstDataRow;
|
|
470
|
+
/// <summary>현재 시트의 헤더 행 수(v1=4, v2=3). 프로토콜 내부 버전 정보 기준.</summary>
|
|
471
|
+
public int HeaderRowCount => _headerRowCount;
|
|
472
|
+
/// <summary>true면 v2(3행 헤더). 쓰기 시 설정 가능.</summary>
|
|
473
|
+
public bool UseCompactHeader { get => _useCompactHeader; set => _useCompactHeader = value; }
|
|
474
|
+
|
|
475
|
+
/// <summary>시트와 워크북/시트 이름으로 버전을 판별하여 행 정보를 프로토콜 내부에 보관. workbookName/sheetName이 null이면 sheet.WorkbookName/SheetName 사용. defaultMainSheetName은 애드인에서 deukpack.config 기준으로 전달.</summary>
|
|
476
|
+
public DpExcelProtocol(IExcelSheet sheet, int dataRow = FIRST_DATA_ROW,
|
|
477
|
+
IContainerSheetResolver resolver = null, string workbookName = null, string sheetName = null, string defaultMainSheetName = null)
|
|
478
|
+
{
|
|
479
|
+
_sheet = sheet ?? throw new ArgumentNullException(nameof(sheet));
|
|
480
|
+
string wb = workbookName ?? sheet.WorkbookName ?? "";
|
|
481
|
+
string sn = sheetName ?? sheet.SheetName ?? "";
|
|
482
|
+
bool isNewVersion = IsNewVersionWorkbook(wb, sn, defaultMainSheetName);
|
|
483
|
+
_useCompactHeader = isNewVersion;
|
|
484
|
+
_firstDataRow = GetFirstDataRow(isNewVersion);
|
|
485
|
+
_headerRowCount = GetHeaderRowCount(isNewVersion);
|
|
486
|
+
_sheetFirstDataRow = _firstDataRow;
|
|
487
|
+
_dataRow = dataRow;
|
|
488
|
+
_maxDataRow = dataRow;
|
|
489
|
+
_resolver = resolver;
|
|
490
|
+
BuildColumnMap();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/// <summary>Source file path this protocol was created from. Set by caller for tracking.</summary>
|
|
494
|
+
public string SourceFilePath { get; set; }
|
|
495
|
+
|
|
496
|
+
/// <summary>
|
|
497
|
+
/// Optional schema resolver for generating separated sheet headers during Write.
|
|
498
|
+
/// Set before calling obj.Write() so WriteListBegin can create sheets with correct headers.
|
|
499
|
+
/// </summary>
|
|
500
|
+
public Func<string, DpSchema> ResolveTypeName { get; set; }
|
|
501
|
+
|
|
502
|
+
/// <summary>
|
|
503
|
+
/// Resolves meta schema name (e.g. "mo_map") to the data DpSchema for Write.
|
|
504
|
+
/// Set by add-in so BeginWrite(sheet, row, metaSchemaName) can use DLL schema when name is given.
|
|
505
|
+
/// </summary>
|
|
506
|
+
public Func<string, DpSchema> ResolveMetaSchema { get; set; }
|
|
507
|
+
|
|
508
|
+
/// <summary>
|
|
509
|
+
/// 현재 쓰기 중인 메타 카테고리(예: "mo_skill", "level"). keyed 테이블(level 등) 시 시트에서 meta_id 제외·키 선행 정렬용.
|
|
510
|
+
/// BeginWrite 전에 설정. GetKeyFieldNames와 함께 사용.
|
|
511
|
+
/// </summary>
|
|
512
|
+
public string WriteCategory { get; set; }
|
|
513
|
+
|
|
514
|
+
/// <summary>
|
|
515
|
+
/// 카테고리별 키 필드명 조회(예: Generated.MetaTableRegistry.GetKeyFieldNames). keyed 테이블 시 고정 열 순서·meta_id 미생성에 사용.
|
|
516
|
+
/// </summary>
|
|
517
|
+
public Func<string, IReadOnlyList<string>> GetKeyFieldNames { get; set; }
|
|
518
|
+
|
|
519
|
+
/// <summary>
|
|
520
|
+
/// For WriteI32: (hierarchyId) → field descriptor. Used only to detect enum and resolve display; set by add-in.
|
|
521
|
+
/// </summary>
|
|
522
|
+
public Func<string, object> GetFieldDescriptor { get; set; }
|
|
523
|
+
|
|
524
|
+
/// <summary>
|
|
525
|
+
/// For WriteI32: (fieldDescriptor) → "value:name" list for that enum type, or null if not enum. Set by add-in.
|
|
526
|
+
/// </summary>
|
|
527
|
+
public Func<object, List<string>> GetEnumValuesForField { get; set; }
|
|
528
|
+
|
|
529
|
+
public int DataRow => _dataRow;
|
|
530
|
+
/// <summary>The furthest row reached during Read(). Use to determine how many rows were consumed.</summary>
|
|
531
|
+
public int MaxDataRow => _maxDataRow;
|
|
532
|
+
public IExcelSheet Sheet => _sheet;
|
|
533
|
+
public void SetDataRow(int row) { _dataRow = row; if (row > _maxDataRow) _maxDataRow = row; }
|
|
534
|
+
|
|
535
|
+
/// <summary>
|
|
536
|
+
/// Reset protocol navigation state for reading a new record at the given row.
|
|
537
|
+
/// Reuses the existing column map (built once in the constructor).
|
|
538
|
+
/// </summary>
|
|
539
|
+
public void ResetForRow(int row)
|
|
540
|
+
{
|
|
541
|
+
_dataRow = row;
|
|
542
|
+
_maxDataRow = row;
|
|
543
|
+
_pathStack.Clear();
|
|
544
|
+
_structStack.Clear();
|
|
545
|
+
_listStack.Clear();
|
|
546
|
+
_dataRowStack.Clear();
|
|
547
|
+
_currentPath = "";
|
|
548
|
+
_pendingFields = null;
|
|
549
|
+
_pendingIdx = 0;
|
|
550
|
+
_lastField = default;
|
|
551
|
+
_currentSeparatedSheet = null;
|
|
552
|
+
_currentSeparatedRow = 0;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/// <summary>
|
|
556
|
+
/// Iterate all top-level records in this sheet.
|
|
557
|
+
/// Automatically handles row advancement (including multi-row list records).
|
|
558
|
+
/// The handler receives this protocol positioned at each record's start row.
|
|
559
|
+
/// </summary>
|
|
560
|
+
public void ForEachRecord(Action<DpExcelProtocol> handler)
|
|
561
|
+
{
|
|
562
|
+
int lastRow = _sheet.LastRow;
|
|
563
|
+
for (int row = _sheetFirstDataRow; row <= lastRow; )
|
|
564
|
+
{
|
|
565
|
+
ResetForRow(row);
|
|
566
|
+
handler(this);
|
|
567
|
+
row = Math.Max(_maxDataRow + 1, row + 1);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
#region Column Map
|
|
572
|
+
|
|
573
|
+
private readonly List<string> _diagColumnHeaders = new List<string>();
|
|
574
|
+
|
|
575
|
+
/// <summary>Diagnostic: column headers that were read from Row 1. Use for debugging.</summary>
|
|
576
|
+
public IReadOnlyList<string> DiagColumnHeaders => _diagColumnHeaders;
|
|
577
|
+
|
|
578
|
+
private int ColumnNameRowForRead => COLUMN_NAME_ROW;
|
|
579
|
+
|
|
580
|
+
private void BuildColumnMap()
|
|
581
|
+
{
|
|
582
|
+
_fieldIdToCol.Clear();
|
|
583
|
+
_colDataType.Clear();
|
|
584
|
+
_colColumnName.Clear();
|
|
585
|
+
_diagColumnHeaders.Clear();
|
|
586
|
+
int nameRow = ColumnNameRowForRead;
|
|
587
|
+
for (int col = 1; col <= _sheet.LastColumn; col++)
|
|
588
|
+
{
|
|
589
|
+
var hier = _sheet.CellValue(HIERARCHY_ID_ROW, col)?.Trim();
|
|
590
|
+
if (string.IsNullOrEmpty(hier))
|
|
591
|
+
continue;
|
|
592
|
+
var dtRaw = _sheet.CellValue(DATATYPE_ROW, col)?.Trim() ?? "";
|
|
593
|
+
var dtNorm = string.IsNullOrEmpty(dtRaw) ? "" : DpTypeNames.ToDisplayTypeName(dtRaw);
|
|
594
|
+
var cn = _sheet.CellValue(nameRow, col)?.Trim() ?? "";
|
|
595
|
+
_fieldIdToCol[hier] = col;
|
|
596
|
+
_colDataType[col] = dtNorm;
|
|
597
|
+
_colColumnName[col] = cn;
|
|
598
|
+
_diagColumnHeaders.Add($"col{col}={hier}({dtNorm})");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/// <summary>
|
|
603
|
+
/// Build a tree from sheet headers (rows 1–3) only. No DLL dependency; use for display (e.g. schema tree).
|
|
604
|
+
/// List/rec appear as parent nodes with children from hierarchy IDs.
|
|
605
|
+
/// Sorted by column position (physical sheet order = define order).
|
|
606
|
+
/// </summary>
|
|
607
|
+
public List<ExcelHeaderEntry> GetHeaderTree()
|
|
608
|
+
{
|
|
609
|
+
var allPaths = new List<string>(_fieldIdToCol.Keys);
|
|
610
|
+
|
|
611
|
+
var roots = new List<string>();
|
|
612
|
+
foreach (var p in allPaths)
|
|
613
|
+
{
|
|
614
|
+
if (string.IsNullOrEmpty(p)) continue;
|
|
615
|
+
if (p.IndexOf('.') < 0)
|
|
616
|
+
roots.Add(p);
|
|
617
|
+
}
|
|
618
|
+
roots.Sort(ComparePathByCol);
|
|
619
|
+
|
|
620
|
+
var result = new List<ExcelHeaderEntry>();
|
|
621
|
+
foreach (var path in roots)
|
|
622
|
+
result.Add(BuildHeaderNode(path, allPaths));
|
|
623
|
+
return result;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/// <summary>
|
|
627
|
+
/// Excel 시트 헤더(Row 1–3)를 FlatHeaderField 리스트로 반환 (컬럼 물리 순서).
|
|
628
|
+
/// FlattenSchema()와 동일한 형태이므로 두 결과를 직접 비교 가능.
|
|
629
|
+
/// </summary>
|
|
630
|
+
public List<FlatHeaderField> GetFlatHeaders()
|
|
631
|
+
{
|
|
632
|
+
var byCol = new List<KeyValuePair<string, int>>(_fieldIdToCol.Count);
|
|
633
|
+
foreach (var kv in _fieldIdToCol)
|
|
634
|
+
byCol.Add(new KeyValuePair<string, int>(kv.Key, kv.Value));
|
|
635
|
+
byCol.Sort((a, b) => a.Value.CompareTo(b.Value));
|
|
636
|
+
|
|
637
|
+
var result = new List<FlatHeaderField>(byCol.Count);
|
|
638
|
+
foreach (var kv in byCol)
|
|
639
|
+
{
|
|
640
|
+
result.Add(new FlatHeaderField
|
|
641
|
+
{
|
|
642
|
+
HierarchyId = kv.Key,
|
|
643
|
+
DataType = _colDataType.TryGetValue(kv.Value, out var dt) ? dt : "",
|
|
644
|
+
ColumnName = _sheet.CellValue(ColumnNameRowForRead, kv.Value)?.Trim() ?? "",
|
|
645
|
+
DocComment = "",
|
|
646
|
+
Col = kv.Value
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private int ComparePathByCol(string a, string b)
|
|
653
|
+
{
|
|
654
|
+
int colA = _fieldIdToCol.TryGetValue(a, out int ca) ? ca : int.MaxValue;
|
|
655
|
+
int colB = _fieldIdToCol.TryGetValue(b, out int cb) ? cb : int.MaxValue;
|
|
656
|
+
return colA.CompareTo(colB);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private ExcelHeaderEntry BuildHeaderNode(string path, List<string> allPaths)
|
|
660
|
+
{
|
|
661
|
+
int col = _fieldIdToCol.TryGetValue(path, out int c) ? c : -1;
|
|
662
|
+
string dataType = col > 0 && _colDataType.TryGetValue(col, out var dt) ? dt : "";
|
|
663
|
+
string columnName = col > 0 ? (_sheet.CellValue(ColumnNameRowForRead, col)?.Trim() ?? "") : "";
|
|
664
|
+
|
|
665
|
+
var entry = new ExcelHeaderEntry
|
|
666
|
+
{
|
|
667
|
+
HierarchyId = path,
|
|
668
|
+
DataType = dataType,
|
|
669
|
+
ColumnName = columnName,
|
|
670
|
+
Col = col
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
string prefix = path + ".";
|
|
674
|
+
foreach (var p in allPaths)
|
|
675
|
+
{
|
|
676
|
+
if (!p.StartsWith(prefix)) continue;
|
|
677
|
+
string remainder = p.Substring(prefix.Length);
|
|
678
|
+
if (remainder.IndexOf('.') >= 0) continue;
|
|
679
|
+
entry.Children.Add(BuildHeaderNode(p, allPaths));
|
|
680
|
+
}
|
|
681
|
+
entry.Children.Sort((a, b) => a.Col.CompareTo(b.Col));
|
|
682
|
+
return entry;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/// <summary>
|
|
686
|
+
/// Collect direct children of parentPath.
|
|
687
|
+
/// Type is ALWAYS determined from DATATYPE (Row 2), not from structural position.
|
|
688
|
+
/// - rec/lst/map with explicit column → type from DATATYPE
|
|
689
|
+
/// - Implicit parent (no column, inferred from descendants) → Struct
|
|
690
|
+
/// (lst/map always have explicit root columns in Excel format)
|
|
691
|
+
/// Explicit columns always take priority regardless of Dictionary iteration order.
|
|
692
|
+
/// </summary>
|
|
693
|
+
private List<FieldEntry> CollectChildFields(string parentPath)
|
|
694
|
+
{
|
|
695
|
+
var entries = new Dictionary<short, FieldEntry>();
|
|
696
|
+
string prefix = string.IsNullOrEmpty(parentPath) ? "" : parentPath + ".";
|
|
697
|
+
|
|
698
|
+
foreach (var kvp in _fieldIdToCol)
|
|
699
|
+
{
|
|
700
|
+
string path = kvp.Key;
|
|
701
|
+
int col = kvp.Value;
|
|
702
|
+
|
|
703
|
+
string remainder;
|
|
704
|
+
if (!string.IsNullOrEmpty(prefix))
|
|
705
|
+
{
|
|
706
|
+
if (!path.StartsWith(prefix)) continue;
|
|
707
|
+
remainder = path.Substring(prefix.Length);
|
|
708
|
+
}
|
|
709
|
+
else
|
|
710
|
+
{
|
|
711
|
+
remainder = path;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
int dotPos = remainder.IndexOf('.');
|
|
715
|
+
string firstSeg = dotPos >= 0 ? remainder.Substring(0, dotPos) : remainder;
|
|
716
|
+
if (!short.TryParse(firstSeg, out short fieldId)) continue;
|
|
717
|
+
|
|
718
|
+
string childPath = string.IsNullOrEmpty(prefix) ? firstSeg : prefix + firstSeg;
|
|
719
|
+
bool isExactColumn = (path == childPath);
|
|
720
|
+
|
|
721
|
+
if (isExactColumn)
|
|
722
|
+
{
|
|
723
|
+
// This column IS the direct child → type from its DATATYPE (rec, lst, map, i64, str, etc.)
|
|
724
|
+
var dt = _colDataType.ContainsKey(col) ? _colDataType[col] : "";
|
|
725
|
+
var cn = _colColumnName.ContainsKey(col) ? _colColumnName[col] : "";
|
|
726
|
+
entries[fieldId] = new FieldEntry
|
|
727
|
+
{
|
|
728
|
+
Id = fieldId,
|
|
729
|
+
Type = DataTypeToDpWireType(dt),
|
|
730
|
+
Col = col,
|
|
731
|
+
Path = childPath,
|
|
732
|
+
ColumnName = cn
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
else if (!entries.ContainsKey(fieldId))
|
|
736
|
+
{
|
|
737
|
+
// Descendant column — check if the direct child has its own explicit column
|
|
738
|
+
if (_fieldIdToCol.TryGetValue(childPath, out int explicitCol))
|
|
739
|
+
{
|
|
740
|
+
var dt = _colDataType.ContainsKey(explicitCol) ? _colDataType[explicitCol] : "";
|
|
741
|
+
var cn = _colColumnName.ContainsKey(explicitCol) ? _colColumnName[explicitCol] : "";
|
|
742
|
+
entries[fieldId] = new FieldEntry
|
|
743
|
+
{
|
|
744
|
+
Id = fieldId,
|
|
745
|
+
Type = DataTypeToDpWireType(dt),
|
|
746
|
+
Col = explicitCol,
|
|
747
|
+
Path = childPath,
|
|
748
|
+
ColumnName = cn
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
else
|
|
752
|
+
{
|
|
753
|
+
// No explicit column: implicit parent.
|
|
754
|
+
// Lists/maps always have root columns, so this must be a struct.
|
|
755
|
+
entries[fieldId] = new FieldEntry
|
|
756
|
+
{
|
|
757
|
+
Id = fieldId,
|
|
758
|
+
Type = DpWireType.Struct,
|
|
759
|
+
Col = -1,
|
|
760
|
+
Path = childPath,
|
|
761
|
+
ColumnName = ""
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// else: explicit column already recorded for this fieldId — keep it
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
var result = new List<FieldEntry>(entries.Values);
|
|
769
|
+
result.Sort((a, b) => a.Id.CompareTo(b.Id));
|
|
770
|
+
return result;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
private static DpWireType DataTypeToDpWireType(string dt)
|
|
774
|
+
{
|
|
775
|
+
return DpTypeNames.FromProtocolName(dt);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
private static string ExtractContainerElemType(string dt)
|
|
779
|
+
{
|
|
780
|
+
int lt = dt.IndexOf('<');
|
|
781
|
+
int gt = dt.LastIndexOf('>');
|
|
782
|
+
if (lt >= 0 && gt > lt)
|
|
783
|
+
return dt.Substring(lt + 1, gt - lt - 1).Trim();
|
|
784
|
+
return "record";
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
#endregion
|
|
788
|
+
|
|
789
|
+
#region Read - Struct
|
|
790
|
+
|
|
791
|
+
public DpRecord ReadStructBegin()
|
|
792
|
+
{
|
|
793
|
+
if (_listStack.Count > 0)
|
|
794
|
+
{
|
|
795
|
+
var ls = _listStack.Peek();
|
|
796
|
+
bool isDirectListElement = (ls.StructDepth == 0);
|
|
797
|
+
|
|
798
|
+
if (isDirectListElement)
|
|
799
|
+
{
|
|
800
|
+
// Balance the pathStack: ReadFieldBegin pushes for normal struct fields,
|
|
801
|
+
// but list elements don't go through ReadFieldBegin, so push here.
|
|
802
|
+
_pathStack.Push(_currentPath);
|
|
803
|
+
|
|
804
|
+
if (ls.SeparatedSheet != null)
|
|
805
|
+
{
|
|
806
|
+
// Separated sheet: row comes from SeparatedDataRows
|
|
807
|
+
if (ls.SeparatedDataRows != null && ls.CurrentElement < ls.SeparatedDataRows.Length)
|
|
808
|
+
_dataRow = ls.SeparatedDataRows[ls.CurrentElement];
|
|
809
|
+
}
|
|
810
|
+
else
|
|
811
|
+
{
|
|
812
|
+
_dataRow = ls.StartRow + ls.CurrentElement;
|
|
813
|
+
if (_dataRow > _maxDataRow) _maxDataRow = _dataRow;
|
|
814
|
+
}
|
|
815
|
+
ls.CurrentElement++;
|
|
816
|
+
}
|
|
817
|
+
ls.StructDepth++;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (_pendingFields != null)
|
|
821
|
+
_structStack.Push(new StructState { Fields = _pendingFields, Index = _pendingIdx });
|
|
822
|
+
|
|
823
|
+
// If inside a separated list, collect fields from the separated sheet's column map (full-path headers)
|
|
824
|
+
if (_listStack.Count > 0 && _listStack.Peek().SeparatedSheet != null && _listStack.Peek().StructDepth == 1)
|
|
825
|
+
{
|
|
826
|
+
var ls = _listStack.Peek();
|
|
827
|
+
var sepProto = new DpExcelProtocol(ls.SeparatedSheet, _dataRow);
|
|
828
|
+
_pendingFields = sepProto.CollectChildFields(ls.ListPath ?? "");
|
|
829
|
+
_pendingIdx = 0;
|
|
830
|
+
// Remap: redirect _lastField reads to the separated sheet
|
|
831
|
+
_currentSeparatedSheet = ls.SeparatedSheet;
|
|
832
|
+
_currentSeparatedRow = _dataRow;
|
|
833
|
+
}
|
|
834
|
+
else
|
|
835
|
+
{
|
|
836
|
+
_pendingFields = CollectChildFields(_currentPath);
|
|
837
|
+
_pendingIdx = 0;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return new DpRecord("struct");
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
public void ReadStructEnd()
|
|
844
|
+
{
|
|
845
|
+
if (_listStack.Count > 0)
|
|
846
|
+
_listStack.Peek().StructDepth--;
|
|
847
|
+
|
|
848
|
+
// Clear separated sheet context when leaving the struct
|
|
849
|
+
if (_listStack.Count == 0 || _listStack.Peek().StructDepth == 0)
|
|
850
|
+
{
|
|
851
|
+
_currentSeparatedSheet = null;
|
|
852
|
+
_currentSeparatedRow = 0;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (_pathStack.Count > 0)
|
|
856
|
+
_currentPath = _pathStack.Pop();
|
|
857
|
+
if (_structStack.Count > 0)
|
|
858
|
+
{
|
|
859
|
+
var s = _structStack.Pop();
|
|
860
|
+
_pendingFields = s.Fields;
|
|
861
|
+
_pendingIdx = s.Index;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
public DpColumn ReadFieldBegin()
|
|
866
|
+
{
|
|
867
|
+
while (_pendingIdx < _pendingFields.Count)
|
|
868
|
+
{
|
|
869
|
+
var entry = _pendingFields[_pendingIdx];
|
|
870
|
+
_pendingIdx++;
|
|
871
|
+
|
|
872
|
+
// Skip empty primitives
|
|
873
|
+
if (entry.Type != DpWireType.Struct && entry.Type != DpWireType.List &&
|
|
874
|
+
entry.Type != DpWireType.Map && entry.Type != DpWireType.Set)
|
|
875
|
+
{
|
|
876
|
+
if (entry.Col <= 0 || _sheet.IsCellEmpty(_dataRow, entry.Col))
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Skip structs with no child data
|
|
881
|
+
if (entry.Type == DpWireType.Struct && !HasAnyChildData(entry.Path))
|
|
882
|
+
continue;
|
|
883
|
+
|
|
884
|
+
// Skip lists with no elements
|
|
885
|
+
if (entry.Type == DpWireType.List && CountListElements(entry) == 0)
|
|
886
|
+
continue;
|
|
887
|
+
|
|
888
|
+
_lastField = entry;
|
|
889
|
+
|
|
890
|
+
// For struct, push path navigation
|
|
891
|
+
if (entry.Type == DpWireType.Struct)
|
|
892
|
+
{
|
|
893
|
+
_pathStack.Push(_currentPath);
|
|
894
|
+
_currentPath = entry.Path;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return new DpColumn(entry.Path, entry.Type, entry.Id);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return new DpColumn("", DpWireType.Stop, 0);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
public void ReadFieldEnd() { }
|
|
904
|
+
|
|
905
|
+
private bool HasAnyChildData(string parentPath)
|
|
906
|
+
{
|
|
907
|
+
string prefix = parentPath + ".";
|
|
908
|
+
foreach (var kvp in _fieldIdToCol)
|
|
909
|
+
{
|
|
910
|
+
if (!kvp.Key.StartsWith(prefix)) continue;
|
|
911
|
+
|
|
912
|
+
if (_colDataType.TryGetValue(kvp.Value, out var dt) &&
|
|
913
|
+
DpTypeNames.IsContainerType(DpTypeNames.FromProtocolName(dt)))
|
|
914
|
+
continue;
|
|
915
|
+
|
|
916
|
+
if (!_sheet.IsCellEmpty(_dataRow, kvp.Value))
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
#endregion
|
|
923
|
+
|
|
924
|
+
#region Read - List
|
|
925
|
+
|
|
926
|
+
/// <summary>
|
|
927
|
+
/// Count list elements starting from _dataRow.
|
|
928
|
+
/// A list element = a row where either:
|
|
929
|
+
/// - The list root column has a non-empty index value, OR
|
|
930
|
+
/// - Any child column of the list has data (same meta_id)
|
|
931
|
+
/// Ends when: different meta_id, or gap row with no data in list columns.
|
|
932
|
+
/// </summary>
|
|
933
|
+
private int CountListElements(FieldEntry listField)
|
|
934
|
+
{
|
|
935
|
+
int rootCol = listField.Col;
|
|
936
|
+
string listPath = listField.Path;
|
|
937
|
+
string prefix = listPath + ".";
|
|
938
|
+
int count = 0;
|
|
939
|
+
string metaId = _sheet.CellValue(_dataRow, 1) ?? "";
|
|
940
|
+
|
|
941
|
+
for (int r = _dataRow; r <= _sheet.LastRow; r++)
|
|
942
|
+
{
|
|
943
|
+
// Different meta_id = new record, stop
|
|
944
|
+
if (r > _dataRow)
|
|
945
|
+
{
|
|
946
|
+
var rid = _sheet.CellValue(r, 1) ?? "";
|
|
947
|
+
if (!string.IsNullOrEmpty(rid) && rid != metaId)
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
bool hasData = false;
|
|
952
|
+
// Check root column (list index)
|
|
953
|
+
if (rootCol > 0 && !_sheet.IsCellEmpty(r, rootCol))
|
|
954
|
+
hasData = true;
|
|
955
|
+
|
|
956
|
+
// Check child columns
|
|
957
|
+
if (!hasData)
|
|
958
|
+
{
|
|
959
|
+
foreach (var kvp in _fieldIdToCol)
|
|
960
|
+
{
|
|
961
|
+
if (kvp.Key.StartsWith(prefix) && !_sheet.IsCellEmpty(r, kvp.Value))
|
|
962
|
+
{
|
|
963
|
+
hasData = true;
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (hasData) count++;
|
|
970
|
+
else if (count > 0) break; // gap = end
|
|
971
|
+
}
|
|
972
|
+
return count;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/// <summary>
|
|
976
|
+
/// Determine list element type from DATATYPE + child column structure.
|
|
977
|
+
/// Priority: explicit <type> in root datatype → child column DATATYPE analysis.
|
|
978
|
+
/// </summary>
|
|
979
|
+
private DpWireType InferListElementType(FieldEntry listEntry)
|
|
980
|
+
{
|
|
981
|
+
var dt = (listEntry.Col > 0 && _colDataType.ContainsKey(listEntry.Col))
|
|
982
|
+
? _colDataType[listEntry.Col] : "list";
|
|
983
|
+
|
|
984
|
+
// 1) Explicit element type in DATATYPE (e.g., "lst<i64>", "lst<rec>")
|
|
985
|
+
int lt = dt.IndexOf('<');
|
|
986
|
+
if (lt >= 0)
|
|
987
|
+
{
|
|
988
|
+
var inner = ExtractContainerElemType(dt);
|
|
989
|
+
return DataTypeToDpWireType(inner);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// 2) Infer from direct children using their DATATYPE
|
|
993
|
+
string prefix = listEntry.Path + ".";
|
|
994
|
+
var childIds = new Dictionary<short, (string dt, int col, string path)>();
|
|
995
|
+
|
|
996
|
+
foreach (var kvp in _fieldIdToCol)
|
|
997
|
+
{
|
|
998
|
+
if (!kvp.Key.StartsWith(prefix)) continue;
|
|
999
|
+
string remainder = kvp.Key.Substring(prefix.Length);
|
|
1000
|
+
int dotPos = remainder.IndexOf('.');
|
|
1001
|
+
string firstSeg = dotPos >= 0 ? remainder.Substring(0, dotPos) : remainder;
|
|
1002
|
+
if (!short.TryParse(firstSeg, out short fid)) continue;
|
|
1003
|
+
|
|
1004
|
+
string childPath = listEntry.Path + "." + firstSeg;
|
|
1005
|
+
bool isExactChild = (kvp.Key == childPath);
|
|
1006
|
+
|
|
1007
|
+
if (isExactChild)
|
|
1008
|
+
{
|
|
1009
|
+
// Exact child column: its DATATYPE is authoritative
|
|
1010
|
+
string childDt = _colDataType.ContainsKey(kvp.Value) ? _colDataType[kvp.Value] : "";
|
|
1011
|
+
childIds[fid] = (childDt, kvp.Value, childPath);
|
|
1012
|
+
}
|
|
1013
|
+
else if (!childIds.ContainsKey(fid))
|
|
1014
|
+
{
|
|
1015
|
+
// Descendant: check if the direct child has an explicit column
|
|
1016
|
+
if (_fieldIdToCol.TryGetValue(childPath, out int explicitCol))
|
|
1017
|
+
{
|
|
1018
|
+
string childDt = _colDataType.ContainsKey(explicitCol) ? _colDataType[explicitCol] : "";
|
|
1019
|
+
childIds[fid] = (childDt, explicitCol, childPath);
|
|
1020
|
+
}
|
|
1021
|
+
else
|
|
1022
|
+
{
|
|
1023
|
+
childIds[fid] = ("record", -1, childPath); // implicit struct child
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (childIds.Count == 0) return DpWireType.Struct;
|
|
1029
|
+
|
|
1030
|
+
// Single direct child whose DATATYPE is a primitive → primitive list
|
|
1031
|
+
if (childIds.Count == 1)
|
|
1032
|
+
{
|
|
1033
|
+
foreach (var c in childIds.Values)
|
|
1034
|
+
{
|
|
1035
|
+
var childDpWireType = DataTypeToDpWireType(c.dt);
|
|
1036
|
+
bool isPrimitive = (childDpWireType != DpWireType.Struct && childDpWireType != DpWireType.List &&
|
|
1037
|
+
childDpWireType != DpWireType.Map && childDpWireType != DpWireType.Set);
|
|
1038
|
+
if (isPrimitive) return childDpWireType;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Multiple children or container child → struct element
|
|
1043
|
+
return DpWireType.Struct;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
public DpList ReadListBegin()
|
|
1047
|
+
{
|
|
1048
|
+
var entry = _lastField;
|
|
1049
|
+
|
|
1050
|
+
// ── Separated sheet mode (정책에서 시트 이름 후보 조회) ─────────────────────
|
|
1051
|
+
if (_resolver != null)
|
|
1052
|
+
{
|
|
1053
|
+
var namesToTry = ContainerSheetNaming.GetSheetNamesForLookup(entry.Path, DpWireType.List, entry.ColumnName);
|
|
1054
|
+
foreach (var sheetName in namesToTry)
|
|
1055
|
+
{
|
|
1056
|
+
var sepSheet = _resolver.GetSheet(sheetName);
|
|
1057
|
+
if (sepSheet != null)
|
|
1058
|
+
return ReadListBeginFromSeparatedSheet(entry, sepSheet, ContainerSheetPolicy.GetKindForWireType(DpWireType.List));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// ── Embedded (original) mode ──────────────────────────────────────
|
|
1063
|
+
var elemDpWireType = InferListElementType(entry);
|
|
1064
|
+
int count = CountListElements(entry);
|
|
1065
|
+
|
|
1066
|
+
// For primitive lists, find the child element column
|
|
1067
|
+
int primitiveElemCol = -1;
|
|
1068
|
+
if (elemDpWireType != DpWireType.Struct)
|
|
1069
|
+
{
|
|
1070
|
+
string prefix = entry.Path + ".";
|
|
1071
|
+
foreach (var kvp in _fieldIdToCol)
|
|
1072
|
+
{
|
|
1073
|
+
if (kvp.Key.StartsWith(prefix))
|
|
1074
|
+
{
|
|
1075
|
+
primitiveElemCol = kvp.Value;
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
var state = new ListState
|
|
1082
|
+
{
|
|
1083
|
+
StartRow = _dataRow,
|
|
1084
|
+
CurrentElement = 0,
|
|
1085
|
+
Count = count,
|
|
1086
|
+
ElementType = elemDpWireType,
|
|
1087
|
+
RootCol = entry.Col,
|
|
1088
|
+
ListPath = entry.Path,
|
|
1089
|
+
SavedDataRow = _dataRow,
|
|
1090
|
+
PrimitiveElemCol = primitiveElemCol
|
|
1091
|
+
};
|
|
1092
|
+
_listStack.Push(state);
|
|
1093
|
+
|
|
1094
|
+
// For primitive lists, redirect _lastField to the element column
|
|
1095
|
+
if (primitiveElemCol > 0)
|
|
1096
|
+
_lastField = new FieldEntry { Id = 0, Type = elemDpWireType, Col = primitiveElemCol, Path = entry.Path };
|
|
1097
|
+
|
|
1098
|
+
// Push path for list children
|
|
1099
|
+
_pathStack.Push(_currentPath);
|
|
1100
|
+
_currentPath = entry.Path;
|
|
1101
|
+
|
|
1102
|
+
return new DpList { ElementType = elemDpWireType, Count = count };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/// <summary>
|
|
1106
|
+
/// Read list elements from a separated container sheet.
|
|
1107
|
+
/// Rows whose meta_id matches the current record's meta_id (col 1 of main sheet) are used.
|
|
1108
|
+
/// Empty-element rows (all element cols empty) are treated as an empty list placeholder.
|
|
1109
|
+
/// </summary>
|
|
1110
|
+
private DpList ReadListBeginFromSeparatedSheet(FieldEntry entry, IExcelSheet sepSheet, string kind)
|
|
1111
|
+
{
|
|
1112
|
+
string metaId = _sheet.CellValue(_dataRow, 1) ?? "";
|
|
1113
|
+
|
|
1114
|
+
// Detect meta_id column dynamically: new layout has _nav in col 1 → meta_id in col 2;
|
|
1115
|
+
// old layout has meta_id directly in col 1.
|
|
1116
|
+
int metaIdCol = 1;
|
|
1117
|
+
int firstElemCol = 4;
|
|
1118
|
+
string col1Hier = sepSheet.CellValue(HIERARCHY_ID_ROW, 1)?.Trim() ?? "";
|
|
1119
|
+
if (string.Equals(col1Hier, ContainerSheetNaming.NAV_BACK_COLUMN, StringComparison.OrdinalIgnoreCase))
|
|
1120
|
+
{
|
|
1121
|
+
metaIdCol = 2;
|
|
1122
|
+
firstElemCol = 4;
|
|
1123
|
+
}
|
|
1124
|
+
else
|
|
1125
|
+
{
|
|
1126
|
+
metaIdCol = 1;
|
|
1127
|
+
firstElemCol = 3;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
var matchingRows = new List<int>();
|
|
1131
|
+
for (int r = _sheetFirstDataRow; r <= sepSheet.LastRow; r++)
|
|
1132
|
+
{
|
|
1133
|
+
string rid = ParseMetaIdFromListCell(sepSheet.CellValue(r, metaIdCol)?.Trim() ?? "");
|
|
1134
|
+
if (rid == metaId)
|
|
1135
|
+
matchingRows.Add(r);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
var sepProto = new DpExcelProtocol(sepSheet, _sheetFirstDataRow);
|
|
1139
|
+
DpWireType elemDpWireType = DpWireType.Struct;
|
|
1140
|
+
bool allElemEmpty = true;
|
|
1141
|
+
if (matchingRows.Count > 0)
|
|
1142
|
+
{
|
|
1143
|
+
for (int r = 0; r < matchingRows.Count && allElemEmpty; r++)
|
|
1144
|
+
{
|
|
1145
|
+
for (int c = firstElemCol; c <= sepSheet.LastColumn; c++)
|
|
1146
|
+
{
|
|
1147
|
+
if (!sepSheet.IsCellEmpty(matchingRows[r], c)) { allElemEmpty = false; break; }
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
int count = (matchingRows.Count == 0 || allElemEmpty) ? 0 : matchingRows.Count;
|
|
1153
|
+
|
|
1154
|
+
elemDpWireType = sepProto.InferSeparatedListElementType();
|
|
1155
|
+
|
|
1156
|
+
int primitiveElemCol = -1;
|
|
1157
|
+
if (elemDpWireType != DpWireType.Struct && sepSheet.LastColumn >= firstElemCol)
|
|
1158
|
+
primitiveElemCol = firstElemCol;
|
|
1159
|
+
|
|
1160
|
+
var state = new ListState
|
|
1161
|
+
{
|
|
1162
|
+
StartRow = matchingRows.Count > 0 ? matchingRows[0] : _sheetFirstDataRow,
|
|
1163
|
+
CurrentElement = 0,
|
|
1164
|
+
Count = count,
|
|
1165
|
+
ElementType = elemDpWireType,
|
|
1166
|
+
RootCol = entry.Col,
|
|
1167
|
+
ListPath = entry.Path,
|
|
1168
|
+
SavedDataRow = _dataRow,
|
|
1169
|
+
PrimitiveElemCol = primitiveElemCol,
|
|
1170
|
+
SeparatedSheet = sepSheet,
|
|
1171
|
+
SeparatedDataRows = matchingRows.ToArray()
|
|
1172
|
+
};
|
|
1173
|
+
_listStack.Push(state);
|
|
1174
|
+
|
|
1175
|
+
if (primitiveElemCol > 0)
|
|
1176
|
+
_lastField = new FieldEntry { Id = 0, Type = elemDpWireType, Col = primitiveElemCol, Path = entry.Path };
|
|
1177
|
+
|
|
1178
|
+
_pathStack.Push(_currentPath);
|
|
1179
|
+
_currentPath = entry.Path;
|
|
1180
|
+
|
|
1181
|
+
return new DpList { ElementType = elemDpWireType, Count = count };
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/// <summary>
|
|
1185
|
+
/// Infer list element type for a separated sheet.
|
|
1186
|
+
/// The sheet's first column after tuid/name (col 3+) determines the type.
|
|
1187
|
+
/// </summary>
|
|
1188
|
+
private DpWireType InferSeparatedListElementType()
|
|
1189
|
+
{
|
|
1190
|
+
if (_sheet.LastColumn < 3) return DpWireType.Struct;
|
|
1191
|
+
|
|
1192
|
+
string commonPrefix = null;
|
|
1193
|
+
foreach (var kv in _fieldIdToCol)
|
|
1194
|
+
{
|
|
1195
|
+
if (string.Equals(kv.Key, ContainerSheetNaming.NAV_BACK_COLUMN, StringComparison.OrdinalIgnoreCase)) continue;
|
|
1196
|
+
if (string.Equals(kv.Key, ContainerSheetNaming.META_ID_COLUMN, StringComparison.OrdinalIgnoreCase)) continue;
|
|
1197
|
+
if (string.Equals(kv.Key, ContainerSheetNaming.META_NAME_COLUMN, StringComparison.OrdinalIgnoreCase)) continue;
|
|
1198
|
+
if (kv.Key == "value") continue;
|
|
1199
|
+
|
|
1200
|
+
if (commonPrefix == null) { commonPrefix = kv.Key; continue; }
|
|
1201
|
+
int minLen = Math.Min(commonPrefix.Length, kv.Key.Length);
|
|
1202
|
+
int match = 0;
|
|
1203
|
+
for (int i = 0; i < minLen; i++)
|
|
1204
|
+
{
|
|
1205
|
+
if (commonPrefix[i] == kv.Key[i]) match++;
|
|
1206
|
+
else break;
|
|
1207
|
+
}
|
|
1208
|
+
commonPrefix = commonPrefix.Substring(0, match);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (string.IsNullOrEmpty(commonPrefix)) return DpWireType.Struct;
|
|
1212
|
+
// Trim to last complete segment boundary
|
|
1213
|
+
int lastDot = commonPrefix.LastIndexOf('.');
|
|
1214
|
+
string listPath = lastDot > 0 ? commonPrefix.Substring(0, lastDot) : "";
|
|
1215
|
+
|
|
1216
|
+
// Count direct children under listPath
|
|
1217
|
+
string prefix = string.IsNullOrEmpty(listPath) ? "" : listPath + ".";
|
|
1218
|
+
var directChildren = new HashSet<string>();
|
|
1219
|
+
foreach (var kv in _fieldIdToCol)
|
|
1220
|
+
{
|
|
1221
|
+
if (string.Equals(kv.Key, ContainerSheetNaming.META_ID_COLUMN, StringComparison.OrdinalIgnoreCase)) continue;
|
|
1222
|
+
if (string.Equals(kv.Key, ContainerSheetNaming.META_NAME_COLUMN, StringComparison.OrdinalIgnoreCase)) continue;
|
|
1223
|
+
string remainder = !string.IsNullOrEmpty(prefix) && kv.Key.StartsWith(prefix)
|
|
1224
|
+
? kv.Key.Substring(prefix.Length)
|
|
1225
|
+
: (string.IsNullOrEmpty(prefix) ? kv.Key : null);
|
|
1226
|
+
if (remainder == null) continue;
|
|
1227
|
+
int dot = remainder.IndexOf('.');
|
|
1228
|
+
string firstSeg = dot >= 0 ? remainder.Substring(0, dot) : remainder;
|
|
1229
|
+
directChildren.Add(firstSeg);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (directChildren.Count == 1)
|
|
1233
|
+
{
|
|
1234
|
+
string childSeg = null;
|
|
1235
|
+
foreach (var s in directChildren) { childSeg = s; break; }
|
|
1236
|
+
string childPath = string.IsNullOrEmpty(prefix) ? childSeg : prefix + childSeg;
|
|
1237
|
+
if (_fieldIdToCol.TryGetValue(childPath, out int col))
|
|
1238
|
+
{
|
|
1239
|
+
string dt = _colDataType.ContainsKey(col) ? _colDataType[col] : "";
|
|
1240
|
+
var tt = DataTypeToDpWireType(dt);
|
|
1241
|
+
if (tt != DpWireType.Struct && tt != DpWireType.List && tt != DpWireType.Map && tt != DpWireType.Set)
|
|
1242
|
+
return tt;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return DpWireType.Struct;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
public void ReadListEnd()
|
|
1249
|
+
{
|
|
1250
|
+
if (_listStack.Count > 0)
|
|
1251
|
+
{
|
|
1252
|
+
var state = _listStack.Pop();
|
|
1253
|
+
// Restore _dataRow: for the caller, _dataRow should still be on the original record row
|
|
1254
|
+
// But if this is within a ReadFromExcelDirect loop, the caller advances rows
|
|
1255
|
+
_dataRow = state.SavedDataRow;
|
|
1256
|
+
}
|
|
1257
|
+
// Pop the list path
|
|
1258
|
+
if (_pathStack.Count > 0)
|
|
1259
|
+
_currentPath = _pathStack.Pop();
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/// <summary>
|
|
1263
|
+
/// For primitive list elements: advance _dataRow to the next element row.
|
|
1264
|
+
/// Only triggers when inside a list AND the current path matches the list path
|
|
1265
|
+
/// (struct elements are advanced in ReadStructBegin instead).
|
|
1266
|
+
/// </summary>
|
|
1267
|
+
private void AdvanceListElementIfPrimitive()
|
|
1268
|
+
{
|
|
1269
|
+
if (_listStack.Count == 0) return;
|
|
1270
|
+
var state = _listStack.Peek();
|
|
1271
|
+
// Only advance for primitive list elements (StructDepth==0 means not inside a struct)
|
|
1272
|
+
if (state.StructDepth > 0) return;
|
|
1273
|
+
|
|
1274
|
+
if (state.SeparatedSheet != null)
|
|
1275
|
+
{
|
|
1276
|
+
// Separated sheet: index into SeparatedDataRows
|
|
1277
|
+
if (state.SeparatedDataRows != null && state.CurrentElement < state.SeparatedDataRows.Length)
|
|
1278
|
+
_dataRow = state.SeparatedDataRows[state.CurrentElement];
|
|
1279
|
+
state.CurrentElement++;
|
|
1280
|
+
}
|
|
1281
|
+
else
|
|
1282
|
+
{
|
|
1283
|
+
_dataRow = state.StartRow + state.CurrentElement;
|
|
1284
|
+
if (_dataRow > _maxDataRow) _maxDataRow = _dataRow;
|
|
1285
|
+
state.CurrentElement++;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
#endregion
|
|
1290
|
+
|
|
1291
|
+
#region Read - Primitives
|
|
1292
|
+
|
|
1293
|
+
public bool ReadBool()
|
|
1294
|
+
{
|
|
1295
|
+
AdvanceListElementIfPrimitive();
|
|
1296
|
+
var val = ReadCurrentCellValue();
|
|
1297
|
+
if (string.IsNullOrEmpty(val)) return false;
|
|
1298
|
+
if (val == "1") return true;
|
|
1299
|
+
if (val == "0") return false;
|
|
1300
|
+
bool.TryParse(val, out bool bv);
|
|
1301
|
+
return bv;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
public byte ReadByte()
|
|
1305
|
+
{
|
|
1306
|
+
AdvanceListElementIfPrimitive();
|
|
1307
|
+
var val = ReadCurrentCellValue();
|
|
1308
|
+
if (string.IsNullOrEmpty(val)) return 0;
|
|
1309
|
+
byte.TryParse(val.Split(':')[0].Trim(), out byte r);
|
|
1310
|
+
return r;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
public short ReadI16()
|
|
1314
|
+
{
|
|
1315
|
+
AdvanceListElementIfPrimitive();
|
|
1316
|
+
var val = ReadCurrentCellValue();
|
|
1317
|
+
if (string.IsNullOrEmpty(val)) return 0;
|
|
1318
|
+
short.TryParse(val.Split(':')[0].Trim(), out short r);
|
|
1319
|
+
return r;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
public int ReadI32()
|
|
1323
|
+
{
|
|
1324
|
+
AdvanceListElementIfPrimitive();
|
|
1325
|
+
var val = ReadCurrentCellValue();
|
|
1326
|
+
if (string.IsNullOrEmpty(val)) return 0;
|
|
1327
|
+
var token = val.Split(':')[0].Trim();
|
|
1328
|
+
if (int.TryParse(token, out int r))
|
|
1329
|
+
return r;
|
|
1330
|
+
// Enum string name → resolve via DATATYPE "enum<ns.EnumType>"
|
|
1331
|
+
return ResolveEnumName(token);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
public long ReadI64()
|
|
1335
|
+
{
|
|
1336
|
+
AdvanceListElementIfPrimitive();
|
|
1337
|
+
var val = ReadCurrentCellValue();
|
|
1338
|
+
if (string.IsNullOrEmpty(val)) return 0L;
|
|
1339
|
+
var parts = val.Split(':');
|
|
1340
|
+
long.TryParse(parts[0].Trim(), out long r);
|
|
1341
|
+
return r;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
public double ReadDouble()
|
|
1345
|
+
{
|
|
1346
|
+
AdvanceListElementIfPrimitive();
|
|
1347
|
+
var val = ReadCurrentCellValue();
|
|
1348
|
+
double.TryParse(val, System.Globalization.NumberStyles.Float,
|
|
1349
|
+
System.Globalization.CultureInfo.InvariantCulture, out double r);
|
|
1350
|
+
return r;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
public string ReadString()
|
|
1354
|
+
{
|
|
1355
|
+
AdvanceListElementIfPrimitive();
|
|
1356
|
+
return ReadCurrentCellValue()?.Trim() ?? "";
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
public byte[] ReadBinary()
|
|
1360
|
+
{
|
|
1361
|
+
AdvanceListElementIfPrimitive();
|
|
1362
|
+
var val = ReadCurrentCellValue();
|
|
1363
|
+
return string.IsNullOrEmpty(val) ? Array.Empty<byte>() : Encoding.UTF8.GetBytes(val);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
public DpSet ReadSetBegin()
|
|
1367
|
+
{
|
|
1368
|
+
var entry = _lastField;
|
|
1369
|
+
|
|
1370
|
+
// Try separated sheet (정책에서 시트 이름 조회)
|
|
1371
|
+
if (_resolver != null)
|
|
1372
|
+
{
|
|
1373
|
+
var namesToTry = ContainerSheetNaming.GetSheetNamesForLookup(entry.Path, DpWireType.Set, entry.ColumnName);
|
|
1374
|
+
foreach (var sheetName in namesToTry)
|
|
1375
|
+
{
|
|
1376
|
+
var sepSheet = _resolver.GetSheet(sheetName);
|
|
1377
|
+
if (sepSheet != null)
|
|
1378
|
+
{
|
|
1379
|
+
var tl = ReadListBeginFromSeparatedSheet(entry, sepSheet, ContainerSheetPolicy.GetKindForWireType(DpWireType.Set));
|
|
1380
|
+
return new DpSet { ElementType = tl.ElementType, Count = tl.Count };
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
var lb = ReadListBegin();
|
|
1386
|
+
return new DpSet { ElementType = lb.ElementType, Count = lb.Count };
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
public void ReadSetEnd() { ReadListEnd(); }
|
|
1390
|
+
|
|
1391
|
+
public DpDict ReadMapBegin()
|
|
1392
|
+
{
|
|
1393
|
+
return new DpDict { KeyType = DpWireType.I64, ValueType = DpWireType.Struct, Count = 0 };
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
public void ReadMapEnd() { }
|
|
1397
|
+
|
|
1398
|
+
private string ReadCurrentCellValue()
|
|
1399
|
+
{
|
|
1400
|
+
if (_lastField.Col <= 0) return "";
|
|
1401
|
+
// If reading from a separated container sheet struct element, use that sheet
|
|
1402
|
+
if (_currentSeparatedSheet != null)
|
|
1403
|
+
return _currentSeparatedSheet.CellValue(_currentSeparatedRow, _lastField.Col) ?? "";
|
|
1404
|
+
return _sheet.CellValue(_dataRow, _lastField.Col) ?? "";
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/// <summary>
|
|
1408
|
+
/// Resolve enum string name to integer value.
|
|
1409
|
+
/// Uses the column's DATATYPE "enum<ns.EnumType>" to find the CLR enum type via reflection.
|
|
1410
|
+
/// </summary>
|
|
1411
|
+
private int ResolveEnumName(string name)
|
|
1412
|
+
{
|
|
1413
|
+
if (_lastField.Col <= 0) return 0;
|
|
1414
|
+
if (!_colDataType.TryGetValue(_lastField.Col, out var dt)) return 0;
|
|
1415
|
+
var enumTypeName = ExtractContainerElemType(dt);
|
|
1416
|
+
if (string.IsNullOrEmpty(enumTypeName) || enumTypeName == "rec" || enumTypeName == "record") return 0;
|
|
1417
|
+
|
|
1418
|
+
// Search loaded assemblies for the enum type
|
|
1419
|
+
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
|
1420
|
+
{
|
|
1421
|
+
foreach (var t in asm.GetTypes())
|
|
1422
|
+
{
|
|
1423
|
+
if (!t.IsEnum) continue;
|
|
1424
|
+
if (t.Name == enumTypeName || t.FullName == enumTypeName ||
|
|
1425
|
+
t.FullName?.EndsWith("." + enumTypeName) == true)
|
|
1426
|
+
{
|
|
1427
|
+
try
|
|
1428
|
+
{
|
|
1429
|
+
var v = Enum.Parse(t, name, true);
|
|
1430
|
+
return Convert.ToInt32(v);
|
|
1431
|
+
}
|
|
1432
|
+
catch (ArgumentException) { }
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return 0;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
#endregion
|
|
1440
|
+
|
|
1441
|
+
#region Write
|
|
1442
|
+
|
|
1443
|
+
private IWritableExcelSheet _writeSheet;
|
|
1444
|
+
private int _writeRow;
|
|
1445
|
+
private int _maxWriteRow;
|
|
1446
|
+
private string _writeCurrentPath = "";
|
|
1447
|
+
private readonly Stack<string> _writePathStack = new Stack<string>();
|
|
1448
|
+
private DpColumn _writeCurrentField;
|
|
1449
|
+
private DpSchema _writeSchema;
|
|
1450
|
+
|
|
1451
|
+
private class WriteListState
|
|
1452
|
+
{
|
|
1453
|
+
public int StartRow;
|
|
1454
|
+
public int CurrentElement;
|
|
1455
|
+
public string ListPath;
|
|
1456
|
+
public int StructDepth;
|
|
1457
|
+
public bool IsPrimitiveList;
|
|
1458
|
+
public int PrimitiveElemCol;
|
|
1459
|
+
|
|
1460
|
+
// Separated sheet write state
|
|
1461
|
+
public IWritableExcelSheet SeparatedSheet; // non-null when writing to a container sheet
|
|
1462
|
+
public string SeparatedSheetName; // sheet name for merge tracking in FinishWrite
|
|
1463
|
+
public string SeparatedMetaId; // meta_id value to write in col 1
|
|
1464
|
+
public string SeparatedMetaName; // name value to write in col 2
|
|
1465
|
+
public int MainSheetRow; // _writeRow before entering separated list
|
|
1466
|
+
public int MainSheetMaxRow; // _maxWriteRow before entering separated list
|
|
1467
|
+
}
|
|
1468
|
+
private readonly Stack<WriteListState> _writeListStack = new Stack<WriteListState>();
|
|
1469
|
+
private int _mainSheetFirstDataRow = FIRST_DATA_ROW;
|
|
1470
|
+
private int _globalFirstDataRow = FIRST_DATA_ROW;
|
|
1471
|
+
private int _globalMaxWriteRow = FIRST_DATA_ROW;
|
|
1472
|
+
private readonly List<(string sheetName, int firstRow, int lastRow, int colCount)> _containerSheetsWritten = new List<(string, int, int, int)>();
|
|
1473
|
+
/// <summary>Per container sheet name: last data row written. Used to compute append row for next record (avoids stale UsedRange).</summary>
|
|
1474
|
+
private readonly Dictionary<string, int> _containerSheetLastRow = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
1475
|
+
private IExcelSheet _cachedSepSheet;
|
|
1476
|
+
private DpExcelProtocol _cachedSepProto;
|
|
1477
|
+
|
|
1478
|
+
/// <summary>
|
|
1479
|
+
/// Enable write mode. Call before Write() on an IDeukPack object.
|
|
1480
|
+
/// metaSchemaName: when non-null, use DLL schema (ResolveMetaSchema) for column mapping; otherwise use sheet headers.
|
|
1481
|
+
/// When writeHeaders is true, the schema-derived column map is physically written to the sheet (rows 1–3).
|
|
1482
|
+
/// For keyed tables (e.g. level): set WriteCategory and GetKeyFieldNames before calling so meta_id is omitted and columns order key→name→note.
|
|
1483
|
+
/// </summary>
|
|
1484
|
+
public void BeginWrite(IWritableExcelSheet sheet, int startRow = FIRST_DATA_ROW, string metaSchemaName = null, bool writeHeaders = false)
|
|
1485
|
+
{
|
|
1486
|
+
_writeSheet = sheet ?? throw new ArgumentNullException(nameof(sheet));
|
|
1487
|
+
_writeRow = startRow;
|
|
1488
|
+
_maxWriteRow = startRow;
|
|
1489
|
+
_mainSheetFirstDataRow = startRow;
|
|
1490
|
+
_writeCurrentPath = "";
|
|
1491
|
+
_writePathStack.Clear();
|
|
1492
|
+
_writeListStack.Clear();
|
|
1493
|
+
if (writeHeaders)
|
|
1494
|
+
{
|
|
1495
|
+
_globalFirstDataRow = startRow;
|
|
1496
|
+
_globalMaxWriteRow = startRow;
|
|
1497
|
+
_containerSheetsWritten.Clear();
|
|
1498
|
+
_containerSheetLastRow.Clear();
|
|
1499
|
+
}
|
|
1500
|
+
else
|
|
1501
|
+
{
|
|
1502
|
+
_globalMaxWriteRow = Math.Max(_globalMaxWriteRow, _maxWriteRow);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
DpSchema schema = null;
|
|
1506
|
+
if (!string.IsNullOrEmpty(metaSchemaName))
|
|
1507
|
+
schema = ResolveMetaSchema?.Invoke(metaSchemaName);
|
|
1508
|
+
_writeSchema = schema;
|
|
1509
|
+
|
|
1510
|
+
if (schema != null)
|
|
1511
|
+
{
|
|
1512
|
+
BuildColumnMapFromSchema(schema);
|
|
1513
|
+
if (writeHeaders)
|
|
1514
|
+
{
|
|
1515
|
+
WriteHeadersToSheet(sheet);
|
|
1516
|
+
EnsureContainerSheetsExist();
|
|
1517
|
+
ApplyHeaderStyles(sheet, _writeFlatFields);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/// <summary>
|
|
1523
|
+
/// Create empty container sheets for all list/set/map fields so that console Write matches add-in.
|
|
1524
|
+
/// Uses _writeFlatFields, _resolver, BuildContainerSheetHeaders; applies meta_id col NumberFormat "0".
|
|
1525
|
+
/// </summary>
|
|
1526
|
+
private void EnsureContainerSheetsExist()
|
|
1527
|
+
{
|
|
1528
|
+
if (_writeFlatFields == null || _resolver == null) return;
|
|
1529
|
+
foreach (var f in _writeFlatFields)
|
|
1530
|
+
{
|
|
1531
|
+
string dt = (f.DataType ?? "").Trim();
|
|
1532
|
+
var wt = DpTypeNames.FromProtocolName(dt);
|
|
1533
|
+
if (!ContainerSheetPolicy.IsContainerType(wt)) continue;
|
|
1534
|
+
|
|
1535
|
+
string kind = ContainerSheetPolicy.GetKindForWireType(wt);
|
|
1536
|
+
if (string.IsNullOrEmpty(kind)) continue;
|
|
1537
|
+
string sheetName = ContainerSheetNaming.FormatContainerSheetName(f.HierarchyId, kind, f.ColumnName ?? "");
|
|
1538
|
+
|
|
1539
|
+
string innerType = ResolveElementTypeNameFromSchema(f.HierarchyId);
|
|
1540
|
+
if (string.IsNullOrEmpty(innerType)) innerType = DpTypeNames.StripOuterGeneric(dt);
|
|
1541
|
+
DpWireType elemType = (ResolveTypeName != null && !string.IsNullOrEmpty(innerType) && ResolveTypeName(innerType) != null)
|
|
1542
|
+
? DpWireType.Struct
|
|
1543
|
+
: DpTypeNames.FromProtocolName(innerType ?? "");
|
|
1544
|
+
|
|
1545
|
+
BuildContainerSheetHeaders(f.HierarchyId, elemType, out string[] hdrIds, out string[] hdrTypes, out string[] hdrNames, out string[] hdrStructNames, out bool[] hdrIsMarker);
|
|
1546
|
+
if (hdrIds == null) continue;
|
|
1547
|
+
|
|
1548
|
+
var sepSheet = _resolver.GetOrCreateSheet(sheetName, hdrIds, hdrTypes, hdrNames,
|
|
1549
|
+
_useCompactHeader ? hdrStructNames : null);
|
|
1550
|
+
if (sepSheet != null)
|
|
1551
|
+
{
|
|
1552
|
+
sepSheet.SetColumnNumberFormat(1, "0");
|
|
1553
|
+
sepSheet.SetColumnNumberFormat(2, "@");
|
|
1554
|
+
var listFlat = HeaderArraysToFlatFields(hdrIds, hdrTypes, hdrNames, hdrStructNames, hdrIsMarker);
|
|
1555
|
+
ApplyHeaderStyles(sepSheet, listFlat);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/// <summary>
|
|
1561
|
+
/// Build column mapping directly from the DpSchema via FlattenSchema.
|
|
1562
|
+
/// When WriteCategory and GetKeyFieldNames are set: keyed tables exclude meta_id column and order key→name→note.
|
|
1563
|
+
/// </summary>
|
|
1564
|
+
private void BuildColumnMapFromSchema(DpSchema schema)
|
|
1565
|
+
{
|
|
1566
|
+
_fieldIdToCol.Clear();
|
|
1567
|
+
_colDataType.Clear();
|
|
1568
|
+
_colColumnName.Clear();
|
|
1569
|
+
|
|
1570
|
+
var fullFlat = FlattenSchema(schema, ResolveTypeName, "", mainSheetOnly: true);
|
|
1571
|
+
if (!string.IsNullOrEmpty(WriteCategory) && GetKeyFieldNames != null)
|
|
1572
|
+
{
|
|
1573
|
+
var fixedNames = GetMainSheetFixedColumnNamesForCategory(WriteCategory, GetKeyFieldNames);
|
|
1574
|
+
bool isKeyed = fixedNames != null && fixedNames.Count > 0 &&
|
|
1575
|
+
!string.Equals(fixedNames[0], "tuid", StringComparison.OrdinalIgnoreCase);
|
|
1576
|
+
if (isKeyed)
|
|
1577
|
+
{
|
|
1578
|
+
fullFlat = fullFlat.Where(f => !string.Equals(f.ColumnName, "meta_id", StringComparison.OrdinalIgnoreCase)
|
|
1579
|
+
&& !string.Equals(f.ColumnName, "tuid", StringComparison.OrdinalIgnoreCase)).ToList();
|
|
1580
|
+
}
|
|
1581
|
+
fullFlat = ReorderFlatWithMetaIdTidMetaNameFirst(fullFlat, WriteCategory, GetKeyFieldNames);
|
|
1582
|
+
}
|
|
1583
|
+
for (int i = 0; i < fullFlat.Count; i++)
|
|
1584
|
+
{
|
|
1585
|
+
int col = i + 1;
|
|
1586
|
+
var f = fullFlat[i];
|
|
1587
|
+
_fieldIdToCol[f.HierarchyId] = col;
|
|
1588
|
+
_colDataType[col] = f.DataType ?? "";
|
|
1589
|
+
_colColumnName[col] = f.ColumnName ?? "";
|
|
1590
|
+
}
|
|
1591
|
+
_writeFlatFields = fullFlat;
|
|
1592
|
+
_writeStructRanges = null;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
private static List<(int colStart, int colEnd, string structName)> BuildStructRangesForCompact(List<FlatHeaderField> fullFlat)
|
|
1596
|
+
{
|
|
1597
|
+
var list = new List<(int, int, string)>();
|
|
1598
|
+
int dataCol = 0;
|
|
1599
|
+
string currentStruct = null;
|
|
1600
|
+
int runStart = 0;
|
|
1601
|
+
for (int i = 0; i < fullFlat.Count; i++)
|
|
1602
|
+
{
|
|
1603
|
+
if (fullFlat[i].IsStructMarker)
|
|
1604
|
+
{
|
|
1605
|
+
if (currentStruct != null && runStart > 0)
|
|
1606
|
+
list.Add((runStart, dataCol, currentStruct));
|
|
1607
|
+
string tn = fullFlat[i].DataType?.Trim();
|
|
1608
|
+
currentStruct = (!string.IsNullOrEmpty(tn) && !string.Equals(tn, "rec", StringComparison.OrdinalIgnoreCase) && !string.Equals(tn, "record", StringComparison.OrdinalIgnoreCase))
|
|
1609
|
+
? tn : (fullFlat[i].ColumnName?.Trim() ?? "");
|
|
1610
|
+
runStart = dataCol + 1;
|
|
1611
|
+
}
|
|
1612
|
+
else
|
|
1613
|
+
{
|
|
1614
|
+
dataCol++;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (currentStruct != null && runStart > 0)
|
|
1618
|
+
list.Add((runStart, dataCol, currentStruct));
|
|
1619
|
+
return list;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
private List<FlatHeaderField> _writeFlatFields;
|
|
1623
|
+
#pragma warning disable CS0414
|
|
1624
|
+
private List<(int colStart, int colEnd, string structName)> _writeStructRanges;
|
|
1625
|
+
#pragma warning restore CS0414
|
|
1626
|
+
|
|
1627
|
+
private const int STRUCT_NAME_ROW_COMPACT = 3;
|
|
1628
|
+
private const int VARIABLE_NAME_ROW_COMPACT = 4;
|
|
1629
|
+
|
|
1630
|
+
/// <summary>
|
|
1631
|
+
/// Physically write header rows to the given sheet from the current column map.
|
|
1632
|
+
/// 신버전 3행: Row 1 = HierarchyId, Row 2 = DataType, Row 3 = ColumnName.
|
|
1633
|
+
/// </summary>
|
|
1634
|
+
private void WriteHeadersToSheet(IWritableExcelSheet sheet)
|
|
1635
|
+
{
|
|
1636
|
+
if (_writeFlatFields == null) return;
|
|
1637
|
+
|
|
1638
|
+
for (int i = 0; i < _writeFlatFields.Count; i++)
|
|
1639
|
+
{
|
|
1640
|
+
int col = i + 1;
|
|
1641
|
+
var f = _writeFlatFields[i];
|
|
1642
|
+
sheet.SetCellValue(HIERARCHY_ID_ROW, col, f.HierarchyId);
|
|
1643
|
+
sheet.SetCellValue(DATATYPE_ROW, col, f.DataType ?? "");
|
|
1644
|
+
string nameRow3 = f.ColumnName ?? "";
|
|
1645
|
+
if (string.IsNullOrWhiteSpace(nameRow3) && f.IsStructMarker)
|
|
1646
|
+
nameRow3 = f.ParentStructName ?? f.HierarchyId ?? "";
|
|
1647
|
+
sheet.SetCellValue(COLUMN_NAME_ROW, col, nameRow3 ?? "");
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
for (int c = 1; c <= _writeFlatFields.Count; c++)
|
|
1651
|
+
{
|
|
1652
|
+
string dt = (_writeFlatFields[c - 1].DataType ?? "").Trim();
|
|
1653
|
+
var wt = DpTypeNames.FromProtocolName(dt);
|
|
1654
|
+
if (wt == DpWireType.I64 || wt == DpWireType.I32 || wt == DpWireType.I16 || wt == DpWireType.Byte)
|
|
1655
|
+
sheet.SetColumnNumberFormat(c, "0");
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/// <summary>The highest row written so far. Use to determine how many rows were consumed.</summary>
|
|
1660
|
+
public int WriteRow => _maxWriteRow;
|
|
1661
|
+
|
|
1662
|
+
/// <summary>
|
|
1663
|
+
/// Call after all records have been written. Applies data row stripes to main sheet and to each written container sheet.
|
|
1664
|
+
/// Console Write must call this to get the same result as add-in.
|
|
1665
|
+
/// </summary>
|
|
1666
|
+
public void FinishWrite()
|
|
1667
|
+
{
|
|
1668
|
+
if (_writeSheet == null || _writeFlatFields == null) return;
|
|
1669
|
+
_globalMaxWriteRow = Math.Max(_globalMaxWriteRow, _maxWriteRow);
|
|
1670
|
+
int colCount = _writeFlatFields.Count;
|
|
1671
|
+
if (colCount > 0 && _globalMaxWriteRow >= _globalFirstDataRow)
|
|
1672
|
+
ApplyDataStripes(_writeSheet, _globalFirstDataRow, _globalMaxWriteRow, colCount, _writeFlatFields);
|
|
1673
|
+
// Merge ranges per sheet name so meta_id grouping colors all groups across all records
|
|
1674
|
+
var byName = new Dictionary<string, (int firstRow, int lastRow, int cols)>(StringComparer.OrdinalIgnoreCase);
|
|
1675
|
+
foreach (var (sheetName, firstRow, lastRow, cols) in _containerSheetsWritten)
|
|
1676
|
+
{
|
|
1677
|
+
if (string.IsNullOrEmpty(sheetName) || cols < 1) continue;
|
|
1678
|
+
if (byName.TryGetValue(sheetName, out var existing))
|
|
1679
|
+
{
|
|
1680
|
+
int mergedFirst = Math.Min(existing.firstRow, firstRow);
|
|
1681
|
+
int mergedLast = Math.Max(existing.lastRow, lastRow);
|
|
1682
|
+
byName[sheetName] = (mergedFirst, mergedLast, Math.Max(existing.cols, cols));
|
|
1683
|
+
}
|
|
1684
|
+
else
|
|
1685
|
+
byName[sheetName] = (firstRow, lastRow, cols);
|
|
1686
|
+
}
|
|
1687
|
+
foreach (var kv in byName)
|
|
1688
|
+
{
|
|
1689
|
+
var (firstRow, lastRow, cols) = kv.Value;
|
|
1690
|
+
if (lastRow < firstRow) continue;
|
|
1691
|
+
// Get fresh writable adapter with current LastRow/LastColumn
|
|
1692
|
+
var freshSheet = _resolver?.GetOrCreateSheet(kv.Key, null, null, null);
|
|
1693
|
+
if (freshSheet != null)
|
|
1694
|
+
ApplyContainerDataStripes(freshSheet, firstRow, lastRow, cols);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
#region Excel styling (header colors + data stripes; same result as add-in)
|
|
1699
|
+
|
|
1700
|
+
private static int ToOle(int r, int g, int b) => (r & 0xFF) | ((g & 0xFF) << 8) | ((b & 0xFF) << 16);
|
|
1701
|
+
private static int Lighten(int ole, float amount)
|
|
1702
|
+
{
|
|
1703
|
+
int r = (ole & 0xFF) + (int)((255 - (ole & 0xFF)) * amount);
|
|
1704
|
+
int g = ((ole >> 8) & 0xFF) + (int)((255 - ((ole >> 8) & 0xFF)) * amount);
|
|
1705
|
+
int b = ((ole >> 16) & 0xFF) + (int)((255 - ((ole >> 16) & 0xFF)) * amount);
|
|
1706
|
+
return ToOle(Math.Min(r, 255), Math.Min(g, 255), Math.Min(b, 255));
|
|
1707
|
+
}
|
|
1708
|
+
private static int Darken(int ole, float amount)
|
|
1709
|
+
{
|
|
1710
|
+
int r = (ole & 0xFF) - (int)((ole & 0xFF) * amount);
|
|
1711
|
+
int g = ((ole >> 8) & 0xFF) - (int)(((ole >> 8) & 0xFF) * amount);
|
|
1712
|
+
int b = ((ole >> 16) & 0xFF) - (int)(((ole >> 16) & 0xFF) * amount);
|
|
1713
|
+
return ToOle(Math.Max(0, r), Math.Max(0, g), Math.Max(0, b));
|
|
1714
|
+
}
|
|
1715
|
+
private static readonly int ColHdrPrimitive = ToOle(240, 253, 244);
|
|
1716
|
+
private static readonly int ColHdrStruct = ToOle(238, 242, 255);
|
|
1717
|
+
private static readonly int ColHdrList = ToOle(255, 250, 235);
|
|
1718
|
+
private static readonly int ColHdrMap = ToOle(245, 243, 255);
|
|
1719
|
+
private static readonly int ColHdrEnum = ToOle(255, 241, 242);
|
|
1720
|
+
private static readonly int ColHdrLink = ToOle(236, 254, 255);
|
|
1721
|
+
private static readonly int ColHdrRow3Base = ToOle(51, 65, 85);
|
|
1722
|
+
private static readonly int ColHdrRow3Struct = ToOle(49, 46, 129);
|
|
1723
|
+
private static readonly int ColHdrRow3List = ToOle(180, 83, 9);
|
|
1724
|
+
private static readonly int ColHdrRow3Map = ToOle(91, 33, 182);
|
|
1725
|
+
private static readonly int ColHdrRow3Enum = ToOle(157, 23, 77);
|
|
1726
|
+
private static readonly int ColHdrRow3Link = ToOle(21, 94, 117);
|
|
1727
|
+
private static readonly int FontGray1 = ToOle(120, 120, 120);
|
|
1728
|
+
private static readonly int FontGray2 = ToOle(130, 130, 130);
|
|
1729
|
+
private static readonly int FontWhite = ToOle(255, 255, 255);
|
|
1730
|
+
private static readonly int FontGray4 = ToOle(160, 160, 160);
|
|
1731
|
+
|
|
1732
|
+
private static void ClassifyColumnColor(string dt, FlatHeaderField field, out int pastelOle, out int row3Ole)
|
|
1733
|
+
{
|
|
1734
|
+
if (field != null && field.IsStructMarker)
|
|
1735
|
+
{ pastelOle = ColHdrStruct; row3Ole = ColHdrRow3Struct; return; }
|
|
1736
|
+
string lower = (dt ?? "").ToLowerInvariant();
|
|
1737
|
+
if (lower.StartsWith("enum", StringComparison.Ordinal))
|
|
1738
|
+
{ pastelOle = ColHdrEnum; row3Ole = ColHdrRow3Enum; return; }
|
|
1739
|
+
if (lower.StartsWith("_link") || lower.StartsWith("link<") || lower.StartsWith("linktid<") || lower.StartsWith("_linktid"))
|
|
1740
|
+
{ pastelOle = ColHdrLink; row3Ole = ColHdrRow3Link; return; }
|
|
1741
|
+
var ttype = DpTypeNames.FromProtocolName(dt);
|
|
1742
|
+
switch (ttype)
|
|
1743
|
+
{
|
|
1744
|
+
case DpWireType.List:
|
|
1745
|
+
case DpWireType.Set: pastelOle = ColHdrList; row3Ole = ColHdrRow3List; return;
|
|
1746
|
+
case DpWireType.Map: pastelOle = ColHdrMap; row3Ole = ColHdrRow3Map; return;
|
|
1747
|
+
case DpWireType.Struct: pastelOle = ColHdrStruct; row3Ole = ColHdrRow3Struct; return;
|
|
1748
|
+
default: pastelOle = ColHdrPrimitive; row3Ole = ColHdrRow3Base; return;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
private static void ApplyHeaderStyles(IWritableExcelSheet sheet, List<FlatHeaderField> flatFields)
|
|
1753
|
+
{
|
|
1754
|
+
if (sheet == null || flatFields == null || flatFields.Count == 0) return;
|
|
1755
|
+
int colCount = flatFields.Count;
|
|
1756
|
+
sheet.SetRangeFont(1, 1, 1, colCount, FontGray1, true, false, 9);
|
|
1757
|
+
sheet.SetRangeFont(2, 2, 1, colCount, FontGray2, false, true, 9);
|
|
1758
|
+
sheet.SetRangeFont(3, 3, 1, colCount, FontWhite, true, false, 9);
|
|
1759
|
+
|
|
1760
|
+
for (int c = 1; c <= colCount; c++)
|
|
1761
|
+
{
|
|
1762
|
+
var f = c - 1 < flatFields.Count ? flatFields[c - 1] : null;
|
|
1763
|
+
string dt = (f?.DataType ?? "").Trim();
|
|
1764
|
+
ClassifyColumnColor(dt, f, out int pastel, out int row3Bg);
|
|
1765
|
+
sheet.SetRangeInteriorColor(1, 1, c, c, pastel);
|
|
1766
|
+
sheet.SetRangeInteriorColor(2, 2, c, c, Lighten(pastel, 0.4f));
|
|
1767
|
+
bool hasParentStruct = !string.IsNullOrEmpty(f?.ParentStructName);
|
|
1768
|
+
sheet.SetRangeInteriorColor(3, 3, c, c, hasParentStruct ? ColHdrRow3Struct : row3Bg);
|
|
1769
|
+
}
|
|
1770
|
+
sheet.AutoFitColumns(1, colCount);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// ── 그룹 색상: 파랑톤(짝수 그룹) / 분홍톤(홀수 그룹), 그룹 내 행 교차 ──
|
|
1774
|
+
|
|
1775
|
+
// 파랑톤 (Group A) — 밝은/어두운
|
|
1776
|
+
private static readonly int BlueLight = ToOle(232, 240, 254);
|
|
1777
|
+
private static readonly int BlueDark = ToOle(210, 224, 245);
|
|
1778
|
+
// 분홍톤 (Group B) — 밝은/어두운
|
|
1779
|
+
private static readonly int PinkLight = ToOle(254, 236, 243);
|
|
1780
|
+
private static readonly int PinkDark = ToOle(245, 218, 232);
|
|
1781
|
+
// 특수 컬럼 마커 (list/struct/map) — 파랑 계열
|
|
1782
|
+
private static readonly int ColDataListBlueL = ToOle(225, 237, 255);
|
|
1783
|
+
private static readonly int ColDataListBlueD = ToOle(205, 222, 248);
|
|
1784
|
+
private static readonly int ColDataStructBlueL = ToOle(228, 234, 255);
|
|
1785
|
+
private static readonly int ColDataStructBlueD = ToOle(212, 222, 252);
|
|
1786
|
+
private static readonly int ColDataMapBlueL = ToOle(235, 228, 255);
|
|
1787
|
+
private static readonly int ColDataMapBlueD = ToOle(222, 212, 252);
|
|
1788
|
+
// 특수 컬럼 마커 — 분홍 계열
|
|
1789
|
+
private static readonly int ColDataListPinkL = ToOle(255, 232, 240);
|
|
1790
|
+
private static readonly int ColDataListPinkD = ToOle(248, 212, 225);
|
|
1791
|
+
private static readonly int ColDataStructPinkL = ToOle(255, 228, 235);
|
|
1792
|
+
private static readonly int ColDataStructPinkD = ToOle(248, 212, 222);
|
|
1793
|
+
private static readonly int ColDataMapPinkL = ToOle(248, 228, 245);
|
|
1794
|
+
private static readonly int ColDataMapPinkD = ToOle(240, 212, 238);
|
|
1795
|
+
|
|
1796
|
+
private static void ApplyDataStripes(IWritableExcelSheet sheet, int firstDataRow, int lastDataRow, int colCount, List<FlatHeaderField> flatFields = null)
|
|
1797
|
+
{
|
|
1798
|
+
if (sheet == null || colCount < 1 || lastDataRow < firstDataRow) return;
|
|
1799
|
+
|
|
1800
|
+
int[] colCategory = null;
|
|
1801
|
+
if (flatFields != null && flatFields.Count >= colCount)
|
|
1802
|
+
{
|
|
1803
|
+
colCategory = new int[colCount + 1]; // 0=normal, 1=list/set, 2=struct, 3=map
|
|
1804
|
+
for (int c = 1; c <= colCount; c++)
|
|
1805
|
+
{
|
|
1806
|
+
var f = flatFields[c - 1];
|
|
1807
|
+
if (f.IsStructMarker) { colCategory[c] = 2; continue; }
|
|
1808
|
+
var wt = DpTypeNames.FromProtocolName(f.DataType ?? "");
|
|
1809
|
+
if (wt == DpWireType.List || wt == DpWireType.Set) colCategory[c] = 1;
|
|
1810
|
+
else if (wt == DpWireType.Map) colCategory[c] = 3;
|
|
1811
|
+
else colCategory[c] = 0;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
var groups = BuildMetaIdGroups(sheet, firstDataRow, lastDataRow);
|
|
1816
|
+
|
|
1817
|
+
for (int gi = 0; gi < groups.Count; gi++)
|
|
1818
|
+
{
|
|
1819
|
+
var (gStart, gEnd) = groups[gi];
|
|
1820
|
+
bool blue = (gi % 2) == 0;
|
|
1821
|
+
|
|
1822
|
+
for (int r = gStart; r <= gEnd; r++)
|
|
1823
|
+
{
|
|
1824
|
+
bool light = ((r - gStart) % 2) == 0;
|
|
1825
|
+
for (int c = 1; c <= colCount; c++)
|
|
1826
|
+
{
|
|
1827
|
+
int cat = (colCategory != null) ? colCategory[c] : 0;
|
|
1828
|
+
int bg = PickGroupColor(blue, light, cat);
|
|
1829
|
+
if ((c % 2) == 1) bg = Darken(bg, 0.04f);
|
|
1830
|
+
try { sheet.SetRangeInteriorColor(r, r, c, c, bg); } catch { }
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
if (gi < groups.Count - 1)
|
|
1835
|
+
try { sheet.SetBottomBorder(gEnd, 1, colCount, GrpBorderColor); } catch { }
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
private static int PickGroupColor(bool blue, bool light, int cat)
|
|
1840
|
+
{
|
|
1841
|
+
if (cat == 1) return blue ? (light ? ColDataListBlueL : ColDataListBlueD) : (light ? ColDataListPinkL : ColDataListPinkD);
|
|
1842
|
+
if (cat == 2) return blue ? (light ? ColDataStructBlueL : ColDataStructBlueD) : (light ? ColDataStructPinkL : ColDataStructPinkD);
|
|
1843
|
+
if (cat == 3) return blue ? (light ? ColDataMapBlueL : ColDataMapBlueD) : (light ? ColDataMapPinkL : ColDataMapPinkD);
|
|
1844
|
+
return blue ? (light ? BlueLight : BlueDark) : (light ? PinkLight : PinkDark);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
private static List<(int start, int end)> BuildMetaIdGroups(IExcelSheet sheet, int firstDataRow, int lastDataRow)
|
|
1848
|
+
{
|
|
1849
|
+
var groups = new List<(int start, int end)>();
|
|
1850
|
+
string currentId = null;
|
|
1851
|
+
int groupStart = firstDataRow;
|
|
1852
|
+
for (int r = firstDataRow; r <= lastDataRow; r++)
|
|
1853
|
+
{
|
|
1854
|
+
string id = sheet.CellValue(r, 1)?.Trim() ?? "";
|
|
1855
|
+
if (!string.Equals(id, currentId, StringComparison.Ordinal))
|
|
1856
|
+
{
|
|
1857
|
+
if (currentId != null)
|
|
1858
|
+
groups.Add((groupStart, r - 1));
|
|
1859
|
+
currentId = id;
|
|
1860
|
+
groupStart = r;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (currentId != null)
|
|
1864
|
+
groups.Add((groupStart, lastDataRow));
|
|
1865
|
+
if (groups.Count == 0)
|
|
1866
|
+
groups.Add((firstDataRow, lastDataRow));
|
|
1867
|
+
return groups;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// ── Container sheet: group-aware data stripes ─────────────────────────
|
|
1871
|
+
|
|
1872
|
+
private static readonly int GrpBorderColor = ToOle(160, 170, 185);
|
|
1873
|
+
|
|
1874
|
+
/// <summary>
|
|
1875
|
+
/// Apply group-aware coloring to a container (list/set/map) sheet:
|
|
1876
|
+
/// - 짝수 그룹 = 파랑톤, 홀수 그룹 = 분홍톤 (그룹 내 행은 해당 톤의 밝은/어두운으로 교차)
|
|
1877
|
+
/// - Bottom border at group boundaries
|
|
1878
|
+
/// </summary>
|
|
1879
|
+
public static void ApplyContainerDataStripes(IWritableExcelSheet sheet, int firstDataRow, int lastDataRow, int colCount)
|
|
1880
|
+
{
|
|
1881
|
+
if (sheet == null || colCount < 1 || lastDataRow < firstDataRow) return;
|
|
1882
|
+
|
|
1883
|
+
var groups = BuildMetaIdGroups(sheet, firstDataRow, lastDataRow);
|
|
1884
|
+
|
|
1885
|
+
for (int gi = 0; gi < groups.Count; gi++)
|
|
1886
|
+
{
|
|
1887
|
+
var (gStart, gEnd) = groups[gi];
|
|
1888
|
+
bool blue = (gi % 2) == 0;
|
|
1889
|
+
|
|
1890
|
+
for (int r = gStart; r <= gEnd; r++)
|
|
1891
|
+
{
|
|
1892
|
+
bool light = ((r - gStart) % 2) == 0;
|
|
1893
|
+
int baseBg = blue ? (light ? BlueLight : BlueDark) : (light ? PinkLight : PinkDark);
|
|
1894
|
+
for (int c = 1; c <= colCount; c++)
|
|
1895
|
+
{
|
|
1896
|
+
int bg = (c % 2) == 1 ? Darken(baseBg, 0.04f) : baseBg;
|
|
1897
|
+
try { sheet.SetRangeInteriorColor(r, r, c, c, bg); } catch { }
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
if (gi < groups.Count - 1)
|
|
1902
|
+
try { sheet.SetBottomBorder(gEnd, 1, colCount, GrpBorderColor); } catch { }
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
/// <summary>
|
|
1907
|
+
/// Format enum columns to "value:name" and link columns to "value:name" or "value:NIA"/"0:MIA".
|
|
1908
|
+
/// Used after schema migration and when opening schema pane so numeric cells show resolved names.
|
|
1909
|
+
/// </summary>
|
|
1910
|
+
/// <param name="metaIdCol">1-based column index; when > 0, only rows with non-empty value in this column are formatted (각 시트 첫 번째 칼럼 데이터 유무로 판단).</param>
|
|
1911
|
+
/// <param name="getEnumValuesForField">Returns "value:name" or "value:name:comment" list for the given schema field (caller resolves via loader/schema); null/empty if unknown.</param>
|
|
1912
|
+
/// <param name="getLinkNameForId">Optional. For link columns: (field, idString) => display name; null if not found. When provided, cells are written as "id:name" instead of "id:NIA".</param>
|
|
1913
|
+
public static void ApplyEnumAndLinkDisplayOnly(
|
|
1914
|
+
IExcelSheet sheet,
|
|
1915
|
+
IWritableExcelSheet writable,
|
|
1916
|
+
int firstDataRow,
|
|
1917
|
+
int lastDataRow,
|
|
1918
|
+
List<FlatHeaderField> flatFields,
|
|
1919
|
+
Dictionary<string, int> colByPath,
|
|
1920
|
+
Func<FlatHeaderField, List<string>> getEnumValuesForField,
|
|
1921
|
+
Func<FlatHeaderField, string, string> getLinkNameForId = null,
|
|
1922
|
+
int metaIdCol = 0)
|
|
1923
|
+
{
|
|
1924
|
+
if (flatFields == null || colByPath == null || sheet == null || writable == null) return;
|
|
1925
|
+
int effectiveLast = lastDataRow < firstDataRow ? firstDataRow : lastDataRow;
|
|
1926
|
+
|
|
1927
|
+
foreach (var f in flatFields)
|
|
1928
|
+
{
|
|
1929
|
+
if (!colByPath.TryGetValue(f.HierarchyId, out int col) || col < 1) continue;
|
|
1930
|
+
string dt = (f.DataType ?? "").Trim();
|
|
1931
|
+
|
|
1932
|
+
if (DpTypeNames.IsEnumDataType(dt))
|
|
1933
|
+
{
|
|
1934
|
+
var values = getEnumValuesForField?.Invoke(f);
|
|
1935
|
+
if (values == null) values = new List<string>();
|
|
1936
|
+
for (int r = firstDataRow; r <= effectiveLast; r++)
|
|
1937
|
+
{
|
|
1938
|
+
try
|
|
1939
|
+
{
|
|
1940
|
+
if (metaIdCol > 0 && string.IsNullOrWhiteSpace(sheet.CellValue(r, metaIdCol)?.Trim()))
|
|
1941
|
+
continue;
|
|
1942
|
+
string str = sheet.CellValue(r, col)?.Trim() ?? "";
|
|
1943
|
+
if (string.IsNullOrEmpty(str))
|
|
1944
|
+
{
|
|
1945
|
+
writable.SetColumnNumberFormat(col, "@");
|
|
1946
|
+
writable.SetCellValue(r, col, "0:MIA");
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
if (str.IndexOf(':') >= 0) continue;
|
|
1950
|
+
if (int.TryParse(str, out int num))
|
|
1951
|
+
{
|
|
1952
|
+
string line = null;
|
|
1953
|
+
foreach (var v in values)
|
|
1954
|
+
{
|
|
1955
|
+
var parts = v.Split(new[] { ':' }, 2);
|
|
1956
|
+
if (parts.Length > 0 && int.TryParse(parts[0].Trim(), out int vv) && vv == num)
|
|
1957
|
+
{ line = v; break; }
|
|
1958
|
+
}
|
|
1959
|
+
writable.SetColumnNumberFormat(col, "@");
|
|
1960
|
+
writable.SetCellValue(r, col, !string.IsNullOrEmpty(line) ? line : (num == 0 ? "0:MIA" : $"{num}:NIA"));
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
catch { }
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
else if (IsLinkDataType(dt))
|
|
1967
|
+
{
|
|
1968
|
+
for (int r = firstDataRow; r <= effectiveLast; r++)
|
|
1969
|
+
{
|
|
1970
|
+
try
|
|
1971
|
+
{
|
|
1972
|
+
if (metaIdCol > 0 && string.IsNullOrWhiteSpace(sheet.CellValue(r, metaIdCol)?.Trim()))
|
|
1973
|
+
continue;
|
|
1974
|
+
string str = sheet.CellValue(r, col)?.Trim() ?? "";
|
|
1975
|
+
if (string.IsNullOrEmpty(str))
|
|
1976
|
+
{
|
|
1977
|
+
writable.SetColumnNumberFormat(col, "@");
|
|
1978
|
+
writable.SetCellValue(r, col, "0:MIA");
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
if (str.IndexOf(':') >= 0) continue;
|
|
1982
|
+
if (long.TryParse(str, out long numL))
|
|
1983
|
+
{
|
|
1984
|
+
writable.SetColumnNumberFormat(col, "@");
|
|
1985
|
+
string name = getLinkNameForId?.Invoke(f, str);
|
|
1986
|
+
if (numL == 0)
|
|
1987
|
+
writable.SetCellValue(r, col, "0:MIA");
|
|
1988
|
+
else if (!string.IsNullOrEmpty(name))
|
|
1989
|
+
writable.SetCellValue(r, col, $"{numL}:{name}");
|
|
1990
|
+
else
|
|
1991
|
+
writable.SetCellValue(r, col, $"{numL}:NIA");
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
catch { }
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/// <summary>True if the data type string denotes a link (meta_id or tid) column.</summary>
|
|
2001
|
+
public static bool IsLinkDataType(string dt)
|
|
2002
|
+
{
|
|
2003
|
+
if (string.IsNullOrEmpty(dt)) return false;
|
|
2004
|
+
return dt.StartsWith("_link_", StringComparison.OrdinalIgnoreCase)
|
|
2005
|
+
|| dt.StartsWith("_linktid_", StringComparison.OrdinalIgnoreCase)
|
|
2006
|
+
|| dt.StartsWith("link<", StringComparison.OrdinalIgnoreCase)
|
|
2007
|
+
|| dt.StartsWith("lnk<", StringComparison.OrdinalIgnoreCase)
|
|
2008
|
+
|| dt.StartsWith("linktid<", StringComparison.OrdinalIgnoreCase);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/// <summary>
|
|
2012
|
+
/// Walks the schema by HierarchyId path (e.g. "30.2.1") and returns the field descriptor.
|
|
2013
|
+
/// Caller supplies getOrderedFields(schema) and getSchemaByTypeName(typeName) so the protocol stays agnostic of loader/reflection source.
|
|
2014
|
+
/// </summary>
|
|
2015
|
+
public static object GetFieldDescriptorByHierarchyId(
|
|
2016
|
+
object schema,
|
|
2017
|
+
string hierarchyId,
|
|
2018
|
+
Func<object, IEnumerable<KeyValuePair<string, object>>> getOrderedFields,
|
|
2019
|
+
Func<string, object> getSchemaByTypeName)
|
|
2020
|
+
{
|
|
2021
|
+
if (schema == null || string.IsNullOrEmpty(hierarchyId) || getOrderedFields == null || getSchemaByTypeName == null) return null;
|
|
2022
|
+
var parts = hierarchyId.Split('.');
|
|
2023
|
+
object currentSchema = schema;
|
|
2024
|
+
for (int i = 0; i < parts.Length; i++)
|
|
2025
|
+
{
|
|
2026
|
+
var fields = getOrderedFields(currentSchema);
|
|
2027
|
+
if (fields == null) return null;
|
|
2028
|
+
var pair = fields.FirstOrDefault(kv => string.Equals(kv.Key?.ToString(), parts[i].Trim(), StringComparison.Ordinal));
|
|
2029
|
+
if (pair.Value == null) return null;
|
|
2030
|
+
if (i == parts.Length - 1) return pair.Value;
|
|
2031
|
+
string typeName = GetTypeNameFromFieldDescriptor(pair.Value);
|
|
2032
|
+
if (string.IsNullOrEmpty(typeName)) return null;
|
|
2033
|
+
string typeKind = GetFieldTypeString(pair.Value);
|
|
2034
|
+
if (typeKind == "List" || typeKind == "Set" || typeKind == "Map")
|
|
2035
|
+
typeName = DpTypeNames.StripOuterGeneric(typeName);
|
|
2036
|
+
currentSchema = getSchemaByTypeName(typeName);
|
|
2037
|
+
if (currentSchema == null) return null;
|
|
2038
|
+
}
|
|
2039
|
+
return null;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
/// <summary>득팩 필드 디스크립터에서 타입 이름을 가져옴. typedef인 경우 typedef 이름이 반환됨.</summary>
|
|
2043
|
+
public static string GetFieldTypeName(object fieldDescriptor)
|
|
2044
|
+
{
|
|
2045
|
+
if (fieldDescriptor == null) return null;
|
|
2046
|
+
try
|
|
2047
|
+
{
|
|
2048
|
+
var prop = fieldDescriptor.GetType().GetProperty("TypeName");
|
|
2049
|
+
return prop?.GetValue(fieldDescriptor)?.ToString()?.Trim();
|
|
2050
|
+
}
|
|
2051
|
+
catch { return null; }
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/// <summary>득팩 필드 디스크립터가 typedef 타입인지 여부. Type이 primitive이고 TypeName이 있으면 typedef로 간주.</summary>
|
|
2055
|
+
public static bool IsTypedefField(object fieldDescriptor)
|
|
2056
|
+
{
|
|
2057
|
+
if (fieldDescriptor == null) return false;
|
|
2058
|
+
string typeName = GetFieldTypeName(fieldDescriptor);
|
|
2059
|
+
if (string.IsNullOrWhiteSpace(typeName)) return false;
|
|
2060
|
+
string typeStr = GetFieldTypeString(fieldDescriptor);
|
|
2061
|
+
if (string.IsNullOrEmpty(typeStr)) return false;
|
|
2062
|
+
switch (typeStr)
|
|
2063
|
+
{
|
|
2064
|
+
case "Bool": case "Byte": case "I16": case "I32": case "I64": case "Double": case "String":
|
|
2065
|
+
return typeName.IndexOf('<') < 0;
|
|
2066
|
+
default: return false;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
private static string GetTypeNameFromFieldDescriptor(object fieldDescriptor) => GetFieldTypeName(fieldDescriptor);
|
|
2071
|
+
|
|
2072
|
+
private static string GetFieldTypeString(object fieldDescriptor)
|
|
2073
|
+
{
|
|
2074
|
+
if (fieldDescriptor == null) return null;
|
|
2075
|
+
try
|
|
2076
|
+
{
|
|
2077
|
+
var prop = fieldDescriptor.GetType().GetProperty("Type");
|
|
2078
|
+
return prop?.GetValue(fieldDescriptor)?.ToString()?.Trim();
|
|
2079
|
+
}
|
|
2080
|
+
catch { return null; }
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
/// <summary>
|
|
2084
|
+
/// Build path → column index map from sheet row 1 (HIERARCHY_ID). All Excel-style header column resolution goes through the protocol.
|
|
2085
|
+
/// </summary>
|
|
2086
|
+
public static Dictionary<string, int> BuildColByPath(IExcelSheet sheet)
|
|
2087
|
+
{
|
|
2088
|
+
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
2089
|
+
if (sheet == null) return map;
|
|
2090
|
+
for (int col = 1; col <= sheet.LastColumn; col++)
|
|
2091
|
+
{
|
|
2092
|
+
string path = sheet.CellValue(HIERARCHY_ID_ROW, col)?.Trim();
|
|
2093
|
+
if (!string.IsNullOrEmpty(path) && !map.ContainsKey(path))
|
|
2094
|
+
map[path] = col;
|
|
2095
|
+
}
|
|
2096
|
+
return map;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
/// <summary>
|
|
2100
|
+
/// Get ordered (key, fieldDescriptor) list from a schema object via reflection. Single source for Excel/schema field enumeration.
|
|
2101
|
+
/// </summary>
|
|
2102
|
+
public static List<KeyValuePair<string, object>> GetOrderedSchemaFields(object schema)
|
|
2103
|
+
{
|
|
2104
|
+
if (schema == null) return new List<KeyValuePair<string, object>>();
|
|
2105
|
+
try
|
|
2106
|
+
{
|
|
2107
|
+
var fieldsProp = schema.GetType().GetProperty("Fields");
|
|
2108
|
+
if (fieldsProp == null) return new List<KeyValuePair<string, object>>();
|
|
2109
|
+
var fieldsVal = fieldsProp.GetValue(schema);
|
|
2110
|
+
if (fieldsVal == null || !(fieldsVal is IEnumerable en)) return new List<KeyValuePair<string, object>>();
|
|
2111
|
+
var list = new List<KeyValuePair<string, object>>();
|
|
2112
|
+
foreach (var item in en)
|
|
2113
|
+
{
|
|
2114
|
+
if (item == null) continue;
|
|
2115
|
+
var kvType = item.GetType();
|
|
2116
|
+
string key = kvType.GetProperty("Key")?.GetValue(item)?.ToString() ?? "";
|
|
2117
|
+
object val = kvType.GetProperty("Value")?.GetValue(item);
|
|
2118
|
+
list.Add(new KeyValuePair<string, object>(key, val));
|
|
2119
|
+
}
|
|
2120
|
+
return list.OrderBy(kv =>
|
|
2121
|
+
{
|
|
2122
|
+
try
|
|
2123
|
+
{
|
|
2124
|
+
if (kv.Value == null) return 0;
|
|
2125
|
+
var orderProp = kv.Value.GetType().GetProperty("Order");
|
|
2126
|
+
if (orderProp != null)
|
|
2127
|
+
return Convert.ToInt32(orderProp.GetValue(kv.Value));
|
|
2128
|
+
}
|
|
2129
|
+
catch { }
|
|
2130
|
+
return int.TryParse(kv.Key, out int id) ? id : 0;
|
|
2131
|
+
}).ToList();
|
|
2132
|
+
}
|
|
2133
|
+
catch { return new List<KeyValuePair<string, object>>(); }
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
/// <summary>
|
|
2137
|
+
/// Convert schema object (from DLL/reflection) to DpSchema. All Excel-style schema object handling in the protocol.
|
|
2138
|
+
/// </summary>
|
|
2139
|
+
public static DpSchema BuildDpSchemaFromObject(object schema)
|
|
2140
|
+
{
|
|
2141
|
+
if (schema == null) return null;
|
|
2142
|
+
#pragma warning disable CS0618
|
|
2143
|
+
if (schema is ThriftSchema ts) return ts.ToDpSchema();
|
|
2144
|
+
#pragma warning restore CS0618
|
|
2145
|
+
if (schema is DpSchema ds) return ds;
|
|
2146
|
+
try
|
|
2147
|
+
{
|
|
2148
|
+
var schemaType = schema.GetType();
|
|
2149
|
+
var result = new DpSchema();
|
|
2150
|
+
result.Name = ReflectString(schemaType, schema, "Name") ?? "";
|
|
2151
|
+
result.Fields = new Dictionary<int, DpFieldSchema>();
|
|
2152
|
+
var fields = GetOrderedSchemaFields(schema);
|
|
2153
|
+
foreach (var kv in fields)
|
|
2154
|
+
{
|
|
2155
|
+
if (!int.TryParse(kv.Key, out int id)) continue;
|
|
2156
|
+
object f = kv.Value;
|
|
2157
|
+
if (f == null) continue;
|
|
2158
|
+
var ft = f.GetType();
|
|
2159
|
+
var fs = new DpFieldSchema
|
|
2160
|
+
{
|
|
2161
|
+
Id = id,
|
|
2162
|
+
Name = ReflectString(ft, f, "Name") ?? "",
|
|
2163
|
+
TypeName = ReflectString(ft, f, "TypeName") ?? "",
|
|
2164
|
+
DocComment = ReflectString(ft, f, "DocComment") ?? ""
|
|
2165
|
+
};
|
|
2166
|
+
var orderProp = ft.GetProperty("Order");
|
|
2167
|
+
fs.Order = orderProp != null ? Convert.ToInt32(orderProp.GetValue(f)) : id;
|
|
2168
|
+
string typeStr = "String";
|
|
2169
|
+
try
|
|
2170
|
+
{
|
|
2171
|
+
var typeProp = ft.GetProperty("Type");
|
|
2172
|
+
if (typeProp != null) typeStr = typeProp.GetValue(f)?.ToString() ?? "String";
|
|
2173
|
+
}
|
|
2174
|
+
catch { }
|
|
2175
|
+
switch (typeStr)
|
|
2176
|
+
{
|
|
2177
|
+
case "Bool": fs.Type = DpSchemaType.Bool; break;
|
|
2178
|
+
case "Byte": fs.Type = DpSchemaType.Byte; break;
|
|
2179
|
+
case "I16": fs.Type = DpSchemaType.I16; break;
|
|
2180
|
+
case "I32": fs.Type = DpSchemaType.I32; break;
|
|
2181
|
+
case "I64": fs.Type = DpSchemaType.I64; break;
|
|
2182
|
+
case "Double": fs.Type = DpSchemaType.Double; break;
|
|
2183
|
+
case "Struct": fs.Type = DpSchemaType.Struct; break;
|
|
2184
|
+
case "List": fs.Type = DpSchemaType.List; break;
|
|
2185
|
+
case "Set": fs.Type = DpSchemaType.Set; break;
|
|
2186
|
+
case "Map": fs.Type = DpSchemaType.Map; break;
|
|
2187
|
+
case "Enum": fs.Type = DpSchemaType.Enum; break;
|
|
2188
|
+
default: fs.Type = DpSchemaType.String; break;
|
|
2189
|
+
}
|
|
2190
|
+
result.Fields[id] = fs;
|
|
2191
|
+
}
|
|
2192
|
+
return result;
|
|
2193
|
+
}
|
|
2194
|
+
catch { return null; }
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
/// <summary>Compatibility: use BuildDpSchemaFromObject.</summary>
|
|
2198
|
+
public static DpSchema BuildThriftSchemaFromObject(object schema) => BuildDpSchemaFromObject(schema);
|
|
2199
|
+
|
|
2200
|
+
/// <summary>
|
|
2201
|
+
/// Field descriptor → Excel DATATYPE string (e.g. "enum<X>", "i64"). Protocol owns all Excel-style type string derivation.
|
|
2202
|
+
/// </summary>
|
|
2203
|
+
public static string GetDataTypeFromFieldDescriptor(object fieldDescriptor)
|
|
2204
|
+
{
|
|
2205
|
+
if (fieldDescriptor == null) return "string";
|
|
2206
|
+
try
|
|
2207
|
+
{
|
|
2208
|
+
var ft = fieldDescriptor.GetType();
|
|
2209
|
+
var fs = new DpFieldSchema
|
|
2210
|
+
{
|
|
2211
|
+
Name = ReflectString(ft, fieldDescriptor, "Name") ?? "",
|
|
2212
|
+
TypeName = ReflectString(ft, fieldDescriptor, "TypeName") ?? ""
|
|
2213
|
+
};
|
|
2214
|
+
string typeStr = "String";
|
|
2215
|
+
var typeProp = ft.GetProperty("Type");
|
|
2216
|
+
if (typeProp != null) typeStr = typeProp.GetValue(fieldDescriptor)?.ToString() ?? "String";
|
|
2217
|
+
switch (typeStr)
|
|
2218
|
+
{
|
|
2219
|
+
case "Bool": fs.Type = DpSchemaType.Bool; break;
|
|
2220
|
+
case "Byte": fs.Type = DpSchemaType.Byte; break;
|
|
2221
|
+
case "I16": fs.Type = DpSchemaType.I16; break;
|
|
2222
|
+
case "I32": fs.Type = DpSchemaType.I32; break;
|
|
2223
|
+
case "I64": fs.Type = DpSchemaType.I64; break;
|
|
2224
|
+
case "Double": fs.Type = DpSchemaType.Double; break;
|
|
2225
|
+
case "Struct": fs.Type = DpSchemaType.Struct; break;
|
|
2226
|
+
case "List": fs.Type = DpSchemaType.List; break;
|
|
2227
|
+
case "Set": fs.Type = DpSchemaType.Set; break;
|
|
2228
|
+
case "Map": fs.Type = DpSchemaType.Map; break;
|
|
2229
|
+
case "Enum": fs.Type = DpSchemaType.Enum; break;
|
|
2230
|
+
default: fs.Type = DpSchemaType.String; break;
|
|
2231
|
+
}
|
|
2232
|
+
return DpTypeNames.SchemaFieldToDataType(fs);
|
|
2233
|
+
}
|
|
2234
|
+
catch { return "string"; }
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
/// <summary>Read a string property from an object via reflection. Used for schema/field descriptor access.</summary>
|
|
2238
|
+
public static string ReflectString(Type t, object obj, string propName)
|
|
2239
|
+
{
|
|
2240
|
+
if (t == null || obj == null) return null;
|
|
2241
|
+
try
|
|
2242
|
+
{
|
|
2243
|
+
var prop = t.GetProperty(propName);
|
|
2244
|
+
return prop?.GetValue(obj)?.ToString();
|
|
2245
|
+
}
|
|
2246
|
+
catch { return null; }
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
#endregion
|
|
2250
|
+
|
|
2251
|
+
private int WriteFieldPathToCol(string fieldPath)
|
|
2252
|
+
{
|
|
2253
|
+
if (_fieldIdToCol.TryGetValue(fieldPath, out int col))
|
|
2254
|
+
return col;
|
|
2255
|
+
return -1;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
private void TrackWriteRow(int row)
|
|
2259
|
+
{
|
|
2260
|
+
if (row > _maxWriteRow) _maxWriteRow = row;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
public void WriteStructBegin(DpRecord s)
|
|
2264
|
+
{
|
|
2265
|
+
if (_writeSheet == null) return;
|
|
2266
|
+
|
|
2267
|
+
if (_writeListStack.Count > 0)
|
|
2268
|
+
{
|
|
2269
|
+
var ls = _writeListStack.Peek();
|
|
2270
|
+
if (ls.StructDepth == 0)
|
|
2271
|
+
{
|
|
2272
|
+
if (ls.SeparatedSheet != null)
|
|
2273
|
+
{
|
|
2274
|
+
int r = ls.StartRow + ls.CurrentElement;
|
|
2275
|
+
ls.SeparatedSheet.SetCellValue(r, 1, ls.SeparatedMetaId ?? "");
|
|
2276
|
+
ls.SeparatedSheet.SetCellValue(r, 2, ls.SeparatedMetaName);
|
|
2277
|
+
_writeRow = r;
|
|
2278
|
+
TrackWriteRow(r);
|
|
2279
|
+
}
|
|
2280
|
+
else
|
|
2281
|
+
{
|
|
2282
|
+
_writeRow = ls.StartRow + ls.CurrentElement;
|
|
2283
|
+
TrackWriteRow(_writeRow);
|
|
2284
|
+
}
|
|
2285
|
+
ls.CurrentElement++;
|
|
2286
|
+
_writePathStack.Push(_writeCurrentPath);
|
|
2287
|
+
_writeCurrentPath = ls.ListPath;
|
|
2288
|
+
}
|
|
2289
|
+
ls.StructDepth++;
|
|
2290
|
+
}
|
|
2291
|
+
// Non-list struct path is already set by WriteFieldBegin (DpWireType.Struct case)
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
public void WriteStructEnd()
|
|
2295
|
+
{
|
|
2296
|
+
if (_writeSheet == null) return;
|
|
2297
|
+
|
|
2298
|
+
if (_writeListStack.Count > 0)
|
|
2299
|
+
{
|
|
2300
|
+
var ls = _writeListStack.Peek();
|
|
2301
|
+
ls.StructDepth--;
|
|
2302
|
+
if (ls.StructDepth == 0)
|
|
2303
|
+
{
|
|
2304
|
+
// Pop the path that WriteStructBegin pushed for this list element
|
|
2305
|
+
if (_writePathStack.Count > 0)
|
|
2306
|
+
_writeCurrentPath = _writePathStack.Pop();
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
else
|
|
2310
|
+
{
|
|
2311
|
+
// Normal struct: WriteFieldBegin pushed the path, pop it here
|
|
2312
|
+
if (_writePathStack.Count > 0)
|
|
2313
|
+
_writeCurrentPath = _writePathStack.Pop();
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
public void WriteFieldBegin(DpColumn f)
|
|
2318
|
+
{
|
|
2319
|
+
if (_writeSheet == null) return;
|
|
2320
|
+
_writeCurrentField = f;
|
|
2321
|
+
|
|
2322
|
+
if (f.Type == DpWireType.Struct)
|
|
2323
|
+
{
|
|
2324
|
+
string fieldPath = string.IsNullOrEmpty(_writeCurrentPath)
|
|
2325
|
+
? f.ID.ToString()
|
|
2326
|
+
: _writeCurrentPath + "." + f.ID;
|
|
2327
|
+
_writePathStack.Push(_writeCurrentPath);
|
|
2328
|
+
_writeCurrentPath = fieldPath;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
public void WriteFieldEnd()
|
|
2333
|
+
{
|
|
2334
|
+
if (_writeSheet == null) return;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
public void WriteFieldStop()
|
|
2338
|
+
{
|
|
2339
|
+
if (_writeSheet == null) return;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
private void WriteCellForCurrentField(string value)
|
|
2343
|
+
{
|
|
2344
|
+
if (_writeSheet == null) return;
|
|
2345
|
+
|
|
2346
|
+
// Primitive inside a primitive list
|
|
2347
|
+
if (_writeListStack.Count > 0)
|
|
2348
|
+
{
|
|
2349
|
+
var ls = _writeListStack.Peek();
|
|
2350
|
+
if (ls.IsPrimitiveList && ls.StructDepth == 0)
|
|
2351
|
+
{
|
|
2352
|
+
if (ls.SeparatedSheet != null)
|
|
2353
|
+
{
|
|
2354
|
+
int r = ls.StartRow + ls.CurrentElement;
|
|
2355
|
+
ls.SeparatedSheet.SetCellValue(r, 1, ls.SeparatedMetaId ?? "");
|
|
2356
|
+
ls.SeparatedSheet.SetCellValue(r, 2, ls.SeparatedMetaName);
|
|
2357
|
+
ls.SeparatedSheet.SetCellValue(r, 3, value);
|
|
2358
|
+
ls.CurrentElement++;
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
else
|
|
2362
|
+
{
|
|
2363
|
+
_writeRow = ls.StartRow + ls.CurrentElement;
|
|
2364
|
+
TrackWriteRow(_writeRow);
|
|
2365
|
+
ls.CurrentElement++;
|
|
2366
|
+
|
|
2367
|
+
if (ls.PrimitiveElemCol > 0)
|
|
2368
|
+
{
|
|
2369
|
+
_writeSheet.SetCellValue(_writeRow, ls.PrimitiveElemCol, value);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
else if (ls.SeparatedSheet != null && ls.StructDepth > 0)
|
|
2375
|
+
{
|
|
2376
|
+
// Full path for the field in the separated sheet (headers use full path)
|
|
2377
|
+
string fullPath = string.IsNullOrEmpty(_writeCurrentPath)
|
|
2378
|
+
? _writeCurrentField.ID.ToString()
|
|
2379
|
+
: _writeCurrentPath + "." + _writeCurrentField.ID;
|
|
2380
|
+
|
|
2381
|
+
// Reuse cached separated protocol, or create one
|
|
2382
|
+
if (_cachedSepProto == null || _cachedSepSheet != ls.SeparatedSheet)
|
|
2383
|
+
{
|
|
2384
|
+
_cachedSepSheet = ls.SeparatedSheet;
|
|
2385
|
+
// 쓰기 시 분리 시트는 항상 v2(3행 헤더)
|
|
2386
|
+
_cachedSepProto = new DpExcelProtocol(ls.SeparatedSheet, FIRST_DATA_ROW, null, "meta", ls.SeparatedSheet?.SheetName ?? "");
|
|
2387
|
+
}
|
|
2388
|
+
int sepCol = _cachedSepProto.WriteFieldPathToCol(fullPath);
|
|
2389
|
+
if (sepCol > 0)
|
|
2390
|
+
{
|
|
2391
|
+
ls.SeparatedSheet.SetCellValue(_writeRow, sepCol, value);
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
string fieldPath = string.IsNullOrEmpty(_writeCurrentPath)
|
|
2398
|
+
? _writeCurrentField.ID.ToString()
|
|
2399
|
+
: _writeCurrentPath + "." + _writeCurrentField.ID;
|
|
2400
|
+
|
|
2401
|
+
int col = WriteFieldPathToCol(fieldPath);
|
|
2402
|
+
if (col > 0)
|
|
2403
|
+
{
|
|
2404
|
+
_writeSheet.SetCellValue(_writeRow, col, value);
|
|
2405
|
+
TrackWriteRow(_writeRow);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
public void WriteBool(bool b)
|
|
2410
|
+
{
|
|
2411
|
+
WriteCellForCurrentField(b ? "1" : "0");
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
public void WriteByte(byte b)
|
|
2415
|
+
{
|
|
2416
|
+
WriteCellForCurrentField(b.ToString());
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
public void WriteI16(short i16)
|
|
2420
|
+
{
|
|
2421
|
+
WriteCellForCurrentField(i16.ToString());
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
public void WriteI32(int i32)
|
|
2425
|
+
{
|
|
2426
|
+
string fieldPath = string.IsNullOrEmpty(_writeCurrentPath)
|
|
2427
|
+
? _writeCurrentField.ID.ToString()
|
|
2428
|
+
: _writeCurrentPath + "." + _writeCurrentField.ID;
|
|
2429
|
+
string value = ResolveEnumDisplayValue(fieldPath, i32);
|
|
2430
|
+
WriteCellForCurrentField(value ?? i32.ToString());
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/// <summary>
|
|
2434
|
+
/// Uses schema _colDataType to decide if field is enum; only then resolves "value:name" via delegates. For paths not in map (e.g. list element), falls back to delegates to detect enum.
|
|
2435
|
+
/// </summary>
|
|
2436
|
+
private string ResolveEnumDisplayValue(string fieldPath, int i32)
|
|
2437
|
+
{
|
|
2438
|
+
object fd = GetFieldDescriptor?.Invoke(fieldPath);
|
|
2439
|
+
bool isEnum = _fieldIdToCol.TryGetValue(fieldPath, out int col)
|
|
2440
|
+
&& _colDataType.TryGetValue(col, out var dt)
|
|
2441
|
+
&& DpTypeNames.IsEnumDataType(dt ?? "");
|
|
2442
|
+
if (!isEnum && fd != null && GetEnumValuesForField != null && GetEnumValuesForField(fd) != null)
|
|
2443
|
+
isEnum = true;
|
|
2444
|
+
if (!isEnum || fd == null || GetEnumValuesForField == null) return null;
|
|
2445
|
+
var list = GetEnumValuesForField(fd);
|
|
2446
|
+
if (list == null || list.Count == 0) return null;
|
|
2447
|
+
foreach (var line in list)
|
|
2448
|
+
{
|
|
2449
|
+
var parts = line.Split(new[] { ':' }, 2);
|
|
2450
|
+
if (parts.Length >= 1 && int.TryParse(parts[0].Trim(), out int v) && v == i32)
|
|
2451
|
+
return line;
|
|
2452
|
+
}
|
|
2453
|
+
return i32 == 0 ? "0:MIA" : $"{i32}:NIA";
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
public void WriteI64(long i64)
|
|
2457
|
+
{
|
|
2458
|
+
WriteCellForCurrentField(i64.ToString());
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
public void WriteDouble(double d)
|
|
2462
|
+
{
|
|
2463
|
+
WriteCellForCurrentField(d.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
public void WriteString(string s)
|
|
2467
|
+
{
|
|
2468
|
+
WriteCellForCurrentField(s ?? "");
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
public void WriteBinary(byte[] b)
|
|
2472
|
+
{
|
|
2473
|
+
WriteCellForCurrentField(b != null ? Convert.ToBase64String(b) : "");
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
public void WriteListBegin(DpList list)
|
|
2477
|
+
{
|
|
2478
|
+
if (_writeSheet == null) return;
|
|
2479
|
+
|
|
2480
|
+
string fieldPath = string.IsNullOrEmpty(_writeCurrentPath)
|
|
2481
|
+
? _writeCurrentField.ID.ToString()
|
|
2482
|
+
: _writeCurrentPath + "." + _writeCurrentField.ID;
|
|
2483
|
+
|
|
2484
|
+
// ── Separated sheet mode (정책에서 kind·시트 이름 사용) ─────────────────────
|
|
2485
|
+
if (_resolver != null)
|
|
2486
|
+
{
|
|
2487
|
+
string fieldName = "";
|
|
2488
|
+
if (_fieldIdToCol.TryGetValue(fieldPath, out int rootCol2) && _colColumnName.ContainsKey(rootCol2))
|
|
2489
|
+
fieldName = _colColumnName[rootCol2];
|
|
2490
|
+
|
|
2491
|
+
string kind = ContainerSheetPolicy.GetKindForWireType(DpWireType.List);
|
|
2492
|
+
string sheetName = ContainerSheetNaming.FormatContainerSheetName(fieldPath, kind, fieldName);
|
|
2493
|
+
|
|
2494
|
+
string[] hdrIds = null, hdrTypes = null, hdrNames = null, hdrStructNames = null;
|
|
2495
|
+
bool[] hdrIsMarker = null;
|
|
2496
|
+
BuildContainerSheetHeaders(fieldPath, list.ElementType, out hdrIds, out hdrTypes, out hdrNames, out hdrStructNames, out hdrIsMarker);
|
|
2497
|
+
|
|
2498
|
+
var sepWriteSheet = _resolver.GetOrCreateSheet(
|
|
2499
|
+
sheetName,
|
|
2500
|
+
headerHierarchyIds: hdrIds,
|
|
2501
|
+
headerDataTypes: hdrTypes,
|
|
2502
|
+
headerColumnNames: hdrNames,
|
|
2503
|
+
headerStructNames: _useCompactHeader ? hdrStructNames : null);
|
|
2504
|
+
|
|
2505
|
+
if (sepWriteSheet != null)
|
|
2506
|
+
{
|
|
2507
|
+
// Next append row: use tracked last row per sheet so multiple records append correctly (UsedRange/LastRow can be stale after NumberFormat or COM).
|
|
2508
|
+
int appendRow = _containerSheetLastRow.TryGetValue(sheetName, out int lastRow) ? lastRow + 1 : FIRST_DATA_ROW;
|
|
2509
|
+
|
|
2510
|
+
// Get tuid/name from the write sheet (already written by preceding fields)
|
|
2511
|
+
string metaId = _writeSheet.CellValue(_writeRow, 1) ?? "";
|
|
2512
|
+
string metaName = _writeSheet.CellValue(_writeRow, 2) ?? "";
|
|
2513
|
+
|
|
2514
|
+
// Write list root column on main sheet: "[N >]" 표시 (마우스 클릭 시 리스트 탭으로 이동)
|
|
2515
|
+
int rootColMain = WriteFieldPathToCol(fieldPath);
|
|
2516
|
+
if (rootColMain > 0)
|
|
2517
|
+
_writeSheet.SetCellValue(_writeRow, rootColMain, "[" + list.Count + " >]");
|
|
2518
|
+
|
|
2519
|
+
bool isPrimitive = (list.ElementType != DpWireType.Struct
|
|
2520
|
+
&& list.ElementType != DpWireType.List
|
|
2521
|
+
&& list.ElementType != DpWireType.Map
|
|
2522
|
+
&& list.ElementType != DpWireType.Set);
|
|
2523
|
+
int primitiveElemCol = isPrimitive ? 3 : -1; // col 3 = first element col (after tuid, name)
|
|
2524
|
+
|
|
2525
|
+
var state = new WriteListState
|
|
2526
|
+
{
|
|
2527
|
+
StartRow = appendRow,
|
|
2528
|
+
CurrentElement = 0,
|
|
2529
|
+
ListPath = fieldPath,
|
|
2530
|
+
StructDepth = 0,
|
|
2531
|
+
IsPrimitiveList = isPrimitive,
|
|
2532
|
+
PrimitiveElemCol = primitiveElemCol,
|
|
2533
|
+
SeparatedSheet = sepWriteSheet,
|
|
2534
|
+
SeparatedSheetName = sheetName,
|
|
2535
|
+
SeparatedMetaId = metaId,
|
|
2536
|
+
SeparatedMetaName = metaName,
|
|
2537
|
+
MainSheetRow = _writeRow,
|
|
2538
|
+
MainSheetMaxRow = _maxWriteRow
|
|
2539
|
+
};
|
|
2540
|
+
_maxWriteRow = appendRow - 1;
|
|
2541
|
+
_writeListStack.Push(state);
|
|
2542
|
+
_writePathStack.Push(_writeCurrentPath);
|
|
2543
|
+
_writeCurrentPath = fieldPath;
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
// ── Embedded (original) mode ──────────────────────────────────────
|
|
2549
|
+
bool isPrimitive2 = (list.ElementType != DpWireType.Struct
|
|
2550
|
+
&& list.ElementType != DpWireType.List
|
|
2551
|
+
&& list.ElementType != DpWireType.Map
|
|
2552
|
+
&& list.ElementType != DpWireType.Set);
|
|
2553
|
+
int primitiveElemCol2 = -1;
|
|
2554
|
+
if (isPrimitive2)
|
|
2555
|
+
{
|
|
2556
|
+
string prefix = fieldPath + ".";
|
|
2557
|
+
foreach (var kvp in _fieldIdToCol)
|
|
2558
|
+
{
|
|
2559
|
+
if (kvp.Key.StartsWith(prefix))
|
|
2560
|
+
{
|
|
2561
|
+
primitiveElemCol2 = kvp.Value;
|
|
2562
|
+
break;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// Write list root column: record count
|
|
2568
|
+
int rootCol = WriteFieldPathToCol(fieldPath);
|
|
2569
|
+
if (rootCol > 0 && list.Count > 0)
|
|
2570
|
+
_writeSheet.SetCellValue(_writeRow, rootCol, list.Count.ToString());
|
|
2571
|
+
|
|
2572
|
+
var embState = new WriteListState
|
|
2573
|
+
{
|
|
2574
|
+
StartRow = _writeRow,
|
|
2575
|
+
CurrentElement = 0,
|
|
2576
|
+
ListPath = fieldPath,
|
|
2577
|
+
StructDepth = 0,
|
|
2578
|
+
IsPrimitiveList = isPrimitive2,
|
|
2579
|
+
PrimitiveElemCol = primitiveElemCol2
|
|
2580
|
+
};
|
|
2581
|
+
_writeListStack.Push(embState);
|
|
2582
|
+
|
|
2583
|
+
_writePathStack.Push(_writeCurrentPath);
|
|
2584
|
+
_writeCurrentPath = fieldPath;
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
public void WriteListEnd()
|
|
2588
|
+
{
|
|
2589
|
+
if (_writeSheet == null) return;
|
|
2590
|
+
|
|
2591
|
+
if (_writeListStack.Count > 0)
|
|
2592
|
+
{
|
|
2593
|
+
var state = _writeListStack.Pop();
|
|
2594
|
+
if (state.SeparatedSheet != null)
|
|
2595
|
+
{
|
|
2596
|
+
if (state.CurrentElement > 0)
|
|
2597
|
+
{
|
|
2598
|
+
int lastRow = state.StartRow + state.CurrentElement - 1;
|
|
2599
|
+
_containerSheetsWritten.Add((state.SeparatedSheetName, state.StartRow, lastRow, state.SeparatedSheet.LastColumn));
|
|
2600
|
+
_containerSheetLastRow[state.SeparatedSheetName] = lastRow;
|
|
2601
|
+
}
|
|
2602
|
+
_writeRow = state.MainSheetRow;
|
|
2603
|
+
_maxWriteRow = state.MainSheetMaxRow;
|
|
2604
|
+
}
|
|
2605
|
+
else
|
|
2606
|
+
{
|
|
2607
|
+
// Embedded: advance _writeRow past rows consumed by this list
|
|
2608
|
+
int lastListRow = state.StartRow + state.CurrentElement - 1;
|
|
2609
|
+
if (lastListRow > _maxWriteRow) _maxWriteRow = lastListRow;
|
|
2610
|
+
_writeRow = lastListRow > state.StartRow ? lastListRow : state.StartRow;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
if (_writePathStack.Count > 0)
|
|
2614
|
+
_writeCurrentPath = _writePathStack.Pop();
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
public void WriteSetBegin(DpSet set)
|
|
2618
|
+
{
|
|
2619
|
+
WriteListBegin(new DpList { ElementType = set.ElementType, Count = set.Count });
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
public void WriteSetEnd() { WriteListEnd(); }
|
|
2623
|
+
|
|
2624
|
+
public void WriteMapBegin(DpDict map)
|
|
2625
|
+
{
|
|
2626
|
+
if (_writeSheet == null) return;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
public void WriteMapEnd()
|
|
2630
|
+
{
|
|
2631
|
+
if (_writeSheet == null) return;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
/// <summary>
|
|
2635
|
+
/// Walk _writeSchema (and nested schemas via ResolveTypeName) to find the element TypeName
|
|
2636
|
+
/// for a list/set field at the given fieldPath (e.g. "30" → spawners → "List<Spawner>" → "Spawner").
|
|
2637
|
+
/// Returns null if schema is not available or field not found.
|
|
2638
|
+
/// </summary>
|
|
2639
|
+
private string ResolveElementTypeNameFromSchema(string fieldPath)
|
|
2640
|
+
{
|
|
2641
|
+
if (_writeSchema == null || _writeSchema.Fields == null || string.IsNullOrEmpty(fieldPath))
|
|
2642
|
+
return null;
|
|
2643
|
+
|
|
2644
|
+
string[] parts = fieldPath.Split('.');
|
|
2645
|
+
DpSchema current = _writeSchema;
|
|
2646
|
+
|
|
2647
|
+
for (int i = 0; i < parts.Length; i++)
|
|
2648
|
+
{
|
|
2649
|
+
if (current?.Fields == null) return null;
|
|
2650
|
+
if (!int.TryParse(parts[i], out int fid)) return null;
|
|
2651
|
+
if (!current.Fields.TryGetValue(fid, out var field)) return null;
|
|
2652
|
+
|
|
2653
|
+
if (i == parts.Length - 1)
|
|
2654
|
+
{
|
|
2655
|
+
// This is the target list/set field - strip outer generic to get element type
|
|
2656
|
+
return DpTypeNames.StripOuterGeneric(field.TypeName ?? "");
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// Intermediate field: must be struct or list<struct> - resolve to descend
|
|
2660
|
+
string innerType = field.Type == DpSchemaType.Struct
|
|
2661
|
+
? field.TypeName
|
|
2662
|
+
: DpTypeNames.StripOuterGeneric(field.TypeName ?? "");
|
|
2663
|
+
if (string.IsNullOrEmpty(innerType) || ResolveTypeName == null) return null;
|
|
2664
|
+
current = ResolveTypeName(innerType);
|
|
2665
|
+
}
|
|
2666
|
+
return null;
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
#region Excel header arrays (shared main / container)
|
|
2670
|
+
|
|
2671
|
+
/// <summary>Container 시트 상단 2열 고정 (meta_id=[< meta_id] 표현, name). nav 제거.</summary>
|
|
2672
|
+
private static void GetContainerFixedHeaderArrays(
|
|
2673
|
+
out string[] hierIds, out string[] dataTypes, out string[] colNames, out string[] structNames, out bool[] isMarker)
|
|
2674
|
+
{
|
|
2675
|
+
hierIds = new[] { ContainerSheetNaming.META_ID_COLUMN, ContainerSheetNaming.META_NAME_COLUMN };
|
|
2676
|
+
dataTypes = new[] { DpTypeNames.ToProtocolName(DpWireType.I64), DpTypeNames.ToProtocolName(DpWireType.String) };
|
|
2677
|
+
colNames = new[] { "meta_id", "name" };
|
|
2678
|
+
structNames = new[] { "", "" };
|
|
2679
|
+
isMarker = new[] { false, false };
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
/// <summary>리스트 시트 meta_id 셀 값 "[< id]"에서 id 추출. 매칭/이동 시 사용.</summary>
|
|
2683
|
+
public static string ParseMetaIdFromListCell(string cellValue)
|
|
2684
|
+
{
|
|
2685
|
+
if (string.IsNullOrEmpty(cellValue)) return cellValue;
|
|
2686
|
+
var s = cellValue.Trim();
|
|
2687
|
+
if (s.StartsWith("[< ", StringComparison.Ordinal) && s.EndsWith("]", StringComparison.Ordinal) && s.Length > 4)
|
|
2688
|
+
return s.Substring(3, s.Length - 4).Trim();
|
|
2689
|
+
return s;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
/// <summary>FlatHeaderField 리스트 → 헤더 배열로 변환. pathPrefix 있으면 HierarchyId 앞에 붙임 (리스트 요소 열용).</summary>
|
|
2693
|
+
private static void FlatFieldsToHeaderArrays(IReadOnlyList<FlatHeaderField> fields, string pathPrefix,
|
|
2694
|
+
out string[] hierIds, out string[] dataTypes, out string[] colNames, out string[] structNames, out bool[] isMarker)
|
|
2695
|
+
{
|
|
2696
|
+
int n = fields?.Count ?? 0;
|
|
2697
|
+
hierIds = new string[n];
|
|
2698
|
+
dataTypes = new string[n];
|
|
2699
|
+
colNames = new string[n];
|
|
2700
|
+
structNames = new string[n];
|
|
2701
|
+
isMarker = new bool[n];
|
|
2702
|
+
string prefix = pathPrefix ?? "";
|
|
2703
|
+
for (int i = 0; i < n; i++)
|
|
2704
|
+
{
|
|
2705
|
+
var f = fields[i];
|
|
2706
|
+
hierIds[i] = string.IsNullOrEmpty(prefix) ? f.HierarchyId : (prefix + f.HierarchyId);
|
|
2707
|
+
dataTypes[i] = f.DataType ?? "";
|
|
2708
|
+
colNames[i] = (string.IsNullOrEmpty(f.ColumnName) && f.IsStructMarker ? (f.ParentStructName ?? f.HierarchyId) : f.ColumnName) ?? "";
|
|
2709
|
+
structNames[i] = f.ParentStructName ?? "";
|
|
2710
|
+
isMarker[i] = f.IsStructMarker;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
/// <summary>헤더 배열 5개 → List<FlatHeaderField>. 스타일 적용용. null 배열은 길이 0으로 취급.</summary>
|
|
2715
|
+
private static List<FlatHeaderField> HeaderArraysToFlatFields(
|
|
2716
|
+
string[] hierIds, string[] dataTypes, string[] colNames, string[] structNames, bool[] isMarker)
|
|
2717
|
+
{
|
|
2718
|
+
int n = hierIds?.Length ?? 0;
|
|
2719
|
+
var list = new List<FlatHeaderField>(n);
|
|
2720
|
+
for (int i = 0; i < n; i++)
|
|
2721
|
+
{
|
|
2722
|
+
list.Add(new FlatHeaderField
|
|
2723
|
+
{
|
|
2724
|
+
HierarchyId = i < hierIds.Length ? hierIds[i] : "",
|
|
2725
|
+
DataType = dataTypes != null && i < dataTypes.Length ? dataTypes[i] : "",
|
|
2726
|
+
ColumnName = colNames != null && i < colNames.Length ? colNames[i] : "",
|
|
2727
|
+
ParentStructName = structNames != null && i < structNames.Length ? structNames[i] : "",
|
|
2728
|
+
IsStructMarker = isMarker != null && i < isMarker.Length && isMarker[i]
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
return list;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
#endregion
|
|
2735
|
+
|
|
2736
|
+
/// <summary>
|
|
2737
|
+
/// Build header arrays (meta_id, name, element columns) for a container sheet.
|
|
2738
|
+
/// Same rule as main sheet: include all FlattenSchema output (do not filter struct markers)
|
|
2739
|
+
/// so schema/read mapping stays valid. Uses ResolveTypeName to look up element schema.
|
|
2740
|
+
/// Returns null arrays if element schema is not resolvable (primitive list gets fixed headers).
|
|
2741
|
+
/// </summary>
|
|
2742
|
+
private void BuildContainerSheetHeaders(string fieldPath, DpWireType elementType,
|
|
2743
|
+
out string[] hierIds, out string[] dataTypes, out string[] colNames, out string[] structNames, out bool[] isMarker)
|
|
2744
|
+
{
|
|
2745
|
+
hierIds = null; dataTypes = null; colNames = null; structNames = null; isMarker = null;
|
|
2746
|
+
|
|
2747
|
+
bool isPrimitive = (elementType != DpWireType.Struct && elementType != DpWireType.List
|
|
2748
|
+
&& elementType != DpWireType.Map && elementType != DpWireType.Set);
|
|
2749
|
+
if (isPrimitive)
|
|
2750
|
+
{
|
|
2751
|
+
GetContainerFixedHeaderArrays(out string[] fixHier, out string[] fixTypes, out string[] fixCols, out string[] fixStruct, out bool[] fixMark);
|
|
2752
|
+
hierIds = new string[3];
|
|
2753
|
+
dataTypes = new string[3];
|
|
2754
|
+
colNames = new string[3];
|
|
2755
|
+
structNames = new string[3];
|
|
2756
|
+
isMarker = new bool[3];
|
|
2757
|
+
Array.Copy(fixHier, hierIds, 2);
|
|
2758
|
+
Array.Copy(fixTypes, dataTypes, 2);
|
|
2759
|
+
Array.Copy(fixCols, colNames, 2);
|
|
2760
|
+
Array.Copy(fixStruct, structNames, 2);
|
|
2761
|
+
Array.Copy(fixMark, isMarker, 2);
|
|
2762
|
+
hierIds[2] = "value";
|
|
2763
|
+
dataTypes[2] = DpTypeNames.ToProtocolName(elementType);
|
|
2764
|
+
colNames[2] = "value";
|
|
2765
|
+
structNames[2] = "";
|
|
2766
|
+
isMarker[2] = false;
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
if (elementType == DpWireType.Struct && ResolveTypeName != null)
|
|
2771
|
+
{
|
|
2772
|
+
string typeName = ResolveElementTypeNameFromSchema(fieldPath);
|
|
2773
|
+
if (string.IsNullOrEmpty(typeName))
|
|
2774
|
+
{
|
|
2775
|
+
if (_fieldIdToCol.TryGetValue(fieldPath, out int rootCol) && _colDataType.ContainsKey(rootCol))
|
|
2776
|
+
{
|
|
2777
|
+
string dt = _colDataType[rootCol];
|
|
2778
|
+
typeName = DpTypeNames.StripOuterGeneric(dt);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
if (string.IsNullOrEmpty(typeName)) return;
|
|
2782
|
+
|
|
2783
|
+
var elemSchema = ResolveTypeName(typeName);
|
|
2784
|
+
if (elemSchema == null) return;
|
|
2785
|
+
|
|
2786
|
+
// Same as main sheet: full FlattenSchema (no filtering of struct markers).
|
|
2787
|
+
var elemFields = FlattenSchema(elemSchema, ResolveTypeName, "");
|
|
2788
|
+
GetContainerFixedHeaderArrays(out string[] fixHier, out string[] fixTypes, out string[] fixCols, out string[] fixStruct, out bool[] fixMark);
|
|
2789
|
+
FlatFieldsToHeaderArrays(elemFields, fieldPath + ".", out string[] elemHier, out string[] elemTypes, out string[] elemCols, out string[] elemStruct, out bool[] elemMark);
|
|
2790
|
+
|
|
2791
|
+
int total = fixHier.Length + elemHier.Length;
|
|
2792
|
+
hierIds = new string[total];
|
|
2793
|
+
dataTypes = new string[total];
|
|
2794
|
+
colNames = new string[total];
|
|
2795
|
+
structNames = new string[total];
|
|
2796
|
+
isMarker = new bool[total];
|
|
2797
|
+
Array.Copy(fixHier, hierIds, fixHier.Length);
|
|
2798
|
+
Array.Copy(fixTypes, dataTypes, fixTypes.Length);
|
|
2799
|
+
Array.Copy(fixCols, colNames, fixCols.Length);
|
|
2800
|
+
Array.Copy(fixStruct, structNames, fixStruct.Length);
|
|
2801
|
+
Array.Copy(fixMark, isMarker, fixMark.Length);
|
|
2802
|
+
Array.Copy(elemHier, 0, hierIds, fixHier.Length, elemHier.Length);
|
|
2803
|
+
Array.Copy(elemTypes, 0, dataTypes, fixTypes.Length, elemTypes.Length);
|
|
2804
|
+
Array.Copy(elemCols, 0, colNames, fixCols.Length, elemCols.Length);
|
|
2805
|
+
Array.Copy(elemStruct, 0, structNames, fixStruct.Length, elemStruct.Length);
|
|
2806
|
+
Array.Copy(elemMark, 0, isMarker, fixMark.Length, elemMark.Length);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
#endregion
|
|
2811
|
+
|
|
2812
|
+
#region Schema → Flat Header
|
|
2813
|
+
|
|
2814
|
+
/// <summary>
|
|
2815
|
+
/// 각 FlatHeaderField의 ColumnName을 getDisplayNameForHierarchyId(HierarchyId) 결과로 덮어씀.
|
|
2816
|
+
/// Excel에서 읽은 헤더는 리스트 내부 등에서 부모명이 들어올 수 있으므로, 스키마 필드명으로 보정할 때 사용.
|
|
2817
|
+
/// </summary>
|
|
2818
|
+
public static void ApplyDisplayNames(List<FlatHeaderField> fields, Func<string, string> getDisplayNameForHierarchyId)
|
|
2819
|
+
{
|
|
2820
|
+
if (fields == null || getDisplayNameForHierarchyId == null) return;
|
|
2821
|
+
foreach (var f in fields)
|
|
2822
|
+
{
|
|
2823
|
+
if (string.IsNullOrEmpty(f?.HierarchyId)) continue;
|
|
2824
|
+
string name = getDisplayNameForHierarchyId(f.HierarchyId);
|
|
2825
|
+
if (!string.IsNullOrEmpty(name))
|
|
2826
|
+
f.ColumnName = name;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
/// <summary>
|
|
2831
|
+
/// Reorder flat fields so tuid, tid, name (by ColumnName) come first, then the rest.
|
|
2832
|
+
/// Used for meta_data main sheet column fix.
|
|
2833
|
+
/// </summary>
|
|
2834
|
+
public static List<FlatHeaderField> ReorderFlatWithMetaIdTidMetaNameFirst(List<FlatHeaderField> flat)
|
|
2835
|
+
{
|
|
2836
|
+
return ReorderFlatWithMetaIdTidMetaNameFirst(flat, null, null);
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
/// <summary>
|
|
2840
|
+
/// Reorder flat fields by key→name→note. category와 getKeyFieldNames가 있으면 해당 카테고리 고정 열 순서 사용(keyed면 meta_id 제외).
|
|
2841
|
+
/// </summary>
|
|
2842
|
+
public static List<FlatHeaderField> ReorderFlatWithMetaIdTidMetaNameFirst(List<FlatHeaderField> flat, string category, Func<string, IReadOnlyList<string>> getKeyFieldNames)
|
|
2843
|
+
{
|
|
2844
|
+
if (flat == null || flat.Count == 0) return flat;
|
|
2845
|
+
var fixedNames = (category != null && getKeyFieldNames != null)
|
|
2846
|
+
? GetMainSheetFixedColumnNamesForCategory(category, getKeyFieldNames)
|
|
2847
|
+
: MainSheetFixedColumnNames;
|
|
2848
|
+
var reordered = new List<FlatHeaderField>(flat.Count);
|
|
2849
|
+
foreach (string name in fixedNames)
|
|
2850
|
+
{
|
|
2851
|
+
for (int i = 0; i < flat.Count; i++)
|
|
2852
|
+
{
|
|
2853
|
+
string cn = flat[i].ColumnName ?? "";
|
|
2854
|
+
if (string.Equals(cn, name, StringComparison.OrdinalIgnoreCase) ||
|
|
2855
|
+
(name == "tuid" && string.Equals(cn, "meta_id", StringComparison.OrdinalIgnoreCase)) ||
|
|
2856
|
+
(name == "name" && string.Equals(cn, "meta_name", StringComparison.OrdinalIgnoreCase)) ||
|
|
2857
|
+
(name == "note" && string.Equals(cn, "meta_note", StringComparison.OrdinalIgnoreCase)))
|
|
2858
|
+
{
|
|
2859
|
+
reordered.Add(flat[i]);
|
|
2860
|
+
break;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
for (int i = 0; i < flat.Count; i++)
|
|
2865
|
+
{
|
|
2866
|
+
string cn = flat[i].ColumnName ?? "";
|
|
2867
|
+
bool isFixed = false;
|
|
2868
|
+
foreach (string name in fixedNames)
|
|
2869
|
+
{
|
|
2870
|
+
if (string.Equals(cn, name, StringComparison.OrdinalIgnoreCase) ||
|
|
2871
|
+
(name == "tuid" && string.Equals(cn, "meta_id", StringComparison.OrdinalIgnoreCase)) ||
|
|
2872
|
+
(name == "name" && string.Equals(cn, "meta_name", StringComparison.OrdinalIgnoreCase)) ||
|
|
2873
|
+
(name == "note" && string.Equals(cn, "meta_note", StringComparison.OrdinalIgnoreCase)))
|
|
2874
|
+
{ isFixed = true; break; }
|
|
2875
|
+
}
|
|
2876
|
+
if (!isFixed) reordered.Add(flat[i]);
|
|
2877
|
+
}
|
|
2878
|
+
return reordered;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
/// <summary>Excel 스키마 정보용 DataType. typedef이고 TypeName에 _link 포함인 경우만 typedef 이름 사용.</summary>
|
|
2882
|
+
private static string GetExcelDataTypeForField(DpFieldSchema f)
|
|
2883
|
+
{
|
|
2884
|
+
if (f == null) return "string";
|
|
2885
|
+
string tn = (f.TypeName ?? "").Trim();
|
|
2886
|
+
if (tn.IndexOf("_link", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
2887
|
+
return DpTypeNames.StripNamespaceFromTypeName(tn);
|
|
2888
|
+
return DpTypeNames.SchemaFieldToDataType(f);
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
/// <summary>
|
|
2892
|
+
/// DpSchema를 Excel 헤더용 flat 리스트로 변환.
|
|
2893
|
+
/// Struct/List<Struct> 필드는 재귀적으로 하위 필드를 펼침.
|
|
2894
|
+
/// resolveTypeName: TypeName → 하위 DpSchema 조회 콜백 (DefineLoader.GetSchemaByTypeName 등).
|
|
2895
|
+
/// mainSheetOnly: true이면 list/set/map 필드는 루트 엔트리 1개만 반환 (분리 시트 모드 메인용).
|
|
2896
|
+
/// </summary>
|
|
2897
|
+
public static List<FlatHeaderField> FlattenSchema(
|
|
2898
|
+
DpSchema schema,
|
|
2899
|
+
Func<string, DpSchema> resolveTypeName,
|
|
2900
|
+
string parentPath = "",
|
|
2901
|
+
bool mainSheetOnly = false,
|
|
2902
|
+
string parentStructName = null)
|
|
2903
|
+
{
|
|
2904
|
+
var result = new List<FlatHeaderField>();
|
|
2905
|
+
if (schema?.Fields == null) return result;
|
|
2906
|
+
|
|
2907
|
+
var ordered = new List<KeyValuePair<int, DpFieldSchema>>(schema.Fields);
|
|
2908
|
+
ordered.Sort((a, b) => a.Value.Order.CompareTo(b.Value.Order));
|
|
2909
|
+
|
|
2910
|
+
foreach (var kv in ordered)
|
|
2911
|
+
{
|
|
2912
|
+
int fieldId = kv.Key;
|
|
2913
|
+
var f = kv.Value;
|
|
2914
|
+
string path = string.IsNullOrEmpty(parentPath)
|
|
2915
|
+
? fieldId.ToString()
|
|
2916
|
+
: parentPath + "." + fieldId;
|
|
2917
|
+
|
|
2918
|
+
bool isContainer = (f.Type == DpSchemaType.List || f.Type == DpSchemaType.Set || f.Type == DpSchemaType.Map);
|
|
2919
|
+
|
|
2920
|
+
if (f.Type == DpSchemaType.Struct && resolveTypeName != null)
|
|
2921
|
+
{
|
|
2922
|
+
var nested = resolveTypeName(f.TypeName);
|
|
2923
|
+
if (nested?.Fields != null)
|
|
2924
|
+
{
|
|
2925
|
+
result.Add(new FlatHeaderField
|
|
2926
|
+
{
|
|
2927
|
+
HierarchyId = path,
|
|
2928
|
+
DataType = f.TypeName ?? "record",
|
|
2929
|
+
ColumnName = f.Name ?? "",
|
|
2930
|
+
DocComment = f.DocComment ?? "",
|
|
2931
|
+
IsStructMarker = true,
|
|
2932
|
+
ParentStructName = parentStructName ?? ""
|
|
2933
|
+
});
|
|
2934
|
+
string sn = !string.IsNullOrEmpty(f.TypeName) ? DpTypeNames.StripNamespaceFromTypeName(f.TypeName) : "";
|
|
2935
|
+
string childStructName = !string.IsNullOrEmpty(sn) ? sn : (f.Name ?? "");
|
|
2936
|
+
result.AddRange(FlattenSchema(nested, resolveTypeName, path, mainSheetOnly, childStructName));
|
|
2937
|
+
}
|
|
2938
|
+
else
|
|
2939
|
+
result.Add(new FlatHeaderField
|
|
2940
|
+
{
|
|
2941
|
+
HierarchyId = path,
|
|
2942
|
+
DataType = "record",
|
|
2943
|
+
ColumnName = f.Name ?? "",
|
|
2944
|
+
DocComment = f.DocComment ?? "",
|
|
2945
|
+
ParentStructName = parentStructName ?? ""
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
else if (isContainer && mainSheetOnly)
|
|
2949
|
+
{
|
|
2950
|
+
result.Add(new FlatHeaderField
|
|
2951
|
+
{
|
|
2952
|
+
HierarchyId = path,
|
|
2953
|
+
DataType = GetExcelDataTypeForField(f),
|
|
2954
|
+
ColumnName = f.Name ?? "",
|
|
2955
|
+
DocComment = f.DocComment ?? "",
|
|
2956
|
+
ParentStructName = parentStructName ?? ""
|
|
2957
|
+
});
|
|
2958
|
+
}
|
|
2959
|
+
else if ((f.Type == DpSchemaType.List || f.Type == DpSchemaType.Set) && resolveTypeName != null)
|
|
2960
|
+
{
|
|
2961
|
+
string innerTypeName = DpTypeNames.StripOuterGeneric(f.TypeName ?? "");
|
|
2962
|
+
var elemSchema = resolveTypeName(innerTypeName);
|
|
2963
|
+
if (elemSchema?.Fields != null)
|
|
2964
|
+
{
|
|
2965
|
+
result.Add(new FlatHeaderField
|
|
2966
|
+
{
|
|
2967
|
+
HierarchyId = path,
|
|
2968
|
+
DataType = DpTypeNames.ToProtocolName(
|
|
2969
|
+
DpTypeNames.FromSchemaTypeName(f.Type.ToString())),
|
|
2970
|
+
ColumnName = f.Name ?? "",
|
|
2971
|
+
DocComment = f.DocComment ?? "",
|
|
2972
|
+
ParentStructName = parentStructName ?? ""
|
|
2973
|
+
});
|
|
2974
|
+
result.AddRange(FlattenSchema(elemSchema, resolveTypeName, path, mainSheetOnly, parentStructName));
|
|
2975
|
+
}
|
|
2976
|
+
else
|
|
2977
|
+
{
|
|
2978
|
+
result.Add(new FlatHeaderField
|
|
2979
|
+
{
|
|
2980
|
+
HierarchyId = path,
|
|
2981
|
+
DataType = GetExcelDataTypeForField(f),
|
|
2982
|
+
ColumnName = f.Name ?? "",
|
|
2983
|
+
DocComment = f.DocComment ?? "",
|
|
2984
|
+
ParentStructName = parentStructName ?? ""
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
else
|
|
2989
|
+
{
|
|
2990
|
+
result.Add(new FlatHeaderField
|
|
2991
|
+
{
|
|
2992
|
+
HierarchyId = path,
|
|
2993
|
+
DataType = GetExcelDataTypeForField(f),
|
|
2994
|
+
ColumnName = f.Name ?? "",
|
|
2995
|
+
DocComment = f.DocComment ?? "",
|
|
2996
|
+
ParentStructName = parentStructName ?? ""
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
return result;
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
#endregion
|
|
3004
|
+
}
|
|
3005
|
+
}
|