Skip to content

Commit a6ffff8

Browse files
committed
엑셀 시트 템플릿 적용
1 parent ca5b8bd commit a6ffff8

File tree

4 files changed

+315
-30
lines changed

4 files changed

+315
-30
lines changed

queries/queries-sample.xml

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,8 @@
11
<queries>
22
<excel db="sampleDB" output="d:/temp/매출집계_2024.xlsx" separateToc="false" maxRows="20">
3-
<header>
4-
<font name="맑은 고딕" size="12" color="FFFFFF" bold="true"/>
5-
<fill color="4F81BD"/>
6-
<colwidths min="20" max="50"/>
7-
<alignment horizontal="center" vertical="middle"/>
8-
<border>
9-
<all style="thin" color="000000"/>
10-
</border>
11-
</header>
12-
<body>
13-
<font name="맑은 고딕" size="11" color="000000" bold="false"/>
14-
<fill color="FFFFCC"/>
15-
<alignment horizontal="left" vertical="middle"/>
16-
<border>
17-
<all style="thin" color="CCCCCC"/>
18-
</border>
19-
</body>
3+
<!-- 스타일 템플릿 ID 지정 (기본값: default) -->
4+
<!-- 사용 가능한 템플릿: default, modern, dark, colorful, minimal, business, premium -->
5+
<!-- 필요시 개별 스타일 속성으로 덮어쓰기 가능 -->
206
</excel>
217
<vars>
228
<var name="envType">운영</var>

queries/queries-with-template.xml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<queries>
2+
<excel db="sampleDB" output="d:/temp/매출집계_템플릿_2024.xlsx" separateToc="false" maxRows="20">
3+
<!-- 스타일 템플릿 ID 지정 (기본값: default) -->
4+
<!-- 사용 가능한 템플릿: default, modern, dark, colorful, minimal, business, premium -->
5+
<!-- 필요시 개별 스타일 속성으로 덮어쓰기 가능 -->
6+
</excel>
7+
<vars>
8+
<var name="envType">운영</var>
9+
<var name="startDate">2024-01-01</var>
10+
<var name="endDate">2024-06-30</var>
11+
<var name="regionList">["서울", "부산"]</var>
12+
<var name="statusList">["ACTIVE", "PENDING", "COMPLETED"]</var>
13+
<var name="categoryIds">[1, 2, 3, 5]</var>
14+
<var name="maxRows">1000</var>
15+
</vars>
16+
<sheet name="${envType}_주문_목록" use="true" aggregateColumn="결제방법" maxRows="10" db="sampleDB">
17+
<![CDATA[
18+
SELECT
19+
OrderNumber as 주문번호,
20+
FORMAT(OrderDate, 'yyyy-MM-dd') as 주문일,
21+
OrderStatus as 주문상태,
22+
PaymentStatus as 결제상태,
23+
FORMAT(TotalAmount, 'N0') as 총금액,
24+
PaymentMethod as 결제방법
25+
FROM SampleDB.dbo.Orders
26+
WHERE OrderDate >= '${startDate}' AND OrderDate <= '${endDate}'
27+
ORDER BY OrderDate DESC
28+
]]>
29+
</sheet>
30+
<sheet name="고객_목록" use="true" aggregateColumn="지역">
31+
<![CDATA[
32+
SELECT
33+
CustomerCode as 고객코드,
34+
CustomerName as 고객명,
35+
ContactName as 담당자명,
36+
City as 도시,
37+
Region as 지역,
38+
CustomerType as 고객유형,
39+
FORMAT(CreditLimit, 'N0') as 신용한도
40+
FROM SampleDB.dbo.Customers
41+
WHERE Region IN (${regionList}) AND IsActive = 1
42+
ORDER BY CreditLimit DESC
43+
]]>
44+
</sheet>
45+
</queries>

src/index.js

Lines changed: 118 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,91 @@ function validateFilename(filepath) {
2525
return true;
2626
}
2727

28+
// 엑셀 스타일 템플릿 로더
29+
let styleTemplates = null;
30+
31+
async function loadStyleTemplates() {
32+
if (styleTemplates) return styleTemplates;
33+
34+
const templatePath = path.join(__dirname, '..', 'templates', 'excel-styles.xml');
35+
36+
try {
37+
const xml = fs.readFileSync(templatePath, 'utf8');
38+
const parsed = await xml2js.parseStringPromise(xml, { trim: true });
39+
40+
styleTemplates = {};
41+
if (parsed.excelStyles && parsed.excelStyles.style) {
42+
for (const style of parsed.excelStyles.style) {
43+
if (style.$ && style.$.id) {
44+
const styleId = style.$.id;
45+
const styleName = style.$.name || styleId;
46+
const description = style.$.description || '';
47+
48+
styleTemplates[styleId] = {
49+
id: styleId,
50+
name: styleName,
51+
description: description,
52+
header: parseStyleSection(style.header && style.header[0]),
53+
body: parseStyleSection(style.body && style.body[0])
54+
};
55+
}
56+
}
57+
}
58+
59+
console.log(`📋 로드된 스타일 템플릿: ${Object.keys(styleTemplates).length}개`);
60+
return styleTemplates;
61+
} catch (error) {
62+
console.warn(`⚠️ 스타일 템플릿 로드 실패: ${templatePath}`);
63+
console.warn(` 오류: ${error.message}`);
64+
console.warn(` 💡 기본 스타일을 사용합니다.`);
65+
return {};
66+
}
67+
}
68+
69+
// 스타일 섹션 파싱
70+
function parseStyleSection(section) {
71+
if (!section) return {};
72+
73+
const result = {};
74+
75+
if (section.font && section.font[0] && section.font[0].$) {
76+
result.font = section.font[0].$;
77+
}
78+
if (section.fill && section.fill[0] && section.fill[0].$) {
79+
result.fill = section.fill[0].$;
80+
}
81+
if (section.colwidths && section.colwidths[0] && section.colwidths[0].$) {
82+
result.colwidths = section.colwidths[0].$;
83+
}
84+
if (section.alignment && section.alignment[0] && section.alignment[0].$) {
85+
result.alignment = section.alignment[0].$;
86+
}
87+
if (section.border && section.border[0]) {
88+
result.border = parseXmlBorder(section.border[0]);
89+
}
90+
91+
return result;
92+
}
93+
94+
// 스타일 ID로 스타일 가져오기
95+
async function getStyleById(styleId) {
96+
const templates = await loadStyleTemplates();
97+
return templates[styleId] || templates['default'] || null;
98+
}
99+
100+
// 사용 가능한 스타일 목록 출력
101+
async function listAvailableStyles() {
102+
const templates = await loadStyleTemplates();
103+
104+
console.log('\n📋 사용 가능한 엑셀 스타일 템플릿:');
105+
console.log('─'.repeat(60));
106+
107+
for (const [id, style] of Object.entries(templates)) {
108+
console.log(` ${id.padEnd(12)} | ${style.name.padEnd(15)} | ${style.description}`);
109+
}
110+
console.log('─'.repeat(60));
111+
}
112+
28113
function substituteVars(str, vars) {
29114
return str.replace(/\$\{(\w+)\}/g, (_, v) => {
30115
const value = vars[v];
@@ -268,15 +353,23 @@ function isSheetEnabled(sheetDef) {
268353
}
269354

270355
async function main() {
271-
printAvailableXmlFiles();
272-
273356
const argv = yargs
274357
.option('query', { alias: 'q', describe: '쿼리 정의 파일 경로 (JSON)', default: '' })
275358
.option('xml', { alias: 'x', describe: '쿼리 정의 파일 경로 (XML)', default: '' })
276359
.option('config', { alias: 'c', describe: 'DB 접속 정보 파일', default: 'config/dbinfo.json' })
277360
.option('var', { alias: 'v', describe: '쿼리 변수 (key=value)', array: true, default: [] })
361+
.option('style', { alias: 's', describe: '엑셀 스타일 템플릿 ID', default: 'default' })
362+
.option('list-styles', { describe: '사용 가능한 스타일 템플릿 목록 출력', boolean: true })
278363
.help().argv;
279364

365+
printAvailableXmlFiles();
366+
367+
// 스타일 목록 출력 옵션 처리
368+
if (argv['list-styles']) {
369+
await listAvailableStyles();
370+
return;
371+
}
372+
280373
// CLI 변수 파싱
281374
const cliVars = {};
282375
for (const v of argv.var) {
@@ -348,6 +441,19 @@ async function main() {
348441
let createSeparateToc = false; // 별도 목차 파일 생성 여부
349442
let globalMaxRows = null; // 전역 최대 조회 건수
350443

444+
// 스타일 템플릿 적용
445+
const selectedStyle = await getStyleById(argv.style);
446+
if (selectedStyle) {
447+
console.log(`🎨 적용된 스타일: ${selectedStyle.name} (${selectedStyle.description})`);
448+
excelStyle = {
449+
header: selectedStyle.header || {},
450+
body: selectedStyle.body || {}
451+
};
452+
} else {
453+
console.warn(`⚠️ 스타일 템플릿을 찾을 수 없습니다: ${argv.style}`);
454+
console.warn(` 💡 기본 스타일을 사용합니다.`);
455+
}
456+
351457
if (argv.xml && fs.existsSync(resolvePath(argv.xml))) {
352458
let xml;
353459
try {
@@ -374,29 +480,28 @@ async function main() {
374480
// excel 엘리먼트의 maxRows 읽기
375481
if (excel.$ && excel.$.maxRows) globalMaxRows = parseInt(excel.$.maxRows);
376482

377-
excelStyle.header = {};
378-
excelStyle.body = {};
483+
// XML에서 스타일 속성이 있으면 템플릿 스타일을 덮어씀
379484
if (excel.header && excel.header[0]) {
380485
const h = excel.header[0];
381-
if (h.font && h.font[0] && h.font[0].$) excelStyle.header.font = h.font[0].$;
382-
if (h.fill && h.fill[0] && h.fill[0].$) excelStyle.header.fill = h.fill[0].$;
383-
if (h.colwidths && h.colwidths[0] && h.colwidths[0].$) excelStyle.header.colwidths = h.colwidths[0].$;
486+
if (h.font && h.font[0] && h.font[0].$) excelStyle.header.font = { ...excelStyle.header.font, ...h.font[0].$ };
487+
if (h.fill && h.fill[0] && h.fill[0].$) excelStyle.header.fill = { ...excelStyle.header.fill, ...h.fill[0].$ };
488+
if (h.colwidths && h.colwidths[0] && h.colwidths[0].$) excelStyle.header.colwidths = { ...excelStyle.header.colwidths, ...h.colwidths[0].$ };
384489
if (h.alignment && h.alignment[0] && h.alignment[0].$) {
385-
excelStyle.header.alignment = h.alignment[0].$;
490+
excelStyle.header.alignment = { ...excelStyle.header.alignment, ...h.alignment[0].$ };
386491
}
387492
if (h.border && h.border[0]) {
388-
excelStyle.header.border = parseXmlBorder(h.border[0]);
493+
excelStyle.header.border = { ...excelStyle.header.border, ...parseXmlBorder(h.border[0]) };
389494
}
390495
}
391496
if (excel.body && excel.body[0]) {
392497
const b = excel.body[0];
393-
if (b.font && b.font[0] && b.font[0].$) excelStyle.body.font = b.font[0].$;
394-
if (b.fill && b.fill[0] && b.fill[0].$) excelStyle.body.fill = b.fill[0].$;
498+
if (b.font && b.font[0] && b.font[0].$) excelStyle.body.font = { ...excelStyle.body.font, ...b.font[0].$ };
499+
if (b.fill && b.fill[0] && b.fill[0].$) excelStyle.body.fill = { ...excelStyle.body.fill, ...b.fill[0].$ };
395500
if (b.alignment && b.alignment[0] && b.alignment[0].$) {
396-
excelStyle.body.alignment = b.alignment[0].$;
501+
excelStyle.body.alignment = { ...excelStyle.body.alignment, ...b.alignment[0].$ };
397502
}
398503
if (b.border && b.border[0]) {
399-
excelStyle.body.border = parseXmlBorder(b.border[0]);
504+
excelStyle.body.border = { ...excelStyle.body.border, ...parseXmlBorder(b.border[0]) };
400505
}
401506
}
402507
}

templates/excel-styles.xml

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<excelStyles>
3+
<!-- 기본 스타일 -->
4+
<style id="default" name="기본 스타일" description="기본 엑셀 스타일">
5+
<header>
6+
<font name="맑은 고딕" size="12" color="FFFFFF" bold="true"/>
7+
<fill color="4F81BD"/>
8+
<colwidths min="20" max="50"/>
9+
<alignment horizontal="center" vertical="middle"/>
10+
<border>
11+
<all style="thin" color="000000"/>
12+
</border>
13+
</header>
14+
<body>
15+
<font name="맑은 고딕" size="11" color="000000" bold="false"/>
16+
<fill color="FFFFCC"/>
17+
<alignment horizontal="left" vertical="middle"/>
18+
<border>
19+
<all style="thin" color="CCCCCC"/>
20+
</border>
21+
</body>
22+
</style>
23+
24+
<!-- 모던 스타일 -->
25+
<style id="modern" name="모던 스타일" description="현대적인 디자인">
26+
<header>
27+
<font name="맑은 고딕" size="11" color="FFFFFF" bold="true"/>
28+
<fill color="2E75B6"/>
29+
<colwidths min="15" max="40"/>
30+
<alignment horizontal="center" vertical="middle"/>
31+
<border>
32+
<all style="thin" color="1F4E79"/>
33+
</border>
34+
</header>
35+
<body>
36+
<font name="맑은 고딕" size="10" color="2F2F2F" bold="false"/>
37+
<fill color="F8F9FA"/>
38+
<alignment horizontal="left" vertical="middle"/>
39+
<border>
40+
<all style="thin" color="E9ECEF"/>
41+
</border>
42+
</body>
43+
</style>
44+
45+
<!-- 다크 스타일 -->
46+
<style id="dark" name="다크 스타일" description="어두운 테마">
47+
<header>
48+
<font name="맑은 고딕" size="12" color="FFFFFF" bold="true"/>
49+
<fill color="343A40"/>
50+
<colwidths min="20" max="50"/>
51+
<alignment horizontal="center" vertical="middle"/>
52+
<border>
53+
<all style="thin" color="495057"/>
54+
</border>
55+
</header>
56+
<body>
57+
<font name="맑은 고딕" size="11" color="FFFFFF" bold="false"/>
58+
<fill color="495057"/>
59+
<alignment horizontal="left" vertical="middle"/>
60+
<border>
61+
<all style="thin" color="6C757D"/>
62+
</border>
63+
</body>
64+
</style>
65+
66+
<!-- 컬러풀 스타일 -->
67+
<style id="colorful" name="컬러풀 스타일" description="다채로운 색상">
68+
<header>
69+
<font name="맑은 고딕" size="12" color="FFFFFF" bold="true"/>
70+
<fill color="FF6B6B"/>
71+
<colwidths min="18" max="45"/>
72+
<alignment horizontal="center" vertical="middle"/>
73+
<border>
74+
<all style="thin" color="E74C3C"/>
75+
</border>
76+
</header>
77+
<body>
78+
<font name="맑은 고딕" size="11" color="2C3E50" bold="false"/>
79+
<fill color="FFF8DC"/>
80+
<alignment horizontal="left" vertical="middle"/>
81+
<border>
82+
<all style="thin" color="F39C12"/>
83+
</border>
84+
</body>
85+
</style>
86+
87+
<!-- 미니멀 스타일 -->
88+
<style id="minimal" name="미니멀 스타일" description="간결한 디자인">
89+
<header>
90+
<font name="맑은 고딕" size="11" color="333333" bold="true"/>
91+
<fill color="F5F5F5"/>
92+
<colwidths min="15" max="35"/>
93+
<alignment horizontal="left" vertical="middle"/>
94+
<border>
95+
<all style="thin" color="DDDDDD"/>
96+
</border>
97+
</header>
98+
<body>
99+
<font name="맑은 고딕" size="10" color="666666" bold="false"/>
100+
<fill color="FFFFFF"/>
101+
<alignment horizontal="left" vertical="middle"/>
102+
<border>
103+
<all style="thin" color="EEEEEE"/>
104+
</border>
105+
</body>
106+
</style>
107+
108+
<!-- 비즈니스 스타일 -->
109+
<style id="business" name="비즈니스 스타일" description="업무용 스타일">
110+
<header>
111+
<font name="맑은 고딕" size="12" color="FFFFFF" bold="true"/>
112+
<fill color="1E3A8A"/>
113+
<colwidths min="20" max="50"/>
114+
<alignment horizontal="center" vertical="middle"/>
115+
<border>
116+
<all style="thin" color="1E40AF"/>
117+
</border>
118+
</header>
119+
<body>
120+
<font name="맑은 고딕" size="11" color="1F2937" bold="false"/>
121+
<fill color="F9FAFB"/>
122+
<alignment horizontal="left" vertical="middle"/>
123+
<border>
124+
<all style="thin" color="E5E7EB"/>
125+
</border>
126+
</body>
127+
</style>
128+
129+
<!-- 프리미엄 스타일 -->
130+
<style id="premium" name="프리미엄 스타일" description="고급스러운 디자인">
131+
<header>
132+
<font name="맑은 고딕" size="12" color="FFFFFF" bold="true"/>
133+
<fill color="8B4513"/>
134+
<colwidths min="22" max="55"/>
135+
<alignment horizontal="center" vertical="middle"/>
136+
<border>
137+
<all style="thin" color="654321"/>
138+
</border>
139+
</header>
140+
<body>
141+
<font name="맑은 고딕" size="11" color="2F2F2F" bold="false"/>
142+
<fill color="FFF8DC"/>
143+
<alignment horizontal="left" vertical="middle"/>
144+
<border>
145+
<all style="thin" color="DEB887"/>
146+
</border>
147+
</body>
148+
</style>
149+
</excelStyles>

0 commit comments

Comments
 (0)