@simplysm/excel 13.0.69 → 13.0.71

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 (75) hide show
  1. package/README.md +19 -516
  2. package/dist/excel-cell.d.ts +28 -28
  3. package/dist/excel-cell.d.ts.map +1 -1
  4. package/dist/excel-cell.js +24 -24
  5. package/dist/excel-cell.js.map +1 -1
  6. package/dist/excel-col.d.ts +4 -4
  7. package/dist/excel-col.d.ts.map +1 -1
  8. package/dist/excel-col.js +3 -3
  9. package/dist/excel-row.d.ts +3 -3
  10. package/dist/excel-row.d.ts.map +1 -1
  11. package/dist/excel-row.js +2 -2
  12. package/dist/excel-workbook.d.ts +23 -23
  13. package/dist/excel-workbook.d.ts.map +1 -1
  14. package/dist/excel-workbook.js +15 -15
  15. package/dist/excel-workbook.js.map +1 -1
  16. package/dist/excel-worksheet.d.ts +32 -32
  17. package/dist/excel-worksheet.d.ts.map +1 -1
  18. package/dist/excel-worksheet.js +32 -32
  19. package/dist/excel-worksheet.js.map +1 -1
  20. package/dist/excel-wrapper.d.ts +7 -7
  21. package/dist/excel-wrapper.js +7 -7
  22. package/dist/excel-wrapper.js.map +1 -1
  23. package/dist/types.d.ts +16 -16
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/utils/excel-utils.d.ts +23 -23
  26. package/dist/utils/excel-utils.d.ts.map +1 -1
  27. package/dist/utils/excel-utils.js +25 -25
  28. package/dist/utils/excel-utils.js.map +1 -1
  29. package/dist/utils/zip-cache.d.ts +6 -6
  30. package/dist/xml/excel-xml-content-type.d.ts +2 -2
  31. package/dist/xml/excel-xml-drawing.d.ts +2 -2
  32. package/dist/xml/excel-xml-relationship.d.ts +2 -2
  33. package/dist/xml/excel-xml-relationship.js +1 -1
  34. package/dist/xml/excel-xml-relationship.js.map +1 -1
  35. package/dist/xml/excel-xml-shared-string.d.ts +2 -2
  36. package/dist/xml/excel-xml-style.d.ts +2 -2
  37. package/dist/xml/excel-xml-style.js +6 -6
  38. package/dist/xml/excel-xml-style.js.map +1 -1
  39. package/dist/xml/excel-xml-unknown.d.ts +2 -2
  40. package/dist/xml/excel-xml-workbook.d.ts +2 -2
  41. package/dist/xml/excel-xml-workbook.js +1 -1
  42. package/dist/xml/excel-xml-workbook.js.map +1 -1
  43. package/dist/xml/excel-xml-worksheet.d.ts +6 -6
  44. package/dist/xml/excel-xml-worksheet.d.ts.map +1 -1
  45. package/dist/xml/excel-xml-worksheet.js +9 -8
  46. package/dist/xml/excel-xml-worksheet.js.map +1 -1
  47. package/package.json +6 -5
  48. package/src/excel-cell.ts +35 -35
  49. package/src/excel-col.ts +4 -4
  50. package/src/excel-row.ts +3 -3
  51. package/src/excel-workbook.ts +30 -30
  52. package/src/excel-worksheet.ts +47 -47
  53. package/src/excel-wrapper.ts +18 -18
  54. package/src/index.ts +3 -3
  55. package/src/types.ts +15 -17
  56. package/src/utils/excel-utils.ts +38 -38
  57. package/src/utils/zip-cache.ts +6 -6
  58. package/src/xml/excel-xml-content-type.ts +3 -3
  59. package/src/xml/excel-xml-drawing.ts +2 -2
  60. package/src/xml/excel-xml-relationship.ts +3 -3
  61. package/src/xml/excel-xml-shared-string.ts +2 -2
  62. package/src/xml/excel-xml-style.ts +11 -11
  63. package/src/xml/excel-xml-unknown.ts +2 -2
  64. package/src/xml/excel-xml-workbook.ts +5 -5
  65. package/src/xml/excel-xml-worksheet.ts +44 -43
  66. package/tests/excel-cell.spec.ts +448 -0
  67. package/tests/excel-col.spec.ts +112 -0
  68. package/tests/excel-row.spec.ts +71 -0
  69. package/tests/excel-workbook.spec.ts +219 -0
  70. package/tests/excel-worksheet.spec.ts +389 -0
  71. package/tests/excel-wrapper.spec.ts +296 -0
  72. package/tests/fixtures/logo.png +0 -0
  73. package/tests/fixtures//354/264/210/352/270/260/355/231/224.xlsx +0 -0
  74. package/tests/image-insert.spec.ts +190 -0
  75. package/tests/utils/excel-utils.spec.ts +242 -0
package/src/excel-cell.ts CHANGED
@@ -18,23 +18,23 @@ import { ExcelXmlStyle as ExcelXmlStyleClass } from "./xml/excel-xml-style";
18
18
  import { ExcelUtils } from "./utils/excel-utils";
19
19
 
20
20
  /**
21
- * Excel 셀을 나타내는 클래스.
22
- * 읽기/쓰기, 수식 설정, 스타일 설정, 병합 등의 기능을 제공한다.
21
+ * Class representing an Excel cell.
22
+ * Provides value read/write, formula, style, and cell merge functionality.
23
23
  *
24
24
  * @remarks
25
- * ## 비동기 메서드 설계
25
+ * ## Async Method Design
26
26
  *
27
- * `getVal()`, `setVal()` 모든 셀 메서드가 `async`인 이유:
28
- * - 타입에 따라 필요한 XML만 선택적으로 로드한다
29
- * - 문자열 셀: SharedStrings.xml 로드
30
- * - 숫자 셀: SharedStrings 로드 안함
31
- * - 스타일이 있는 셀: Styles.xml 로드
27
+ * Why all cell methods like `getVal()`, `setVal()` are `async`:
28
+ * - Only the XML needed for the cell type is selectively loaded
29
+ * - String cell: loads SharedStrings.xml
30
+ * - Number cell: does not load SharedStrings
31
+ * - Styled cell: loads Styles.xml
32
32
  *
33
- * 어떤 셀을 읽을지 미리 없기 때문에 동기 구조로는 구현할 없다.
34
- * 동기 구조로 만들려면 모든 XML을 미리 로드해야 하므로 대용량 파일에서 메모리 문제가 발생한다.
33
+ * Since the cell to be read cannot be known in advance, a synchronous design is not feasible.
34
+ * A synchronous design would require preloading all XML, causing memory issues with large files.
35
35
  */
36
36
  export class ExcelCell {
37
- /** 주소 (0-based 행/열 인덱스) */
37
+ /** Cell address (0-based row/column index) */
38
38
  readonly addr: ExcelAddressPoint;
39
39
 
40
40
  constructor(
@@ -48,7 +48,7 @@ export class ExcelCell {
48
48
 
49
49
  //#region Value Methods
50
50
 
51
- /** 셀에 수식 설정 (undefined: 수식 삭제) */
51
+ /** Set formula on cell (undefined: remove formula) */
52
52
  async setFormula(val: string | undefined): Promise<void> {
53
53
  if (val === undefined) {
54
54
  await this._deleteCell(this.addr);
@@ -60,13 +60,13 @@ export class ExcelCell {
60
60
  }
61
61
  }
62
62
 
63
- /** 셀의 수식 반환 */
63
+ /** Return cell formula */
64
64
  async getFormula(): Promise<string | undefined> {
65
65
  const wsData = await this._getWsData();
66
66
  return wsData.getCellFormula(this.addr);
67
67
  }
68
68
 
69
- /** 설정 (undefined: 삭제) */
69
+ /** Set cell value (undefined: delete cell) */
70
70
  async setVal(val: ExcelValueType): Promise<void> {
71
71
  if (val === undefined) {
72
72
  await this._deleteCell(this.addr);
@@ -102,12 +102,12 @@ export class ExcelCell {
102
102
  });
103
103
  } else {
104
104
  throw new Error(
105
- `[${ExcelUtils.stringifyAddr(this.addr)}] 지원되지 않는 타입입니다: ${typeof val}`,
105
+ `[${ExcelUtils.stringifyAddr(this.addr)}] Unsupported type: ${typeof val}`,
106
106
  );
107
107
  }
108
108
  }
109
109
 
110
- /** 반환 */
110
+ /** Return cell value */
111
111
  async getVal(): Promise<ExcelValueType> {
112
112
  const wsData = await this._getWsData();
113
113
  const cellVal = wsData.getCellVal(this.addr);
@@ -121,7 +121,7 @@ export class ExcelCell {
121
121
  const ssId = numParseInt(cellVal);
122
122
  if (ssId == null) {
123
123
  throw new Error(
124
- `[${ExcelUtils.stringifyAddr(this.addr)}] SharedString ID 파싱 실패: ${cellVal}`,
124
+ `[${ExcelUtils.stringifyAddr(this.addr)}] Failed to parse SharedString ID: ${cellVal}`,
125
125
  );
126
126
  }
127
127
  return ssData.getStringById(ssId);
@@ -135,10 +135,10 @@ export class ExcelCell {
135
135
  return parseFloat(cellVal);
136
136
  } else if (cellType === "e") {
137
137
  throw new Error(
138
- `[${ExcelUtils.stringifyAddr(this.addr)}] 타입 분석 실패: 셀에 에러 값이 포함되어 있습니다 (${cellVal})`,
138
+ `[${ExcelUtils.stringifyAddr(this.addr)}] Cell type analysis failed: cell contains error value (${cellVal})`,
139
139
  );
140
140
  } else {
141
- // cellType === undefined: 숫자 또는 날짜/시간 타입
141
+ // cellType === undefined: number or date/time type
142
142
  const cellStyleId = wsData.getCellStyleId(this.addr);
143
143
  if (cellStyleId === undefined) {
144
144
  return parseFloat(cellVal);
@@ -162,7 +162,7 @@ export class ExcelCell {
162
162
  const numFmtIdNum = numParseInt(numFmtId);
163
163
  if (numFmtIdNum == null) {
164
164
  throw new Error(
165
- `[${ExcelUtils.stringifyAddr(this.addr)}] numFmtId 파싱 실패: ${numFmtId}`,
165
+ `[${ExcelUtils.stringifyAddr(this.addr)}] Failed to parse numFmtId: ${numFmtId}`,
166
166
  );
167
167
  }
168
168
  numFmt = ExcelUtils.convertNumFmtIdToName(numFmtIdNum);
@@ -177,7 +177,7 @@ export class ExcelCell {
177
177
  const dateNum = numParseFloat(cellVal);
178
178
  if (dateNum == null) {
179
179
  throw new Error(
180
- `[${ExcelUtils.stringifyAddr(this.addr)}] 날짜 숫자 파싱 실패: ${cellVal}`,
180
+ `[${ExcelUtils.stringifyAddr(this.addr)}] Failed to parse date number: ${cellVal}`,
181
181
  );
182
182
  }
183
183
  const tick = ExcelUtils.convertNumberToTimeTick(dateNum);
@@ -197,11 +197,11 @@ export class ExcelCell {
197
197
  //#region Merge Methods
198
198
 
199
199
  /**
200
- * 현재 셀부터 지정된 좌표까지 병합
201
- * @param r 병합 인덱스 (0-based)
202
- * @param c 병합 인덱스 (0-based)
200
+ * Merge cells from current cell to the specified end coordinates
201
+ * @param r End row index of merge (0-based)
202
+ * @param c End column index of merge (0-based)
203
203
  * @example
204
- * // A1 셀에서 호출하면 A1:C3 범위 (3 x 3)를 병합
204
+ * // Called from cell A1, merges range A1:C3 (3 rows x 3 columns)
205
205
  * await ws.cell(0, 0).merge(2, 2);
206
206
  */
207
207
  async merge(r: number, c: number): Promise<void> {
@@ -213,33 +213,33 @@ export class ExcelCell {
213
213
 
214
214
  //#region Style Methods
215
215
 
216
- /** 셀의 스타일 ID 반환 */
216
+ /** Return cell style ID */
217
217
  async getStyleId(): Promise<string | undefined> {
218
218
  const wsData = await this._getWsData();
219
219
  return wsData.getCellStyleId(this.addr);
220
220
  }
221
221
 
222
- /** 셀의 스타일 ID 설정 */
222
+ /** Set cell style ID */
223
223
  async setStyleId(styleId: string | undefined): Promise<void> {
224
224
  const wsData = await this._getWsData();
225
225
  wsData.setCellStyleId(this.addr, styleId);
226
226
  }
227
227
 
228
228
  /**
229
- * 스타일 설정
230
- * @param opts 스타일 옵션
231
- * @param opts.background 배경색 (ARGB 형식, 8자리 16진수. 예: "FFFF0000")
232
- * @param opts.border 테두리 위치 배열 (예: ["left", "right", "top", "bottom"])
233
- * @param opts.horizontalAlign 가로 정렬 ("left", "center", "right")
234
- * @param opts.verticalAlign 세로 정렬 ("top", "center", "bottom")
235
- * @param opts.numberFormat 숫자 형식 ("number", "DateOnly", "DateTime", "Time", "string")
229
+ * Set cell style
230
+ * @param opts Style options
231
+ * @param opts.background Background color (ARGB format, 8-digit hex. e.g. "FFFF0000")
232
+ * @param opts.border Border position array (e.g. ["left", "right", "top", "bottom"])
233
+ * @param opts.horizontalAlign Horizontal alignment ("left", "center", "right")
234
+ * @param opts.verticalAlign Vertical alignment ("top", "center", "bottom")
235
+ * @param opts.numberFormat Number format ("number", "DateOnly", "DateTime", "Time", "string")
236
236
  */
237
237
  async setStyle(opts: ExcelStyleOptions): Promise<void> {
238
238
  const style: ExcelStyle = {};
239
239
 
240
240
  if (opts.background != null) {
241
241
  if (!/^[0-9A-F]{8}$/i.test(opts.background)) {
242
- throw new Error("색상 형식이 잘못되었습니다. (형식: 00000000: alpha()+rgb)");
242
+ throw new Error("Invalid color format. (Format: 00000000: alpha(reversed)+rgb)");
243
243
  }
244
244
  style.background = opts.background;
245
245
  }
package/src/excel-col.ts CHANGED
@@ -3,7 +3,7 @@ import { ExcelCell } from "./excel-cell";
3
3
  import type { ExcelXmlWorksheet } from "./xml/excel-xml-worksheet";
4
4
  import type { ZipCache } from "./utils/zip-cache";
5
5
 
6
- /** Excel 워크시트의 열을 나타내는 클래스. 접근 너비 설정 기능을 제공한다. */
6
+ /** Class representing a column in an Excel worksheet. Provides cell access and column width configuration. */
7
7
  export class ExcelCol {
8
8
  private readonly _cellMap = new Map<number, ExcelCell>();
9
9
 
@@ -13,7 +13,7 @@ export class ExcelCol {
13
13
  private readonly _c: number,
14
14
  ) {}
15
15
 
16
- /** 인덱스에 해당하는 반환 (0-based) */
16
+ /** Return cell at the given row index (0-based) */
17
17
  cell(r: number): ExcelCell {
18
18
  return this._cellMap.getOrCreate(
19
19
  r,
@@ -21,7 +21,7 @@ export class ExcelCol {
21
21
  );
22
22
  }
23
23
 
24
- /** 열의 모든 반환 */
24
+ /** Return all cells in the column */
25
25
  async getCells(): Promise<ExcelCell[]> {
26
26
  const result: ExcelCell[] = [];
27
27
  const wsData = await this._getWsData();
@@ -34,7 +34,7 @@ export class ExcelCol {
34
34
  return result;
35
35
  }
36
36
 
37
- /** 너비 설정 */
37
+ /** Set column width */
38
38
  async setWidth(size: number): Promise<void> {
39
39
  const wsData = await this._getWsData();
40
40
  wsData.setColWidth((this._c + 1).toString(), size.toString());
package/src/excel-row.ts CHANGED
@@ -3,7 +3,7 @@ import { ExcelCell } from "./excel-cell";
3
3
  import type { ExcelXmlWorksheet } from "./xml/excel-xml-worksheet";
4
4
  import type { ZipCache } from "./utils/zip-cache";
5
5
 
6
- /** Excel 워크시트의 행을 나타내는 클래스. 접근 기능을 제공한다. */
6
+ /** Class representing a row in an Excel worksheet. Provides cell access functionality. */
7
7
  export class ExcelRow {
8
8
  private readonly _cellMap = new Map<number, ExcelCell>();
9
9
 
@@ -13,7 +13,7 @@ export class ExcelRow {
13
13
  private readonly _r: number,
14
14
  ) {}
15
15
 
16
- /** 인덱스에 해당하는 반환 (0-based) */
16
+ /** Return cell at the given column index (0-based) */
17
17
  cell(c: number): ExcelCell {
18
18
  return this._cellMap.getOrCreate(
19
19
  c,
@@ -21,7 +21,7 @@ export class ExcelRow {
21
21
  );
22
22
  }
23
23
 
24
- /** 행의 모든 반환 */
24
+ /** Return all cells in the row */
25
25
  async getCells(): Promise<ExcelCell[]> {
26
26
  const result: ExcelCell[] = [];
27
27
  const wsData = await this._getWsData();
@@ -8,32 +8,32 @@ import { ExcelXmlWorkbook as ExcelXmlWorkbookClass } from "./xml/excel-xml-workb
8
8
  import { ExcelXmlWorksheet as ExcelXmlWorksheetClass } from "./xml/excel-xml-worksheet";
9
9
 
10
10
  /**
11
- * Excel 워크북 처리 클래스
11
+ * Excel workbook processing class
12
12
  *
13
13
  * @remarks
14
- * 클래스는 내부적으로 ZIP 리소스를 관리합니다.
15
- * 사용 완료 반드시 리소스를 해제해야 합니다.
14
+ * This class internally manages ZIP resources.
15
+ * Resources must be released after use.
16
16
  *
17
- * ## 비동기 설계
17
+ * ## Async Design
18
18
  *
19
- * 대용량 Excel 파일의 메모리 효율성을 위해 Lazy Loading 구조를 채택합니다:
20
- * - ZIP 파일 내부의 XML은 접근 시점에만 읽고 파싱한다
21
- * - SharedStrings, Styles 대용량 XML은 필요할 때만 로드한다
22
- * - 극단적 케이스(예: SharedStrings가 1TB인 파일에서 숫자 하나만 읽기)에서도 메모리 효율적이다
19
+ * Adopts a Lazy Loading architecture for memory efficiency with large Excel files:
20
+ * - XML inside the ZIP is read and parsed only at the point of access
21
+ * - Large XML such as SharedStrings and Styles is loaded only when needed
22
+ * - Memory efficient even in extreme cases (e.g., reading a single number cell from a file with 1TB SharedStrings)
23
23
  *
24
24
  * @example
25
25
  * ```typescript
26
- * // await using 사용 (권장)
26
+ * // Using await using (recommended)
27
27
  * await using wb = new ExcelWorkbook(bytes);
28
28
  * const ws = await wb.getWorksheet(0);
29
- * // ... 작업 수행
30
- * // 스코프 종료 자동으로 리소스 해제
29
+ * // ... perform operations
30
+ * // Resources automatically released at scope exit
31
31
  *
32
- * // 또는 try-finally 사용
32
+ * // Or using try-finally
33
33
  * const wb = new ExcelWorkbook(bytes);
34
34
  * try {
35
35
  * const ws = await wb.getWorksheet(0);
36
- * // ... 작업 수행
36
+ * // ... perform operations
37
37
  * } finally {
38
38
  * await wb.close();
39
39
  * }
@@ -45,7 +45,7 @@ export class ExcelWorkbook {
45
45
  private _isClosed = false;
46
46
 
47
47
  /**
48
- * @param arg 기존 Excel 파일 데이터 (Blob 또는 Uint8Array). 생략 워크북을 생성한다.
48
+ * @param arg Existing Excel file data (Blob or Uint8Array). Creates a new workbook if omitted.
49
49
  */
50
50
  constructor(arg?: Blob | Bytes) {
51
51
  if (arg != null) {
@@ -80,18 +80,18 @@ export class ExcelWorkbook {
80
80
 
81
81
  private _ensureNotClosed(): void {
82
82
  if (this._isClosed) {
83
- throw new Error("ExcelWorkbook 이미 닫혔습니다. close() 호출 후에는 사용할 없습니다.");
83
+ throw new Error("ExcelWorkbook is already closed. Cannot use after calling close().");
84
84
  }
85
85
  }
86
86
 
87
- /** 워크북의 모든 워크시트 이름을 반환 */
87
+ /** Return all worksheet names in the workbook */
88
88
  async getWorksheetNames(): Promise<string[]> {
89
89
  this._ensureNotClosed();
90
90
  const wbData = (await this.zipCache.get("xl/workbook.xml")) as ExcelXmlWorkbook;
91
91
  return wbData.sheetNames;
92
92
  }
93
93
 
94
- /** 워크시트를 생성하고 반환 */
94
+ /** Create and return a new worksheet */
95
95
  async createWorksheet(name: string): Promise<ExcelWorksheet> {
96
96
  this._ensureNotClosed();
97
97
  // Workbook
@@ -124,7 +124,7 @@ export class ExcelWorkbook {
124
124
  return ws;
125
125
  }
126
126
 
127
- /** 이름 또는 인덱스(0-based) 워크시트를 조회 */
127
+ /** Look up a worksheet by name or index (0-based) */
128
128
  async getWorksheet(nameOrIndex: string | number): Promise<ExcelWorksheet> {
129
129
  this._ensureNotClosed();
130
130
  const wbData = (await this.zipCache.get("xl/workbook.xml")) as ExcelXmlWorkbook;
@@ -135,9 +135,9 @@ export class ExcelWorkbook {
135
135
 
136
136
  if (wsId === undefined) {
137
137
  if (typeof nameOrIndex === "string") {
138
- throw new Error(`시트명이 '${nameOrIndex}'인 시트를 찾을 수 없습니다.`);
138
+ throw new Error(`Cannot find sheet with name '${nameOrIndex}'.`);
139
139
  } else {
140
- throw new Error(`'${nameOrIndex}'번째 시트를 찾을 수 없습니다.`);
140
+ throw new Error(`Cannot find sheet at index '${nameOrIndex}'.`);
141
141
  }
142
142
  }
143
143
 
@@ -148,13 +148,13 @@ export class ExcelWorkbook {
148
148
  const relData = (await this.zipCache.get("xl/_rels/workbook.xml.rels")) as ExcelXmlRelationship;
149
149
  const targetFilePath = relData.getTargetByRelId(wsId);
150
150
  if (targetFilePath == null) {
151
- throw new Error(`시트 관계 정보를 찾을 없습니다: rId${wsId}`);
151
+ throw new Error(`Cannot find sheet relationship information: rId${wsId}`);
152
152
  }
153
153
 
154
- // path.basename 대신 직접 파일명 추출 (브라우저 호환성)
154
+ // Extract filename directly instead of path.basename (browser compatibility)
155
155
  const fileName = targetFilePath.split("/").pop();
156
156
  if (fileName == null) {
157
- throw new Error(`시트 파일명을 추출할 없습니다: ${targetFilePath}`);
157
+ throw new Error(`Cannot extract sheet filename: ${targetFilePath}`);
158
158
  }
159
159
 
160
160
  const ws = new ExcelWorksheet(this.zipCache, wsId, fileName);
@@ -166,13 +166,13 @@ export class ExcelWorkbook {
166
166
 
167
167
  //#region Export Methods
168
168
 
169
- /** 워크북을 바이트 배열로 출력 */
169
+ /** Export workbook as byte array */
170
170
  async getBytes(): Promise<Bytes> {
171
171
  this._ensureNotClosed();
172
172
  return this.zipCache.toBytes();
173
173
  }
174
174
 
175
- /** 워크북을 Blob으로 출력 */
175
+ /** Export workbook as Blob */
176
176
  async getBlob(): Promise<Blob> {
177
177
  this._ensureNotClosed();
178
178
  const bytes = await this.zipCache.toBytes();
@@ -186,16 +186,16 @@ export class ExcelWorkbook {
186
186
  //#region Lifecycle Methods
187
187
 
188
188
  /**
189
- * 워크북 리소스 해제
189
+ * Release workbook resources
190
190
  *
191
191
  * @remarks
192
- * ZIP 리더와 내부 캐시를 정리합니다.
193
- * 호출 후에는 워크북 인스턴스를 사용할 없습니다.
194
- * 이미 닫힌 워크북에 대해 호출해도 안전합니다 (no-op).
192
+ * Cleans up the ZIP reader and internal cache.
193
+ * The workbook instance cannot be used after this call.
194
+ * Safe to call on an already closed workbook (no-op).
195
195
  */
196
196
  async close(): Promise<void> {
197
197
  if (this._isClosed) {
198
- return; // 이미 닫힌 경우 무시
198
+ return; // Ignore if already closed
199
199
  }
200
200
  this._isClosed = true;
201
201
  this._wsMap.clear();