@simplysm/excel 13.0.85 → 13.0.87
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 +282 -253
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,371 +1,400 @@
|
|
|
1
1
|
# @simplysm/excel
|
|
2
2
|
|
|
3
|
-
Excel
|
|
3
|
+
Excel 파일(.xlsx) 처리 라이브러리. 지연 로딩 아키텍처로 대용량 파일도 메모리 효율적으로 처리한다.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
5
|
+
## 설치
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
8
|
npm install @simplysm/excel
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## Quick Start
|
|
16
|
-
|
|
17
|
-
### Create and export a workbook
|
|
18
|
-
|
|
19
|
-
```typescript
|
|
20
|
-
import { ExcelWorkbook } from "@simplysm/excel";
|
|
11
|
+
**의존성:** `@simplysm/core-common`, `mime`, `zod`
|
|
21
12
|
|
|
22
|
-
|
|
23
|
-
const ws = await wb.addWorksheet("Sheet1");
|
|
13
|
+
## 아키텍처
|
|
24
14
|
|
|
25
|
-
|
|
26
|
-
await ws.cell(0, 1).setValue("Age");
|
|
27
|
-
await ws.cell(1, 0).setValue("Alice");
|
|
28
|
-
await ws.cell(1, 1).setValue(30);
|
|
15
|
+
ZIP 내부의 XML을 지연 로딩(Lazy Loading)하여 필요한 시점에만 파싱한다. 대용량 SharedStrings나 Styles XML이 있어도 해당 셀에 접근할 때만 로드되므로 메모리 효율적이다.
|
|
29
16
|
|
|
30
|
-
const bytes = await wb.toBytes(); // Uint8Array
|
|
31
|
-
const blob = await wb.toBlob(); // Blob (for browser downloads)
|
|
32
17
|
```
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
await using wb = new ExcelWorkbook(fileBytes); // Uint8Array or Blob
|
|
40
|
-
const ws = await wb.getWorksheet(0); // by index
|
|
41
|
-
// const ws = await wb.getWorksheet("Sheet1"); // or by name
|
|
42
|
-
|
|
43
|
-
const value = await ws.cell(0, 0).getValue(); // read cell value
|
|
18
|
+
ExcelWorkbook (워크북)
|
|
19
|
+
├── ExcelWorksheet (워크시트)
|
|
20
|
+
│ ├── ExcelRow (행) → ExcelCell (셀)
|
|
21
|
+
│ └── ExcelCol (열) → ExcelCell (셀)
|
|
22
|
+
├── ZipCache (ZIP 파일 캐시, 지연 로딩)
|
|
23
|
+
└── ExcelWrapper (Zod 스키마 기반 래퍼)
|
|
44
24
|
```
|
|
45
25
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
import { z } from "zod";
|
|
50
|
-
import { ExcelWrapper } from "@simplysm/excel";
|
|
26
|
+
**모든 행/열/셀 인덱스는 0-based이다.**
|
|
51
27
|
|
|
52
|
-
|
|
53
|
-
name: z.string().describe("Name"), // .describe() sets the Excel header
|
|
54
|
-
age: z.number().describe("Age"),
|
|
55
|
-
email: z.string().optional().describe("Email"),
|
|
56
|
-
active: z.boolean().default(false).describe("Active"),
|
|
57
|
-
});
|
|
28
|
+
## 리소스 관리
|
|
58
29
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// Write records to Excel
|
|
62
|
-
await using wb = await wrapper.write("Users", [
|
|
63
|
-
{ name: "Alice", age: 30, email: "alice@example.com", active: true },
|
|
64
|
-
{ name: "Bob", age: 25 },
|
|
65
|
-
]);
|
|
66
|
-
const bytes = await wb.toBytes();
|
|
67
|
-
|
|
68
|
-
// Read records from Excel
|
|
69
|
-
const records = await wrapper.read(bytes, "Users");
|
|
70
|
-
// records[0] => { name: "Alice", age: 30, email: "alice@example.com", active: true }
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## API Reference
|
|
74
|
-
|
|
75
|
-
### ExcelWorkbook
|
|
76
|
-
|
|
77
|
-
Excel workbook processing class. Manages ZIP resources internally and uses lazy-loading for memory efficiency.
|
|
78
|
-
|
|
79
|
-
**Resource management:** Always release resources after use via `await using` (recommended) or `close()`.
|
|
30
|
+
ExcelWorkbook은 내부적으로 ZIP 리소스를 관리하므로, 사용 후 반드시 리소스를 해제해야 한다.
|
|
80
31
|
|
|
81
32
|
```typescript
|
|
82
|
-
//
|
|
33
|
+
// 방법 1: await using (권장)
|
|
83
34
|
await using wb = new ExcelWorkbook(bytes);
|
|
35
|
+
const ws = await wb.getWorksheet(0);
|
|
36
|
+
// ... 작업 수행
|
|
37
|
+
// 스코프 종료 시 자동 해제
|
|
84
38
|
|
|
85
|
-
//
|
|
39
|
+
// 방법 2: try-finally
|
|
86
40
|
const wb = new ExcelWorkbook(bytes);
|
|
87
41
|
try {
|
|
88
|
-
|
|
42
|
+
const ws = await wb.getWorksheet(0);
|
|
43
|
+
// ... 작업 수행
|
|
89
44
|
} finally {
|
|
90
45
|
await wb.close();
|
|
91
46
|
}
|
|
92
47
|
```
|
|
93
48
|
|
|
94
|
-
|
|
49
|
+
## 워크북 (ExcelWorkbook)
|
|
50
|
+
|
|
51
|
+
### 생성/열기
|
|
95
52
|
|
|
96
53
|
```typescript
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
54
|
+
import { ExcelWorkbook } from "@simplysm/excel";
|
|
55
|
+
|
|
56
|
+
// 새 워크북 생성
|
|
57
|
+
await using wb = new ExcelWorkbook();
|
|
58
|
+
|
|
59
|
+
// 기존 파일 열기 (Uint8Array 또는 Blob)
|
|
60
|
+
await using wb = new ExcelWorkbook(fileBytes);
|
|
61
|
+
await using wb = new ExcelWorkbook(blob);
|
|
100
62
|
```
|
|
101
63
|
|
|
102
|
-
|
|
64
|
+
### 워크시트 관리
|
|
103
65
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
| `getWorksheet(index: number)` | `Promise<ExcelWorksheet>` | Get worksheet by 0-based index |
|
|
108
|
-
| `getWorksheet(name: string)` | `Promise<ExcelWorksheet>` | Get worksheet by name |
|
|
109
|
-
| `addWorksheet(name: string)` | `Promise<ExcelWorksheet>` | Create a new worksheet |
|
|
110
|
-
| `toBytes()` | `Promise<Bytes>` | Export workbook as byte array |
|
|
111
|
-
| `toBlob()` | `Promise<Blob>` | Export workbook as Blob |
|
|
112
|
-
| `close()` | `Promise<void>` | Release resources (safe to call multiple times) |
|
|
66
|
+
```typescript
|
|
67
|
+
// 워크시트 이름 목록
|
|
68
|
+
const names = await wb.getWorksheetNames(); // string[]
|
|
113
69
|
|
|
114
|
-
|
|
70
|
+
// 워크시트 조회 (인덱스: 0-based)
|
|
71
|
+
const ws = await wb.getWorksheet(0);
|
|
72
|
+
const ws = await wb.getWorksheet("Sheet1");
|
|
115
73
|
|
|
116
|
-
|
|
74
|
+
// 워크시트 추가
|
|
75
|
+
const ws = await wb.addWorksheet("새시트");
|
|
117
76
|
|
|
118
|
-
|
|
77
|
+
// 워크시트 이름 변경
|
|
78
|
+
await ws.setName("새이름");
|
|
79
|
+
const name = await ws.getName();
|
|
80
|
+
```
|
|
119
81
|
|
|
120
|
-
|
|
82
|
+
### 파일 내보내기
|
|
121
83
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
| `col(c)` | `ExcelCol` | Get column object (0-based) |
|
|
127
|
-
| `getCells()` | `Promise<ExcelCell[][]>` | Get all cells as a 2D array |
|
|
128
|
-
| `getRange()` | `Promise<ExcelAddressRangePoint>` | Get the data range of the worksheet |
|
|
84
|
+
```typescript
|
|
85
|
+
const bytes = await wb.toBytes(); // Uint8Array (Bytes)
|
|
86
|
+
const blob = await wb.toBlob(); // Blob (MIME: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)
|
|
87
|
+
```
|
|
129
88
|
|
|
130
|
-
|
|
89
|
+
## 워크시트 (ExcelWorksheet)
|
|
131
90
|
|
|
132
|
-
|
|
133
|
-
|--------|---------|-------------|
|
|
134
|
-
| `getName()` | `Promise<string>` | Get worksheet name |
|
|
135
|
-
| `setName(name)` | `Promise<void>` | Rename worksheet |
|
|
91
|
+
### 셀/행/열 접근
|
|
136
92
|
|
|
137
|
-
|
|
93
|
+
모든 인덱스는 0-based이다.
|
|
138
94
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
| `setDataMatrix(matrix)` | `Promise<void>` | Write 2D array data |
|
|
143
|
-
| `setRecords(records)` | `Promise<void>` | Write record array (auto-generates headers) |
|
|
95
|
+
```typescript
|
|
96
|
+
// 셀 접근
|
|
97
|
+
const cell = ws.cell(0, 0); // A1 (r=0, c=0)
|
|
144
98
|
|
|
145
|
-
|
|
99
|
+
// 행 접근
|
|
100
|
+
const row = ws.row(0); // 첫 번째 행
|
|
101
|
+
const cells = await row.getCells(); // 행의 모든 셀
|
|
146
102
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
| `checkEndColIndex` | `number` | Column index to detect data end (stops when empty) |
|
|
151
|
-
| `usableHeaderNameFn` | `(name: string) => boolean` | Filter function for usable headers |
|
|
103
|
+
// 열 접근
|
|
104
|
+
const col = ws.col(0); // 첫 번째 열
|
|
105
|
+
const cells = await col.getCells(); // 열의 모든 셀
|
|
152
106
|
|
|
153
|
-
|
|
107
|
+
// 열에서 특정 행의 셀
|
|
108
|
+
const cell = ws.col(2).cell(5); // C6
|
|
109
|
+
// 행에서 특정 열의 셀
|
|
110
|
+
const cell = ws.row(5).cell(2); // C6
|
|
111
|
+
```
|
|
154
112
|
|
|
155
|
-
|
|
156
|
-
|--------|---------|-------------|
|
|
157
|
-
| `copyRow(srcR, targetR)` | `Promise<void>` | Copy row (overwrite target) |
|
|
158
|
-
| `copyCell(srcAddr, targetAddr)` | `Promise<void>` | Copy cell |
|
|
159
|
-
| `copyRowStyle(srcR, targetR)` | `Promise<void>` | Copy row style only |
|
|
160
|
-
| `copyCellStyle(srcAddr, targetAddr)` | `Promise<void>` | Copy cell style only |
|
|
161
|
-
| `insertCopyRow(srcR, targetR)` | `Promise<void>` | Insert-copy row (shifts existing rows down) |
|
|
113
|
+
### 데이터 범위
|
|
162
114
|
|
|
163
|
-
|
|
115
|
+
```typescript
|
|
116
|
+
const range = await ws.getRange();
|
|
117
|
+
// { s: { r: 0, c: 0 }, e: { r: 9, c: 4 } }
|
|
164
118
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
| `freezeAt({ r?, c? })` | `Promise<void>` | Freeze panes at row/column |
|
|
119
|
+
// 모든 셀을 2차원 배열로
|
|
120
|
+
const cells = await ws.getCells(); // ExcelCell[][]
|
|
121
|
+
```
|
|
169
122
|
|
|
170
|
-
|
|
123
|
+
### 행 복사
|
|
171
124
|
|
|
172
125
|
```typescript
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
ext: "png", // file extension
|
|
176
|
-
from: { r: 0, c: 0 }, // start position (0-based)
|
|
177
|
-
to: { r: 5, c: 3 }, // end position (optional)
|
|
178
|
-
});
|
|
179
|
-
```
|
|
126
|
+
// 행 복사 (덮어쓰기)
|
|
127
|
+
await ws.copyRow(0, 5); // 0번 행을 5번 행으로 복사
|
|
180
128
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
| `bytes` | `Bytes` | Image binary data |
|
|
184
|
-
| `ext` | `string` | Image extension (png, jpg, etc.) |
|
|
185
|
-
| `from` | `{ r, c, rOff?, cOff? }` | Start position. `rOff`/`cOff` are EMU offsets. |
|
|
186
|
-
| `to` | `{ r, c, rOff?, cOff? }` | End position. Defaults to one cell from `from`. |
|
|
129
|
+
// 삽입 복사 (기존 행은 아래로 밀림)
|
|
130
|
+
await ws.insertCopyRow(0, 5); // 0번 행을 5번 위치에 삽입 복사
|
|
187
131
|
|
|
188
|
-
|
|
132
|
+
// 행 스타일만 복사
|
|
133
|
+
await ws.copyRowStyle(0, 5);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 셀 복사
|
|
189
137
|
|
|
190
|
-
|
|
138
|
+
```typescript
|
|
139
|
+
// 셀 복사 (값 + 스타일)
|
|
140
|
+
await ws.copyCell({ r: 0, c: 0 }, { r: 1, c: 1 });
|
|
191
141
|
|
|
192
|
-
|
|
142
|
+
// 셀 스타일만 복사
|
|
143
|
+
await ws.copyCellStyle({ r: 0, c: 0 }, { r: 1, c: 1 });
|
|
144
|
+
```
|
|
193
145
|
|
|
194
|
-
|
|
146
|
+
### 뷰 설정
|
|
195
147
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
148
|
+
```typescript
|
|
149
|
+
// 줌 설정 (퍼센트)
|
|
150
|
+
await ws.setZoom(150);
|
|
199
151
|
|
|
200
|
-
|
|
152
|
+
// 틀고정 (r, c 모두 선택적)
|
|
153
|
+
await ws.freezeAt({ r: 1 }); // 1번 행 이후 고정 (첫 행 고정)
|
|
154
|
+
await ws.freezeAt({ c: 2 }); // 2번 열 이후 고정
|
|
155
|
+
await ws.freezeAt({ r: 1, c: 1 }); // 행+열 모두 고정
|
|
156
|
+
```
|
|
201
157
|
|
|
202
|
-
|
|
203
|
-
|--------|---------|-------------|
|
|
204
|
-
| `getValue()` | `Promise<ExcelValueType>` | Get cell value |
|
|
205
|
-
| `setValue(val)` | `Promise<void>` | Set cell value (`undefined` deletes the cell) |
|
|
206
|
-
| `getFormula()` | `Promise<string \| undefined>` | Get cell formula |
|
|
207
|
-
| `setFormula(val)` | `Promise<void>` | Set cell formula (`undefined` removes it) |
|
|
158
|
+
### 데이터 테이블
|
|
208
159
|
|
|
209
|
-
|
|
160
|
+
```typescript
|
|
161
|
+
// 시트를 레코드 배열로 읽기
|
|
162
|
+
const data = await ws.getDataTable();
|
|
163
|
+
// Record<string, ExcelValueType>[]
|
|
164
|
+
|
|
165
|
+
// 옵션 지정
|
|
166
|
+
const data = await ws.getDataTable({
|
|
167
|
+
headerRowIndex: 0, // 헤더 행 인덱스 (기본: 범위의 첫 행)
|
|
168
|
+
checkEndColIndex: 0, // 이 열이 비어있으면 데이터 종료
|
|
169
|
+
usableHeaderNameFn: (name) => name !== "", // 사용할 헤더 필터
|
|
170
|
+
});
|
|
210
171
|
|
|
211
|
-
|
|
172
|
+
// 2차원 배열 쓰기
|
|
173
|
+
await ws.setDataMatrix([
|
|
174
|
+
["이름", "나이", "이메일"],
|
|
175
|
+
["Alice", 30, "alice@example.com"],
|
|
176
|
+
["Bob", 25, "bob@example.com"],
|
|
177
|
+
]);
|
|
212
178
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
179
|
+
// 레코드 배열 쓰기 (헤더 자동 생성)
|
|
180
|
+
await ws.setRecords([
|
|
181
|
+
{ name: "Alice", age: 30 },
|
|
182
|
+
{ name: "Bob", age: 25 },
|
|
183
|
+
]);
|
|
184
|
+
```
|
|
218
185
|
|
|
219
|
-
|
|
186
|
+
### 이미지 삽입
|
|
220
187
|
|
|
221
188
|
```typescript
|
|
222
|
-
await
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
189
|
+
await ws.addImage({
|
|
190
|
+
bytes: imageBytes, // Uint8Array (이미지 바이너리)
|
|
191
|
+
ext: "png", // 확장자 (png, jpg 등)
|
|
192
|
+
from: { // 시작 위치 (0-based)
|
|
193
|
+
r: 0, c: 0,
|
|
194
|
+
rOff: 0, // 행 오프셋 (EMU 단위, 선택적)
|
|
195
|
+
cOff: 0, // 열 오프셋 (EMU 단위, 선택적)
|
|
196
|
+
},
|
|
197
|
+
to: { // 종료 위치 (생략 시 from + 1행 1열)
|
|
198
|
+
r: 5, c: 3,
|
|
199
|
+
rOff: 0,
|
|
200
|
+
cOff: 0,
|
|
201
|
+
},
|
|
228
202
|
});
|
|
229
203
|
```
|
|
230
204
|
|
|
231
|
-
|
|
205
|
+
## 셀 (ExcelCell)
|
|
232
206
|
|
|
233
|
-
|
|
234
|
-
|--------|---------|-------------|
|
|
235
|
-
| `merge(r, c)` | `Promise<void>` | Merge from this cell to end position `(r, c)` |
|
|
207
|
+
### 값 읽기/쓰기
|
|
236
208
|
|
|
237
209
|
```typescript
|
|
238
|
-
//
|
|
239
|
-
await ws.cell(0, 0).
|
|
210
|
+
// 값 설정
|
|
211
|
+
await ws.cell(0, 0).setValue("텍스트");
|
|
212
|
+
await ws.cell(0, 1).setValue(1234);
|
|
213
|
+
await ws.cell(0, 2).setValue(true);
|
|
214
|
+
await ws.cell(0, 3).setValue(new DateOnly(2024, 1, 15));
|
|
215
|
+
await ws.cell(0, 4).setValue(new DateTime(2024, 1, 15, 10, 30));
|
|
216
|
+
await ws.cell(0, 5).setValue(new Time(10, 30, 0));
|
|
217
|
+
await ws.cell(0, 6).setValue(undefined); // 셀 삭제
|
|
218
|
+
|
|
219
|
+
// 값 읽기
|
|
220
|
+
const val = await ws.cell(0, 0).getValue();
|
|
221
|
+
// ExcelValueType = number | string | DateOnly | DateTime | Time | boolean | undefined
|
|
240
222
|
```
|
|
241
223
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
### ExcelRow
|
|
224
|
+
### 수식
|
|
245
225
|
|
|
246
|
-
|
|
226
|
+
```typescript
|
|
227
|
+
await ws.cell(1, 0).setFormula("SUM(B1:B10)");
|
|
228
|
+
await ws.cell(1, 0).setFormula(undefined); // 수식 제거
|
|
247
229
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
| `cell(c)` | `ExcelCell` | Get cell at column `c` (0-based) |
|
|
251
|
-
| `getCells()` | `Promise<ExcelCell[]>` | Get all cells in the row |
|
|
230
|
+
const formula = await ws.cell(1, 0).getFormula(); // string | undefined
|
|
231
|
+
```
|
|
252
232
|
|
|
253
|
-
|
|
233
|
+
### 셀 병합
|
|
254
234
|
|
|
255
|
-
|
|
235
|
+
```typescript
|
|
236
|
+
// cell(startR, startC).merge(endR, endC)
|
|
237
|
+
// 종료 좌표는 0-based 절대 좌표
|
|
238
|
+
await ws.cell(0, 0).merge(2, 2); // A1:C3 범위 병합 (3행 x 3열)
|
|
239
|
+
await ws.cell(0, 0).merge(0, 3); // A1:D1 범위 병합 (1행 x 4열)
|
|
240
|
+
```
|
|
256
241
|
|
|
257
|
-
|
|
242
|
+
### 스타일
|
|
258
243
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
244
|
+
```typescript
|
|
245
|
+
await ws.cell(0, 0).setStyle({
|
|
246
|
+
background: "00FF0000", // 배경색 (ARGB 8자리 hex, alpha 반전)
|
|
247
|
+
border: ["left", "right", "top", "bottom"], // 테두리 위치
|
|
248
|
+
horizontalAlign: "center", // 수평 정렬: "left" | "center" | "right"
|
|
249
|
+
verticalAlign: "center", // 수직 정렬: "top" | "center" | "bottom"
|
|
250
|
+
numberFormat: "number", // 숫자 서식: "number" | "string" | "DateOnly" | "DateTime" | "Time"
|
|
251
|
+
});
|
|
264
252
|
|
|
265
|
-
|
|
253
|
+
// 스타일 ID 직접 접근
|
|
254
|
+
const styleId = await ws.cell(0, 0).getStyleId();
|
|
255
|
+
await ws.cell(0, 0).setStyleId(styleId);
|
|
256
|
+
```
|
|
266
257
|
|
|
267
|
-
|
|
258
|
+
**ExcelStyleOptions:**
|
|
268
259
|
|
|
269
|
-
|
|
260
|
+
| 필드 | 타입 | 설명 |
|
|
261
|
+
|------|------|------|
|
|
262
|
+
| `background` | `string` | ARGB 8자리 hex (예: `"00FF0000"` = 빨강) |
|
|
263
|
+
| `border` | `ExcelBorderPosition[]` | `"left"`, `"right"`, `"top"`, `"bottom"` |
|
|
264
|
+
| `horizontalAlign` | `ExcelHorizontalAlign` | `"left"`, `"center"`, `"right"` |
|
|
265
|
+
| `verticalAlign` | `ExcelVerticalAlign` | `"top"`, `"center"`, `"bottom"` |
|
|
266
|
+
| `numberFormat` | `ExcelNumberFormat` | `"number"`, `"string"`, `"DateOnly"`, `"DateTime"`, `"Time"` |
|
|
270
267
|
|
|
271
|
-
|
|
268
|
+
## 열 너비
|
|
272
269
|
|
|
273
270
|
```typescript
|
|
274
|
-
|
|
271
|
+
await ws.col(0).setWidth(20); // A열 너비 설정
|
|
275
272
|
```
|
|
276
273
|
|
|
277
|
-
|
|
274
|
+
## ExcelWrapper (Zod 스키마 기반)
|
|
278
275
|
|
|
279
|
-
|
|
280
|
-
|--------|---------|-------------|
|
|
281
|
-
| `read(file, wsNameOrIndex?, options?)` | `Promise<z.infer<TSchema>[]>` | Read Excel file into typed record array |
|
|
282
|
-
| `write(wsName, records, options?)` | `Promise<ExcelWorkbook>` | Write records to a new workbook |
|
|
276
|
+
Zod 스키마를 기반으로 타입 안전한 Excel 읽기/쓰기를 제공한다.
|
|
283
277
|
|
|
284
|
-
|
|
278
|
+
### 스키마 정의
|
|
285
279
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
| `wsNameOrIndex` | `string \| number` | `0` | Worksheet name or index |
|
|
290
|
-
| `options.excludes` | `(keyof T)[]` | | Fields to exclude from reading |
|
|
280
|
+
```typescript
|
|
281
|
+
import { ExcelWrapper } from "@simplysm/excel";
|
|
282
|
+
import { z } from "zod";
|
|
291
283
|
|
|
292
|
-
|
|
284
|
+
const schema = z.object({
|
|
285
|
+
name: z.string().describe("이름"), // .describe()로 Excel 헤더 이름 지정
|
|
286
|
+
age: z.number().describe("나이"),
|
|
287
|
+
email: z.string().optional().describe("이메일"), // optional: 필수 아닌 필드
|
|
288
|
+
active: z.boolean().describe("활성"), // boolean: 기본값 false
|
|
289
|
+
});
|
|
293
290
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
| `wsName` | `string` | Worksheet name |
|
|
297
|
-
| `records` | `Partial<T>[]` | Record array to write |
|
|
298
|
-
| `options.excludes` | `(keyof T)[]` | Fields to exclude from columns |
|
|
291
|
+
const wrapper = new ExcelWrapper(schema);
|
|
292
|
+
```
|
|
299
293
|
|
|
300
|
-
|
|
301
|
-
-
|
|
302
|
-
-
|
|
303
|
-
- Highlights required field headers with yellow background
|
|
304
|
-
- Sets zoom to 85% and freezes the header row
|
|
294
|
+
- `.describe("헤더이름")`: Excel 헤더에 표시될 이름 지정 (미지정 시 필드 키 사용)
|
|
295
|
+
- `z.optional()` / `z.nullable()` / `z.default()`: 선택 필드 (헤더 강조 없음)
|
|
296
|
+
- 필수 필드(boolean 제외)는 `write()` 시 헤더가 노란색으로 강조됨
|
|
305
297
|
|
|
306
|
-
|
|
298
|
+
### 읽기
|
|
307
299
|
|
|
308
|
-
|
|
300
|
+
```typescript
|
|
301
|
+
const records = await wrapper.read(fileBytes, "Sheet1");
|
|
302
|
+
// z.infer<typeof schema>[] 타입으로 반환
|
|
309
303
|
|
|
310
|
-
|
|
304
|
+
// 인덱스로 시트 지정 (기본: 0)
|
|
305
|
+
const records = await wrapper.read(fileBytes, 0);
|
|
311
306
|
|
|
312
|
-
|
|
307
|
+
// 특정 필드 제외
|
|
308
|
+
const records = await wrapper.read(fileBytes, "Sheet1", {
|
|
309
|
+
excludes: ["email"],
|
|
310
|
+
});
|
|
311
|
+
```
|
|
313
312
|
|
|
314
|
-
|
|
313
|
+
**값 변환 규칙:**
|
|
314
|
+
- `ZodString`: 문자열로 변환
|
|
315
|
+
- `ZodNumber`: `parseFloat`로 변환
|
|
316
|
+
- `ZodBoolean`: `"1"`, `"true"` = true / `"0"`, `"false"` = false
|
|
317
|
+
- `DateOnly`, `DateTime`, `Time`: 그대로 전달
|
|
318
|
+
- 빈 값: `ZodDefault` → 기본값, `ZodOptional`/`ZodNullable` → `undefined`, `ZodBoolean` → `false`
|
|
315
319
|
|
|
316
|
-
|
|
317
|
-
|--------|---------|-------------|
|
|
318
|
-
| `stringifyAddr({ r, c })` | `string` | Coordinates to "A1" format |
|
|
319
|
-
| `stringifyRowAddr(r)` | `string` | Row index to string (0 -> "1") |
|
|
320
|
-
| `stringifyColAddr(c)` | `string` | Column index to string (0 -> "A", 26 -> "AA") |
|
|
321
|
-
| `parseCellAddr(addr)` | `ExcelAddressPoint` | "B3" -> `{ r: 2, c: 1 }` |
|
|
322
|
-
| `parseRowAddr(addr)` | `number` | Extract row index from address |
|
|
323
|
-
| `parseColAddr(addr)` | `number` | Extract column index from address |
|
|
324
|
-
| `parseRangeAddr(rangeAddr)` | `ExcelAddressRangePoint` | "A1:C3" -> `{ s: {r:0,c:0}, e: {r:2,c:2} }` |
|
|
325
|
-
| `stringifyRangeAddr(point)` | `string` | Range coordinates to "A1:C3" format |
|
|
320
|
+
### 쓰기
|
|
326
321
|
|
|
327
|
-
|
|
322
|
+
```typescript
|
|
323
|
+
// ExcelWorkbook을 반환하므로 리소스 관리 필요
|
|
324
|
+
await using wb = await wrapper.write("Sheet1", [
|
|
325
|
+
{ name: "Alice", age: 30, email: "alice@example.com", active: true },
|
|
326
|
+
{ name: "Bob", age: 25, active: false },
|
|
327
|
+
]);
|
|
328
|
+
const bytes = await wb.toBytes();
|
|
328
329
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
// 특정 필드 제외
|
|
331
|
+
await using wb = await wrapper.write("Sheet1", records, {
|
|
332
|
+
excludes: ["email"],
|
|
333
|
+
});
|
|
334
|
+
```
|
|
333
335
|
|
|
334
|
-
|
|
336
|
+
`write()` 자동 적용 사항:
|
|
337
|
+
- 모든 셀에 테두리(상하좌우) 적용
|
|
338
|
+
- 필수 필드(boolean 제외) 헤더에 노란색 배경
|
|
339
|
+
- 줌 85%, 첫 행 틀고정
|
|
335
340
|
|
|
336
|
-
|
|
337
|
-
|--------|---------|-------------|
|
|
338
|
-
| `convertNumFmtIdToName(id)` | `ExcelNumberFormat` | Built-in format ID to name |
|
|
339
|
-
| `convertNumFmtCodeToName(code)` | `ExcelNumberFormat` | Format code string to name |
|
|
340
|
-
| `convertNumFmtNameToId(name)` | `number` | Format name to built-in ID |
|
|
341
|
+
## 유틸리티 (ExcelUtils)
|
|
341
342
|
|
|
342
|
-
|
|
343
|
+
```typescript
|
|
344
|
+
import { ExcelUtils } from "@simplysm/excel";
|
|
345
|
+
|
|
346
|
+
// 주소 변환 (0-based 좌표 <-> "A1" 형식)
|
|
347
|
+
ExcelUtils.stringifyAddr({ r: 0, c: 0 }); // "A1"
|
|
348
|
+
ExcelUtils.stringifyAddr({ r: 2, c: 1 }); // "B3"
|
|
349
|
+
ExcelUtils.stringifyColAddr(0); // "A"
|
|
350
|
+
ExcelUtils.stringifyColAddr(26); // "AA"
|
|
351
|
+
ExcelUtils.stringifyRowAddr(0); // "1"
|
|
352
|
+
|
|
353
|
+
ExcelUtils.parseCellAddr("B3"); // { r: 2, c: 1 }
|
|
354
|
+
ExcelUtils.parseColAddr("B3"); // 1
|
|
355
|
+
ExcelUtils.parseRowAddr("B3"); // 2
|
|
356
|
+
|
|
357
|
+
// 범위 주소 변환
|
|
358
|
+
ExcelUtils.parseRangeAddr("A1:C5"); // { s: {r:0,c:0}, e: {r:4,c:2} }
|
|
359
|
+
ExcelUtils.stringifyRangeAddr({ s: {r:0,c:0}, e: {r:4,c:2} }); // "A1:C5"
|
|
360
|
+
|
|
361
|
+
// 날짜 변환 (JavaScript tick <-> Excel 날짜 숫자)
|
|
362
|
+
ExcelUtils.convertTimeTickToNumber(Date.now()); // Excel 날짜 숫자
|
|
363
|
+
ExcelUtils.convertNumberToTimeTick(45678); // JavaScript timestamp (ms)
|
|
364
|
+
|
|
365
|
+
// 숫자 서식 변환
|
|
366
|
+
ExcelUtils.convertNumFmtNameToId("DateOnly"); // 14
|
|
367
|
+
ExcelUtils.convertNumFmtIdToName(14); // "DateOnly"
|
|
368
|
+
ExcelUtils.convertNumFmtCodeToName("yyyy-mm-dd"); // "DateOnly"
|
|
369
|
+
```
|
|
343
370
|
|
|
344
|
-
|
|
371
|
+
## 타입
|
|
345
372
|
|
|
346
373
|
```typescript
|
|
347
|
-
|
|
374
|
+
// 셀 값 타입
|
|
348
375
|
type ExcelValueType = number | string | DateOnly | DateTime | Time | boolean | undefined;
|
|
349
376
|
|
|
350
|
-
|
|
377
|
+
// 숫자 서식
|
|
351
378
|
type ExcelNumberFormat = "number" | "string" | "DateOnly" | "DateTime" | "Time";
|
|
352
379
|
|
|
353
|
-
|
|
380
|
+
// 좌표
|
|
354
381
|
interface ExcelAddressPoint { r: number; c: number; }
|
|
355
|
-
|
|
356
|
-
/** Range address (0-based) */
|
|
357
382
|
interface ExcelAddressRangePoint { s: ExcelAddressPoint; e: ExcelAddressPoint; }
|
|
358
383
|
|
|
359
|
-
|
|
384
|
+
// 스타일
|
|
385
|
+
type ExcelBorderPosition = "left" | "right" | "top" | "bottom";
|
|
386
|
+
type ExcelHorizontalAlign = "center" | "left" | "right";
|
|
387
|
+
type ExcelVerticalAlign = "center" | "top" | "bottom";
|
|
360
388
|
interface ExcelStyleOptions {
|
|
361
|
-
background?: string;
|
|
362
|
-
border?: ExcelBorderPosition[];
|
|
363
|
-
horizontalAlign?: ExcelHorizontalAlign;
|
|
364
|
-
verticalAlign?: ExcelVerticalAlign;
|
|
389
|
+
background?: string;
|
|
390
|
+
border?: ExcelBorderPosition[];
|
|
391
|
+
horizontalAlign?: ExcelHorizontalAlign;
|
|
392
|
+
verticalAlign?: ExcelVerticalAlign;
|
|
365
393
|
numberFormat?: ExcelNumberFormat;
|
|
366
394
|
}
|
|
367
395
|
|
|
368
|
-
|
|
369
|
-
type
|
|
370
|
-
|
|
396
|
+
// 셀 타입
|
|
397
|
+
type ExcelCellType = "s" | "b" | "str" | "n" | "inlineStr" | "e";
|
|
398
|
+
// s: SharedString, b: boolean, str: 수식 결과 문자열,
|
|
399
|
+
// n: 숫자, inlineStr: 인라인 문자열, e: 에러
|
|
371
400
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/excel",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.87",
|
|
4
4
|
"description": "Excel file processing library",
|
|
5
5
|
"author": "simplysm",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -21,6 +21,6 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"mime": "^4.1.0",
|
|
23
23
|
"zod": "^4.3.6",
|
|
24
|
-
"@simplysm/core-common": "13.0.
|
|
24
|
+
"@simplysm/core-common": "13.0.87"
|
|
25
25
|
}
|
|
26
26
|
}
|