@simplysm/excel 1.0.138 → 13.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/.cache/typecheck-browser.tsbuildinfo +1 -0
  2. package/.cache/typecheck-node.tsbuildinfo +1 -0
  3. package/.cache/typecheck-tests-browser.tsbuildinfo +1 -0
  4. package/.cache/typecheck-tests-node.tsbuildinfo +1 -0
  5. package/README.md +491 -0
  6. package/dist/core-common/src/common.types.d.ts +74 -0
  7. package/dist/core-common/src/common.types.d.ts.map +1 -0
  8. package/dist/core-common/src/env.d.ts +6 -0
  9. package/dist/core-common/src/env.d.ts.map +1 -0
  10. package/dist/core-common/src/errors/argument-error.d.ts +25 -0
  11. package/dist/core-common/src/errors/argument-error.d.ts.map +1 -0
  12. package/dist/core-common/src/errors/not-implemented-error.d.ts +29 -0
  13. package/dist/core-common/src/errors/not-implemented-error.d.ts.map +1 -0
  14. package/dist/core-common/src/errors/sd-error.d.ts +27 -0
  15. package/dist/core-common/src/errors/sd-error.d.ts.map +1 -0
  16. package/dist/core-common/src/errors/timeout-error.d.ts +31 -0
  17. package/dist/core-common/src/errors/timeout-error.d.ts.map +1 -0
  18. package/dist/core-common/src/extensions/arr-ext.d.ts +15 -0
  19. package/dist/core-common/src/extensions/arr-ext.d.ts.map +1 -0
  20. package/dist/core-common/src/extensions/arr-ext.helpers.d.ts +19 -0
  21. package/dist/core-common/src/extensions/arr-ext.helpers.d.ts.map +1 -0
  22. package/dist/core-common/src/extensions/arr-ext.types.d.ts +215 -0
  23. package/dist/core-common/src/extensions/arr-ext.types.d.ts.map +1 -0
  24. package/dist/core-common/src/extensions/map-ext.d.ts +57 -0
  25. package/dist/core-common/src/extensions/map-ext.d.ts.map +1 -0
  26. package/dist/core-common/src/extensions/set-ext.d.ts +36 -0
  27. package/dist/core-common/src/extensions/set-ext.d.ts.map +1 -0
  28. package/dist/core-common/src/features/debounce-queue.d.ts +53 -0
  29. package/dist/core-common/src/features/debounce-queue.d.ts.map +1 -0
  30. package/dist/core-common/src/features/event-emitter.d.ts +66 -0
  31. package/dist/core-common/src/features/event-emitter.d.ts.map +1 -0
  32. package/dist/core-common/src/features/serial-queue.d.ts +47 -0
  33. package/dist/core-common/src/features/serial-queue.d.ts.map +1 -0
  34. package/dist/core-common/src/index.d.ts +32 -0
  35. package/dist/core-common/src/index.d.ts.map +1 -0
  36. package/dist/core-common/src/types/date-only.d.ts +152 -0
  37. package/dist/core-common/src/types/date-only.d.ts.map +1 -0
  38. package/dist/core-common/src/types/date-time.d.ts +96 -0
  39. package/dist/core-common/src/types/date-time.d.ts.map +1 -0
  40. package/dist/core-common/src/types/lazy-gc-map.d.ts +80 -0
  41. package/dist/core-common/src/types/lazy-gc-map.d.ts.map +1 -0
  42. package/dist/core-common/src/types/time.d.ts +68 -0
  43. package/dist/core-common/src/types/time.d.ts.map +1 -0
  44. package/dist/core-common/src/types/uuid.d.ts +35 -0
  45. package/dist/core-common/src/types/uuid.d.ts.map +1 -0
  46. package/dist/core-common/src/utils/bytes.d.ts +51 -0
  47. package/dist/core-common/src/utils/bytes.d.ts.map +1 -0
  48. package/dist/core-common/src/utils/date-format.d.ts +90 -0
  49. package/dist/core-common/src/utils/date-format.d.ts.map +1 -0
  50. package/dist/core-common/src/utils/json.d.ts +34 -0
  51. package/dist/core-common/src/utils/json.d.ts.map +1 -0
  52. package/dist/core-common/src/utils/num.d.ts +60 -0
  53. package/dist/core-common/src/utils/num.d.ts.map +1 -0
  54. package/dist/core-common/src/utils/obj.d.ts +258 -0
  55. package/dist/core-common/src/utils/obj.d.ts.map +1 -0
  56. package/dist/core-common/src/utils/path.d.ts +23 -0
  57. package/dist/core-common/src/utils/path.d.ts.map +1 -0
  58. package/dist/core-common/src/utils/primitive.d.ts +18 -0
  59. package/dist/core-common/src/utils/primitive.d.ts.map +1 -0
  60. package/dist/core-common/src/utils/str.d.ts +103 -0
  61. package/dist/core-common/src/utils/str.d.ts.map +1 -0
  62. package/dist/core-common/src/utils/template-strings.d.ts +84 -0
  63. package/dist/core-common/src/utils/template-strings.d.ts.map +1 -0
  64. package/dist/core-common/src/utils/transferable.d.ts +47 -0
  65. package/dist/core-common/src/utils/transferable.d.ts.map +1 -0
  66. package/dist/core-common/src/utils/wait.d.ts +19 -0
  67. package/dist/core-common/src/utils/wait.d.ts.map +1 -0
  68. package/dist/core-common/src/utils/xml.d.ts +36 -0
  69. package/dist/core-common/src/utils/xml.d.ts.map +1 -0
  70. package/dist/core-common/src/zip/sd-zip.d.ts +80 -0
  71. package/dist/core-common/src/zip/sd-zip.d.ts.map +1 -0
  72. package/dist/excel/src/excel-cell.d.ts +68 -0
  73. package/dist/excel/src/excel-cell.d.ts.map +1 -0
  74. package/dist/excel/src/excel-col.d.ts +19 -0
  75. package/dist/excel/src/excel-col.d.ts.map +1 -0
  76. package/dist/excel/src/excel-row.d.ts +17 -0
  77. package/dist/excel/src/excel-row.d.ts.map +1 -0
  78. package/dist/excel/src/excel-workbook.d.ts +66 -0
  79. package/dist/excel/src/excel-workbook.d.ts.map +1 -0
  80. package/dist/excel/src/excel-worksheet.d.ts +102 -0
  81. package/dist/excel/src/excel-worksheet.d.ts.map +1 -0
  82. package/dist/excel/src/excel-wrapper.d.ts +42 -0
  83. package/dist/excel/src/excel-wrapper.d.ts.map +1 -0
  84. package/dist/excel/src/index.d.ts +9 -0
  85. package/dist/excel/src/index.d.ts.map +1 -0
  86. package/dist/excel/src/types.d.ts +445 -0
  87. package/dist/excel/src/types.d.ts.map +1 -0
  88. package/dist/excel/src/utils/excel-utils.d.ts +50 -0
  89. package/dist/excel/src/utils/excel-utils.d.ts.map +1 -0
  90. package/dist/excel/src/utils/zip-cache.d.ts +23 -0
  91. package/dist/excel/src/utils/zip-cache.d.ts.map +1 -0
  92. package/dist/excel/src/xml/excel-xml-content-type.d.ts +12 -0
  93. package/dist/excel/src/xml/excel-xml-content-type.d.ts.map +1 -0
  94. package/dist/excel/src/xml/excel-xml-drawing.d.ts +26 -0
  95. package/dist/excel/src/xml/excel-xml-drawing.d.ts.map +1 -0
  96. package/dist/excel/src/xml/excel-xml-relationship.d.ts +18 -0
  97. package/dist/excel/src/xml/excel-xml-relationship.d.ts.map +1 -0
  98. package/dist/excel/src/xml/excel-xml-shared-string.d.ts +19 -0
  99. package/dist/excel/src/xml/excel-xml-shared-string.d.ts.map +1 -0
  100. package/dist/excel/src/xml/excel-xml-style.d.ts +31 -0
  101. package/dist/excel/src/xml/excel-xml-style.d.ts.map +1 -0
  102. package/dist/excel/src/xml/excel-xml-unknown.d.ts +11 -0
  103. package/dist/excel/src/xml/excel-xml-unknown.d.ts.map +1 -0
  104. package/dist/excel/src/xml/excel-xml-workbook.d.ts +22 -0
  105. package/dist/excel/src/xml/excel-xml-workbook.d.ts.map +1 -0
  106. package/dist/excel/src/xml/excel-xml-worksheet.d.ts +103 -0
  107. package/dist/excel/src/xml/excel-xml-worksheet.d.ts.map +1 -0
  108. package/dist/excel-cell.js +261 -0
  109. package/dist/excel-cell.js.map +7 -0
  110. package/dist/excel-col.js +36 -0
  111. package/dist/excel-col.js.map +7 -0
  112. package/dist/excel-row.js +31 -0
  113. package/dist/excel-row.js.map +7 -0
  114. package/dist/excel-workbook.js +137 -0
  115. package/dist/excel-workbook.js.map +7 -0
  116. package/dist/excel-worksheet.js +279 -0
  117. package/dist/excel-worksheet.js.map +7 -0
  118. package/dist/excel-wrapper.js +220 -0
  119. package/dist/excel-wrapper.js.map +7 -0
  120. package/dist/index.js +9 -15
  121. package/dist/index.js.map +7 -1
  122. package/dist/types.js +1 -0
  123. package/dist/types.js.map +7 -0
  124. package/dist/utils/excel-utils.js +162 -0
  125. package/dist/utils/excel-utils.js.map +7 -0
  126. package/dist/utils/zip-cache.js +74 -0
  127. package/dist/utils/zip-cache.js.map +7 -0
  128. package/dist/xml/excel-xml-content-type.js +57 -0
  129. package/dist/xml/excel-xml-content-type.js.map +7 -0
  130. package/dist/xml/excel-xml-drawing.js +77 -0
  131. package/dist/xml/excel-xml-drawing.js.map +7 -0
  132. package/dist/xml/excel-xml-relationship.js +72 -0
  133. package/dist/xml/excel-xml-relationship.js.map +7 -0
  134. package/dist/xml/excel-xml-shared-string.js +61 -0
  135. package/dist/xml/excel-xml-shared-string.js.map +7 -0
  136. package/dist/xml/excel-xml-style.js +313 -0
  137. package/dist/xml/excel-xml-style.js.map +7 -0
  138. package/dist/xml/excel-xml-unknown.js +11 -0
  139. package/dist/xml/excel-xml-unknown.js.map +7 -0
  140. package/dist/xml/excel-xml-workbook.js +94 -0
  141. package/dist/xml/excel-xml-workbook.js.map +7 -0
  142. package/dist/xml/excel-xml-worksheet.js +405 -0
  143. package/dist/xml/excel-xml-worksheet.js.map +7 -0
  144. package/package.json +13 -7
  145. package/src/excel-cell.ts +326 -0
  146. package/src/excel-col.ts +43 -0
  147. package/src/excel-row.ts +37 -0
  148. package/src/excel-workbook.ts +206 -0
  149. package/src/excel-worksheet.ts +380 -0
  150. package/src/excel-wrapper.ts +219 -0
  151. package/src/index.ts +13 -9
  152. package/src/types.ts +396 -0
  153. package/src/utils/excel-utils.ts +201 -0
  154. package/src/utils/zip-cache.ts +103 -0
  155. package/src/xml/excel-xml-content-type.ts +64 -0
  156. package/src/xml/excel-xml-drawing.ts +87 -0
  157. package/src/xml/excel-xml-relationship.ts +86 -0
  158. package/src/xml/excel-xml-shared-string.ts +80 -0
  159. package/src/xml/excel-xml-style.ts +393 -0
  160. package/src/xml/excel-xml-unknown.ts +11 -0
  161. package/src/xml/excel-xml-workbook.ts +112 -0
  162. package/src/xml/excel-xml-worksheet.ts +544 -0
  163. package/tests/excel-cell.spec.ts +407 -0
  164. package/tests/excel-col.spec.ts +112 -0
  165. package/tests/excel-row.spec.ts +71 -0
  166. package/tests/excel-workbook.spec.ts +166 -0
  167. package/tests/excel-worksheet.spec.ts +389 -0
  168. package/tests/excel-wrapper.spec.ts +275 -0
  169. package/tests/fixtures/logo.png +0 -0
  170. package/tests/image-insert.spec.ts +188 -0
  171. package/tests/utils/excel-utils.spec.ts +240 -0
  172. package/dist/ExcelCell.d.ts +0 -13
  173. package/dist/ExcelCell.js +0 -161
  174. package/dist/ExcelCell.js.map +0 -1
  175. package/dist/ExcelCellStyle.d.ts +0 -31
  176. package/dist/ExcelCellStyle.js +0 -312
  177. package/dist/ExcelCellStyle.js.map +0 -1
  178. package/dist/ExcelColumn.d.ts +0 -8
  179. package/dist/ExcelColumn.js +0 -49
  180. package/dist/ExcelColumn.js.map +0 -1
  181. package/dist/ExcelRow.d.ts +0 -7
  182. package/dist/ExcelRow.js +0 -21
  183. package/dist/ExcelRow.js.map +0 -1
  184. package/dist/ExcelWorkbook.d.ts +0 -24
  185. package/dist/ExcelWorkbook.js +0 -418
  186. package/dist/ExcelWorkbook.js.map +0 -1
  187. package/dist/ExcelWorksheet.d.ts +0 -14
  188. package/dist/ExcelWorksheet.js +0 -31
  189. package/dist/ExcelWorksheet.js.map +0 -1
  190. package/dist/index.d.ts +0 -9
  191. package/dist/utils/ExcelUtils.d.ts +0 -14
  192. package/dist/utils/ExcelUtils.js +0 -66
  193. package/dist/utils/ExcelUtils.js.map +0 -1
  194. package/dist/utils/XmlConvert.d.ts +0 -4
  195. package/dist/utils/XmlConvert.js +0 -64
  196. package/dist/utils/XmlConvert.js.map +0 -1
  197. package/src/ExcelCell.ts +0 -163
  198. package/src/ExcelCellStyle.ts +0 -297
  199. package/src/ExcelColumn.ts +0 -46
  200. package/src/ExcelRow.ts +0 -17
  201. package/src/ExcelWorkbook.ts +0 -369
  202. package/src/ExcelWorksheet.ts +0 -27
  203. package/src/utils/ExcelUtils.ts +0 -68
  204. package/src/utils/XmlConvert.ts +0 -20
  205. package/tsconfig.build.json +0 -18
  206. package/tsconfig.json +0 -18
  207. package/tslint.json +0 -5
@@ -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
+ }
@@ -0,0 +1,219 @@
1
+ import type { Bytes } from "@simplysm/core-common";
2
+ import { DateOnly, DateTime, numParseFloat, Time } from "@simplysm/core-common";
3
+ import { type z, ZodBoolean, ZodDefault, ZodNullable, ZodNumber, ZodOptional, ZodString } from "zod";
4
+ import { ExcelWorkbook } from "./excel-workbook";
5
+ import type { ExcelValueType } from "./types";
6
+
7
+ /**
8
+ * Zod 스키마 기반 Excel 래퍼
9
+ *
10
+ * 스키마에서 타입 정보를 추론하여 타입 안전한 읽기/쓰기 제공
11
+ */
12
+ export class ExcelWrapper<T extends z.ZodObject<z.ZodRawShape>> {
13
+ /**
14
+ * @param _schema Zod 스키마 (레코드 구조 정의)
15
+ * @param _displayNameMap 필드명-표시명 매핑 (Excel 헤더로 사용)
16
+ */
17
+ constructor(
18
+ private readonly _schema: T,
19
+ private readonly _displayNameMap: Record<keyof z.infer<T>, string>,
20
+ ) {}
21
+
22
+ /**
23
+ * Excel 파일 읽기 → 레코드 배열
24
+ */
25
+ async read(file: Bytes | Blob, wsNameOrIndex: string | number = 0): Promise<z.infer<T>[]> {
26
+ await using wb = new ExcelWorkbook(file);
27
+
28
+ const ws = await wb.getWorksheet(wsNameOrIndex);
29
+ const wsName = await ws.getName();
30
+
31
+ const displayNames = Object.values(this._displayNameMap);
32
+ const rawData = await ws.getDataTable({
33
+ usableHeaderNameFn: (headerName) => displayNames.includes(headerName),
34
+ });
35
+
36
+ if (rawData.length === 0) {
37
+ throw new Error(`[${wsName}] 엑셀파일에서 데이터를 찾을 수 없습니다. (기대 헤더: ${displayNames.join(", ")})`);
38
+ }
39
+
40
+ const reverseMap = this._getReverseDisplayNameMap();
41
+ const shape = this._schema.shape;
42
+ const result: z.infer<T>[] = [];
43
+
44
+ for (const row of rawData) {
45
+ const record: Record<string, unknown> = {};
46
+ let hasNonNullValue = false;
47
+
48
+ for (const [displayName, fieldKey] of reverseMap) {
49
+ const rawValue = row[displayName];
50
+ const fieldSchema = shape[fieldKey] as z.ZodType;
51
+
52
+ if (rawValue != null && rawValue !== "") {
53
+ hasNonNullValue = true;
54
+ }
55
+
56
+ record[fieldKey] = this._convertValue(rawValue, fieldSchema);
57
+ }
58
+
59
+ if (!hasNonNullValue) {
60
+ continue;
61
+ }
62
+
63
+ // Zod 스키마로 검증
64
+ const parseResult = this._schema.safeParse(record);
65
+ if (!parseResult.success) {
66
+ const errors = parseResult.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", ");
67
+ throw new Error(`[${wsName}] 데이터 검증 실패: ${errors}`);
68
+ }
69
+
70
+ result.push(parseResult.data);
71
+ }
72
+
73
+ return result;
74
+ }
75
+
76
+ /**
77
+ * 레코드 배열 → Excel 워크북
78
+ *
79
+ * @remarks
80
+ * 반환된 워크북은 호출자가 리소스를 관리해야 합니다.
81
+ * `await using`을 사용하거나 작업 완료 후 `close()`를 호출하세요.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * await using wb = await wrapper.write("Sheet1", records);
86
+ * const bytes = await wb.getBytes();
87
+ * ```
88
+ */
89
+ async write(wsName: string, records: Partial<z.infer<T>>[]): Promise<ExcelWorkbook> {
90
+ const wb = new ExcelWorkbook();
91
+ const ws = await wb.createWorksheet(wsName);
92
+
93
+ const keys = Object.keys(this._displayNameMap) as (keyof z.infer<T>)[];
94
+ const headers = keys.map((key) => this._displayNameMap[key]);
95
+
96
+ // 헤더 행 작성
97
+ for (let c = 0; c < headers.length; c++) {
98
+ await ws.cell(0, c).setVal(headers[c]);
99
+ }
100
+
101
+ // 데이터 행 작성
102
+ for (let r = 0; r < records.length; r++) {
103
+ for (let c = 0; c < keys.length; c++) {
104
+ const key = keys[c];
105
+ const value = records[r][key] as ExcelValueType;
106
+ await ws.cell(r + 1, c).setVal(value);
107
+ }
108
+ }
109
+
110
+ // 테두리 스타일 적용
111
+ for (let r = 0; r < records.length + 1; r++) {
112
+ for (let c = 0; c < keys.length; c++) {
113
+ await ws.cell(r, c).setStyle({
114
+ border: ["left", "right", "top", "bottom"],
115
+ });
116
+ }
117
+ }
118
+
119
+ // 필수 필드 헤더 강조 (노란색)
120
+ const shape = this._schema.shape;
121
+ for (let c = 0; c < keys.length; c++) {
122
+ const fieldKey = keys[c] as string;
123
+ const fieldSchema = shape[fieldKey] as z.ZodType;
124
+
125
+ if (this._isRequired(fieldSchema) && !this._isBoolean(fieldSchema)) {
126
+ await ws.cell(0, c).setStyle({
127
+ background: "00FFFF00",
128
+ });
129
+ }
130
+ }
131
+
132
+ // 뷰 설정
133
+ await ws.setZoom(85);
134
+ await ws.setFix({ r: 0 });
135
+
136
+ return wb;
137
+ }
138
+
139
+ //#region Private Methods
140
+
141
+ private _getReverseDisplayNameMap(): Map<string, string> {
142
+ const map = new Map<string, string>();
143
+ for (const [fieldKey, displayName] of Object.entries(this._displayNameMap)) {
144
+ map.set(displayName, fieldKey);
145
+ }
146
+ return map;
147
+ }
148
+
149
+ private _convertValue(rawValue: ExcelValueType, fieldSchema: z.ZodType): unknown {
150
+ if (rawValue == null || rawValue === "") {
151
+ return this._getDefaultForSchema(fieldSchema);
152
+ }
153
+
154
+ const innerSchema = this._unwrapSchema(fieldSchema);
155
+
156
+ if (innerSchema instanceof ZodString) {
157
+ return typeof rawValue === "string" ? rawValue : String(rawValue);
158
+ }
159
+
160
+ if (innerSchema instanceof ZodNumber) {
161
+ if (typeof rawValue === "number") return rawValue;
162
+ return numParseFloat(String(rawValue));
163
+ }
164
+
165
+ if (innerSchema instanceof ZodBoolean) {
166
+ if (typeof rawValue === "boolean") return rawValue;
167
+ if (rawValue === "1" || rawValue === "true") return true;
168
+ if (rawValue === "0" || rawValue === "false") return false;
169
+ return Boolean(rawValue);
170
+ }
171
+
172
+ // DateOnly, DateTime, Time은 instanceof로 처리
173
+ if (rawValue instanceof DateOnly || rawValue instanceof DateTime || rawValue instanceof Time) {
174
+ return rawValue;
175
+ }
176
+
177
+ return rawValue;
178
+ }
179
+
180
+ private _unwrapSchema(schema: z.ZodType): z.ZodType {
181
+ if (schema instanceof ZodOptional || schema instanceof ZodNullable) {
182
+ return this._unwrapSchema(schema.unwrap() as z.ZodType);
183
+ }
184
+ if (schema instanceof ZodDefault) {
185
+ return this._unwrapSchema(schema.removeDefault() as z.ZodType);
186
+ }
187
+ return schema;
188
+ }
189
+
190
+ private _getDefaultForSchema(schema: z.ZodType): unknown {
191
+ if (schema instanceof ZodDefault) {
192
+ // ZodDefault.parse(undefined)는 기본값을 반환함
193
+ return schema.parse(undefined);
194
+ }
195
+
196
+ if (schema instanceof ZodOptional || schema instanceof ZodNullable) {
197
+ return undefined;
198
+ }
199
+
200
+ // Boolean 필수 필드의 기본값은 false
201
+ const innerSchema = this._unwrapSchema(schema);
202
+ if (innerSchema instanceof ZodBoolean) {
203
+ return false;
204
+ }
205
+
206
+ return undefined;
207
+ }
208
+
209
+ private _isRequired(schema: z.ZodType): boolean {
210
+ return !(schema instanceof ZodOptional) && !(schema instanceof ZodNullable) && !(schema instanceof ZodDefault);
211
+ }
212
+
213
+ private _isBoolean(schema: z.ZodType): boolean {
214
+ const innerSchema = this._unwrapSchema(schema);
215
+ return innerSchema instanceof ZodBoolean;
216
+ }
217
+
218
+ //#endregion
219
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,13 @@
1
- import "@simplysm/common";
2
- export * from "./ExcelCell";
3
- export * from "./ExcelCellStyle";
4
- export * from "./ExcelColumn";
5
- export * from "./ExcelRow";
6
- export * from "./ExcelWorkbook";
7
- export * from "./ExcelWorksheet";
8
- export * from "./utils/ExcelUtils";
9
- export * from "./utils/XmlConvert";
1
+ // 타입 및 유틸리티
2
+ export * from "./types";
3
+ export * from "./utils/excel-utils";
4
+
5
+ // 핵심 클래스
6
+ export * from "./excel-cell";
7
+ export * from "./excel-row";
8
+ export * from "./excel-col";
9
+ export * from "./excel-worksheet";
10
+ export * from "./excel-workbook";
11
+
12
+ // 래퍼 클래스
13
+ export * from "./excel-wrapper";