@simplysm/excel 14.0.65 → 14.0.69
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 +1 -0
- package/dist/excel-cell.d.ts.map +1 -1
- package/dist/excel-cell.js +5 -34
- package/dist/excel-cell.js.map +1 -1
- package/dist/excel-workbook.d.ts +19 -0
- package/dist/excel-workbook.d.ts.map +1 -1
- package/dist/excel-workbook.js +26 -0
- package/dist/excel-workbook.js.map +1 -1
- package/dist/excel-worksheet.d.ts +27 -1
- package/dist/excel-worksheet.d.ts.map +1 -1
- package/dist/excel-worksheet.js +104 -0
- package/dist/excel-worksheet.js.map +1 -1
- package/dist/types.d.ts +148 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/excel-style-data.d.ts +10 -0
- package/dist/utils/excel-style-data.d.ts.map +1 -0
- package/dist/utils/excel-style-data.js +22 -0
- package/dist/utils/excel-style-data.js.map +1 -0
- package/dist/xml/excel-xml-style.d.ts +23 -1
- package/dist/xml/excel-xml-style.d.ts.map +1 -1
- package/dist/xml/excel-xml-style.js +197 -25
- package/dist/xml/excel-xml-style.js.map +1 -1
- package/dist/xml/excel-xml-worksheet.d.ts +11 -1
- package/dist/xml/excel-xml-worksheet.d.ts.map +1 -1
- package/dist/xml/excel-xml-worksheet.js +36 -0
- package/dist/xml/excel-xml-worksheet.js.map +1 -1
- package/package.json +3 -3
- package/src/excel-cell.ts +5 -47
- package/src/excel-workbook.ts +30 -0
- package/src/excel-worksheet.ts +143 -1
- package/src/types.ts +147 -1
- package/src/utils/excel-style-data.ts +31 -0
- package/src/xml/excel-xml-style.ts +226 -32
- package/src/xml/excel-xml-worksheet.ts +40 -0
package/src/types.ts
CHANGED
|
@@ -128,10 +128,45 @@ export interface ExcelXmlWorksheetData {
|
|
|
128
128
|
}[];
|
|
129
129
|
},
|
|
130
130
|
];
|
|
131
|
+
conditionalFormatting?: ExcelXmlConditionalFormattingData[];
|
|
131
132
|
drawing?: { $: { "r:id": string } }[];
|
|
132
133
|
};
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
export interface ExcelXmlConditionalFormattingData {
|
|
137
|
+
$: { sqref: string };
|
|
138
|
+
cfRule: ExcelXmlCfRuleData[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ExcelXmlCfRuleData {
|
|
142
|
+
$: {
|
|
143
|
+
type:
|
|
144
|
+
| "cellIs"
|
|
145
|
+
| "containsText"
|
|
146
|
+
| "notContainsText"
|
|
147
|
+
| "beginsWith"
|
|
148
|
+
| "endsWith"
|
|
149
|
+
| "expression";
|
|
150
|
+
operator?:
|
|
151
|
+
| "lessThan"
|
|
152
|
+
| "lessThanOrEqual"
|
|
153
|
+
| "equal"
|
|
154
|
+
| "notEqual"
|
|
155
|
+
| "greaterThanOrEqual"
|
|
156
|
+
| "greaterThan"
|
|
157
|
+
| "between"
|
|
158
|
+
| "notBetween"
|
|
159
|
+
| "containsText"
|
|
160
|
+
| "notContains"
|
|
161
|
+
| "beginsWith"
|
|
162
|
+
| "endsWith";
|
|
163
|
+
priority: string;
|
|
164
|
+
dxfId: string;
|
|
165
|
+
text?: string;
|
|
166
|
+
};
|
|
167
|
+
formula: string[];
|
|
168
|
+
}
|
|
169
|
+
|
|
135
170
|
export interface ExcelRowData {
|
|
136
171
|
$: {
|
|
137
172
|
r: string; // 주소 (1~)
|
|
@@ -236,7 +271,7 @@ export interface ExcelXmlStyleData {
|
|
|
236
271
|
fonts: [
|
|
237
272
|
{
|
|
238
273
|
$: { count: string };
|
|
239
|
-
font:
|
|
274
|
+
font: ExcelXmlStyleDataFont[];
|
|
240
275
|
},
|
|
241
276
|
];
|
|
242
277
|
fills: [
|
|
@@ -257,9 +292,44 @@ export interface ExcelXmlStyleData {
|
|
|
257
292
|
xf: ExcelXmlStyleDataXf[];
|
|
258
293
|
},
|
|
259
294
|
];
|
|
295
|
+
dxfs?: [
|
|
296
|
+
{
|
|
297
|
+
$: { count: string };
|
|
298
|
+
dxf: ExcelXmlStyleDataDxf[];
|
|
299
|
+
},
|
|
300
|
+
];
|
|
260
301
|
};
|
|
261
302
|
}
|
|
262
303
|
|
|
304
|
+
export interface ExcelXmlStyleDataFont {
|
|
305
|
+
sz?: [{ $: { val: string } }];
|
|
306
|
+
name?: [{ $: { val: string } }];
|
|
307
|
+
b?: [{}];
|
|
308
|
+
i?: [{}];
|
|
309
|
+
u?: [{ $?: { val?: ExcelFontUnderline } }];
|
|
310
|
+
strike?: [{}];
|
|
311
|
+
color?: [{ $: { rgb: string } }];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface ExcelXmlStyleDataDxf {
|
|
315
|
+
font?: [
|
|
316
|
+
{
|
|
317
|
+
b?: [{ $: { val: "0" | "1" } }];
|
|
318
|
+
color?: [{ $: { rgb: string } }];
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
fill?: [
|
|
322
|
+
{
|
|
323
|
+
patternFill: [
|
|
324
|
+
{
|
|
325
|
+
$: { patternType?: "solid" };
|
|
326
|
+
bgColor?: [{ $: { rgb: string } }];
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
},
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
|
|
263
333
|
export interface ExcelXmlStyleDataXf {
|
|
264
334
|
$: {
|
|
265
335
|
numFmtId?: string;
|
|
@@ -364,6 +434,30 @@ export interface ExcelXml {
|
|
|
364
434
|
export type ExcelBorderPosition = "left" | "right" | "top" | "bottom";
|
|
365
435
|
export type ExcelHorizontalAlign = "center" | "left" | "right";
|
|
366
436
|
export type ExcelVerticalAlign = "center" | "top" | "bottom";
|
|
437
|
+
export type ExcelFontUnderline = "single" | "double" | "singleAccounting" | "doubleAccounting";
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 폰트 속성. cell 단위 override (`ExcelStyleOptions.font`) 와
|
|
441
|
+
* workbook default (`wb.setDefaultStyle({ font })`) 양쪽이 공유한다.
|
|
442
|
+
*
|
|
443
|
+
* 미지정 속성은 OOXML `<font>` 자식 엘리먼트로 emit 되지 않으며, Excel 자체 기본값으로 표시된다.
|
|
444
|
+
*/
|
|
445
|
+
export interface ExcelFont {
|
|
446
|
+
/** 폰트 크기 (pt) */
|
|
447
|
+
size?: number;
|
|
448
|
+
/** 폰트명 (예: "맑은 고딕", "Calibri") */
|
|
449
|
+
family?: string;
|
|
450
|
+
/** 굵게 */
|
|
451
|
+
bold?: boolean;
|
|
452
|
+
/** 기울임 */
|
|
453
|
+
italic?: boolean;
|
|
454
|
+
/** 밑줄 — `<u val="..."/>` 의 val 에 그대로 매핑 */
|
|
455
|
+
underline?: ExcelFontUnderline;
|
|
456
|
+
/** 글자색 (ARGB 8자리, 예: "00FF0000") */
|
|
457
|
+
color?: string;
|
|
458
|
+
/** 취소선 */
|
|
459
|
+
strike?: boolean;
|
|
460
|
+
}
|
|
367
461
|
|
|
368
462
|
/**
|
|
369
463
|
* 셀 스타일 옵션
|
|
@@ -397,6 +491,58 @@ export interface ExcelStyleOptions {
|
|
|
397
491
|
* `numberFormat`과 동시 지정 시 이 필드가 우선 적용된다.
|
|
398
492
|
*/
|
|
399
493
|
numberFormatCode?: string;
|
|
494
|
+
/** 폰트 (size/family/bold/italic/underline/color/strike) */
|
|
495
|
+
font?: ExcelFont;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
//#endregion
|
|
499
|
+
|
|
500
|
+
//#region Conditional Format Types
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 조건부 서식 강조 스타일.
|
|
504
|
+
* 미지정 필드는 base 셀 스타일을 그대로 두고, 지정 필드만 OOXML dxf 로 emit 되어 native CF 오버레이로 합성된다.
|
|
505
|
+
*/
|
|
506
|
+
export interface ExcelConditionalRuleStyle {
|
|
507
|
+
/** 배경색 (ARGB 8자리, 예: "00FFFF00") */
|
|
508
|
+
background?: string;
|
|
509
|
+
/** 글자색 (ARGB 8자리) */
|
|
510
|
+
fontColor?: string;
|
|
511
|
+
/** 글자 굵기. "normal" 은 base 가 bold 라도 강제 normal. */
|
|
512
|
+
fontWeight?: "bold" | "normal";
|
|
400
513
|
}
|
|
401
514
|
|
|
515
|
+
/**
|
|
516
|
+
* 조건부 서식 규칙.
|
|
517
|
+
* - `cellIs` 단일 비교(`<`, `>`, `<=`, `>=`, `=`, `<>`): `value` 는 number 또는 string.
|
|
518
|
+
* - `cellIs` 구간(`between`, `notBetween`): `value` 는 [a, b] 튜플(양 끝 inclusive).
|
|
519
|
+
* - `text` 매칭(`contains`, `notContains`, `beginsWith`, `endsWith`): `value` 는 string. SEARCH 기반(대소문자 무시) 고정.
|
|
520
|
+
*
|
|
521
|
+
* `value: number` 는 raw formula(`<formula>4999</formula>`), `value: string` 은 따옴표 둘러싼 리터럴 formula(`<formula>"OK"</formula>`) 로 emit.
|
|
522
|
+
*/
|
|
523
|
+
export type ExcelConditionalRule =
|
|
524
|
+
| {
|
|
525
|
+
type: "cellIs";
|
|
526
|
+
op: "<" | ">" | "<=" | ">=" | "=" | "<>";
|
|
527
|
+
value: number | string;
|
|
528
|
+
style: ExcelConditionalRuleStyle;
|
|
529
|
+
}
|
|
530
|
+
| {
|
|
531
|
+
type: "cellIs";
|
|
532
|
+
op: "between" | "notBetween";
|
|
533
|
+
value: [number, number] | [string, string];
|
|
534
|
+
style: ExcelConditionalRuleStyle;
|
|
535
|
+
}
|
|
536
|
+
| {
|
|
537
|
+
type: "text";
|
|
538
|
+
op: "contains" | "notContains" | "beginsWith" | "endsWith";
|
|
539
|
+
value: string;
|
|
540
|
+
style: ExcelConditionalRuleStyle;
|
|
541
|
+
}
|
|
542
|
+
| {
|
|
543
|
+
type: "expression";
|
|
544
|
+
formula: string;
|
|
545
|
+
style: ExcelConditionalRuleStyle;
|
|
546
|
+
};
|
|
547
|
+
|
|
402
548
|
//#endregion
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ZipCache } from "./zip-cache";
|
|
2
|
+
import { ExcelXmlContentType } from "../xml/excel-xml-content-type";
|
|
3
|
+
import { ExcelXmlRelationship } from "../xml/excel-xml-relationship";
|
|
4
|
+
import { ExcelXmlStyle } from "../xml/excel-xml-style";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `xl/styles.xml` 을 가져오거나, 없으면 새로 만들어 ZipCache 에 등록한다.
|
|
8
|
+
* Content_Types / workbook.xml.rels 에도 styles.xml 항목을 추가한다.
|
|
9
|
+
*
|
|
10
|
+
* `ExcelCell.setStyle` 과 `ExcelWorkbook.setDefaultStyle` 양쪽이 공유한다.
|
|
11
|
+
*/
|
|
12
|
+
export async function getOrCreateStyleData(zipCache: ZipCache): Promise<ExcelXmlStyle> {
|
|
13
|
+
let styleData = (await zipCache.get("xl/styles.xml")) as ExcelXmlStyle | undefined;
|
|
14
|
+
if (styleData == null) {
|
|
15
|
+
styleData = new ExcelXmlStyle();
|
|
16
|
+
zipCache.set("xl/styles.xml", styleData);
|
|
17
|
+
|
|
18
|
+
const typeData = (await zipCache.get("[Content_Types].xml")) as ExcelXmlContentType;
|
|
19
|
+
typeData.add(
|
|
20
|
+
"/xl/styles.xml",
|
|
21
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const wbRelData = (await zipCache.get("xl/_rels/workbook.xml.rels")) as ExcelXmlRelationship;
|
|
25
|
+
wbRelData.add(
|
|
26
|
+
"styles.xml",
|
|
27
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return styleData;
|
|
31
|
+
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ExcelBorderPosition,
|
|
3
|
+
ExcelConditionalRuleStyle,
|
|
4
|
+
ExcelFont,
|
|
3
5
|
ExcelHorizontalAlign,
|
|
6
|
+
ExcelStyleOptions,
|
|
4
7
|
ExcelVerticalAlign,
|
|
5
8
|
ExcelXml,
|
|
6
9
|
ExcelXmlStyleData,
|
|
7
10
|
ExcelXmlStyleDataBorder,
|
|
11
|
+
ExcelXmlStyleDataDxf,
|
|
8
12
|
ExcelXmlStyleDataFill,
|
|
13
|
+
ExcelXmlStyleDataFont,
|
|
9
14
|
ExcelXmlStyleDataXf,
|
|
10
15
|
} from "../types";
|
|
11
16
|
import "@simplysm/core-common";
|
|
12
17
|
import { num, obj } from "@simplysm/core-common";
|
|
18
|
+
import { ExcelUtils } from "../utils/excel-utils";
|
|
13
19
|
|
|
14
20
|
export interface ExcelStyle {
|
|
15
21
|
numFmtId?: string;
|
|
@@ -18,6 +24,50 @@ export interface ExcelStyle {
|
|
|
18
24
|
background?: string;
|
|
19
25
|
verticalAlign?: ExcelVerticalAlign;
|
|
20
26
|
horizontalAlign?: ExcelHorizontalAlign;
|
|
27
|
+
font?: ExcelFont;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* `ExcelStyleOptions` (사용자 표면) → 내부 `ExcelStyle` 변환.
|
|
32
|
+
* cell.setStyle 과 wb.setDefaultStyle 이 공유한다.
|
|
33
|
+
*
|
|
34
|
+
* - background ARGB 8자리 형식 검증
|
|
35
|
+
* - numberFormatCode 가 numberFormat 보다 우선
|
|
36
|
+
* - font 는 그대로 전달 (구체 검증은 ExcelXmlStyle 내부에서 수행)
|
|
37
|
+
*/
|
|
38
|
+
export function convertExcelStyleOptions(opts: ExcelStyleOptions): ExcelStyle {
|
|
39
|
+
const style: ExcelStyle = {};
|
|
40
|
+
|
|
41
|
+
if (opts.background != null) {
|
|
42
|
+
if (!/^[0-9A-F]{8}$/i.test(opts.background)) {
|
|
43
|
+
throw new Error("잘못된 색상 형식입니다. (형식: 00000000: alpha(반전)+rgb)");
|
|
44
|
+
}
|
|
45
|
+
style.background = opts.background;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (opts.border != null) {
|
|
49
|
+
style.border = opts.border;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (opts.horizontalAlign != null) {
|
|
53
|
+
style.horizontalAlign = opts.horizontalAlign;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (opts.verticalAlign != null) {
|
|
57
|
+
style.verticalAlign = opts.verticalAlign;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (opts.numberFormatCode != null) {
|
|
61
|
+
style.numFmtCode = opts.numberFormatCode;
|
|
62
|
+
} else if (opts.numberFormat != null) {
|
|
63
|
+
style.numFmtId = ExcelUtils.convertNumFmtNameToId(opts.numberFormat).toString();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (opts.font != null) {
|
|
67
|
+
style.font = opts.font;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return style;
|
|
21
71
|
}
|
|
22
72
|
|
|
23
73
|
/**
|
|
@@ -70,41 +120,21 @@ export class ExcelXmlStyle implements ExcelXml {
|
|
|
70
120
|
|
|
71
121
|
add(style: ExcelStyle): string {
|
|
72
122
|
const newXf: ExcelXmlStyleDataXf = { $: {} };
|
|
73
|
-
|
|
74
|
-
if (style.numFmtId != null) {
|
|
75
|
-
newXf.$.numFmtId = style.numFmtId;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (style.numFmtCode != null) {
|
|
79
|
-
newXf.$.numFmtId = this._setNumFmtCode(style.numFmtCode);
|
|
80
|
-
newXf.$.applyNumberFormat = "1";
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (style.background != null) {
|
|
84
|
-
const newFill: ExcelXmlStyleDataFill = {
|
|
85
|
-
patternFill: [
|
|
86
|
-
{
|
|
87
|
-
$: { patternType: "solid" },
|
|
88
|
-
fgColor: [{ $: { rgb: style.background.toUpperCase() } }],
|
|
89
|
-
},
|
|
90
|
-
],
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
newXf.$.applyFill = "1";
|
|
94
|
-
newXf.$.fillId = this._getSameOrCreateFill(newFill);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (style.border != null) {
|
|
98
|
-
const newBorder = this._createBorderFromPositions(style.border);
|
|
99
|
-
newXf.$.applyBorder = "1";
|
|
100
|
-
newXf.$.borderId = this._getSameOrCreateBorder(newBorder);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
this._applyAlignment(newXf, style);
|
|
104
|
-
|
|
123
|
+
this._applyStyleToXf(newXf, style);
|
|
105
124
|
return this._getSameOrCreateXf(newXf);
|
|
106
125
|
}
|
|
107
126
|
|
|
127
|
+
/**
|
|
128
|
+
* 워크북 default cell style 설정. `cellXfs[0].xf[0]` (OOXML default cell style 자리) 을 새로 빌드해 덮어쓴다.
|
|
129
|
+
* fonts/fills/borders/numFmts 자원은 `_getSameOrCreate*` 로 누적·dedup 되고 인덱스가 cellXfs[0] 에 박힌다.
|
|
130
|
+
* 미호출 시 기존 cellXfs[0] 그대로 보존된다.
|
|
131
|
+
*/
|
|
132
|
+
setDefaultStyle(style: ExcelStyle): void {
|
|
133
|
+
const newXf: ExcelXmlStyleDataXf = { $: { numFmtId: "0" } };
|
|
134
|
+
this._applyStyleToXf(newXf, style);
|
|
135
|
+
this.data.styleSheet.cellXfs[0].xf[0] = newXf;
|
|
136
|
+
}
|
|
137
|
+
|
|
108
138
|
addWithClone(id: string, style: ExcelStyle): string {
|
|
109
139
|
const idNum = num.parseInt(id);
|
|
110
140
|
if (idNum == null) {
|
|
@@ -179,6 +209,12 @@ export class ExcelXmlStyle implements ExcelXml {
|
|
|
179
209
|
}
|
|
180
210
|
}
|
|
181
211
|
|
|
212
|
+
if (style.font != null) {
|
|
213
|
+
this._validateFont(style.font);
|
|
214
|
+
cloneXf.$.applyFont = "1";
|
|
215
|
+
cloneXf.$.fontId = this._getSameOrCreateFont(this._buildFontXml(style.font));
|
|
216
|
+
}
|
|
217
|
+
|
|
182
218
|
this._applyAlignment(cloneXf, style);
|
|
183
219
|
|
|
184
220
|
return this._getSameOrCreateXf(cloneXf);
|
|
@@ -248,11 +284,67 @@ export class ExcelXmlStyle implements ExcelXml {
|
|
|
248
284
|
|
|
249
285
|
result.verticalAlign = xf.alignment?.[0].$.vertical;
|
|
250
286
|
result.horizontalAlign = xf.alignment?.[0].$.horizontal;
|
|
287
|
+
|
|
288
|
+
if (xf.$.fontId != null) {
|
|
289
|
+
const fontIdNum = num.parseInt(xf.$.fontId);
|
|
290
|
+
if (fontIdNum != null) {
|
|
291
|
+
const font = this.data.styleSheet.fonts[0].font[fontIdNum] as
|
|
292
|
+
| ExcelXmlStyleDataFont
|
|
293
|
+
| undefined;
|
|
294
|
+
if (font != null) {
|
|
295
|
+
const parsed = this._parseFontXml(font);
|
|
296
|
+
if (Object.keys(parsed).length > 0) {
|
|
297
|
+
result.font = parsed;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
251
302
|
}
|
|
252
303
|
|
|
253
304
|
return result;
|
|
254
305
|
}
|
|
255
306
|
|
|
307
|
+
addDxf(style: ExcelConditionalRuleStyle): string {
|
|
308
|
+
const dxfItem: ExcelXmlStyleDataDxf = {};
|
|
309
|
+
|
|
310
|
+
if (style.fontColor != null || style.fontWeight != null) {
|
|
311
|
+
const font: NonNullable<ExcelXmlStyleDataDxf["font"]>[number] = {};
|
|
312
|
+
if (style.fontWeight != null) {
|
|
313
|
+
font.b = [{ $: { val: style.fontWeight === "bold" ? "1" : "0" } }];
|
|
314
|
+
}
|
|
315
|
+
if (style.fontColor != null) {
|
|
316
|
+
font.color = [{ $: { rgb: style.fontColor.toUpperCase() } }];
|
|
317
|
+
}
|
|
318
|
+
dxfItem.font = [font];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (style.background != null) {
|
|
322
|
+
dxfItem.fill = [
|
|
323
|
+
{
|
|
324
|
+
patternFill: [
|
|
325
|
+
{
|
|
326
|
+
$: { patternType: "solid" },
|
|
327
|
+
bgColor: [{ $: { rgb: style.background.toUpperCase() } }],
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const dxfs = (this.data.styleSheet.dxfs = this.data.styleSheet.dxfs ?? [
|
|
335
|
+
{ $: { count: "0" }, dxf: [] },
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
const prevSameDxf = dxfs[0].dxf.single((item) => obj.equal(item, dxfItem));
|
|
339
|
+
if (prevSameDxf != null) {
|
|
340
|
+
return dxfs[0].dxf.indexOf(prevSameDxf).toString();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
dxfs[0].dxf.push(dxfItem);
|
|
344
|
+
dxfs[0].$.count = dxfs[0].dxf.length.toString();
|
|
345
|
+
return (dxfs[0].dxf.length - 1).toString();
|
|
346
|
+
}
|
|
347
|
+
|
|
256
348
|
getNumFmtCode(numFmtId: string): string | undefined {
|
|
257
349
|
return (this.data.styleSheet.numFmts?.[0].numFmt ?? []).single(
|
|
258
350
|
(item) => item.$.numFmtId === numFmtId,
|
|
@@ -320,6 +412,108 @@ export class ExcelXmlStyle implements ExcelXml {
|
|
|
320
412
|
return nextNumFmtId;
|
|
321
413
|
}
|
|
322
414
|
|
|
415
|
+
private _applyStyleToXf(xf: ExcelXmlStyleDataXf, style: ExcelStyle): void {
|
|
416
|
+
if (style.numFmtId != null) {
|
|
417
|
+
xf.$.numFmtId = style.numFmtId;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (style.numFmtCode != null) {
|
|
421
|
+
xf.$.numFmtId = this._setNumFmtCode(style.numFmtCode);
|
|
422
|
+
xf.$.applyNumberFormat = "1";
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (style.background != null) {
|
|
426
|
+
const newFill: ExcelXmlStyleDataFill = {
|
|
427
|
+
patternFill: [
|
|
428
|
+
{
|
|
429
|
+
$: { patternType: "solid" },
|
|
430
|
+
fgColor: [{ $: { rgb: style.background.toUpperCase() } }],
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
};
|
|
434
|
+
xf.$.applyFill = "1";
|
|
435
|
+
xf.$.fillId = this._getSameOrCreateFill(newFill);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (style.border != null) {
|
|
439
|
+
const newBorder = this._createBorderFromPositions(style.border);
|
|
440
|
+
xf.$.applyBorder = "1";
|
|
441
|
+
xf.$.borderId = this._getSameOrCreateBorder(newBorder);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (style.font != null) {
|
|
445
|
+
this._validateFont(style.font);
|
|
446
|
+
xf.$.applyFont = "1";
|
|
447
|
+
xf.$.fontId = this._getSameOrCreateFont(this._buildFontXml(style.font));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
this._applyAlignment(xf, style);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private _validateFont(font: ExcelFont): void {
|
|
454
|
+
if (font.color != null && !/^[0-9A-F]{8}$/i.test(font.color)) {
|
|
455
|
+
throw new Error("잘못된 폰트 색상 형식입니다. (형식: 00000000: alpha(반전)+rgb)");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private _buildFontXml(font: ExcelFont): ExcelXmlStyleDataFont {
|
|
460
|
+
const result: ExcelXmlStyleDataFont = {};
|
|
461
|
+
if (font.size != null) {
|
|
462
|
+
result.sz = [{ $: { val: font.size.toString() } }];
|
|
463
|
+
}
|
|
464
|
+
if (font.family != null) {
|
|
465
|
+
result.name = [{ $: { val: font.family } }];
|
|
466
|
+
}
|
|
467
|
+
if (font.bold === true) {
|
|
468
|
+
result.b = [{}];
|
|
469
|
+
}
|
|
470
|
+
if (font.italic === true) {
|
|
471
|
+
result.i = [{}];
|
|
472
|
+
}
|
|
473
|
+
if (font.underline != null) {
|
|
474
|
+
result.u = [{ $: { val: font.underline } }];
|
|
475
|
+
}
|
|
476
|
+
if (font.strike === true) {
|
|
477
|
+
result.strike = [{}];
|
|
478
|
+
}
|
|
479
|
+
if (font.color != null) {
|
|
480
|
+
result.color = [{ $: { rgb: font.color.toUpperCase() } }];
|
|
481
|
+
}
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private _parseFontXml(item: ExcelXmlStyleDataFont): ExcelFont {
|
|
486
|
+
const result: ExcelFont = {};
|
|
487
|
+
if (item.sz?.[0].$.val != null) {
|
|
488
|
+
const sz = num.parseFloat(item.sz[0].$.val);
|
|
489
|
+
if (sz != null) result.size = sz;
|
|
490
|
+
}
|
|
491
|
+
if (item.name?.[0].$.val != null) {
|
|
492
|
+
result.family = item.name[0].$.val;
|
|
493
|
+
}
|
|
494
|
+
if (item.b != null) result.bold = true;
|
|
495
|
+
if (item.i != null) result.italic = true;
|
|
496
|
+
if (item.u != null) {
|
|
497
|
+
result.underline = item.u[0].$?.val ?? "single";
|
|
498
|
+
}
|
|
499
|
+
if (item.strike != null) result.strike = true;
|
|
500
|
+
if (item.color?.[0].$.rgb != null) {
|
|
501
|
+
result.color = item.color[0].$.rgb;
|
|
502
|
+
}
|
|
503
|
+
return result;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private _getSameOrCreateFont(item: ExcelXmlStyleDataFont): string {
|
|
507
|
+
const prevSameFont = this.data.styleSheet.fonts[0].font.single((f) => obj.equal(f, item));
|
|
508
|
+
if (prevSameFont != null) {
|
|
509
|
+
return this.data.styleSheet.fonts[0].font.indexOf(prevSameFont).toString();
|
|
510
|
+
} else {
|
|
511
|
+
this.data.styleSheet.fonts[0].font.push(item);
|
|
512
|
+
this.data.styleSheet.fonts[0].$.count = this.data.styleSheet.fonts[0].font.length.toString();
|
|
513
|
+
return (this.data.styleSheet.fonts[0].font.length - 1).toString();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
323
517
|
private _applyAlignment(xf: ExcelXmlStyleDataXf, style: ExcelStyle): void {
|
|
324
518
|
if (style.verticalAlign != null) {
|
|
325
519
|
xf.$.applyAlignment = "1";
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ExcelCellType,
|
|
5
5
|
ExcelRowData,
|
|
6
6
|
ExcelXml,
|
|
7
|
+
ExcelXmlCfRuleData,
|
|
7
8
|
ExcelXmlWorksheetData,
|
|
8
9
|
} from "../types";
|
|
9
10
|
import { ExcelUtils } from "../utils/excel-utils";
|
|
@@ -221,6 +222,41 @@ export class ExcelXmlWorksheet implements ExcelXml {
|
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
|
|
225
|
+
/**
|
|
226
|
+
* `<conditionalFormatting>` 블록을 시트에 push.
|
|
227
|
+
* priority 는 시트 전역 카운터(기존 cfRule 의 최대 priority + 1)부터 호출 내 rules 순서대로 부여.
|
|
228
|
+
*/
|
|
229
|
+
addConditionalFormatting(sqref: string, rules: { dxfId: string; cfRule: Omit<ExcelXmlCfRuleData["$"], "priority" | "dxfId"> & { formula: string[] } }[]): void {
|
|
230
|
+
const cfList = (this.data.worksheet.conditionalFormatting =
|
|
231
|
+
this.data.worksheet.conditionalFormatting ?? []);
|
|
232
|
+
|
|
233
|
+
let nextPriority = 1;
|
|
234
|
+
for (const block of cfList) {
|
|
235
|
+
for (const rule of block.cfRule) {
|
|
236
|
+
const p = num.parseInt(rule.$.priority);
|
|
237
|
+
if (p != null && p >= nextPriority) {
|
|
238
|
+
nextPriority = p + 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const cfRuleData: ExcelXmlCfRuleData[] = rules.map((r) => ({
|
|
244
|
+
$: {
|
|
245
|
+
type: r.cfRule.type,
|
|
246
|
+
priority: (nextPriority++).toString(),
|
|
247
|
+
dxfId: r.dxfId,
|
|
248
|
+
...(r.cfRule.operator != null ? { operator: r.cfRule.operator } : {}),
|
|
249
|
+
...(r.cfRule.text != null ? { text: r.cfRule.text } : {}),
|
|
250
|
+
},
|
|
251
|
+
formula: r.cfRule.formula,
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
cfList.push({
|
|
255
|
+
$: { sqref },
|
|
256
|
+
cfRule: cfRuleData,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
224
260
|
shiftMergeCells(fromRow: number, delta: number): void {
|
|
225
261
|
const mergeCells = this.data.worksheet.mergeCells;
|
|
226
262
|
if (mergeCells == null) return;
|
|
@@ -445,6 +481,7 @@ export class ExcelXmlWorksheet implements ExcelXml {
|
|
|
445
481
|
if (key === "cols") continue;
|
|
446
482
|
if (key === "sheetViews") continue;
|
|
447
483
|
if (key === "sheetFormatPr") continue;
|
|
484
|
+
if (key === "conditionalFormatting") continue;
|
|
448
485
|
|
|
449
486
|
if (key === "sheetData") {
|
|
450
487
|
if (this.data.worksheet.sheetViews != null) {
|
|
@@ -461,6 +498,9 @@ export class ExcelXmlWorksheet implements ExcelXml {
|
|
|
461
498
|
if (this.data.worksheet.mergeCells != null) {
|
|
462
499
|
result.mergeCells = this.data.worksheet.mergeCells;
|
|
463
500
|
}
|
|
501
|
+
if (this.data.worksheet.conditionalFormatting != null) {
|
|
502
|
+
result.conditionalFormatting = this.data.worksheet.conditionalFormatting;
|
|
503
|
+
}
|
|
464
504
|
} else {
|
|
465
505
|
const worksheetRec = this.data.worksheet as Record<string, unknown>;
|
|
466
506
|
const resultRec = result as Record<string, unknown>;
|