@simplysm/excel 13.0.0-beta.45 → 13.0.0-beta.47

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.
Files changed (37) hide show
  1. package/dist/excel-cell.js.map +0 -1
  2. package/dist/excel-col.js.map +0 -1
  3. package/dist/excel-row.js.map +0 -1
  4. package/dist/excel-workbook.js.map +0 -1
  5. package/dist/excel-worksheet.js.map +0 -1
  6. package/dist/excel-wrapper.js.map +0 -1
  7. package/dist/index.js.map +0 -1
  8. package/dist/types.js.map +0 -1
  9. package/dist/utils/excel-utils.js.map +0 -1
  10. package/dist/utils/zip-cache.js.map +0 -1
  11. package/dist/xml/excel-xml-content-type.js.map +0 -1
  12. package/dist/xml/excel-xml-drawing.js.map +0 -1
  13. package/dist/xml/excel-xml-relationship.js.map +0 -1
  14. package/dist/xml/excel-xml-shared-string.js.map +0 -1
  15. package/dist/xml/excel-xml-style.js.map +0 -1
  16. package/dist/xml/excel-xml-unknown.js.map +0 -1
  17. package/dist/xml/excel-xml-workbook.js.map +0 -1
  18. package/dist/xml/excel-xml-worksheet.js.map +0 -1
  19. package/package.json +4 -3
  20. package/src/excel-cell.ts +326 -0
  21. package/src/excel-col.ts +43 -0
  22. package/src/excel-row.ts +37 -0
  23. package/src/excel-workbook.ts +206 -0
  24. package/src/excel-worksheet.ts +380 -0
  25. package/src/excel-wrapper.ts +219 -0
  26. package/src/index.ts +13 -0
  27. package/src/types.ts +396 -0
  28. package/src/utils/excel-utils.ts +201 -0
  29. package/src/utils/zip-cache.ts +103 -0
  30. package/src/xml/excel-xml-content-type.ts +64 -0
  31. package/src/xml/excel-xml-drawing.ts +87 -0
  32. package/src/xml/excel-xml-relationship.ts +86 -0
  33. package/src/xml/excel-xml-shared-string.ts +80 -0
  34. package/src/xml/excel-xml-style.ts +393 -0
  35. package/src/xml/excel-xml-unknown.ts +11 -0
  36. package/src/xml/excel-xml-workbook.ts +112 -0
  37. package/src/xml/excel-xml-worksheet.ts +544 -0
@@ -0,0 +1,11 @@
1
+ import type { ExcelXml } from "../types";
2
+
3
+ /**
4
+ * 알 수 없는 형식의 Excel XML 데이터를 보존하는 클래스.
5
+ * 원본 데이터를 손실 없이 유지한다.
6
+ */
7
+ export class ExcelXmlUnknown implements ExcelXml {
8
+ constructor(public readonly data: Record<string, unknown>) {}
9
+
10
+ cleanup(): void {}
11
+ }
@@ -0,0 +1,112 @@
1
+ import "@simplysm/core-common";
2
+ import { numParseInt } from "@simplysm/core-common";
3
+ import type { ExcelXml, ExcelXmlWorkbookData } from "../types";
4
+
5
+ /**
6
+ * xl/workbook.xml 파일을 관리하는 클래스.
7
+ * 워크시트 목록과 관계 ID를 처리한다.
8
+ */
9
+ export class ExcelXmlWorkbook implements ExcelXml {
10
+ data: ExcelXmlWorkbookData;
11
+
12
+ constructor(data?: ExcelXmlWorkbookData) {
13
+ if (data === undefined) {
14
+ this.data = {
15
+ workbook: {
16
+ $: {
17
+ "xmlns": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
18
+ "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
19
+ },
20
+ },
21
+ };
22
+ } else {
23
+ this.data = data;
24
+ }
25
+ }
26
+
27
+ get lastWsRelId(): number | undefined {
28
+ const sheets = this.data.workbook.sheets?.[0].sheet;
29
+ if (!sheets || sheets.length === 0) return undefined;
30
+ const maxSheet = sheets.orderByDesc((sheet) => numParseInt(sheet.$["r:id"])!).first();
31
+ return maxSheet ? numParseInt(maxSheet.$["r:id"]) : undefined;
32
+ }
33
+
34
+ get sheetNames(): string[] {
35
+ return this.data.workbook.sheets?.[0].sheet.map((item) => item.$.name) ?? [];
36
+ }
37
+
38
+ addWorksheet(name: string): this {
39
+ const replacedName = this._getReplacedName(name);
40
+
41
+ const newWsRelId = (this.lastWsRelId ?? 0) + 1;
42
+
43
+ this.data.workbook.sheets = this.data.workbook.sheets ?? [{ sheet: [] }];
44
+ this.data.workbook.sheets[0].sheet.push({
45
+ $: {
46
+ "name": replacedName,
47
+ "sheetId": newWsRelId.toString(),
48
+ "r:id": `rId${newWsRelId}`,
49
+ },
50
+ });
51
+
52
+ return this;
53
+ }
54
+
55
+ cleanup(): void {
56
+ const result = {} as ExcelXmlWorkbookData["workbook"];
57
+
58
+ // 순서 정렬 ("sheets"기준 앞뒤로, 나머지는 원래위치대로)
59
+
60
+ const workbookRec = this.data.workbook as Record<string, unknown>;
61
+ const resultRec = result as Record<string, unknown>;
62
+
63
+ for (const key of Object.keys(this.data.workbook)) {
64
+ if (key === "bookViews") continue;
65
+
66
+ if (key === "sheets") {
67
+ if (this.data.workbook.bookViews != null) {
68
+ result.bookViews = this.data.workbook.bookViews;
69
+ }
70
+ result.sheets = this.data.workbook.sheets;
71
+ } else {
72
+ resultRec[key] = workbookRec[key];
73
+ }
74
+ }
75
+
76
+ this.data.workbook = result;
77
+ }
78
+
79
+ initializeView(): void {
80
+ this.data.workbook.bookViews = this.data.workbook.bookViews ?? [{ workbookView: [{}] }];
81
+ }
82
+
83
+ getWsRelIdByName(name: string): number | undefined {
84
+ return numParseInt((this.data.workbook.sheets?.[0].sheet ?? []).single((item) => item.$.name === name)?.$["r:id"]);
85
+ }
86
+
87
+ getWsRelIdByIndex(index: number): number | undefined {
88
+ return numParseInt(this.data.workbook.sheets?.[0].sheet[index]?.$["r:id"]);
89
+ }
90
+
91
+ getWorksheetNameById(id: number): string | undefined {
92
+ return this._getSheetDataById(id)?.$.name;
93
+ }
94
+
95
+ setWorksheetNameById(id: number, newName: string): void {
96
+ const sheetData = this._getSheetDataById(id);
97
+ if (sheetData == null) {
98
+ throw new Error(`워크시트 ID ${id}를 찾을 수 없습니다`);
99
+ }
100
+ const replacedName = this._getReplacedName(newName);
101
+ sheetData.$.name = replacedName;
102
+ }
103
+
104
+ private _getSheetDataById(id: number) {
105
+ return (this.data.workbook.sheets?.[0].sheet ?? []).single((item) => numParseInt(item.$["r:id"]) === id);
106
+ }
107
+
108
+ private _getReplacedName(name: string): string {
109
+ //-- 시트명칭 사용불가 텍스트를 "_"로 변환
110
+ return name.replace(/[:\\/?*\[\]']/g, "_");
111
+ }
112
+ }
@@ -0,0 +1,544 @@
1
+ import type {
2
+ ExcelAddressRangePoint,
3
+ ExcelCellData,
4
+ ExcelCellType,
5
+ ExcelRowData,
6
+ ExcelXml,
7
+ ExcelXmlWorksheetData,
8
+ } from "../types";
9
+ import { ExcelUtils } from "../utils/excel-utils";
10
+ import { numParseInt, objClone } from "@simplysm/core-common";
11
+ import "@simplysm/core-common";
12
+
13
+ interface RowInfo {
14
+ data: ExcelRowData;
15
+ cellMap: Map<number, ExcelCellData>;
16
+ }
17
+
18
+ /**
19
+ * xl/worksheets/sheet*.xml 파일을 관리하는 클래스.
20
+ * 셀 데이터, 병합, 열 너비, 행 높이 등을 처리한다.
21
+ */
22
+ export class ExcelXmlWorksheet implements ExcelXml {
23
+ data: ExcelXmlWorksheetData;
24
+
25
+ private readonly _dataMap: Map<number, RowInfo>;
26
+
27
+ constructor(data?: ExcelXmlWorksheetData) {
28
+ if (data === undefined) {
29
+ this.data = {
30
+ worksheet: {
31
+ $: {
32
+ xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
33
+ },
34
+ dimension: [
35
+ {
36
+ $: {
37
+ ref: "A1",
38
+ },
39
+ },
40
+ ],
41
+ sheetData: [{}],
42
+ },
43
+ };
44
+ } else {
45
+ this.data = data;
46
+ }
47
+
48
+ this._dataMap = (this.data.worksheet.sheetData[0].row ?? []).toMap(
49
+ (row) => ExcelUtils.parseRowAddrCode(row.$.r),
50
+ (row) => ({
51
+ data: row,
52
+ cellMap: (row.c ?? []).toMap(
53
+ (cell) => ExcelUtils.parseColAddrCode(cell.$.r),
54
+ (cell) => cell,
55
+ ),
56
+ }),
57
+ );
58
+ }
59
+
60
+ get range(): ExcelAddressRangePoint {
61
+ let maxRow = 0;
62
+ let maxCol = 0;
63
+
64
+ for (const [rowIdx, info] of this._dataMap.entries()) {
65
+ if (rowIdx > maxRow) maxRow = rowIdx;
66
+
67
+ for (const col of info.cellMap.keys()) {
68
+ if (col > maxCol) maxCol = col;
69
+ }
70
+ }
71
+
72
+ return {
73
+ s: { r: 0, c: 0 },
74
+ e: { r: maxRow, c: maxCol },
75
+ };
76
+ }
77
+
78
+ setCellType(addr: { r: number; c: number }, type: ExcelCellType | undefined): void {
79
+ const cellData = this._getOrCreateCellData(addr);
80
+ if (type != null) {
81
+ cellData.$.t = type;
82
+ } else {
83
+ delete cellData.$.t;
84
+ }
85
+ }
86
+
87
+ getCellType(addr: { r: number; c: number }): ExcelCellType | undefined {
88
+ return this._getCellData(addr)?.$.t as ExcelCellType | undefined;
89
+ }
90
+
91
+ setCellVal(addr: { r: number; c: number }, val: string | undefined): void {
92
+ const cellData = this._getOrCreateCellData(addr);
93
+ if (val === undefined) {
94
+ delete cellData.v;
95
+ } else {
96
+ cellData.v = [val];
97
+ }
98
+ }
99
+
100
+ getCellVal(addr: { r: number; c: number }): string | undefined {
101
+ const cellData = this._getCellData(addr);
102
+ const val = cellData?.v?.[0] ?? cellData?.is?.[0]?.t?.[0]?._;
103
+ return typeof val === "string" ? val : undefined;
104
+ }
105
+
106
+ setCellFormula(addr: { r: number; c: number }, val: string | undefined): void {
107
+ const cellData = this._getOrCreateCellData(addr);
108
+ if (val === undefined) {
109
+ delete cellData.f;
110
+ } else {
111
+ cellData.f = [val];
112
+ }
113
+ }
114
+
115
+ getCellFormula(addr: { r: number; c: number }): string | undefined {
116
+ const val = this._getCellData(addr)?.f?.[0];
117
+ return typeof val === "string" ? val : undefined;
118
+ }
119
+
120
+ getCellStyleId(addr: { r: number; c: number }): string | undefined {
121
+ return this._getCellData(addr)?.$.s;
122
+ }
123
+
124
+ setCellStyleId(addr: { r: number; c: number }, styleId: string | undefined): void {
125
+ if (styleId != null) {
126
+ this._getOrCreateCellData(addr).$.s = styleId;
127
+ } else {
128
+ delete this._getOrCreateCellData(addr).$.s;
129
+ }
130
+ }
131
+
132
+ deleteCell(addr: { r: number; c: number }): void {
133
+ // ROW 없으면 무효
134
+ const rowInfo = this._dataMap.get(addr.r);
135
+ if (rowInfo == null) return;
136
+
137
+ // CELL 없으면 무효
138
+ const cellData = rowInfo.cellMap.get(addr.c);
139
+ if (cellData == null) return;
140
+
141
+ // CELL 삭제
142
+ const cellsData = rowInfo.data.c!;
143
+ const cellIndex = cellsData.indexOf(cellData);
144
+ if (cellIndex !== -1) cellsData.splice(cellIndex, 1);
145
+ rowInfo.cellMap.delete(addr.c);
146
+
147
+ // 마지막 CELL이면 ROW도 삭제
148
+ if (rowInfo.cellMap.size === 0) {
149
+ this._deleteRow(addr.r);
150
+ }
151
+ }
152
+
153
+ setMergeCells(startAddr: { r: number; c: number }, endAddr: { r: number; c: number }): void {
154
+ const mergeCells = (this.data.worksheet.mergeCells = this.data.worksheet.mergeCells ?? [
155
+ {
156
+ $: { count: "0" },
157
+ mergeCell: [],
158
+ },
159
+ ]);
160
+
161
+ const newRange = { s: startAddr, e: endAddr };
162
+
163
+ // 머지 겹침 체크
164
+ const existingMergeCells = mergeCells[0].mergeCell;
165
+ for (const mergeCell of existingMergeCells) {
166
+ const existingRange = ExcelUtils.parseRangeAddrCode(mergeCell.$.ref);
167
+
168
+ if (
169
+ newRange.s.r <= existingRange.e.r &&
170
+ newRange.e.r >= existingRange.s.r &&
171
+ newRange.s.c <= existingRange.e.c &&
172
+ newRange.e.c >= existingRange.s.c
173
+ ) {
174
+ throw new Error(
175
+ `병합 셀이 기존 병합 범위(${mergeCell.$.ref})와 겹칩니다: ${ExcelUtils.stringifyRangeAddr(newRange)}`,
176
+ );
177
+ }
178
+ }
179
+
180
+ mergeCells[0].mergeCell.push({ $: { ref: ExcelUtils.stringifyRangeAddr(newRange) } });
181
+ mergeCells[0].$.count = mergeCells[0].mergeCell.length.toString();
182
+
183
+ // 시작셀외 모든셀 삭제
184
+ for (let r = startAddr.r; r <= endAddr.r; r++) {
185
+ for (let c = startAddr.c; c <= endAddr.c; c++) {
186
+ const currentAddr = { r, c };
187
+ if (currentAddr.r !== startAddr.r || currentAddr.c !== startAddr.c) {
188
+ this.deleteCell(currentAddr);
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ getMergeCells(): { s: { r: number; c: number }; e: { r: number; c: number } }[] {
195
+ const mergeCells = this.data.worksheet.mergeCells;
196
+ if (mergeCells === undefined) return [];
197
+ return mergeCells[0].mergeCell.map((item) => ExcelUtils.parseRangeAddrCode(item.$.ref));
198
+ }
199
+
200
+ removeMergeCells(fromAddr: { r: number; c: number }, toAddr: { r: number; c: number }): void {
201
+ if (this.data.worksheet.mergeCells == null) return;
202
+
203
+ const range = { s: fromAddr, e: toAddr };
204
+
205
+ const filteredMergeCells = this.data.worksheet.mergeCells[0].mergeCell.filter((item) => {
206
+ const rangeAddr = ExcelUtils.parseRangeAddrCode(item.$.ref);
207
+ return !(
208
+ rangeAddr.s.r >= range.s.r &&
209
+ rangeAddr.e.r <= range.e.r &&
210
+ rangeAddr.s.c >= range.s.c &&
211
+ rangeAddr.e.c <= range.e.c
212
+ );
213
+ });
214
+
215
+ if (filteredMergeCells.length === 0) {
216
+ delete this.data.worksheet.mergeCells;
217
+ } else {
218
+ this.data.worksheet.mergeCells[0].mergeCell = filteredMergeCells;
219
+ this.data.worksheet.mergeCells[0].$.count = filteredMergeCells.length.toString();
220
+ }
221
+ }
222
+
223
+ /**
224
+ * 특정 열의 너비를 설정한다.
225
+ *
226
+ * @internal
227
+ * 외부에서는 ExcelCol.setWidth()를 사용한다.
228
+ *
229
+ * @param colIndex 열 인덱스 (1-based, 문자열)
230
+ * @param width 설정할 너비
231
+ */
232
+ setColWidth(colIndex: string, width: string): void {
233
+ const colIndexNumber = numParseInt(colIndex);
234
+ if (colIndexNumber == null) {
235
+ throw new Error(`잘못된 열 인덱스: ${colIndex}`);
236
+ }
237
+
238
+ const cols = this.data.worksheet.cols?.[0];
239
+
240
+ // 대상 열을 포함하는 기존 범위 찾기
241
+ const col = cols
242
+ ? cols.col.single(
243
+ (item) =>
244
+ (numParseInt(item.$.min) ?? 0) <= colIndexNumber && (numParseInt(item.$.max) ?? 0) >= colIndexNumber,
245
+ )
246
+ : undefined;
247
+
248
+ if (col != null && cols != null) {
249
+ if (col.$.min === col.$.max) {
250
+ // 기존 범위가 단일 열인 경우: 해당 열의 속성만 변경
251
+ col.$.bestFit = "1";
252
+ col.$.customWidth = "1";
253
+ col.$.width = width;
254
+ } else {
255
+ // 기존 범위가 여러 열인 경우: 범위를 분할하여 대상 열만 새 width 적용
256
+ // 예: 기존 [1~5, width=10], 대상=3, 새 width=20
257
+ // → [1~2, width=10], [3, width=20], [4~5, width=10]
258
+ const minNumber = numParseInt(col.$.min) ?? 0;
259
+ const maxNumber = numParseInt(col.$.max) ?? 0;
260
+
261
+ let insertIndex = cols.col.indexOf(col);
262
+
263
+ // 앞쪽 범위 생성 (min ~ colIndex-1): 원본 속성 유지
264
+ if (minNumber < colIndexNumber) {
265
+ cols.col.splice(insertIndex, 0, {
266
+ $: {
267
+ ...col.$,
268
+ min: col.$.min,
269
+ max: (colIndexNumber - 1).toString(),
270
+ },
271
+ });
272
+ insertIndex++;
273
+ }
274
+
275
+ // 대상 열 생성 (colIndex): 새 width 적용
276
+ cols.col.splice(insertIndex, 0, {
277
+ $: {
278
+ min: colIndex,
279
+ max: colIndex,
280
+ bestFit: "1",
281
+ customWidth: "1",
282
+ width: width,
283
+ },
284
+ });
285
+ insertIndex++;
286
+
287
+ // 뒤쪽 범위 생성 (colIndex+1 ~ max): 원본 속성 유지
288
+ if (maxNumber > colIndexNumber) {
289
+ cols.col.splice(insertIndex, 0, {
290
+ $: {
291
+ ...col.$,
292
+ min: (colIndexNumber + 1).toString(),
293
+ max: col.$.max,
294
+ },
295
+ });
296
+ }
297
+
298
+ // 원본 범위 삭제
299
+ const colIndex2 = cols.col.indexOf(col);
300
+ if (colIndex2 !== -1) cols.col.splice(colIndex2, 1);
301
+ }
302
+ } else {
303
+ // 기존 범위 없음: 새 범위 생성
304
+ this.data.worksheet.cols = this.data.worksheet.cols ?? [{ col: [] }];
305
+ this.data.worksheet.cols[0].col.push({
306
+ $: {
307
+ min: colIndex,
308
+ max: colIndex,
309
+ bestFit: "1",
310
+ customWidth: "1",
311
+ width: width,
312
+ },
313
+ });
314
+ }
315
+ }
316
+
317
+ setZoom(percent: number): void {
318
+ this.data.worksheet.sheetViews = this.data.worksheet.sheetViews ?? [
319
+ { sheetView: [{ $: { workbookViewId: "0" } }] },
320
+ ];
321
+
322
+ this.data.worksheet.sheetViews[0].sheetView[0].$.zoomScale = percent.toString();
323
+ }
324
+
325
+ setFix(point: { r?: number; c?: number }): void {
326
+ this.data.worksheet.sheetViews = this.data.worksheet.sheetViews ?? [
327
+ { sheetView: [{ $: { workbookViewId: "0" } }] },
328
+ ];
329
+
330
+ this.data.worksheet.sheetViews[0].sheetView[0].pane = [
331
+ {
332
+ $: {
333
+ ...(point.c != null
334
+ ? {
335
+ xSplit: (point.c + 1).toString(),
336
+ }
337
+ : {}),
338
+ ...(point.r != null
339
+ ? {
340
+ ySplit: (point.r + 1).toString(),
341
+ }
342
+ : {}),
343
+ topLeftCell: ExcelUtils.stringifyAddr({
344
+ r: (point.r ?? -1) + 1,
345
+ c: (point.c ?? -1) + 1,
346
+ }),
347
+ activePane: point.r == null ? "topRight" : point.c == null ? "bottomLeft" : "bottomRight",
348
+ state: "frozen",
349
+ },
350
+ },
351
+ ];
352
+ }
353
+
354
+ copyRow(sourceR: number, targetR: number): void {
355
+ // 출발지ROW 데이터 복제
356
+ const sourceRowInfo = this._dataMap.get(sourceR);
357
+
358
+ if (sourceRowInfo != null) {
359
+ // rowData 복제
360
+ const newRowData: ExcelRowData = objClone(sourceRowInfo.data);
361
+
362
+ // ROW 주소 변경
363
+ newRowData.$.r = ExcelUtils.stringifyRowAddr(targetR);
364
+
365
+ // 각 CELL 주소 변경
366
+ if (newRowData.c != null) {
367
+ for (const cellData of newRowData.c) {
368
+ const colAddr = ExcelUtils.parseColAddrCode(cellData.$.r);
369
+ cellData.$.r = ExcelUtils.stringifyAddr({ r: targetR, c: colAddr });
370
+ }
371
+ }
372
+
373
+ this._replaceRowData(targetR, newRowData);
374
+ } else {
375
+ this._deleteRow(targetR);
376
+ }
377
+
378
+ // 소스 행의 병합 셀 정보를 먼저 복사하여 저장
379
+ const sourceMergeCells = this.getMergeCells()
380
+ .filter((mc) => mc.s.r <= sourceR && mc.e.r >= sourceR)
381
+ .map((mc) => ({ s: { ...mc.s }, e: { ...mc.e } }));
382
+
383
+ // 타겟 행의 기존 병합 셀 제거
384
+ for (const mergeCell of this.getMergeCells()) {
385
+ if (mergeCell.s.r <= targetR && mergeCell.e.r >= targetR) {
386
+ this.removeMergeCells(mergeCell.s, mergeCell.e);
387
+ }
388
+ }
389
+
390
+ // 저장된 소스 병합 정보로 타겟에 복사
391
+ for (const mergeCell of sourceMergeCells) {
392
+ const rowDiff = targetR - sourceR;
393
+ const newStartAddr = { r: mergeCell.s.r + rowDiff, c: mergeCell.s.c };
394
+ const newEndAddr = { r: mergeCell.e.r + rowDiff, c: mergeCell.e.c };
395
+ this.setMergeCells(newStartAddr, newEndAddr);
396
+ }
397
+ }
398
+
399
+ copyCell(sourceAddr: { r: number; c: number }, targetAddr: { r: number; c: number }): void {
400
+ const sourceCellData = this._getCellData(sourceAddr);
401
+
402
+ if (sourceCellData != null) {
403
+ const newCellData = objClone(sourceCellData);
404
+ newCellData.$.r = ExcelUtils.stringifyAddr(targetAddr);
405
+ this._replaceCellData(targetAddr, newCellData);
406
+ } else {
407
+ this.deleteCell(targetAddr);
408
+ }
409
+ }
410
+
411
+ cleanup(): void {
412
+ const result = {} as ExcelXmlWorksheetData["worksheet"];
413
+
414
+ // 순서 정렬 ("sheetData"기준 앞뒤로, 나머지는 원래위치대로)
415
+
416
+ for (const key of Object.keys(this.data.worksheet)) {
417
+ if (key === "mergeCells") continue;
418
+ if (key === "cols") continue;
419
+ if (key === "sheetViews") continue;
420
+ if (key === "sheetFormatPr") continue;
421
+
422
+ if (key === "sheetData") {
423
+ if (this.data.worksheet.sheetViews != null) {
424
+ result.sheetViews = this.data.worksheet.sheetViews;
425
+ }
426
+ if (this.data.worksheet.sheetFormatPr != null) {
427
+ result.sheetFormatPr = this.data.worksheet.sheetFormatPr;
428
+ }
429
+ if (this.data.worksheet.cols != null) {
430
+ result.cols = this.data.worksheet.cols;
431
+ }
432
+ result.sheetData = this.data.worksheet.sheetData;
433
+
434
+ if (this.data.worksheet.mergeCells != null) {
435
+ result.mergeCells = this.data.worksheet.mergeCells;
436
+ }
437
+ } else {
438
+ const worksheetRec = this.data.worksheet as Record<string, unknown>;
439
+ const resultRec = result as Record<string, unknown>;
440
+ resultRec[key] = worksheetRec[key];
441
+ }
442
+ }
443
+
444
+ // ROW 정렬
445
+ const rowsData = (result.sheetData[0].row = result.sheetData[0].row ?? []);
446
+ rowsData.sort((a, b) => (numParseInt(a.$.r) ?? 0) - (numParseInt(b.$.r) ?? 0));
447
+
448
+ // CELL 정렬
449
+ for (const rowData of rowsData) {
450
+ const cellsData = rowData.c;
451
+ if (cellsData == null) continue;
452
+ cellsData.sort((a, b) => ExcelUtils.parseCellAddrCode(a.$.r).c - ExcelUtils.parseCellAddrCode(b.$.r).c);
453
+ }
454
+
455
+ // Dimension 값 적용
456
+ if (result.dimension != null) {
457
+ result.dimension[0].$.ref = ExcelUtils.stringifyRangeAddr(this.range);
458
+ } else {
459
+ result.dimension = [{ $: { ref: ExcelUtils.stringifyRangeAddr(this.range) } }];
460
+ }
461
+
462
+ this.data.worksheet = result;
463
+ }
464
+
465
+ private _getCellData(addr: { r: number; c: number }): ExcelCellData | undefined {
466
+ return this._dataMap.get(addr.r)?.cellMap.get(addr.c);
467
+ }
468
+
469
+ private _getOrCreateCellData(addr: { r: number; c: number }): ExcelCellData {
470
+ // ROW 없으면 만들기
471
+ const rowInfo = this._getOrCreateRowInfo(addr.r);
472
+
473
+ // CELL 없으면 만들기
474
+ let cellData = rowInfo.cellMap.get(addr.c);
475
+ if (cellData === undefined) {
476
+ rowInfo.data.c = rowInfo.data.c ?? [];
477
+
478
+ cellData = { $: { r: ExcelUtils.stringifyAddr(addr) }, v: [""] };
479
+ rowInfo.data.c.push(cellData);
480
+ rowInfo.cellMap.set(addr.c, cellData);
481
+ }
482
+
483
+ return cellData;
484
+ }
485
+
486
+ private _getOrCreateRowInfo(r: number): RowInfo {
487
+ const rowInfo = this._dataMap.get(r);
488
+ if (rowInfo == null) {
489
+ return this._replaceRowData(r, { $: { r: ExcelUtils.stringifyRowAddr(r) }, c: [] });
490
+ }
491
+ return rowInfo;
492
+ }
493
+
494
+ private _replaceRowData(r: number, rowData: ExcelRowData): RowInfo {
495
+ this._deleteRow(r);
496
+
497
+ // sheet에 기록
498
+ this.data.worksheet.sheetData[0].row = this.data.worksheet.sheetData[0].row ?? [];
499
+ this.data.worksheet.sheetData[0].row.push(rowData);
500
+
501
+ // cache에 기록
502
+ const rowInfo = {
503
+ data: rowData,
504
+ cellMap: (rowData.c ?? []).toMap(
505
+ (cell) => ExcelUtils.parseColAddrCode(cell.$.r),
506
+ (cell) => cell,
507
+ ),
508
+ };
509
+ this._dataMap.set(r, rowInfo);
510
+
511
+ return rowInfo;
512
+ }
513
+
514
+ private _replaceCellData(addr: { r: number; c: number }, cellData: ExcelCellData): void {
515
+ this.deleteCell(addr);
516
+
517
+ // ROW
518
+ const targetRowInfo = this._getOrCreateRowInfo(addr.r);
519
+
520
+ // sheet에 기록
521
+ targetRowInfo.data.c = targetRowInfo.data.c ?? [];
522
+ targetRowInfo.data.c.push(cellData);
523
+
524
+ // cache에 기록
525
+ targetRowInfo.cellMap.set(addr.c, cellData);
526
+ }
527
+
528
+ private _deleteRow(r: number): void {
529
+ const targetRowInfo = this._dataMap.get(r);
530
+ if (targetRowInfo != null) {
531
+ const rows = this.data.worksheet.sheetData[0].row;
532
+ if (rows) {
533
+ const rowIndex = rows.indexOf(targetRowInfo.data);
534
+ if (rowIndex !== -1) rows.splice(rowIndex, 1);
535
+ }
536
+ }
537
+ this._dataMap.delete(r);
538
+
539
+ // ROW가 하나도 없으면 XML의 row부분 자체를 삭제
540
+ if (this.data.worksheet.sheetData[0].row?.length === 0) {
541
+ delete this.data.worksheet.sheetData[0].row;
542
+ }
543
+ }
544
+ }