@simplysm/excel 13.0.99 → 14.0.1
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/dist/excel-cell.d.ts +28 -28
- package/dist/excel-cell.d.ts.map +1 -1
- package/dist/excel-cell.js +273 -264
- package/dist/excel-cell.js.map +1 -6
- package/dist/excel-col.d.ts +4 -4
- package/dist/excel-col.d.ts.map +1 -1
- package/dist/excel-col.js +33 -35
- package/dist/excel-col.js.map +1 -6
- package/dist/excel-row.d.ts +3 -3
- package/dist/excel-row.d.ts.map +1 -1
- package/dist/excel-row.js +28 -30
- package/dist/excel-row.js.map +1 -6
- package/dist/excel-workbook.d.ts +23 -23
- package/dist/excel-workbook.d.ts.map +1 -1
- package/dist/excel-workbook.js +151 -125
- package/dist/excel-workbook.js.map +1 -6
- package/dist/excel-worksheet.d.ts +32 -32
- package/dist/excel-worksheet.d.ts.map +1 -1
- package/dist/excel-worksheet.js +281 -253
- package/dist/excel-worksheet.js.map +1 -6
- package/dist/excel-wrapper.d.ts +7 -7
- package/dist/excel-wrapper.d.ts.map +1 -1
- package/dist/excel-wrapper.js +190 -226
- package/dist/excel-wrapper.js.map +1 -6
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -6
- package/dist/types.d.ts +13 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -1
- package/dist/types.js.map +1 -6
- package/dist/utils/excel-utils.d.ts +23 -23
- package/dist/utils/excel-utils.d.ts.map +1 -1
- package/dist/utils/excel-utils.js +183 -151
- package/dist/utils/excel-utils.js.map +1 -6
- package/dist/utils/zip-cache.d.ts +6 -6
- package/dist/utils/zip-cache.js +78 -60
- package/dist/utils/zip-cache.js.map +1 -6
- package/dist/xml/excel-xml-content-type.d.ts +2 -2
- package/dist/xml/excel-xml-content-type.js +55 -53
- package/dist/xml/excel-xml-content-type.js.map +1 -6
- package/dist/xml/excel-xml-drawing.d.ts +2 -2
- package/dist/xml/excel-xml-drawing.js +74 -73
- package/dist/xml/excel-xml-drawing.js.map +1 -6
- package/dist/xml/excel-xml-relationship.d.ts +2 -2
- package/dist/xml/excel-xml-relationship.js +67 -67
- package/dist/xml/excel-xml-relationship.js.map +1 -6
- package/dist/xml/excel-xml-shared-string.d.ts +2 -2
- package/dist/xml/excel-xml-shared-string.js +57 -55
- package/dist/xml/excel-xml-shared-string.js.map +1 -6
- package/dist/xml/excel-xml-style.d.ts +2 -2
- package/dist/xml/excel-xml-style.js +311 -295
- package/dist/xml/excel-xml-style.js.map +1 -6
- package/dist/xml/excel-xml-unknown.d.ts +2 -2
- package/dist/xml/excel-xml-unknown.js +11 -10
- package/dist/xml/excel-xml-unknown.js.map +1 -6
- package/dist/xml/excel-xml-workbook.d.ts +2 -2
- package/dist/xml/excel-xml-workbook.js +87 -90
- package/dist/xml/excel-xml-workbook.js.map +1 -6
- package/dist/xml/excel-xml-worksheet.d.ts +6 -6
- package/dist/xml/excel-xml-worksheet.js +450 -393
- package/dist/xml/excel-xml-worksheet.js.map +1 -6
- package/package.json +5 -7
- package/src/excel-cell.ts +36 -36
- package/src/excel-col.ts +4 -4
- package/src/excel-row.ts +3 -3
- package/src/excel-workbook.ts +38 -38
- package/src/excel-worksheet.ts +69 -51
- package/src/excel-wrapper.ts +55 -50
- package/src/index.ts +3 -3
- package/src/types.ts +17 -17
- package/src/utils/excel-utils.ts +47 -41
- package/src/utils/zip-cache.ts +6 -6
- package/src/xml/excel-xml-content-type.ts +3 -3
- package/src/xml/excel-xml-drawing.ts +2 -2
- package/src/xml/excel-xml-relationship.ts +3 -3
- package/src/xml/excel-xml-shared-string.ts +2 -2
- package/src/xml/excel-xml-style.ts +11 -11
- package/src/xml/excel-xml-unknown.ts +2 -2
- package/src/xml/excel-xml-workbook.ts +5 -5
- package/src/xml/excel-xml-worksheet.ts +43 -43
- package/README.md +0 -119
- package/docs/core-classes.md +0 -541
- package/docs/types.md +0 -297
- package/docs/utilities.md +0 -128
- package/docs/wrapper.md +0 -100
- package/tests/excel-cell.spec.ts +0 -393
- package/tests/excel-col.spec.ts +0 -81
- package/tests/excel-row.spec.ts +0 -61
- package/tests/excel-workbook.spec.ts +0 -205
- package/tests/excel-worksheet.spec.ts +0 -469
- package/tests/excel-wrapper.spec.ts +0 -273
- package/tests/fixtures/logo.png +0 -0
- package/tests/fixtures//354/264/210/352/270/260/355/231/224.xlsx +0 -0
- package/tests/image-insert.spec.ts +0 -190
- package/tests/utils/excel-utils.spec.ts +0 -198
package/src/excel-worksheet.ts
CHANGED
|
@@ -14,8 +14,8 @@ import type { ExcelXmlWorkbook } from "./xml/excel-xml-workbook";
|
|
|
14
14
|
import type { ExcelXmlWorksheet } from "./xml/excel-xml-worksheet";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* Excel 워크시트를 나타내는 클래스.
|
|
18
|
+
* 셀 접근, 행/열 복사, 데이터 테이블 처리, 이미지 삽입 기능을 제공한다.
|
|
19
19
|
*/
|
|
20
20
|
export class ExcelWorksheet {
|
|
21
21
|
private readonly _rowMap = new Map<number, ExcelRow>();
|
|
@@ -29,17 +29,17 @@ export class ExcelWorksheet {
|
|
|
29
29
|
|
|
30
30
|
//#region Name Methods
|
|
31
31
|
|
|
32
|
-
/**
|
|
32
|
+
/** 워크시트 이름 반환 */
|
|
33
33
|
async getName(): Promise<string> {
|
|
34
34
|
const wbXmlData = await this._getWbData();
|
|
35
35
|
const name = wbXmlData.getWorksheetNameById(this._relId);
|
|
36
36
|
if (name == null) {
|
|
37
|
-
throw new Error(
|
|
37
|
+
throw new Error(`워크시트 ID ${this._relId}의 이름을 찾을 수 없습니다`);
|
|
38
38
|
}
|
|
39
39
|
return name;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
42
|
+
/** 워크시트 이름 변경 */
|
|
43
43
|
async setName(newName: string): Promise<void> {
|
|
44
44
|
const wbXmlData = await this._getWbData();
|
|
45
45
|
wbXmlData.setWorksheetNameById(this._relId, newName);
|
|
@@ -49,17 +49,17 @@ export class ExcelWorksheet {
|
|
|
49
49
|
|
|
50
50
|
//#region Cell Access Methods
|
|
51
51
|
|
|
52
|
-
/**
|
|
52
|
+
/** 행 객체 반환 (0 기반) */
|
|
53
53
|
row(r: number): ExcelRow {
|
|
54
54
|
return this._rowMap.getOrCreate(r, new ExcelRow(this._zipCache, this._targetFileName, r));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
57
|
+
/** 셀 객체 반환 (0 기반 행/열) */
|
|
58
58
|
cell(r: number, c: number): ExcelCell {
|
|
59
59
|
return this.row(r).cell(c);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
62
|
+
/** 열 객체 반환 (0 기반) */
|
|
63
63
|
col(c: number): ExcelCol {
|
|
64
64
|
return this._colMap.getOrCreate(c, new ExcelCol(this._zipCache, this._targetFileName, c));
|
|
65
65
|
}
|
|
@@ -68,7 +68,7 @@ export class ExcelWorksheet {
|
|
|
68
68
|
|
|
69
69
|
//#region Copy Methods
|
|
70
70
|
|
|
71
|
-
/**
|
|
71
|
+
/** 원본 행에서 대상 행으로 스타일 복사 */
|
|
72
72
|
async copyRowStyle(srcR: number, targetR: number): Promise<void> {
|
|
73
73
|
const range = await this.getRange();
|
|
74
74
|
|
|
@@ -77,7 +77,7 @@ export class ExcelWorksheet {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/**
|
|
80
|
+
/** 원본 셀에서 대상 셀로 스타일 복사 */
|
|
81
81
|
async copyCellStyle(srcAddr: ExcelAddressPoint, targetAddr: ExcelAddressPoint): Promise<void> {
|
|
82
82
|
const wsData = await this._getWsData();
|
|
83
83
|
|
|
@@ -87,55 +87,68 @@ export class ExcelWorksheet {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/**
|
|
90
|
+
/** 원본 행을 대상 행으로 복사 (덮어쓰기) */
|
|
91
91
|
async copyRow(srcR: number, targetR: number): Promise<void> {
|
|
92
92
|
const wsData = await this._getWsData();
|
|
93
93
|
wsData.copyRow(srcR, targetR);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/**
|
|
96
|
+
/** 원본 셀을 대상 셀로 복사 */
|
|
97
97
|
async copyCell(srcAddr: ExcelAddressPoint, targetAddr: ExcelAddressPoint): Promise<void> {
|
|
98
98
|
const wsData = await this._getWsData();
|
|
99
99
|
wsData.copyCell(srcAddr, targetAddr);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* @param srcR
|
|
106
|
-
* @param targetR
|
|
103
|
+
* 원본 행을 대상 위치에 삽입 복사한다.
|
|
104
|
+
* 대상 위치 이하의 기존 행은 한 칸 아래로 밀린다.
|
|
105
|
+
* @param srcR 복사할 원본 행 인덱스 (0 기반)
|
|
106
|
+
* @param targetR 삽입할 대상 행 인덱스 (0 기반)
|
|
107
107
|
*/
|
|
108
108
|
async insertCopyRow(srcR: number, targetR: number): Promise<void> {
|
|
109
109
|
const wsData = await this._getWsData();
|
|
110
110
|
const range = wsData.range;
|
|
111
111
|
|
|
112
|
-
//
|
|
112
|
+
// targetR 이하의 병합 셀을 1칸 아래로 이동
|
|
113
|
+
// 삽입 지점을 관통하는 다중행 병합은 자동으로 1행 확장됨
|
|
113
114
|
wsData.shiftMergeCells(targetR, 1);
|
|
114
115
|
|
|
115
|
-
//
|
|
116
|
+
// srcR >= targetR인 경우, srcR의 이동된 위치를 보정
|
|
116
117
|
const adjustedSrcR = srcR >= targetR ? srcR + 1 : srcR;
|
|
117
118
|
|
|
118
|
-
//
|
|
119
|
-
//
|
|
119
|
+
// 기존 행을 아래로 이동 (덮어쓰기 방지를 위해 아래에서 위로)
|
|
120
|
+
// 병합 셀은 위에서 이미 이동했으므로 skipMerge: true 사용
|
|
120
121
|
for (let r = range.e.r; r >= targetR; r--) {
|
|
121
122
|
wsData.copyRow(r, r + 1, { skipMerge: true });
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
//
|
|
125
|
-
wsData.copyRow(adjustedSrcR, targetR);
|
|
125
|
+
// 원본 행을 대상 위치에 복사 (병합은 skipMerge로 건너뜀)
|
|
126
|
+
wsData.copyRow(adjustedSrcR, targetR, { skipMerge: true });
|
|
127
|
+
|
|
128
|
+
// 원본 행의 단일행 병합만 대상 행에 복사
|
|
129
|
+
// (다중행 병합은 shiftMergeCells에서 이미 확장 처리됨)
|
|
130
|
+
const allMergeCells = wsData.getMergeCells();
|
|
131
|
+
const sourceMergeCells = allMergeCells.filter(
|
|
132
|
+
(mc) => mc.s.r === adjustedSrcR && mc.e.r === adjustedSrcR,
|
|
133
|
+
);
|
|
134
|
+
for (const mergeCell of sourceMergeCells) {
|
|
135
|
+
const newStartAddr = { r: targetR, c: mergeCell.s.c };
|
|
136
|
+
const newEndAddr = { r: targetR, c: mergeCell.e.c };
|
|
137
|
+
wsData.setMergeCells(newStartAddr, newEndAddr);
|
|
138
|
+
}
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
//#endregion
|
|
129
142
|
|
|
130
143
|
//#region Range Methods
|
|
131
144
|
|
|
132
|
-
/**
|
|
145
|
+
/** 워크시트의 데이터 범위 반환 */
|
|
133
146
|
async getRange(): Promise<ExcelAddressRangePoint> {
|
|
134
147
|
const xml = await this._getWsData();
|
|
135
148
|
return xml.range;
|
|
136
149
|
}
|
|
137
150
|
|
|
138
|
-
/**
|
|
151
|
+
/** 모든 셀을 2차원 배열로 반환 */
|
|
139
152
|
async getCells(): Promise<ExcelCell[][]> {
|
|
140
153
|
const xml = await this._getWsData();
|
|
141
154
|
const range = xml.range;
|
|
@@ -153,10 +166,10 @@ export class ExcelWorksheet {
|
|
|
153
166
|
//#region Data Methods
|
|
154
167
|
|
|
155
168
|
/**
|
|
156
|
-
*
|
|
157
|
-
* @param opt.headerRowIndex
|
|
158
|
-
* @param opt.checkEndColIndex
|
|
159
|
-
* @param opt.usableHeaderNameFn
|
|
169
|
+
* 워크시트 데이터를 테이블(레코드 배열)로 반환한다.
|
|
170
|
+
* @param opt.headerRowIndex 헤더 행 인덱스 (기본값: 첫 번째 행)
|
|
171
|
+
* @param opt.checkEndColIndex 데이터 끝을 판단할 열 인덱스. 이 열이 비어있으면 데이터가 끝난 것으로 판단한다.
|
|
172
|
+
* @param opt.usableHeaderNameFn 사용 가능한 헤더를 필터링하는 함수
|
|
160
173
|
*/
|
|
161
174
|
async getDataTable(opt?: {
|
|
162
175
|
headerRowIndex?: number;
|
|
@@ -174,6 +187,11 @@ export class ExcelWorksheet {
|
|
|
174
187
|
const headerName = await this.cell(startRow, c).getValue();
|
|
175
188
|
if (typeof headerName === "string") {
|
|
176
189
|
if (opt?.usableHeaderNameFn == null || opt.usableHeaderNameFn(headerName)) {
|
|
190
|
+
if (headerMap.has(headerName)) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`중복된 헤더: "${headerName}" (열 ${headerMap.get(headerName)!}과 열 ${c})`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
177
195
|
headerMap.set(headerName, c);
|
|
178
196
|
}
|
|
179
197
|
}
|
|
@@ -200,8 +218,8 @@ export class ExcelWorksheet {
|
|
|
200
218
|
}
|
|
201
219
|
|
|
202
220
|
/**
|
|
203
|
-
*
|
|
204
|
-
* @param matrix
|
|
221
|
+
* 2차원 배열 데이터를 워크시트에 쓰기
|
|
222
|
+
* @param matrix 2차원 배열 데이터 (행 우선, 인덱스 0이 첫 번째 행)
|
|
205
223
|
*/
|
|
206
224
|
async setDataMatrix(matrix: ExcelValueType[][]): Promise<void> {
|
|
207
225
|
for (let r = 0; r < matrix.length; r++) {
|
|
@@ -212,8 +230,8 @@ export class ExcelWorksheet {
|
|
|
212
230
|
}
|
|
213
231
|
|
|
214
232
|
/**
|
|
215
|
-
*
|
|
216
|
-
* @param records
|
|
233
|
+
* 레코드 배열을 워크시트에 쓰기
|
|
234
|
+
* @param records 레코드 배열. 첫 번째 행에 헤더가 자동 생성되고, 이후 행에 데이터가 기록된다.
|
|
217
235
|
*/
|
|
218
236
|
async setRecords(records: Record<string, ExcelValueType>[]): Promise<void> {
|
|
219
237
|
const headers = records
|
|
@@ -236,7 +254,7 @@ export class ExcelWorksheet {
|
|
|
236
254
|
|
|
237
255
|
//#region View Methods
|
|
238
256
|
|
|
239
|
-
/**
|
|
257
|
+
/** 워크시트 확대/축소 비율 설정 (퍼센트) */
|
|
240
258
|
async setZoom(percent: number): Promise<void> {
|
|
241
259
|
const wbXml = await this._getWbData();
|
|
242
260
|
wbXml.initializeView();
|
|
@@ -245,7 +263,7 @@ export class ExcelWorksheet {
|
|
|
245
263
|
wsXml.setZoom(percent);
|
|
246
264
|
}
|
|
247
265
|
|
|
248
|
-
/**
|
|
266
|
+
/** 행/열 틀 고정 설정 */
|
|
249
267
|
async freezeAt(point: { r?: number; c?: number }): Promise<void> {
|
|
250
268
|
const wbXml = await this._getWbData();
|
|
251
269
|
wbXml.initializeView();
|
|
@@ -259,11 +277,11 @@ export class ExcelWorksheet {
|
|
|
259
277
|
//#region Image Methods
|
|
260
278
|
|
|
261
279
|
/**
|
|
262
|
-
*
|
|
263
|
-
* @param opts.bytes
|
|
264
|
-
* @param opts.ext
|
|
265
|
-
* @param opts.from
|
|
266
|
-
* @param opts.to
|
|
280
|
+
* 워크시트에 이미지를 삽입한다.
|
|
281
|
+
* @param opts.bytes 이미지 바이너리 데이터
|
|
282
|
+
* @param opts.ext 이미지 확장자 (png, jpg 등)
|
|
283
|
+
* @param opts.from 이미지 시작 위치 (0 기반 행/열 인덱스, rOff/cOff는 EMU 오프셋)
|
|
284
|
+
* @param opts.to 이미지 끝 위치 (생략 시 from 위치에 원본 크기로 삽입)
|
|
267
285
|
*/
|
|
268
286
|
async addImage(opts: {
|
|
269
287
|
bytes: Bytes;
|
|
@@ -273,10 +291,10 @@ export class ExcelWorksheet {
|
|
|
273
291
|
}): Promise<void> {
|
|
274
292
|
const mimeType = mime.getType(opts.ext);
|
|
275
293
|
if (mimeType == null) {
|
|
276
|
-
throw new Error(
|
|
294
|
+
throw new Error(`확장자 '${opts.ext}'에 대한 MIME 타입을 결정할 수 없습니다`);
|
|
277
295
|
}
|
|
278
296
|
|
|
279
|
-
// 1.
|
|
297
|
+
// 1. 미디어 파일명 결정 및 저장
|
|
280
298
|
let mediaIndex = 1;
|
|
281
299
|
while ((await this._zipCache.get(`xl/media/image${mediaIndex}.${opts.ext}`)) !== undefined) {
|
|
282
300
|
mediaIndex++;
|
|
@@ -284,16 +302,16 @@ export class ExcelWorksheet {
|
|
|
284
302
|
const mediaPath = `xl/media/image${mediaIndex}.${opts.ext}`;
|
|
285
303
|
this._zipCache.set(mediaPath, opts.bytes);
|
|
286
304
|
|
|
287
|
-
// 2.
|
|
305
|
+
// 2. [Content_Types].xml 갱신
|
|
288
306
|
const typeXml = (await this._zipCache.get("[Content_Types].xml")) as ExcelXmlContentType;
|
|
289
307
|
typeXml.add(`/xl/media/image${mediaIndex}.${opts.ext}`, mimeType);
|
|
290
308
|
|
|
291
|
-
// 3.
|
|
309
|
+
// 3. 워크시트의 기존 drawing 확인
|
|
292
310
|
const wsXml = await this._getWsData();
|
|
293
311
|
const sheetRelsPath = `xl/worksheets/_rels/${this._targetFileName}.rels`;
|
|
294
312
|
let sheetRels = (await this._zipCache.get(sheetRelsPath)) as ExcelXmlRelationship | undefined;
|
|
295
313
|
|
|
296
|
-
//
|
|
314
|
+
// 기존 drawing 찾기
|
|
297
315
|
let drawingIndex: number | undefined;
|
|
298
316
|
let drawingPath: string | undefined;
|
|
299
317
|
let drawing: ExcelXmlDrawing | undefined;
|
|
@@ -306,7 +324,7 @@ export class ExcelWorksheet {
|
|
|
306
324
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
|
|
307
325
|
);
|
|
308
326
|
if (existingDrawingRel != null) {
|
|
309
|
-
//
|
|
327
|
+
// 기존 drawing 경로에서 인덱스 추출
|
|
310
328
|
const match = existingDrawingRel.$.Target.match(/drawing(\d+)\.xml$/);
|
|
311
329
|
if (match != null) {
|
|
312
330
|
drawingIndex = parseInt(match[1], 10);
|
|
@@ -319,7 +337,7 @@ export class ExcelWorksheet {
|
|
|
319
337
|
}
|
|
320
338
|
}
|
|
321
339
|
|
|
322
|
-
// 4.
|
|
340
|
+
// 4. 기존 drawing이 없으면 새로 생성
|
|
323
341
|
if (drawingIndex == null || drawingPath == null || drawing == null) {
|
|
324
342
|
drawingIndex = 1;
|
|
325
343
|
while ((await this._zipCache.get(`xl/drawings/drawing${drawingIndex}.xml`)) !== undefined) {
|
|
@@ -328,10 +346,10 @@ export class ExcelWorksheet {
|
|
|
328
346
|
drawingPath = `xl/drawings/drawing${drawingIndex}.xml`;
|
|
329
347
|
drawing = new ExcelXmlDrawing();
|
|
330
348
|
|
|
331
|
-
//
|
|
349
|
+
// [Content_Types].xml에 drawing 타입 추가
|
|
332
350
|
typeXml.add("/" + drawingPath, "application/vnd.openxmlformats-officedocument.drawing+xml");
|
|
333
351
|
|
|
334
|
-
//
|
|
352
|
+
// 워크시트 rels에 drawing 추가
|
|
335
353
|
sheetRels = sheetRels ?? new ExcelXmlRelationship();
|
|
336
354
|
const sheetRelNum = sheetRels.addAndGetId(
|
|
337
355
|
`../drawings/drawing${drawingIndex}.xml`,
|
|
@@ -340,7 +358,7 @@ export class ExcelWorksheet {
|
|
|
340
358
|
const drawingRelIdOnWorksheet = `rId${sheetRelNum}`;
|
|
341
359
|
this._zipCache.set(sheetRelsPath, sheetRels);
|
|
342
360
|
|
|
343
|
-
//
|
|
361
|
+
// 워크시트 XML에 drawing 추가
|
|
344
362
|
wsXml.data.worksheet.$["xmlns:r"] =
|
|
345
363
|
wsXml.data.worksheet.$["xmlns:r"] ??
|
|
346
364
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
@@ -349,7 +367,7 @@ export class ExcelWorksheet {
|
|
|
349
367
|
this._zipCache.set(`xl/worksheets/${this._targetFileName}`, wsXml);
|
|
350
368
|
}
|
|
351
369
|
|
|
352
|
-
// 5.
|
|
370
|
+
// 5. drawing rels 준비 (없으면 생성)
|
|
353
371
|
drawingRels = drawingRels ?? new ExcelXmlRelationship();
|
|
354
372
|
const mediaFileName = mediaPath.slice(3);
|
|
355
373
|
const drawingTarget = `../${mediaFileName}`;
|
|
@@ -359,7 +377,7 @@ export class ExcelWorksheet {
|
|
|
359
377
|
);
|
|
360
378
|
this._zipCache.set(`xl/drawings/_rels/drawing${drawingIndex}.xml.rels`, drawingRels);
|
|
361
379
|
|
|
362
|
-
// 6.
|
|
380
|
+
// 6. drawing에 이미지 추가
|
|
363
381
|
const blipRelId = `rId${relNum}`;
|
|
364
382
|
drawing.addPicture({
|
|
365
383
|
from: opts.from,
|
package/src/excel-wrapper.ts
CHANGED
|
@@ -13,18 +13,18 @@ import { ExcelWorkbook } from "./excel-workbook";
|
|
|
13
13
|
import type { ExcelValueType } from "./types";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Zod
|
|
16
|
+
* Zod 스키마 기반 Excel 래퍼
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* 스키마에서 타입 정보를 추론하여 타입 안전한 읽기/쓰기를 제공한다
|
|
19
19
|
*/
|
|
20
20
|
export class ExcelWrapper<TSchema extends z.ZodObject<z.ZodRawShape>> {
|
|
21
21
|
/**
|
|
22
|
-
* @param _schema Zod
|
|
22
|
+
* @param _schema Zod 스키마 (레코드 구조를 정의하며, `.describe()`로 Excel 헤더 이름을 지정)
|
|
23
23
|
*/
|
|
24
24
|
constructor(private readonly _schema: TSchema) {}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* Excel 파일을 레코드 배열로 읽기
|
|
28
28
|
*/
|
|
29
29
|
async read(
|
|
30
30
|
file: Bytes | Blob,
|
|
@@ -46,7 +46,7 @@ export class ExcelWrapper<TSchema extends z.ZodObject<z.ZodRawShape>> {
|
|
|
46
46
|
|
|
47
47
|
if (rawData.length === 0) {
|
|
48
48
|
throw new Error(
|
|
49
|
-
`[${wsName}]
|
|
49
|
+
`[${wsName}] Excel 파일에서 데이터를 찾을 수 없습니다. (기대하는 헤더: ${displayNames.join(", ")})`,
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -73,13 +73,13 @@ export class ExcelWrapper<TSchema extends z.ZodObject<z.ZodRawShape>> {
|
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
//
|
|
76
|
+
// Zod 스키마로 유효성 검사
|
|
77
77
|
const parseResult = this._schema.safeParse(record);
|
|
78
78
|
if (!parseResult.success) {
|
|
79
79
|
const errors = parseResult.error.issues
|
|
80
80
|
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
81
81
|
.join(", ");
|
|
82
|
-
throw new Error(`[${wsName}]
|
|
82
|
+
throw new Error(`[${wsName}] 데이터 유효성 검사 실패: ${errors}`);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
result.push(parseResult.data);
|
|
@@ -89,11 +89,11 @@ export class ExcelWrapper<TSchema extends z.ZodObject<z.ZodRawShape>> {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
92
|
+
* 레코드 배열을 Excel 워크북으로 변환
|
|
93
93
|
*
|
|
94
94
|
* @remarks
|
|
95
|
-
*
|
|
96
|
-
*
|
|
95
|
+
* 반환된 워크북의 리소스 관리는 호출자의 책임이다.
|
|
96
|
+
* `await using`을 사용하거나 사용 후 `close()`를 호출해야 한다.
|
|
97
97
|
*
|
|
98
98
|
* @example
|
|
99
99
|
* ```typescript
|
|
@@ -107,53 +107,58 @@ export class ExcelWrapper<TSchema extends z.ZodObject<z.ZodRawShape>> {
|
|
|
107
107
|
options?: { excludes?: (keyof z.infer<TSchema>)[] },
|
|
108
108
|
): Promise<ExcelWorkbook> {
|
|
109
109
|
const wb = new ExcelWorkbook();
|
|
110
|
-
|
|
110
|
+
try {
|
|
111
|
+
const ws = await wb.addWorksheet(wsName);
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
const displayNameMap = this._getDisplayNameMap(options?.excludes as string[] | undefined);
|
|
114
|
+
const keys = Object.keys(displayNameMap) as (keyof z.infer<TSchema>)[];
|
|
115
|
+
const headers = keys.map((key) => displayNameMap[key as string]);
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
// 헤더 행 쓰기
|
|
118
|
+
for (let c = 0; c < headers.length; c++) {
|
|
119
|
+
await ws.cell(0, c).setValue(headers[c]);
|
|
120
|
+
}
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
// 데이터 행 쓰기
|
|
123
|
+
for (let r = 0; r < records.length; r++) {
|
|
124
|
+
for (let c = 0; c < keys.length; c++) {
|
|
125
|
+
const key = keys[c];
|
|
126
|
+
const value = records[r][key] as ExcelValueType;
|
|
127
|
+
await ws.cell(r + 1, c).setValue(value);
|
|
128
|
+
}
|
|
127
129
|
}
|
|
128
|
-
}
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
// 테두리 스타일 적용
|
|
132
|
+
for (let r = 0; r < records.length + 1; r++) {
|
|
133
|
+
for (let c = 0; c < keys.length; c++) {
|
|
134
|
+
await ws.cell(r, c).setStyle({
|
|
135
|
+
border: ["left", "right", "top", "bottom"],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
136
138
|
}
|
|
137
|
-
}
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
140
|
+
// 필수 필드 헤더 강조 (노란색)
|
|
141
|
+
const shape = this._schema.shape;
|
|
142
|
+
for (let c = 0; c < keys.length; c++) {
|
|
143
|
+
const fieldKey = keys[c] as string;
|
|
144
|
+
const fieldSchema = shape[fieldKey] as z.ZodType;
|
|
145
|
+
|
|
146
|
+
if (this._isRequired(fieldSchema) && !this._isBoolean(fieldSchema)) {
|
|
147
|
+
await ws.cell(0, c).setStyle({
|
|
148
|
+
background: "00FFFF00",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
149
151
|
}
|
|
150
|
-
}
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
// 보기 설정
|
|
154
|
+
await ws.setZoom(85);
|
|
155
|
+
await ws.freezeAt({ r: 0 });
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
return wb;
|
|
158
|
+
} catch (e) {
|
|
159
|
+
await wb.close();
|
|
160
|
+
throw e;
|
|
161
|
+
}
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
//#region Private Methods
|
|
@@ -198,7 +203,7 @@ export class ExcelWrapper<TSchema extends z.ZodObject<z.ZodRawShape>> {
|
|
|
198
203
|
return Boolean(rawValue);
|
|
199
204
|
}
|
|
200
205
|
|
|
201
|
-
// DateOnly, DateTime, Time
|
|
206
|
+
// DateOnly, DateTime, Time은 instanceof로 처리
|
|
202
207
|
if (rawValue instanceof DateOnly || rawValue instanceof DateTime || rawValue instanceof Time) {
|
|
203
208
|
return rawValue;
|
|
204
209
|
}
|
|
@@ -218,7 +223,7 @@ export class ExcelWrapper<TSchema extends z.ZodObject<z.ZodRawShape>> {
|
|
|
218
223
|
|
|
219
224
|
private _getDefaultForSchema(schema: z.ZodType): unknown {
|
|
220
225
|
if (schema instanceof ZodDefault) {
|
|
221
|
-
// ZodDefault.parse(undefined)
|
|
226
|
+
// ZodDefault.parse(undefined)는 기본값을 반환한다
|
|
222
227
|
return schema.parse(undefined);
|
|
223
228
|
}
|
|
224
229
|
|
|
@@ -226,7 +231,7 @@ export class ExcelWrapper<TSchema extends z.ZodObject<z.ZodRawShape>> {
|
|
|
226
231
|
return undefined;
|
|
227
232
|
}
|
|
228
233
|
|
|
229
|
-
//
|
|
234
|
+
// 필수 boolean 필드의 기본값은 false
|
|
230
235
|
const innerSchema = this._unwrapSchema(schema);
|
|
231
236
|
if (innerSchema instanceof ZodBoolean) {
|
|
232
237
|
return false;
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 타입 및 유틸리티
|
|
2
2
|
export * from "./types";
|
|
3
3
|
export * from "./utils/excel-utils";
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// 핵심 클래스
|
|
6
6
|
export * from "./excel-cell";
|
|
7
7
|
export * from "./excel-row";
|
|
8
8
|
export * from "./excel-col";
|
|
9
9
|
export * from "./excel-worksheet";
|
|
10
10
|
export * from "./excel-workbook";
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// 래퍼 클래스
|
|
13
13
|
export * from "./excel-wrapper";
|
package/src/types.ts
CHANGED
|
@@ -134,16 +134,16 @@ export interface ExcelXmlWorksheetData {
|
|
|
134
134
|
|
|
135
135
|
export interface ExcelRowData {
|
|
136
136
|
$: {
|
|
137
|
-
r: string; //
|
|
137
|
+
r: string; // 주소 (1~)
|
|
138
138
|
};
|
|
139
139
|
c?: ExcelCellData[];
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
export interface ExcelCellData {
|
|
143
143
|
$: {
|
|
144
|
-
r: string; //
|
|
145
|
-
s?: string; //
|
|
146
|
-
t?: ExcelCellType; //
|
|
144
|
+
r: string; // 주소 (A~)
|
|
145
|
+
s?: string; // 스타일 ID
|
|
146
|
+
t?: ExcelCellType; // 타입: s(sharedString)
|
|
147
147
|
};
|
|
148
148
|
v?: [string];
|
|
149
149
|
f?: [string];
|
|
@@ -324,13 +324,13 @@ export type ExcelValueType = number | string | DateOnly | DateTime | Time | bool
|
|
|
324
324
|
export type ExcelNumberFormat = "number" | "string" | "DateOnly" | "DateTime" | "Time";
|
|
325
325
|
|
|
326
326
|
/**
|
|
327
|
-
* Excel
|
|
328
|
-
* - s:
|
|
327
|
+
* Excel 셀 타입
|
|
328
|
+
* - s: 공유 문자열 (SharedString)
|
|
329
329
|
* - b: boolean
|
|
330
|
-
* - str:
|
|
331
|
-
* - n:
|
|
332
|
-
* - inlineStr:
|
|
333
|
-
* - e:
|
|
330
|
+
* - str: 수식 결과 문자열
|
|
331
|
+
* - n: 숫자
|
|
332
|
+
* - inlineStr: 인라인 문자열 (서식 있는 텍스트)
|
|
333
|
+
* - e: 에러
|
|
334
334
|
*/
|
|
335
335
|
export type ExcelCellType = "s" | "b" | "str" | "n" | "inlineStr" | "e";
|
|
336
336
|
|
|
@@ -366,11 +366,11 @@ export type ExcelHorizontalAlign = "center" | "left" | "right";
|
|
|
366
366
|
export type ExcelVerticalAlign = "center" | "top" | "bottom";
|
|
367
367
|
|
|
368
368
|
/**
|
|
369
|
-
*
|
|
369
|
+
* 셀 스타일 옵션
|
|
370
370
|
* @example
|
|
371
371
|
* ```typescript
|
|
372
372
|
* await cell.setStyle({
|
|
373
|
-
* background: "00FF0000", //
|
|
373
|
+
* background: "00FF0000", // 빨강
|
|
374
374
|
* border: ["left", "right", "top", "bottom"],
|
|
375
375
|
* horizontalAlign: "center",
|
|
376
376
|
* verticalAlign: "center",
|
|
@@ -379,15 +379,15 @@ export type ExcelVerticalAlign = "center" | "top" | "bottom";
|
|
|
379
379
|
* ```
|
|
380
380
|
*/
|
|
381
381
|
export interface ExcelStyleOptions {
|
|
382
|
-
/**
|
|
382
|
+
/** 배경색 (ARGB 형식, 예: "00FF0000") */
|
|
383
383
|
background?: string;
|
|
384
|
-
/**
|
|
384
|
+
/** 테두리 위치 */
|
|
385
385
|
border?: ExcelBorderPosition[];
|
|
386
|
-
/**
|
|
386
|
+
/** 가로 정렬 */
|
|
387
387
|
horizontalAlign?: ExcelHorizontalAlign;
|
|
388
|
-
/**
|
|
388
|
+
/** 세로 정렬 */
|
|
389
389
|
verticalAlign?: ExcelVerticalAlign;
|
|
390
|
-
/**
|
|
390
|
+
/** 숫자 형식 */
|
|
391
391
|
numberFormat?: ExcelNumberFormat;
|
|
392
392
|
}
|
|
393
393
|
|