@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,64 @@
1
+ import type { ExcelXml, ExcelXmlContentTypeData } from "../types";
2
+
3
+ /**
4
+ * [Content_Types].xml 파일을 관리하는 클래스.
5
+ * 파일별 MIME 타입 정보를 관리한다.
6
+ */
7
+ export class ExcelXmlContentType implements ExcelXml {
8
+ data: ExcelXmlContentTypeData;
9
+
10
+ constructor(data?: ExcelXmlContentTypeData) {
11
+ if (data == null) {
12
+ this.data = {
13
+ Types: {
14
+ $: {
15
+ xmlns: "http://schemas.openxmlformats.org/package/2006/content-types",
16
+ },
17
+ Default: [
18
+ {
19
+ $: {
20
+ Extension: "rels",
21
+ ContentType: "application/vnd.openxmlformats-package.relationships+xml",
22
+ },
23
+ },
24
+ {
25
+ $: {
26
+ Extension: "xml",
27
+ ContentType: "application/xml",
28
+ },
29
+ },
30
+ ],
31
+ Override: [
32
+ {
33
+ $: {
34
+ PartName: "/xl/workbook.xml",
35
+ ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
36
+ },
37
+ },
38
+ ],
39
+ },
40
+ };
41
+ } else {
42
+ this.data = data;
43
+ }
44
+ }
45
+
46
+ add(partName: string, contentType: string): this {
47
+ // 중복 체크
48
+ const exists = this.data.Types.Override.some((item) => item.$.PartName === partName);
49
+ if (exists) {
50
+ return this;
51
+ }
52
+
53
+ this.data.Types.Override.push({
54
+ $: {
55
+ PartName: partName,
56
+ ContentType: contentType,
57
+ },
58
+ });
59
+
60
+ return this;
61
+ }
62
+
63
+ cleanup(): void {}
64
+ }
@@ -0,0 +1,87 @@
1
+ import type { ExcelXml, ExcelXmlDrawingData } from "../types";
2
+
3
+ /**
4
+ * xl/drawings/drawing*.xml 파일을 관리하는 클래스.
5
+ * 이미지 삽입 시 위치 및 참조 정보를 처리한다.
6
+ */
7
+ export class ExcelXmlDrawing implements ExcelXml {
8
+ data: ExcelXmlDrawingData;
9
+
10
+ constructor(data?: ExcelXmlDrawingData) {
11
+ if (data == null) {
12
+ this.data = {
13
+ wsDr: {
14
+ $: {
15
+ "xmlns": "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing",
16
+ "xmlns:a": "http://schemas.openxmlformats.org/drawingml/2006/main",
17
+ "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
18
+ },
19
+ twoCellAnchor: [],
20
+ },
21
+ };
22
+ } else {
23
+ this.data = data;
24
+ }
25
+ }
26
+
27
+ addPicture(opts: {
28
+ from: { r: number; c: number; rOff?: number | string; cOff?: number | string };
29
+ to: { r: number; c: number; rOff?: number | string; cOff?: number | string };
30
+ blipRelId: string;
31
+ }): void {
32
+ this.data.wsDr.twoCellAnchor = this.data.wsDr.twoCellAnchor ?? [];
33
+
34
+ const anchors = this.data.wsDr.twoCellAnchor;
35
+ const picId = anchors.length + 1;
36
+ const name = `Picture ${picId}`;
37
+
38
+ this.data.wsDr.twoCellAnchor.push({
39
+ from: [
40
+ {
41
+ col: [opts.from.c.toString()],
42
+ colOff: [opts.from.cOff != null ? opts.from.cOff.toString() : "0"],
43
+ row: [opts.from.r.toString()],
44
+ rowOff: [opts.from.rOff != null ? opts.from.rOff.toString() : "0"],
45
+ },
46
+ ],
47
+ to: [
48
+ {
49
+ col: [opts.to.c.toString()],
50
+ colOff: [opts.to.cOff != null ? opts.to.cOff.toString() : "0"],
51
+ row: [opts.to.r.toString()],
52
+ rowOff: [opts.to.rOff != null ? opts.to.rOff.toString() : "0"],
53
+ },
54
+ ],
55
+ pic: [
56
+ {
57
+ nvPicPr: [
58
+ {
59
+ cNvPr: [{ $: { id: picId.toString(), name } }],
60
+ cNvPicPr: [{ "a:picLocks": [{ $: { noChangeAspect: "1" } }] }],
61
+ },
62
+ ],
63
+ blipFill: [
64
+ {
65
+ "a:blip": [{ $: { "r:embed": opts.blipRelId } }],
66
+ "a:stretch": [{ "a:fillRect": [] }],
67
+ },
68
+ ],
69
+ spPr: [
70
+ {
71
+ "a:xfrm": [
72
+ {
73
+ "a:off": [{ $: { x: "0", y: "0" } }],
74
+ "a:ext": [{ $: { cx: "0", cy: "0" } }],
75
+ },
76
+ ],
77
+ "a:prstGeom": [{ "$": { prst: "rect" }, "a:avLst": [] }],
78
+ },
79
+ ],
80
+ },
81
+ ],
82
+ clientData: [{}],
83
+ });
84
+ }
85
+
86
+ cleanup(): void {}
87
+ }
@@ -0,0 +1,86 @@
1
+ import "@simplysm/core-common";
2
+ import { numParseInt } from "@simplysm/core-common";
3
+ import type { ExcelRelationshipData, ExcelXml, ExcelXmlRelationshipData } from "../types";
4
+
5
+ /**
6
+ * *.rels 파일을 관리하는 클래스.
7
+ * 파일 간의 참조 관계를 처리한다.
8
+ */
9
+ export class ExcelXmlRelationship implements ExcelXml {
10
+ data: ExcelXmlRelationshipData;
11
+
12
+ constructor(data?: ExcelXmlRelationshipData) {
13
+ if (data == null) {
14
+ this.data = {
15
+ Relationships: {
16
+ $: {
17
+ xmlns: "http://schemas.openxmlformats.org/package/2006/relationships",
18
+ },
19
+ },
20
+ };
21
+ } else {
22
+ this.data = data;
23
+ }
24
+ }
25
+
26
+ getTargetByRelId(rId: number): string | undefined {
27
+ return (this.data.Relationships.Relationship ?? []).single((rel) => this._getRelId(rel) === rId)?.$.Target;
28
+ }
29
+
30
+ add(target: string, type: string): this {
31
+ this.addAndGetId(target, type);
32
+ return this;
33
+ }
34
+
35
+ addAndGetId(target: string, type: string): number {
36
+ this.data.Relationships.Relationship = this.data.Relationships.Relationship ?? [];
37
+
38
+ const newId = (this._lastId ?? 0) + 1;
39
+
40
+ this.data.Relationships.Relationship.push({
41
+ $: {
42
+ Id: `rId${newId}`,
43
+ Target: target,
44
+ Type: type,
45
+ },
46
+ });
47
+
48
+ return newId;
49
+ }
50
+
51
+ insert(rId: number, target: string, type: string): this {
52
+ this.data.Relationships.Relationship = this.data.Relationships.Relationship ?? [];
53
+
54
+ const shiftRels = this.data.Relationships.Relationship.filter((rel) => this._getRelId(rel) >= rId);
55
+ for (const shiftRel of shiftRels) {
56
+ shiftRel.$.Id = `rId${this._getRelId(shiftRel) + 1}`;
57
+ }
58
+
59
+ this.data.Relationships.Relationship.push({
60
+ $: {
61
+ Id: `rId${rId}`,
62
+ Target: target,
63
+ Type: type,
64
+ },
65
+ });
66
+
67
+ return this;
68
+ }
69
+
70
+ cleanup(): void {}
71
+
72
+ private get _lastId(): number | undefined {
73
+ const rels = this.data.Relationships.Relationship;
74
+ if (!rels || rels.length === 0) return undefined;
75
+ const maxRel = rels.orderByDesc((rel) => this._getRelId(rel)).first();
76
+ return maxRel ? this._getRelId(maxRel) : undefined;
77
+ }
78
+
79
+ private _getRelId(rel: ExcelRelationshipData): number {
80
+ const match = /[0-9]+$/.exec(rel.$.Id);
81
+ if (match == null) {
82
+ throw new Error(`잘못된 관계 ID 형식입니다: ${rel.$.Id}`);
83
+ }
84
+ return numParseInt(match[0])!;
85
+ }
86
+ }
@@ -0,0 +1,80 @@
1
+ import type {
2
+ ExcelXml,
3
+ ExcelXmlSharedStringData,
4
+ ExcelXmlSharedStringDataSi,
5
+ ExcelXmlSharedStringDataText,
6
+ } from "../types";
7
+ import "@simplysm/core-common";
8
+
9
+ /**
10
+ * xl/sharedStrings.xml 파일을 관리하는 클래스.
11
+ * 문자열 중복을 방지하여 파일 크기를 최적화한다.
12
+ */
13
+ export class ExcelXmlSharedString implements ExcelXml {
14
+ data: ExcelXmlSharedStringData;
15
+
16
+ private readonly _stringIndexesMap: Map<string, number[]>;
17
+
18
+ constructor(data?: ExcelXmlSharedStringData) {
19
+ if (data === undefined) {
20
+ this.data = {
21
+ sst: {
22
+ $: {
23
+ xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
24
+ },
25
+ },
26
+ };
27
+ } else {
28
+ this.data = data;
29
+ }
30
+
31
+ this._stringIndexesMap = this.data.sst.si
32
+ ? this.data.sst.si
33
+ .map((tag, id) => ({ id, tag }))
34
+ .filter((item) => !this._getHasInnerStyleOnSiTag(item.tag))
35
+ .toArrayMap(
36
+ (item) => this._getStringFromSiTag(item.tag),
37
+ (item) => item.id,
38
+ )
39
+ : new Map<string, number[]>();
40
+ }
41
+
42
+ getIdByString(str: string): number | undefined {
43
+ return this._stringIndexesMap.get(str)?.[0];
44
+ }
45
+
46
+ getStringById(id: number): string | undefined {
47
+ const si = this.data.sst.si?.[id];
48
+ return si != null ? this._getStringFromSiTag(si) : undefined;
49
+ }
50
+
51
+ add(str: string): number {
52
+ this.data.sst.si = this.data.sst.si ?? [];
53
+ const newLength = this.data.sst.si.push({ t: [str] });
54
+ const arr = this._stringIndexesMap.getOrCreate(str, []);
55
+ arr.push(newLength - 1);
56
+ return newLength - 1;
57
+ }
58
+
59
+ cleanup(): void {}
60
+
61
+ private _getStringFromSiTag(si: ExcelXmlSharedStringDataSi): string {
62
+ if ("t" in si) {
63
+ return this._getStringFromTTag(si.t);
64
+ } else {
65
+ return si.r.map((item) => this._getStringFromTTag(item.t)).join("");
66
+ }
67
+ }
68
+
69
+ private _getStringFromTTag(t: ExcelXmlSharedStringDataText): string {
70
+ const firstItem = t[0];
71
+ if (typeof firstItem === "string") {
72
+ return firstItem;
73
+ }
74
+ return firstItem._ ?? " ";
75
+ }
76
+
77
+ private _getHasInnerStyleOnSiTag(si: ExcelXmlSharedStringDataSi): boolean {
78
+ return Object.keys(si).some((item) => item !== "t");
79
+ }
80
+ }
@@ -0,0 +1,393 @@
1
+ import type {
2
+ ExcelBorderPosition,
3
+ ExcelHorizontalAlign,
4
+ ExcelVerticalAlign,
5
+ ExcelXml,
6
+ ExcelXmlStyleData,
7
+ ExcelXmlStyleDataBorder,
8
+ ExcelXmlStyleDataFill,
9
+ ExcelXmlStyleDataXf,
10
+ } from "../types";
11
+ import "@simplysm/core-common";
12
+ import { numParseInt, objClone, objEqual } from "@simplysm/core-common";
13
+
14
+ export interface ExcelStyle {
15
+ numFmtId?: string;
16
+ numFmtCode?: string;
17
+ border?: ExcelBorderPosition[];
18
+ background?: string;
19
+ verticalAlign?: ExcelVerticalAlign;
20
+ horizontalAlign?: ExcelHorizontalAlign;
21
+ }
22
+
23
+ /**
24
+ * xl/styles.xml 파일을 관리하는 클래스.
25
+ * 숫자 형식, 배경색, 테두리, 정렬 등의 스타일을 처리한다.
26
+ */
27
+ export class ExcelXmlStyle implements ExcelXml {
28
+ data: ExcelXmlStyleData;
29
+
30
+ constructor(data?: ExcelXmlStyleData) {
31
+ if (data === undefined) {
32
+ this.data = {
33
+ styleSheet: {
34
+ $: {
35
+ xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
36
+ },
37
+ fonts: [
38
+ {
39
+ $: { count: "1" },
40
+ font: [{}],
41
+ },
42
+ ],
43
+ fills: [
44
+ {
45
+ $: { count: "2" },
46
+ fill: [
47
+ { patternFill: [{ $: { patternType: "none" } }] },
48
+ { patternFill: [{ $: { patternType: "gray125" } }] },
49
+ ],
50
+ },
51
+ ],
52
+ borders: [
53
+ {
54
+ $: { count: "1" },
55
+ border: [{}],
56
+ },
57
+ ],
58
+ cellXfs: [
59
+ {
60
+ $: { count: "1" },
61
+ xf: [{ $: { numFmtId: "0" } }],
62
+ },
63
+ ],
64
+ },
65
+ };
66
+ } else {
67
+ this.data = data;
68
+ }
69
+ }
70
+
71
+ add(style: ExcelStyle): string {
72
+ const newXf: ExcelXmlStyleDataXf = { $: {} };
73
+
74
+ if (style.numFmtId !== undefined) {
75
+ newXf.$.numFmtId = style.numFmtId;
76
+ }
77
+
78
+ if (style.numFmtCode !== undefined) {
79
+ newXf.$.numFmtId = this._setNumFmtCode(style.numFmtCode);
80
+ newXf.$.applyNumberFormat = "1";
81
+ }
82
+
83
+ if (style.background !== undefined) {
84
+ const newFill: ExcelXmlStyleDataFill = {
85
+ patternFill: [
86
+ {
87
+ $: { patternType: "solid" },
88
+ fgColor: [{ $: { rgb: style.background.toUpperCase() } }],
89
+ },
90
+ ],
91
+ };
92
+
93
+ newXf.$.applyFill = "1";
94
+ newXf.$.fillId = this._getSameOrCreateFill(newFill);
95
+ }
96
+
97
+ if (style.border !== undefined) {
98
+ const newBorder = this._createBorderFromPositions(style.border);
99
+ newXf.$.applyBorder = "1";
100
+ newXf.$.borderId = this._getSameOrCreateBorder(newBorder);
101
+ }
102
+
103
+ this._applyAlignment(newXf, style);
104
+
105
+ return this._getSameOrCreateXf(newXf);
106
+ }
107
+
108
+ addWithClone(id: string, style: ExcelStyle): string {
109
+ const idNum = numParseInt(id);
110
+ if (idNum == null) {
111
+ throw new Error(`잘못된 스타일 ID: ${id}`);
112
+ }
113
+ const xfArray = this.data.styleSheet.cellXfs[0].xf;
114
+ if (idNum < 0 || idNum >= xfArray.length) {
115
+ throw new Error(`존재하지 않는 스타일 ID: ${id} (범위: 0-${xfArray.length - 1})`);
116
+ }
117
+ const prevXf = xfArray[idNum];
118
+ const cloneXf = objClone(prevXf);
119
+
120
+ if (style.numFmtId !== undefined) {
121
+ cloneXf.$.numFmtId = style.numFmtId;
122
+ }
123
+
124
+ if (style.numFmtCode !== undefined) {
125
+ cloneXf.$.numFmtId = this._setNumFmtCode(style.numFmtCode);
126
+ cloneXf.$.applyNumberFormat = "1";
127
+ }
128
+
129
+ if (style.background !== undefined) {
130
+ const fillIdNum = cloneXf.$.fillId !== undefined ? numParseInt(cloneXf.$.fillId) : undefined;
131
+ const prevFill = fillIdNum !== undefined ? this.data.styleSheet.fills[0].fill[fillIdNum] : undefined;
132
+
133
+ if (prevFill != null) {
134
+ const cloneFill = objClone(prevFill);
135
+ cloneFill.patternFill[0].$.patternType = "solid";
136
+
137
+ if (cloneFill.patternFill[0].fgColor == null) {
138
+ cloneFill.patternFill[0].fgColor = [{ $: { rgb: style.background } }];
139
+ } else {
140
+ cloneFill.patternFill[0].fgColor[0].$.rgb = style.background;
141
+ }
142
+
143
+ cloneXf.$.applyFill = "1";
144
+ cloneXf.$.fillId = this._getSameOrCreateFill(cloneFill);
145
+ } else {
146
+ const newFill: ExcelXmlStyleDataFill = {
147
+ patternFill: [
148
+ {
149
+ $: { patternType: "solid" },
150
+ fgColor: [{ $: { rgb: style.background.toUpperCase() } }],
151
+ },
152
+ ],
153
+ };
154
+ cloneXf.$.applyFill = "1";
155
+ cloneXf.$.fillId = this._getSameOrCreateFill(newFill);
156
+ }
157
+ }
158
+
159
+ if (style.border !== undefined) {
160
+ const borderIdNum = cloneXf.$.borderId !== undefined ? numParseInt(cloneXf.$.borderId) : undefined;
161
+ const prevBorder = borderIdNum !== undefined ? this.data.styleSheet.borders[0].border[borderIdNum] : undefined;
162
+
163
+ if (prevBorder != null) {
164
+ const cloneBorder = objClone(prevBorder);
165
+ this._applyBorderPosition(cloneBorder, "left", style.border.includes("left"));
166
+ this._applyBorderPosition(cloneBorder, "right", style.border.includes("right"));
167
+ this._applyBorderPosition(cloneBorder, "top", style.border.includes("top"));
168
+ this._applyBorderPosition(cloneBorder, "bottom", style.border.includes("bottom"));
169
+
170
+ cloneXf.$.applyBorder = "1";
171
+ cloneXf.$.borderId = this._getSameOrCreateBorder(cloneBorder);
172
+ } else {
173
+ const newBorder = this._createBorderFromPositions(style.border);
174
+ cloneXf.$.applyBorder = "1";
175
+ cloneXf.$.borderId = this._getSameOrCreateBorder(newBorder);
176
+ }
177
+ }
178
+
179
+ this._applyAlignment(cloneXf, style);
180
+
181
+ return this._getSameOrCreateXf(cloneXf);
182
+ }
183
+
184
+ get(id: string): ExcelStyle {
185
+ const idNum = numParseInt(id);
186
+ if (idNum == null) {
187
+ throw new Error(`잘못된 스타일 ID: ${id}`);
188
+ }
189
+ const xf = this.data.styleSheet.cellXfs[0].xf[idNum] as ExcelXmlStyleDataXf | undefined;
190
+
191
+ const result: ExcelStyle = {};
192
+
193
+ if (xf !== undefined) {
194
+ result.numFmtId = xf.$.numFmtId;
195
+
196
+ if (xf.$.fillId !== undefined) {
197
+ const fillIdNum = numParseInt(xf.$.fillId);
198
+ if (fillIdNum != null) {
199
+ const fill = this.data.styleSheet.fills[0].fill[fillIdNum] as ExcelXmlStyleDataFill | undefined;
200
+ if (fill == null) {
201
+ throw new Error(
202
+ `존재하지 않는 fill ID: ${xf.$.fillId} (범위: 0-${this.data.styleSheet.fills[0].fill.length - 1})`,
203
+ );
204
+ }
205
+ result.background = fill.patternFill[0].fgColor?.[0].$.rgb;
206
+ }
207
+ }
208
+
209
+ if (xf.$.borderId !== undefined) {
210
+ const borderIdNum = numParseInt(xf.$.borderId);
211
+ if (borderIdNum == null) {
212
+ throw new Error(`잘못된 border ID: ${xf.$.borderId}`);
213
+ }
214
+ const border = this.data.styleSheet.borders[0].border[borderIdNum] as ExcelXmlStyleDataBorder | undefined;
215
+ if (border == null) {
216
+ throw new Error(
217
+ `존재하지 않는 border ID: ${xf.$.borderId} (범위: 0-${this.data.styleSheet.borders[0].border.length - 1})`,
218
+ );
219
+ }
220
+ if (border.top != null || border.left != null || border.right != null || border.bottom != null) {
221
+ result.border = [];
222
+ if (border.left != null) {
223
+ result.border.push("left");
224
+ }
225
+ if (border.right != null) {
226
+ result.border.push("right");
227
+ }
228
+ if (border.top != null) {
229
+ result.border.push("top");
230
+ }
231
+ if (border.bottom != null) {
232
+ result.border.push("bottom");
233
+ }
234
+ }
235
+ }
236
+
237
+ result.verticalAlign = xf.alignment?.[0].$.vertical;
238
+ result.horizontalAlign = xf.alignment?.[0].$.horizontal;
239
+ }
240
+
241
+ return result;
242
+ }
243
+
244
+ getNumFmtCode(numFmtId: string): string | undefined {
245
+ return (this.data.styleSheet.numFmts?.[0].numFmt ?? []).single((item) => item.$.numFmtId === numFmtId)?.$
246
+ .formatCode;
247
+ }
248
+
249
+ cleanup(): void {
250
+ const result = {} as ExcelXmlStyleData["styleSheet"];
251
+
252
+ // 순서 정렬 (numFmts 맨위로)
253
+
254
+ if (this.data.styleSheet.numFmts != null) {
255
+ result.numFmts = this.data.styleSheet.numFmts;
256
+ }
257
+
258
+ const styleSheetRec = this.data.styleSheet as Record<string, unknown>;
259
+ const resultRec = result as Record<string, unknown>;
260
+ for (const key of Object.keys(styleSheetRec)) {
261
+ if (key === "numFmts") continue;
262
+
263
+ resultRec[key] = styleSheetRec[key];
264
+ }
265
+
266
+ this.data.styleSheet = result;
267
+ }
268
+
269
+ //#region Private Methods
270
+
271
+ private _setNumFmtCode(numFmtCode: string): string {
272
+ // 이미 해당 code가 있으면 넘기기
273
+ const existsNumFmtId = (this.data.styleSheet.numFmts?.[0].numFmt ?? []).single(
274
+ (item) => item.$.formatCode === numFmtCode,
275
+ )?.$.numFmtId;
276
+ if (existsNumFmtId != null) {
277
+ return existsNumFmtId;
278
+ }
279
+
280
+ this.data.styleSheet.numFmts = this.data.styleSheet.numFmts ?? [
281
+ {
282
+ $: { count: "0" },
283
+ numFmt: [],
284
+ },
285
+ ];
286
+
287
+ this.data.styleSheet.numFmts[0].numFmt = this.data.styleSheet.numFmts[0].numFmt ?? [];
288
+
289
+ // Excel 사용자 정의 숫자 형식은 ID 180 이상부터 시작한다 (0-163: 내장 형식, 164-179: 예약됨)
290
+ const numFmts = this.data.styleSheet.numFmts[0].numFmt;
291
+ const maxItem =
292
+ numFmts.length > 0 ? numFmts.orderByDesc((item) => numParseInt(item.$.numFmtId) ?? 180).first() : undefined;
293
+ const maxId = maxItem ? (numParseInt(maxItem.$.numFmtId) ?? 180) : 180;
294
+ const nextNumFmtId = (maxId + 1).toString();
295
+ this.data.styleSheet.numFmts[0].numFmt.push({
296
+ $: {
297
+ numFmtId: nextNumFmtId,
298
+ formatCode: numFmtCode,
299
+ },
300
+ });
301
+ this.data.styleSheet.numFmts[0].$.count = (
302
+ (numParseInt(this.data.styleSheet.numFmts[0].$.count) ?? 0) + 1
303
+ ).toString();
304
+
305
+ return nextNumFmtId;
306
+ }
307
+
308
+ private _applyAlignment(xf: ExcelXmlStyleDataXf, style: ExcelStyle): void {
309
+ if (style.verticalAlign !== undefined) {
310
+ xf.$.applyAlignment = "1";
311
+ if (xf.alignment == null) {
312
+ xf.alignment = [{ $: { vertical: style.verticalAlign } }];
313
+ } else {
314
+ xf.alignment[0].$.vertical = style.verticalAlign;
315
+ }
316
+ }
317
+
318
+ if (style.horizontalAlign !== undefined) {
319
+ xf.$.applyAlignment = "1";
320
+ if (xf.alignment == null) {
321
+ xf.alignment = [{ $: { horizontal: style.horizontalAlign } }];
322
+ } else {
323
+ xf.alignment[0].$.horizontal = style.horizontalAlign;
324
+ }
325
+ }
326
+ }
327
+
328
+ private _createBorderFromPositions(positions: ExcelBorderPosition[]): ExcelXmlStyleDataBorder {
329
+ return {
330
+ ...(positions.includes("left") ? { left: [{ $: { style: "thin" }, color: [{ $: { rgb: "00000000" } }] }] } : {}),
331
+ ...(positions.includes("right")
332
+ ? { right: [{ $: { style: "thin" }, color: [{ $: { rgb: "00000000" } }] }] }
333
+ : {}),
334
+ ...(positions.includes("top") ? { top: [{ $: { style: "thin" }, color: [{ $: { rgb: "00000000" } }] }] } : {}),
335
+ ...(positions.includes("bottom")
336
+ ? { bottom: [{ $: { style: "thin" }, color: [{ $: { rgb: "00000000" } }] }] }
337
+ : {}),
338
+ };
339
+ }
340
+
341
+ private _applyBorderPosition(border: ExcelXmlStyleDataBorder, position: ExcelBorderPosition, enabled: boolean): void {
342
+ if (enabled) {
343
+ const existing = border[position];
344
+ if (existing == null) {
345
+ border[position] = [{ $: { style: "thin" }, color: [{ $: { rgb: "00000000" } }] }];
346
+ } else if (existing[0].color == null) {
347
+ existing[0].color = [{ $: { rgb: "00000000" } }];
348
+ } else {
349
+ existing[0].color[0].$.rgb = "00000000";
350
+ }
351
+ } else {
352
+ delete border[position];
353
+ }
354
+ }
355
+
356
+ private _getSameOrCreateXf(xfItem: ExcelXmlStyleDataXf): string {
357
+ const prevSameXf = this.data.styleSheet.cellXfs[0].xf.single((item) => objEqual(item, xfItem));
358
+
359
+ if (prevSameXf != null) {
360
+ return this.data.styleSheet.cellXfs[0].xf.indexOf(prevSameXf).toString();
361
+ } else {
362
+ this.data.styleSheet.cellXfs[0].xf.push(xfItem);
363
+ this.data.styleSheet.cellXfs[0].$.count = this.data.styleSheet.cellXfs[0].xf.length.toString();
364
+ return (this.data.styleSheet.cellXfs[0].xf.length - 1).toString();
365
+ }
366
+ }
367
+
368
+ private _getSameOrCreateFill(fillItem: ExcelXmlStyleDataFill): string {
369
+ const prevSameFill = this.data.styleSheet.fills[0].fill.single((item) => objEqual(item, fillItem));
370
+
371
+ if (prevSameFill != null) {
372
+ return this.data.styleSheet.fills[0].fill.indexOf(prevSameFill).toString();
373
+ } else {
374
+ this.data.styleSheet.fills[0].fill.push(fillItem);
375
+ this.data.styleSheet.fills[0].$.count = this.data.styleSheet.fills[0].fill.length.toString();
376
+ return (this.data.styleSheet.fills[0].fill.length - 1).toString();
377
+ }
378
+ }
379
+
380
+ private _getSameOrCreateBorder(borderItem: ExcelXmlStyleDataBorder): string {
381
+ const prevSameBorder = this.data.styleSheet.borders[0].border.single((item) => objEqual(item, borderItem));
382
+
383
+ if (prevSameBorder != null) {
384
+ return this.data.styleSheet.borders[0].border.indexOf(prevSameBorder).toString();
385
+ } else {
386
+ this.data.styleSheet.borders[0].border.push(borderItem);
387
+ this.data.styleSheet.borders[0].$.count = this.data.styleSheet.borders[0].border.length.toString();
388
+ return (this.data.styleSheet.borders[0].border.length - 1).toString();
389
+ }
390
+ }
391
+
392
+ //#endregion
393
+ }