@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.
- package/README.md +15 -0
- package/dist/excel-cell.d.ts.map +1 -0
- package/dist/excel-cell.js +3 -3
- package/dist/excel-cell.js.map +0 -1
- package/dist/excel-col.d.ts.map +1 -0
- package/dist/excel-col.js +1 -1
- package/dist/excel-col.js.map +0 -1
- package/dist/excel-row.d.ts.map +1 -0
- package/dist/excel-row.js +1 -1
- package/dist/excel-row.js.map +0 -1
- package/dist/excel-workbook.d.ts.map +1 -0
- package/dist/excel-workbook.js +6 -6
- package/dist/excel-workbook.js.map +0 -1
- package/dist/excel-worksheet.d.ts.map +1 -0
- package/dist/excel-worksheet.js +4 -4
- package/dist/excel-worksheet.js.map +0 -1
- package/dist/excel-wrapper.d.ts.map +1 -0
- package/dist/excel-wrapper.js +1 -1
- package/dist/excel-wrapper.js.map +0 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -8
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js.map +0 -1
- package/dist/utils/excel-utils.d.ts.map +1 -0
- package/dist/utils/excel-utils.js.map +0 -1
- package/dist/utils/zip-cache.d.ts.map +1 -0
- package/dist/utils/zip-cache.js +8 -8
- package/dist/utils/zip-cache.js.map +0 -1
- package/dist/xml/excel-xml-content-type.d.ts.map +1 -0
- package/dist/xml/excel-xml-content-type.js.map +0 -1
- package/dist/xml/excel-xml-drawing.d.ts.map +1 -0
- package/dist/xml/excel-xml-drawing.js.map +0 -1
- package/dist/xml/excel-xml-relationship.d.ts.map +1 -0
- package/dist/xml/excel-xml-relationship.js.map +0 -1
- package/dist/xml/excel-xml-shared-string.d.ts.map +1 -0
- package/dist/xml/excel-xml-shared-string.js.map +0 -1
- package/dist/xml/excel-xml-style.d.ts.map +1 -0
- package/dist/xml/excel-xml-style.js.map +0 -1
- package/dist/xml/excel-xml-unknown.d.ts.map +1 -0
- package/dist/xml/excel-xml-unknown.js.map +0 -1
- package/dist/xml/excel-xml-workbook.d.ts.map +1 -0
- package/dist/xml/excel-xml-workbook.js.map +0 -1
- package/dist/xml/excel-xml-worksheet.d.ts.map +1 -0
- package/dist/xml/excel-xml-worksheet.js +1 -1
- package/dist/xml/excel-xml-worksheet.js.map +0 -1
- package/package.json +6 -5
- package/src/excel-cell.ts +326 -0
- package/src/excel-col.ts +43 -0
- package/src/excel-row.ts +37 -0
- package/src/excel-workbook.ts +206 -0
- package/src/excel-worksheet.ts +380 -0
- package/src/excel-wrapper.ts +219 -0
- package/src/index.ts +13 -0
- package/src/types.ts +396 -0
- package/src/utils/excel-utils.ts +201 -0
- package/src/utils/zip-cache.ts +103 -0
- package/src/xml/excel-xml-content-type.ts +64 -0
- package/src/xml/excel-xml-drawing.ts +87 -0
- package/src/xml/excel-xml-relationship.ts +86 -0
- package/src/xml/excel-xml-shared-string.ts +80 -0
- package/src/xml/excel-xml-style.ts +393 -0
- package/src/xml/excel-xml-unknown.ts +11 -0
- package/src/xml/excel-xml-workbook.ts +112 -0
- package/src/xml/excel-xml-worksheet.ts +544 -0
- package/dist/core-common/src/common.types.d.ts +0 -74
- package/dist/core-common/src/common.types.d.ts.map +0 -1
- package/dist/core-common/src/env.d.ts +0 -6
- package/dist/core-common/src/env.d.ts.map +0 -1
- package/dist/core-common/src/errors/argument-error.d.ts +0 -25
- package/dist/core-common/src/errors/argument-error.d.ts.map +0 -1
- package/dist/core-common/src/errors/not-implemented-error.d.ts +0 -29
- package/dist/core-common/src/errors/not-implemented-error.d.ts.map +0 -1
- package/dist/core-common/src/errors/sd-error.d.ts +0 -27
- package/dist/core-common/src/errors/sd-error.d.ts.map +0 -1
- package/dist/core-common/src/errors/timeout-error.d.ts +0 -31
- package/dist/core-common/src/errors/timeout-error.d.ts.map +0 -1
- package/dist/core-common/src/extensions/arr-ext.d.ts +0 -15
- package/dist/core-common/src/extensions/arr-ext.d.ts.map +0 -1
- package/dist/core-common/src/extensions/arr-ext.helpers.d.ts +0 -19
- package/dist/core-common/src/extensions/arr-ext.helpers.d.ts.map +0 -1
- package/dist/core-common/src/extensions/arr-ext.types.d.ts +0 -215
- package/dist/core-common/src/extensions/arr-ext.types.d.ts.map +0 -1
- package/dist/core-common/src/extensions/map-ext.d.ts +0 -57
- package/dist/core-common/src/extensions/map-ext.d.ts.map +0 -1
- package/dist/core-common/src/extensions/set-ext.d.ts +0 -36
- package/dist/core-common/src/extensions/set-ext.d.ts.map +0 -1
- package/dist/core-common/src/features/debounce-queue.d.ts +0 -53
- package/dist/core-common/src/features/debounce-queue.d.ts.map +0 -1
- package/dist/core-common/src/features/event-emitter.d.ts +0 -66
- package/dist/core-common/src/features/event-emitter.d.ts.map +0 -1
- package/dist/core-common/src/features/serial-queue.d.ts +0 -47
- package/dist/core-common/src/features/serial-queue.d.ts.map +0 -1
- package/dist/core-common/src/index.d.ts +0 -32
- package/dist/core-common/src/index.d.ts.map +0 -1
- package/dist/core-common/src/types/date-only.d.ts +0 -152
- package/dist/core-common/src/types/date-only.d.ts.map +0 -1
- package/dist/core-common/src/types/date-time.d.ts +0 -96
- package/dist/core-common/src/types/date-time.d.ts.map +0 -1
- package/dist/core-common/src/types/lazy-gc-map.d.ts +0 -80
- package/dist/core-common/src/types/lazy-gc-map.d.ts.map +0 -1
- package/dist/core-common/src/types/time.d.ts +0 -68
- package/dist/core-common/src/types/time.d.ts.map +0 -1
- package/dist/core-common/src/types/uuid.d.ts +0 -35
- package/dist/core-common/src/types/uuid.d.ts.map +0 -1
- package/dist/core-common/src/utils/bytes.d.ts +0 -51
- package/dist/core-common/src/utils/bytes.d.ts.map +0 -1
- package/dist/core-common/src/utils/date-format.d.ts +0 -90
- package/dist/core-common/src/utils/date-format.d.ts.map +0 -1
- package/dist/core-common/src/utils/json.d.ts +0 -34
- package/dist/core-common/src/utils/json.d.ts.map +0 -1
- package/dist/core-common/src/utils/num.d.ts +0 -60
- package/dist/core-common/src/utils/num.d.ts.map +0 -1
- package/dist/core-common/src/utils/obj.d.ts +0 -258
- package/dist/core-common/src/utils/obj.d.ts.map +0 -1
- package/dist/core-common/src/utils/path.d.ts +0 -23
- package/dist/core-common/src/utils/path.d.ts.map +0 -1
- package/dist/core-common/src/utils/primitive.d.ts +0 -18
- package/dist/core-common/src/utils/primitive.d.ts.map +0 -1
- package/dist/core-common/src/utils/str.d.ts +0 -103
- package/dist/core-common/src/utils/str.d.ts.map +0 -1
- package/dist/core-common/src/utils/template-strings.d.ts +0 -84
- package/dist/core-common/src/utils/template-strings.d.ts.map +0 -1
- package/dist/core-common/src/utils/transferable.d.ts +0 -47
- package/dist/core-common/src/utils/transferable.d.ts.map +0 -1
- package/dist/core-common/src/utils/wait.d.ts +0 -19
- package/dist/core-common/src/utils/wait.d.ts.map +0 -1
- package/dist/core-common/src/utils/xml.d.ts +0 -36
- package/dist/core-common/src/utils/xml.d.ts.map +0 -1
- package/dist/core-common/src/zip/sd-zip.d.ts +0 -80
- package/dist/core-common/src/zip/sd-zip.d.ts.map +0 -1
- package/dist/excel/src/excel-cell.d.ts.map +0 -1
- package/dist/excel/src/excel-col.d.ts.map +0 -1
- package/dist/excel/src/excel-row.d.ts.map +0 -1
- package/dist/excel/src/excel-workbook.d.ts.map +0 -1
- package/dist/excel/src/excel-worksheet.d.ts.map +0 -1
- package/dist/excel/src/excel-wrapper.d.ts.map +0 -1
- package/dist/excel/src/index.d.ts.map +0 -1
- package/dist/excel/src/types.d.ts.map +0 -1
- package/dist/excel/src/utils/excel-utils.d.ts.map +0 -1
- package/dist/excel/src/utils/zip-cache.d.ts.map +0 -1
- package/dist/excel/src/xml/excel-xml-content-type.d.ts.map +0 -1
- package/dist/excel/src/xml/excel-xml-drawing.d.ts.map +0 -1
- package/dist/excel/src/xml/excel-xml-relationship.d.ts.map +0 -1
- package/dist/excel/src/xml/excel-xml-shared-string.d.ts.map +0 -1
- package/dist/excel/src/xml/excel-xml-style.d.ts.map +0 -1
- package/dist/excel/src/xml/excel-xml-unknown.d.ts.map +0 -1
- package/dist/excel/src/xml/excel-xml-workbook.d.ts.map +0 -1
- package/dist/excel/src/xml/excel-xml-worksheet.d.ts.map +0 -1
- /package/dist/{excel/src/excel-cell.d.ts → excel-cell.d.ts} +0 -0
- /package/dist/{excel/src/excel-col.d.ts → excel-col.d.ts} +0 -0
- /package/dist/{excel/src/excel-row.d.ts → excel-row.d.ts} +0 -0
- /package/dist/{excel/src/excel-workbook.d.ts → excel-workbook.d.ts} +0 -0
- /package/dist/{excel/src/excel-worksheet.d.ts → excel-worksheet.d.ts} +0 -0
- /package/dist/{excel/src/excel-wrapper.d.ts → excel-wrapper.d.ts} +0 -0
- /package/dist/{excel/src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{excel/src/types.d.ts → types.d.ts} +0 -0
- /package/dist/{excel/src/utils → utils}/excel-utils.d.ts +0 -0
- /package/dist/{excel/src/utils → utils}/zip-cache.d.ts +0 -0
- /package/dist/{excel/src/xml → xml}/excel-xml-content-type.d.ts +0 -0
- /package/dist/{excel/src/xml → xml}/excel-xml-drawing.d.ts +0 -0
- /package/dist/{excel/src/xml → xml}/excel-xml-relationship.d.ts +0 -0
- /package/dist/{excel/src/xml → xml}/excel-xml-shared-string.d.ts +0 -0
- /package/dist/{excel/src/xml → xml}/excel-xml-style.d.ts +0 -0
- /package/dist/{excel/src/xml → xml}/excel-xml-unknown.d.ts +0 -0
- /package/dist/{excel/src/xml → xml}/excel-xml-workbook.d.ts +0 -0
- /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
|
+
}
|