@simplysm/excel 13.0.0-beta.7 → 13.0.2

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 (167) hide show
  1. package/README.md +15 -0
  2. package/dist/excel-cell.d.ts.map +1 -0
  3. package/dist/excel-cell.js +3 -3
  4. package/dist/excel-cell.js.map +0 -1
  5. package/dist/excel-col.d.ts.map +1 -0
  6. package/dist/excel-col.js +1 -1
  7. package/dist/excel-col.js.map +0 -1
  8. package/dist/excel-row.d.ts.map +1 -0
  9. package/dist/excel-row.js +1 -1
  10. package/dist/excel-row.js.map +0 -1
  11. package/dist/excel-workbook.d.ts.map +1 -0
  12. package/dist/excel-workbook.js +6 -6
  13. package/dist/excel-workbook.js.map +0 -1
  14. package/dist/excel-worksheet.d.ts.map +1 -0
  15. package/dist/excel-worksheet.js +4 -4
  16. package/dist/excel-worksheet.js.map +0 -1
  17. package/dist/excel-wrapper.d.ts.map +1 -0
  18. package/dist/excel-wrapper.js +1 -1
  19. package/dist/excel-wrapper.js.map +0 -1
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +8 -8
  22. package/dist/index.js.map +0 -1
  23. package/dist/types.d.ts.map +1 -0
  24. package/dist/types.js.map +0 -1
  25. package/dist/utils/excel-utils.d.ts.map +1 -0
  26. package/dist/utils/excel-utils.js.map +0 -1
  27. package/dist/utils/zip-cache.d.ts.map +1 -0
  28. package/dist/utils/zip-cache.js +8 -8
  29. package/dist/utils/zip-cache.js.map +0 -1
  30. package/dist/xml/excel-xml-content-type.d.ts.map +1 -0
  31. package/dist/xml/excel-xml-content-type.js.map +0 -1
  32. package/dist/xml/excel-xml-drawing.d.ts.map +1 -0
  33. package/dist/xml/excel-xml-drawing.js.map +0 -1
  34. package/dist/xml/excel-xml-relationship.d.ts.map +1 -0
  35. package/dist/xml/excel-xml-relationship.js.map +0 -1
  36. package/dist/xml/excel-xml-shared-string.d.ts.map +1 -0
  37. package/dist/xml/excel-xml-shared-string.js.map +0 -1
  38. package/dist/xml/excel-xml-style.d.ts.map +1 -0
  39. package/dist/xml/excel-xml-style.js.map +0 -1
  40. package/dist/xml/excel-xml-unknown.d.ts.map +1 -0
  41. package/dist/xml/excel-xml-unknown.js.map +0 -1
  42. package/dist/xml/excel-xml-workbook.d.ts.map +1 -0
  43. package/dist/xml/excel-xml-workbook.js.map +0 -1
  44. package/dist/xml/excel-xml-worksheet.d.ts.map +1 -0
  45. package/dist/xml/excel-xml-worksheet.js +1 -1
  46. package/dist/xml/excel-xml-worksheet.js.map +0 -1
  47. package/package.json +6 -5
  48. package/src/excel-cell.ts +326 -0
  49. package/src/excel-col.ts +43 -0
  50. package/src/excel-row.ts +37 -0
  51. package/src/excel-workbook.ts +206 -0
  52. package/src/excel-worksheet.ts +380 -0
  53. package/src/excel-wrapper.ts +219 -0
  54. package/src/index.ts +13 -0
  55. package/src/types.ts +396 -0
  56. package/src/utils/excel-utils.ts +201 -0
  57. package/src/utils/zip-cache.ts +103 -0
  58. package/src/xml/excel-xml-content-type.ts +64 -0
  59. package/src/xml/excel-xml-drawing.ts +87 -0
  60. package/src/xml/excel-xml-relationship.ts +86 -0
  61. package/src/xml/excel-xml-shared-string.ts +80 -0
  62. package/src/xml/excel-xml-style.ts +393 -0
  63. package/src/xml/excel-xml-unknown.ts +11 -0
  64. package/src/xml/excel-xml-workbook.ts +112 -0
  65. package/src/xml/excel-xml-worksheet.ts +544 -0
  66. package/dist/core-common/src/common.types.d.ts +0 -74
  67. package/dist/core-common/src/common.types.d.ts.map +0 -1
  68. package/dist/core-common/src/env.d.ts +0 -6
  69. package/dist/core-common/src/env.d.ts.map +0 -1
  70. package/dist/core-common/src/errors/argument-error.d.ts +0 -25
  71. package/dist/core-common/src/errors/argument-error.d.ts.map +0 -1
  72. package/dist/core-common/src/errors/not-implemented-error.d.ts +0 -29
  73. package/dist/core-common/src/errors/not-implemented-error.d.ts.map +0 -1
  74. package/dist/core-common/src/errors/sd-error.d.ts +0 -27
  75. package/dist/core-common/src/errors/sd-error.d.ts.map +0 -1
  76. package/dist/core-common/src/errors/timeout-error.d.ts +0 -31
  77. package/dist/core-common/src/errors/timeout-error.d.ts.map +0 -1
  78. package/dist/core-common/src/extensions/arr-ext.d.ts +0 -15
  79. package/dist/core-common/src/extensions/arr-ext.d.ts.map +0 -1
  80. package/dist/core-common/src/extensions/arr-ext.helpers.d.ts +0 -19
  81. package/dist/core-common/src/extensions/arr-ext.helpers.d.ts.map +0 -1
  82. package/dist/core-common/src/extensions/arr-ext.types.d.ts +0 -215
  83. package/dist/core-common/src/extensions/arr-ext.types.d.ts.map +0 -1
  84. package/dist/core-common/src/extensions/map-ext.d.ts +0 -57
  85. package/dist/core-common/src/extensions/map-ext.d.ts.map +0 -1
  86. package/dist/core-common/src/extensions/set-ext.d.ts +0 -36
  87. package/dist/core-common/src/extensions/set-ext.d.ts.map +0 -1
  88. package/dist/core-common/src/features/debounce-queue.d.ts +0 -53
  89. package/dist/core-common/src/features/debounce-queue.d.ts.map +0 -1
  90. package/dist/core-common/src/features/event-emitter.d.ts +0 -66
  91. package/dist/core-common/src/features/event-emitter.d.ts.map +0 -1
  92. package/dist/core-common/src/features/serial-queue.d.ts +0 -47
  93. package/dist/core-common/src/features/serial-queue.d.ts.map +0 -1
  94. package/dist/core-common/src/index.d.ts +0 -32
  95. package/dist/core-common/src/index.d.ts.map +0 -1
  96. package/dist/core-common/src/types/date-only.d.ts +0 -152
  97. package/dist/core-common/src/types/date-only.d.ts.map +0 -1
  98. package/dist/core-common/src/types/date-time.d.ts +0 -96
  99. package/dist/core-common/src/types/date-time.d.ts.map +0 -1
  100. package/dist/core-common/src/types/lazy-gc-map.d.ts +0 -80
  101. package/dist/core-common/src/types/lazy-gc-map.d.ts.map +0 -1
  102. package/dist/core-common/src/types/time.d.ts +0 -68
  103. package/dist/core-common/src/types/time.d.ts.map +0 -1
  104. package/dist/core-common/src/types/uuid.d.ts +0 -35
  105. package/dist/core-common/src/types/uuid.d.ts.map +0 -1
  106. package/dist/core-common/src/utils/bytes.d.ts +0 -51
  107. package/dist/core-common/src/utils/bytes.d.ts.map +0 -1
  108. package/dist/core-common/src/utils/date-format.d.ts +0 -90
  109. package/dist/core-common/src/utils/date-format.d.ts.map +0 -1
  110. package/dist/core-common/src/utils/json.d.ts +0 -34
  111. package/dist/core-common/src/utils/json.d.ts.map +0 -1
  112. package/dist/core-common/src/utils/num.d.ts +0 -60
  113. package/dist/core-common/src/utils/num.d.ts.map +0 -1
  114. package/dist/core-common/src/utils/obj.d.ts +0 -258
  115. package/dist/core-common/src/utils/obj.d.ts.map +0 -1
  116. package/dist/core-common/src/utils/path.d.ts +0 -23
  117. package/dist/core-common/src/utils/path.d.ts.map +0 -1
  118. package/dist/core-common/src/utils/primitive.d.ts +0 -18
  119. package/dist/core-common/src/utils/primitive.d.ts.map +0 -1
  120. package/dist/core-common/src/utils/str.d.ts +0 -103
  121. package/dist/core-common/src/utils/str.d.ts.map +0 -1
  122. package/dist/core-common/src/utils/template-strings.d.ts +0 -84
  123. package/dist/core-common/src/utils/template-strings.d.ts.map +0 -1
  124. package/dist/core-common/src/utils/transferable.d.ts +0 -47
  125. package/dist/core-common/src/utils/transferable.d.ts.map +0 -1
  126. package/dist/core-common/src/utils/wait.d.ts +0 -19
  127. package/dist/core-common/src/utils/wait.d.ts.map +0 -1
  128. package/dist/core-common/src/utils/xml.d.ts +0 -36
  129. package/dist/core-common/src/utils/xml.d.ts.map +0 -1
  130. package/dist/core-common/src/zip/sd-zip.d.ts +0 -80
  131. package/dist/core-common/src/zip/sd-zip.d.ts.map +0 -1
  132. package/dist/excel/src/excel-cell.d.ts.map +0 -1
  133. package/dist/excel/src/excel-col.d.ts.map +0 -1
  134. package/dist/excel/src/excel-row.d.ts.map +0 -1
  135. package/dist/excel/src/excel-workbook.d.ts.map +0 -1
  136. package/dist/excel/src/excel-worksheet.d.ts.map +0 -1
  137. package/dist/excel/src/excel-wrapper.d.ts.map +0 -1
  138. package/dist/excel/src/index.d.ts.map +0 -1
  139. package/dist/excel/src/types.d.ts.map +0 -1
  140. package/dist/excel/src/utils/excel-utils.d.ts.map +0 -1
  141. package/dist/excel/src/utils/zip-cache.d.ts.map +0 -1
  142. package/dist/excel/src/xml/excel-xml-content-type.d.ts.map +0 -1
  143. package/dist/excel/src/xml/excel-xml-drawing.d.ts.map +0 -1
  144. package/dist/excel/src/xml/excel-xml-relationship.d.ts.map +0 -1
  145. package/dist/excel/src/xml/excel-xml-shared-string.d.ts.map +0 -1
  146. package/dist/excel/src/xml/excel-xml-style.d.ts.map +0 -1
  147. package/dist/excel/src/xml/excel-xml-unknown.d.ts.map +0 -1
  148. package/dist/excel/src/xml/excel-xml-workbook.d.ts.map +0 -1
  149. package/dist/excel/src/xml/excel-xml-worksheet.d.ts.map +0 -1
  150. /package/dist/{excel/src/excel-cell.d.ts → excel-cell.d.ts} +0 -0
  151. /package/dist/{excel/src/excel-col.d.ts → excel-col.d.ts} +0 -0
  152. /package/dist/{excel/src/excel-row.d.ts → excel-row.d.ts} +0 -0
  153. /package/dist/{excel/src/excel-workbook.d.ts → excel-workbook.d.ts} +0 -0
  154. /package/dist/{excel/src/excel-worksheet.d.ts → excel-worksheet.d.ts} +0 -0
  155. /package/dist/{excel/src/excel-wrapper.d.ts → excel-wrapper.d.ts} +0 -0
  156. /package/dist/{excel/src/index.d.ts → index.d.ts} +0 -0
  157. /package/dist/{excel/src/types.d.ts → types.d.ts} +0 -0
  158. /package/dist/{excel/src/utils → utils}/excel-utils.d.ts +0 -0
  159. /package/dist/{excel/src/utils → utils}/zip-cache.d.ts +0 -0
  160. /package/dist/{excel/src/xml → xml}/excel-xml-content-type.d.ts +0 -0
  161. /package/dist/{excel/src/xml → xml}/excel-xml-drawing.d.ts +0 -0
  162. /package/dist/{excel/src/xml → xml}/excel-xml-relationship.d.ts +0 -0
  163. /package/dist/{excel/src/xml → xml}/excel-xml-shared-string.d.ts +0 -0
  164. /package/dist/{excel/src/xml → xml}/excel-xml-style.d.ts +0 -0
  165. /package/dist/{excel/src/xml → xml}/excel-xml-unknown.d.ts +0 -0
  166. /package/dist/{excel/src/xml → xml}/excel-xml-workbook.d.ts +0 -0
  167. /package/dist/{excel/src/xml → xml}/excel-xml-worksheet.d.ts +0 -0
@@ -0,0 +1,206 @@
1
+ import type { Bytes } from "@simplysm/core-common";
2
+ import { ExcelWorksheet } from "./excel-worksheet";
3
+ import { ZipCache } from "./utils/zip-cache";
4
+ import { ExcelXmlContentType } from "./xml/excel-xml-content-type";
5
+ import { ExcelXmlRelationship } from "./xml/excel-xml-relationship";
6
+ import type { ExcelXmlWorkbook } from "./xml/excel-xml-workbook";
7
+ import { ExcelXmlWorkbook as ExcelXmlWorkbookClass } from "./xml/excel-xml-workbook";
8
+ import { ExcelXmlWorksheet as ExcelXmlWorksheetClass } from "./xml/excel-xml-worksheet";
9
+
10
+ /**
11
+ * Excel 워크북 처리 클래스
12
+ *
13
+ * @remarks
14
+ * 이 클래스는 내부적으로 ZIP 리소스를 관리합니다.
15
+ * 사용 완료 후 반드시 리소스를 해제해야 합니다.
16
+ *
17
+ * ## 비동기 설계
18
+ *
19
+ * 대용량 Excel 파일의 메모리 효율성을 위해 Lazy Loading 구조를 채택합니다:
20
+ * - ZIP 파일 내부의 XML은 접근 시점에만 읽고 파싱한다
21
+ * - SharedStrings, Styles 등 대용량 XML은 필요할 때만 로드한다
22
+ * - 극단적 케이스(예: SharedStrings가 1TB인 파일에서 숫자 셀 하나만 읽기)에서도 메모리 효율적이다
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * // await using 사용 (권장)
27
+ * await using wb = new ExcelWorkbook(bytes);
28
+ * const ws = await wb.getWorksheet(0);
29
+ * // ... 작업 수행
30
+ * // 스코프 종료 시 자동으로 리소스 해제
31
+ *
32
+ * // 또는 try-finally 사용
33
+ * const wb = new ExcelWorkbook(bytes);
34
+ * try {
35
+ * const ws = await wb.getWorksheet(0);
36
+ * // ... 작업 수행
37
+ * } finally {
38
+ * await wb.close();
39
+ * }
40
+ * ```
41
+ */
42
+ export class ExcelWorkbook {
43
+ readonly zipCache: ZipCache;
44
+ private readonly _wsMap = new Map<number, ExcelWorksheet>();
45
+ private _isClosed = false;
46
+
47
+ /**
48
+ * @param arg 기존 Excel 파일 데이터 (Blob 또는 Uint8Array). 생략 시 새 워크북을 생성한다.
49
+ */
50
+ constructor(arg?: Blob | Bytes) {
51
+ if (arg != null) {
52
+ this.zipCache = new ZipCache(arg);
53
+ } else {
54
+ this.zipCache = new ZipCache();
55
+
56
+ // Global ContentTypes
57
+ const typeXml = new ExcelXmlContentType();
58
+ this.zipCache.set("[Content_Types].xml", typeXml);
59
+
60
+ // Global Rels
61
+ this.zipCache.set(
62
+ "_rels/.rels",
63
+ new ExcelXmlRelationship().add(
64
+ "xl/workbook.xml",
65
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
66
+ ),
67
+ );
68
+
69
+ // Workbook
70
+ const wbXml = new ExcelXmlWorkbookClass();
71
+ this.zipCache.set("xl/workbook.xml", wbXml);
72
+
73
+ // Workbook Rels
74
+ const wbRelXml = new ExcelXmlRelationship();
75
+ this.zipCache.set("xl/_rels/workbook.xml.rels", wbRelXml);
76
+ }
77
+ }
78
+
79
+ //#region Worksheet Methods
80
+
81
+ private _ensureNotClosed(): void {
82
+ if (this._isClosed) {
83
+ throw new Error("ExcelWorkbook이 이미 닫혔습니다. close() 호출 후에는 사용할 수 없습니다.");
84
+ }
85
+ }
86
+
87
+ /** 워크북의 모든 워크시트 이름을 반환 */
88
+ async getWorksheetNames(): Promise<string[]> {
89
+ this._ensureNotClosed();
90
+ const wbData = (await this.zipCache.get("xl/workbook.xml")) as ExcelXmlWorkbook;
91
+ return wbData.sheetNames;
92
+ }
93
+
94
+ /** 새 워크시트를 생성하고 반환 */
95
+ async createWorksheet(name: string): Promise<ExcelWorksheet> {
96
+ this._ensureNotClosed();
97
+ // Workbook
98
+ const wbXml = (await this.zipCache.get("xl/workbook.xml")) as ExcelXmlWorkbook;
99
+ const newWsRelId = wbXml.addWorksheet(name).lastWsRelId!;
100
+
101
+ // Content Types
102
+ const typeXml = (await this.zipCache.get("[Content_Types].xml")) as ExcelXmlContentType;
103
+ typeXml.add(
104
+ `/xl/worksheets/sheet${newWsRelId}.xml`,
105
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
106
+ );
107
+
108
+ // Workbook Rels
109
+ const wbRelXml = (await this.zipCache.get("xl/_rels/workbook.xml.rels")) as ExcelXmlRelationship;
110
+ wbRelXml.insert(
111
+ newWsRelId,
112
+ `worksheets/sheet${newWsRelId}.xml`,
113
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
114
+ );
115
+
116
+ // Worksheet
117
+ const wsXml = new ExcelXmlWorksheetClass();
118
+ this.zipCache.set(`xl/worksheets/sheet${newWsRelId}.xml`, wsXml);
119
+
120
+ const ws = new ExcelWorksheet(this.zipCache, newWsRelId, `sheet${newWsRelId}.xml`);
121
+ this._wsMap.set(newWsRelId, ws);
122
+ return ws;
123
+ }
124
+
125
+ /** 이름 또는 인덱스(0-based)로 워크시트를 조회 */
126
+ async getWorksheet(nameOrIndex: string | number): Promise<ExcelWorksheet> {
127
+ this._ensureNotClosed();
128
+ const wbData = (await this.zipCache.get("xl/workbook.xml")) as ExcelXmlWorkbook;
129
+ const wsId =
130
+ typeof nameOrIndex === "string" ? wbData.getWsRelIdByName(nameOrIndex) : wbData.getWsRelIdByIndex(nameOrIndex);
131
+
132
+ if (wsId === undefined) {
133
+ if (typeof nameOrIndex === "string") {
134
+ throw new Error(`시트명이 '${nameOrIndex}'인 시트를 찾을 수 없습니다.`);
135
+ } else {
136
+ throw new Error(`'${nameOrIndex}'번째 시트를 찾을 수 없습니다.`);
137
+ }
138
+ }
139
+
140
+ if (this._wsMap.has(wsId)) {
141
+ return this._wsMap.get(wsId)!;
142
+ }
143
+
144
+ const relData = (await this.zipCache.get("xl/_rels/workbook.xml.rels")) as ExcelXmlRelationship;
145
+ const targetFilePath = relData.getTargetByRelId(wsId);
146
+ if (targetFilePath == null) {
147
+ throw new Error(`시트 관계 정보를 찾을 수 없습니다: rId${wsId}`);
148
+ }
149
+
150
+ // path.basename 대신 직접 파일명 추출 (브라우저 호환성)
151
+ const fileName = targetFilePath.split("/").pop();
152
+ if (fileName == null) {
153
+ throw new Error(`시트 파일명을 추출할 수 없습니다: ${targetFilePath}`);
154
+ }
155
+
156
+ const ws = new ExcelWorksheet(this.zipCache, wsId, fileName);
157
+ this._wsMap.set(wsId, ws);
158
+ return ws;
159
+ }
160
+
161
+ //#endregion
162
+
163
+ //#region Export Methods
164
+
165
+ /** 워크북을 바이트 배열로 출력 */
166
+ async getBytes(): Promise<Bytes> {
167
+ this._ensureNotClosed();
168
+ return this.zipCache.toBytes();
169
+ }
170
+
171
+ /** 워크북을 Blob으로 출력 */
172
+ async getBlob(): Promise<Blob> {
173
+ this._ensureNotClosed();
174
+ const bytes = await this.zipCache.toBytes();
175
+ return new Blob([new Uint8Array(bytes)], {
176
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
177
+ });
178
+ }
179
+
180
+ //#endregion
181
+
182
+ //#region Lifecycle Methods
183
+
184
+ /**
185
+ * 워크북 리소스 해제
186
+ *
187
+ * @remarks
188
+ * ZIP 리더와 내부 캐시를 정리합니다.
189
+ * 호출 후에는 이 워크북 인스턴스를 사용할 수 없습니다.
190
+ * 이미 닫힌 워크북에 대해 호출해도 안전합니다 (no-op).
191
+ */
192
+ async close(): Promise<void> {
193
+ if (this._isClosed) {
194
+ return; // 이미 닫힌 경우 무시
195
+ }
196
+ this._isClosed = true;
197
+ this._wsMap.clear();
198
+ await this.zipCache.close();
199
+ }
200
+
201
+ async [Symbol.asyncDispose](): Promise<void> {
202
+ await this.close();
203
+ }
204
+
205
+ //#endregion
206
+ }
@@ -0,0 +1,380 @@
1
+ import type { Bytes } from "@simplysm/core-common";
2
+ import "@simplysm/core-common";
3
+ import { strIsNullOrEmpty } from "@simplysm/core-common";
4
+ import mime from "mime";
5
+ import type { ExcelCell } from "./excel-cell";
6
+ import { ExcelCol } from "./excel-col";
7
+ import { ExcelRow } from "./excel-row";
8
+ import type { ExcelAddressPoint, ExcelAddressRangePoint, ExcelValueType } from "./types";
9
+ import type { ZipCache } from "./utils/zip-cache";
10
+ import type { ExcelXmlContentType } from "./xml/excel-xml-content-type";
11
+ import { ExcelXmlDrawing } from "./xml/excel-xml-drawing";
12
+ import { ExcelXmlRelationship } from "./xml/excel-xml-relationship";
13
+ import type { ExcelXmlWorkbook } from "./xml/excel-xml-workbook";
14
+ import type { ExcelXmlWorksheet } from "./xml/excel-xml-worksheet";
15
+
16
+ /**
17
+ * Excel 워크시트를 나타내는 클래스.
18
+ * 셀 접근, 행/열 복사, 데이터 테이블 처리, 이미지 삽입 등의 기능을 제공한다.
19
+ */
20
+ export class ExcelWorksheet {
21
+ private readonly _rowMap = new Map<number, ExcelRow>();
22
+ private readonly _colMap = new Map<number, ExcelCol>();
23
+
24
+ constructor(
25
+ private readonly _zipCache: ZipCache,
26
+ private readonly _relId: number,
27
+ private readonly _targetFileName: string,
28
+ ) {}
29
+
30
+ //#region Name Methods
31
+
32
+ /** 워크시트 이름 반환 */
33
+ async getName(): Promise<string> {
34
+ const wbXmlData = await this._getWbData();
35
+ const name = wbXmlData.getWorksheetNameById(this._relId);
36
+ if (name == null) {
37
+ throw new Error(`워크시트 ID ${this._relId}에 해당하는 이름을 찾을 수 없습니다`);
38
+ }
39
+ return name;
40
+ }
41
+
42
+ /** 워크시트 이름 변경 */
43
+ async setName(newName: string): Promise<void> {
44
+ const wbXmlData = await this._getWbData();
45
+ wbXmlData.setWorksheetNameById(this._relId, newName);
46
+ }
47
+
48
+ //#endregion
49
+
50
+ //#region Cell Access Methods
51
+
52
+ /** 행 객체 반환 (0-based) */
53
+ row(r: number): ExcelRow {
54
+ return this._rowMap.getOrCreate(r, new ExcelRow(this._zipCache, this._targetFileName, r));
55
+ }
56
+
57
+ /** 셀 객체 반환 (0-based 행/열) */
58
+ cell(r: number, c: number): ExcelCell {
59
+ return this.row(r).cell(c);
60
+ }
61
+
62
+ /** 열 객체 반환 (0-based) */
63
+ col(c: number): ExcelCol {
64
+ return this._colMap.getOrCreate(c, new ExcelCol(this._zipCache, this._targetFileName, c));
65
+ }
66
+
67
+ //#endregion
68
+
69
+ //#region Copy Methods
70
+
71
+ /** 소스 행의 스타일을 타겟 행에 복사 */
72
+ async copyRowStyle(srcR: number, targetR: number): Promise<void> {
73
+ const range = await this.getRange();
74
+
75
+ for (let c = range.s.c; c <= range.e.c; c++) {
76
+ await this.copyCellStyle({ r: srcR, c: c }, { r: targetR, c: c });
77
+ }
78
+ }
79
+
80
+ /** 소스 셀의 스타일을 타겟 셀에 복사 */
81
+ async copyCellStyle(srcAddr: ExcelAddressPoint, targetAddr: ExcelAddressPoint): Promise<void> {
82
+ const wsData = await this._getWsData();
83
+
84
+ const styleId = wsData.getCellStyleId(srcAddr);
85
+ if (styleId != null) {
86
+ wsData.setCellStyleId(targetAddr, styleId);
87
+ }
88
+ }
89
+
90
+ /** 소스 행을 타겟 행에 복사 (덮어쓰기) */
91
+ async copyRow(srcR: number, targetR: number): Promise<void> {
92
+ const wsData = await this._getWsData();
93
+ wsData.copyRow(srcR, targetR);
94
+ }
95
+
96
+ /** 소스 셀을 타겟 셀에 복사 */
97
+ async copyCell(srcAddr: ExcelAddressPoint, targetAddr: ExcelAddressPoint): Promise<void> {
98
+ const wsData = await this._getWsData();
99
+ wsData.copyCell(srcAddr, targetAddr);
100
+ }
101
+
102
+ /**
103
+ * 소스 행을 타겟 위치에 삽입 복사.
104
+ * 타겟 위치 이하의 기존 행들은 한 칸씩 아래로 밀린다.
105
+ * @param srcR 복사할 소스 행 인덱스 (0-based)
106
+ * @param targetR 삽입할 타겟 행 인덱스 (0-based)
107
+ */
108
+ async insertCopyRow(srcR: number, targetR: number): Promise<void> {
109
+ const wsData = await this._getWsData();
110
+ const range = wsData.range;
111
+
112
+ // targetR 이하 모든 병합 셀의 행 인덱스 +1
113
+ const mergeCells = wsData.getMergeCells();
114
+ for (const mc of mergeCells) {
115
+ if (mc.s.r >= targetR) mc.s.r++;
116
+ if (mc.e.r >= targetR) mc.e.r++;
117
+ }
118
+
119
+ // srcR >= targetR인 경우, 밀림 후 srcR 위치가 변경되므로 보정
120
+ const adjustedSrcR = srcR >= targetR ? srcR + 1 : srcR;
121
+
122
+ for (let r = range.e.r; r >= targetR; r--) {
123
+ await this.copyRow(r, r + 1);
124
+ }
125
+
126
+ await this.copyRow(adjustedSrcR, targetR);
127
+ }
128
+
129
+ //#endregion
130
+
131
+ //#region Range Methods
132
+
133
+ /** 워크시트의 데이터 범위 반환 */
134
+ async getRange(): Promise<ExcelAddressRangePoint> {
135
+ const xml = await this._getWsData();
136
+ return xml.range;
137
+ }
138
+
139
+ /** 워크시트의 모든 셀을 2차원 배열로 반환 */
140
+ async getCells(): Promise<ExcelCell[][]> {
141
+ const xml = await this._getWsData();
142
+ const range = xml.range;
143
+ const promises: Promise<ExcelCell[]>[] = [];
144
+
145
+ for (let r = range.s.r; r <= range.e.r; r++) {
146
+ promises.push(this.row(r).getCells());
147
+ }
148
+
149
+ return Promise.all(promises);
150
+ }
151
+
152
+ //#endregion
153
+
154
+ //#region Data Methods
155
+
156
+ /**
157
+ * 워크시트 데이터를 테이블(레코드 배열) 형식으로 반환.
158
+ * @param opt.headerRowIndex 헤더 행 인덱스 (기본값: 첫 번째 행)
159
+ * @param opt.checkEndColIndex 데이터 종료를 판단할 열 인덱스. 해당 열이 비어있으면 데이터 끝으로 간주
160
+ * @param opt.usableHeaderNameFn 사용할 헤더를 필터링하는 함수
161
+ */
162
+ async getDataTable(opt?: {
163
+ headerRowIndex?: number;
164
+ checkEndColIndex?: number;
165
+ usableHeaderNameFn?: (headerName: string) => boolean;
166
+ }): Promise<Record<string, ExcelValueType>[]> {
167
+ const result: Record<string, ExcelValueType>[] = [];
168
+ const headerMap = new Map<string, number>();
169
+
170
+ const xml = await this._getWsData();
171
+ const range = xml.range;
172
+ const startRow = opt?.headerRowIndex ?? range.s.r;
173
+
174
+ for (let c = range.s.c; c <= range.e.c; c++) {
175
+ const headerName = await this.cell(startRow, c).getVal();
176
+ if (typeof headerName === "string") {
177
+ if (opt?.usableHeaderNameFn == null || opt.usableHeaderNameFn(headerName)) {
178
+ headerMap.set(headerName, c);
179
+ }
180
+ }
181
+ }
182
+
183
+ for (let r = startRow + 1; r <= range.e.r; r++) {
184
+ if (opt?.checkEndColIndex !== undefined && (await this.cell(r, opt.checkEndColIndex).getVal()) === undefined) {
185
+ break;
186
+ }
187
+
188
+ const record: Record<string, ExcelValueType> = {};
189
+ for (const header of headerMap.keys()) {
190
+ const c = headerMap.get(header)!;
191
+ record[header] = await this.cell(r, c).getVal();
192
+ }
193
+
194
+ result.push(record);
195
+ }
196
+
197
+ return result;
198
+ }
199
+
200
+ /**
201
+ * 2차원 배열 데이터를 워크시트에 기록
202
+ * @param matrix 2차원 배열 데이터 (행 우선, 0번 인덱스가 첫 번째 행)
203
+ */
204
+ async setDataMatrix(matrix: ExcelValueType[][]): Promise<void> {
205
+ for (let r = 0; r < matrix.length; r++) {
206
+ for (let c = 0; c < matrix[r].length; c++) {
207
+ await this.cell(r, c).setVal(matrix[r][c]);
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * 레코드 배열을 워크시트에 기록
214
+ * @param records 레코드 배열. 첫 행에 헤더가 자동 생성되고, 이후 행에 데이터가 기록된다.
215
+ */
216
+ async setRecords(records: Record<string, ExcelValueType>[]): Promise<void> {
217
+ const headers = records
218
+ .flatMap((item) => Object.keys(item))
219
+ .distinct()
220
+ .filter((item) => !strIsNullOrEmpty(item));
221
+
222
+ for (let c = 0; c < headers.length; c++) {
223
+ await this.cell(0, c).setVal(headers[c]);
224
+ }
225
+
226
+ for (let r = 1; r < records.length + 1; r++) {
227
+ for (let c = 0; c < headers.length; c++) {
228
+ await this.cell(r, c).setVal(records[r - 1][headers[c]]);
229
+ }
230
+ }
231
+ }
232
+
233
+ //#endregion
234
+
235
+ //#region View Methods
236
+
237
+ /** 워크시트 확대/축소 비율 설정 (퍼센트) */
238
+ async setZoom(percent: number): Promise<void> {
239
+ const wbXml = await this._getWbData();
240
+ wbXml.initializeView();
241
+
242
+ const wsXml = await this._getWsData();
243
+ wsXml.setZoom(percent);
244
+ }
245
+
246
+ /** 행/열 틀 고정 설정 */
247
+ async setFix(point: { r?: number; c?: number }): Promise<void> {
248
+ const wbXml = await this._getWbData();
249
+ wbXml.initializeView();
250
+
251
+ const wsXml = await this._getWsData();
252
+ wsXml.setFix(point);
253
+ }
254
+
255
+ //#endregion
256
+
257
+ //#region Image Methods
258
+
259
+ /**
260
+ * 워크시트에 이미지를 삽입.
261
+ * @param opts.bytes 이미지 바이너리 데이터
262
+ * @param opts.ext 이미지 확장자 (png, jpg 등)
263
+ * @param opts.from 이미지 시작 위치 (0-based 행/열 인덱스, rOff/cOff는 EMU 단위 오프셋)
264
+ * @param opts.to 이미지 끝 위치 (생략 시 from 위치에 원본 크기로 삽입)
265
+ */
266
+ async addImage(opts: {
267
+ bytes: Bytes;
268
+ ext: string;
269
+ from: { r: number; c: number; rOff?: number | string; cOff?: number | string };
270
+ to?: { r: number; c: number; rOff?: number | string; cOff?: number | string };
271
+ }): Promise<void> {
272
+ const mimeType = mime.getType(opts.ext);
273
+ if (mimeType == null) {
274
+ throw new Error(`확장자 '${opts.ext}'의 MIME 타입을 확인할 수 없습니다`);
275
+ }
276
+
277
+ // 1. media 파일명 결정 및 저장
278
+ let mediaIndex = 1;
279
+ while ((await this._zipCache.get(`xl/media/image${mediaIndex}.${opts.ext}`)) !== undefined) {
280
+ mediaIndex++;
281
+ }
282
+ const mediaPath = `xl/media/image${mediaIndex}.${opts.ext}`;
283
+ this._zipCache.set(mediaPath, opts.bytes);
284
+
285
+ // 2. [Content_Types].xml 갱신
286
+ const typeXml = (await this._zipCache.get("[Content_Types].xml")) as ExcelXmlContentType;
287
+ typeXml.add(`/xl/media/image${mediaIndex}.${opts.ext}`, mimeType);
288
+
289
+ // 3. worksheet의 기존 drawing 확인
290
+ const wsXml = await this._getWsData();
291
+ const sheetRelsPath = `xl/worksheets/_rels/${this._targetFileName}.rels`;
292
+ let sheetRels = (await this._zipCache.get(sheetRelsPath)) as ExcelXmlRelationship | undefined;
293
+
294
+ // 기존 drawing 찾기
295
+ let drawingIndex: number | undefined;
296
+ let drawingPath: string | undefined;
297
+ let drawing: ExcelXmlDrawing | undefined;
298
+ let drawingRels: ExcelXmlRelationship | undefined;
299
+
300
+ if (sheetRels != null) {
301
+ const existingDrawingRel = sheetRels.data.Relationships.Relationship?.find(
302
+ (r) => r.$.Type === "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
303
+ );
304
+ if (existingDrawingRel != null) {
305
+ // 기존 drawing 경로에서 인덱스 추출
306
+ const match = existingDrawingRel.$.Target.match(/drawing(\d+)\.xml$/);
307
+ if (match != null) {
308
+ drawingIndex = parseInt(match[1], 10);
309
+ drawingPath = `xl/drawings/drawing${drawingIndex}.xml`;
310
+ drawing = (await this._zipCache.get(drawingPath)) as ExcelXmlDrawing | undefined;
311
+ drawingRels = (await this._zipCache.get(`xl/drawings/_rels/drawing${drawingIndex}.xml.rels`)) as
312
+ | ExcelXmlRelationship
313
+ | undefined;
314
+ }
315
+ }
316
+ }
317
+
318
+ // 4. drawing이 없으면 새로 생성
319
+ if (drawingIndex == null || drawingPath == null || drawing == null) {
320
+ drawingIndex = 1;
321
+ while ((await this._zipCache.get(`xl/drawings/drawing${drawingIndex}.xml`)) !== undefined) {
322
+ drawingIndex++;
323
+ }
324
+ drawingPath = `xl/drawings/drawing${drawingIndex}.xml`;
325
+ drawing = new ExcelXmlDrawing();
326
+
327
+ // [Content_Types].xml에 drawing 타입 추가
328
+ typeXml.add("/" + drawingPath, "application/vnd.openxmlformats-officedocument.drawing+xml");
329
+
330
+ // worksheet의 rels에 drawing 추가
331
+ sheetRels = sheetRels ?? new ExcelXmlRelationship();
332
+ const sheetRelNum = sheetRels.addAndGetId(
333
+ `../drawings/drawing${drawingIndex}.xml`,
334
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
335
+ );
336
+ const drawingRelIdOnWorksheet = `rId${sheetRelNum}`;
337
+ this._zipCache.set(sheetRelsPath, sheetRels);
338
+
339
+ // worksheet XML에 drawing 추가
340
+ wsXml.data.worksheet.$["xmlns:r"] =
341
+ wsXml.data.worksheet.$["xmlns:r"] ?? "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
342
+ wsXml.data.worksheet.drawing = wsXml.data.worksheet.drawing ?? [];
343
+ wsXml.data.worksheet.drawing.push({ $: { "r:id": drawingRelIdOnWorksheet } });
344
+ this._zipCache.set(`xl/worksheets/${this._targetFileName}`, wsXml);
345
+ }
346
+
347
+ // 5. drawing rels 준비 (없으면 새로 생성)
348
+ drawingRels = drawingRels ?? new ExcelXmlRelationship();
349
+ const mediaFileName = mediaPath.slice(3);
350
+ const drawingTarget = `../${mediaFileName}`;
351
+ const relNum = drawingRels.addAndGetId(
352
+ drawingTarget,
353
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
354
+ );
355
+ this._zipCache.set(`xl/drawings/_rels/drawing${drawingIndex}.xml.rels`, drawingRels);
356
+
357
+ // 6. drawing에 이미지 추가
358
+ const blipRelId = `rId${relNum}`;
359
+ drawing.addPicture({
360
+ from: opts.from,
361
+ to: opts.to ?? { r: opts.from.r + 1, c: opts.from.c + 1 },
362
+ blipRelId: blipRelId,
363
+ });
364
+ this._zipCache.set(drawingPath, drawing);
365
+ }
366
+
367
+ //#endregion
368
+
369
+ //#region Private Methods
370
+
371
+ private async _getWsData(): Promise<ExcelXmlWorksheet> {
372
+ return (await this._zipCache.get(`xl/worksheets/${this._targetFileName}`)) as ExcelXmlWorksheet;
373
+ }
374
+
375
+ private async _getWbData(): Promise<ExcelXmlWorkbook> {
376
+ return (await this._zipCache.get("xl/workbook.xml")) as ExcelXmlWorkbook;
377
+ }
378
+
379
+ //#endregion
380
+ }