Skip to content

Commit 0e3fc77

Browse files
authored
Merge pull request #1 from mrjung72/dev
refactoring
2 parents e930d03 + d43298b commit 0e3fc77

File tree

8 files changed

+1653
-980
lines changed

8 files changed

+1653
-980
lines changed

queries/test-simple.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"excel": {
3+
"db": "sampleDB",
4+
"output": "output/test_output.xlsx"
5+
},
6+
"vars": {
7+
"testVar": "test_value"
8+
},
9+
"sheets": [
10+
{
11+
"name": "TestSheet",
12+
"use": true,
13+
"query": "SELECT 1 as id, 'test' as name, '${testVar}' as variable"
14+
}
15+
]
16+
}

src/excel-generator.js

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
const ExcelJS = require('exceljs');
2+
const excelStyleHelper = require('./excel-style-helper');
3+
const FileUtils = require('./file-utils');
4+
5+
/**
6+
* 엑셀 생성 관련 함수들을 담당하는 모듈
7+
*/
8+
class ExcelGenerator {
9+
constructor() {
10+
this.fileUtils = FileUtils;
11+
}
12+
13+
/**
14+
* 엑셀 파일 생성
15+
* @param {Object} options - 생성 옵션
16+
* @returns {Promise<string>} 생성된 파일 경로
17+
*/
18+
async generateExcel(options) {
19+
const {
20+
sheets,
21+
outputPath,
22+
createSeparateToc = false,
23+
createdSheetNames = [],
24+
createdSheetCounts = []
25+
} = options;
26+
27+
console.log('-------------------------------------------------------------------------------');
28+
console.log(`[${outputPath}] START WORK`);
29+
console.log('-------------------------------------------------------------------------------');
30+
31+
const workbook = new ExcelJS.Workbook();
32+
const createdSheets = [];
33+
34+
// 목차 시트를 맨 처음에 생성 (내용은 나중에 채움)
35+
let tocSheet = null;
36+
37+
for (const sheetDef of sheets) {
38+
// robust use 속성 체크
39+
if (!this.isSheetEnabled(sheetDef)) {
40+
console.log(`[SKIP] Sheet '${sheetDef.name}' is disabled (use=false)`);
41+
continue;
42+
}
43+
44+
// 첫 번째 활성 시트일 때 목차 시트 생성
45+
if (!tocSheet) {
46+
tocSheet = workbook.addWorksheet('목차');
47+
console.log(`[목차] 맨 첫 번째 시트로 생성됨`);
48+
}
49+
50+
const sheet = workbook.addWorksheet(sheetDef.name);
51+
const recordCount = sheetDef.recordCount || 0;
52+
53+
// 실제 생성된 시트명 가져오기 (31자 초과시 잘린 이름)
54+
const actualSheetName = sheet.name;
55+
56+
// 집계 컬럼이 지정된 경우 집계 데이터 계산
57+
let aggregateData = null;
58+
if (sheetDef.aggregateColumn && recordCount > 0) {
59+
aggregateData = this.calculateAggregateData(sheetDef.aggregateColumn, sheetDef.data);
60+
console.log(`\t[집계] ${sheetDef.aggregateColumn} 컬럼 집계: ${aggregateData.map(item => `${item.key}(${item.count})`).join(', ')}`);
61+
}
62+
63+
createdSheets.push({
64+
displayName: sheetDef.name,
65+
originalName: sheetDef.name,
66+
tabName: actualSheetName,
67+
recordCount: recordCount,
68+
aggregateColumn: sheetDef.aggregateColumn,
69+
aggregateData: aggregateData
70+
});
71+
72+
// 시트명이 잘렸는지 확인하고 로그 출력
73+
if (sheetDef.name !== actualSheetName) {
74+
console.log(`\t[WARN] Sheet name truncated: '${sheetDef.name}' → '${actualSheetName}'`);
75+
}
76+
77+
if (recordCount > 0) {
78+
// 데이터와 스타일 적용 (1행부터 시작)
79+
excelStyleHelper.applySheetStyle(sheet, sheetDef.data, sheetDef.style, 1);
80+
81+
// 데이터 추가 후 맨 앞에 DB 정보 행 삽입
82+
sheet.spliceRows(1, 0, [`📊 출처: ${sheetDef.dbKey} DB`]);
83+
sheet.spliceRows(2, 0, []); // 빈 행 추가
84+
85+
// DB 정보 셀 스타일링
86+
const dbCell = sheet.getCell('A1');
87+
dbCell.font = { bold: true, size: 11, color: { argb: 'FFFFFF' } };
88+
dbCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: '366092' } };
89+
90+
console.log(`\t[DB정보] ${sheetDef.dbKey} DB 출처 표시 완료`);
91+
} else {
92+
// 데이터가 없는 경우
93+
sheet.addRow([`📊 출처: ${sheetDef.dbKey} DB`]);
94+
sheet.addRow([]);
95+
sheet.addRow(['데이터가 없습니다.']);
96+
97+
// 스타일링
98+
sheet.getCell('A1').font = { bold: true, size: 11, color: { argb: 'FFFFFF' } };
99+
sheet.getCell('A1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: '366092' } };
100+
sheet.getCell('A3').font = { italic: true, color: { argb: '999999' } };
101+
102+
console.log(`\t[DB정보] ${sheetDef.dbKey} DB 출처 표시 완료 (데이터 없음)`);
103+
}
104+
console.log(`\t---> ${recordCount} rows were selected `);
105+
}
106+
107+
// 목차 시트에 내용 채우기
108+
if (createdSheets.length > 0 && tocSheet) {
109+
// excel-style-helper 모듈의 함수 사용하여 안전한 목차 생성
110+
excelStyleHelper.populateTableOfContents(tocSheet, createdSheets);
111+
112+
// 목차 시트를 첫 번째로 이동 (ExcelJS에서는 worksheets가 읽기 전용이므로 다른 방법 사용)
113+
// 목차 시트는 이미 첫 번째로 생성되었으므로 추가 조작 불필요
114+
115+
console.log(`[목차] 내용 채우기 완료 (총 ${createdSheets.length}개 시트)`);
116+
117+
if (createSeparateToc) {
118+
await this.createSeparateTocFile(outputPath, createdSheets, createdSheetCounts);
119+
}
120+
}
121+
122+
console.log(`\nGenerating excel file ... `);
123+
console.log(`Wating a few seconds ... `);
124+
await workbook.xlsx.writeFile(outputPath);
125+
console.log(`\n\n[${outputPath}] Excel file created `);
126+
console.log('-------------------------------------------------------------------------------\n\n');
127+
128+
return outputPath;
129+
}
130+
131+
/**
132+
* 별도 목차 파일 생성
133+
* @param {string} outputPath - 원본 출력 파일 경로
134+
* @param {Array} createdSheetNames - 생성된 시트 정보
135+
* @param {Array} createdSheetCounts - 시트별 데이터 개수
136+
*/
137+
async createSeparateTocFile(outputPath, createdSheetNames, createdSheetCounts) {
138+
const tocWb = new ExcelJS.Workbook();
139+
const tocOnly = tocWb.addWorksheet('목차');
140+
tocOnly.addRow(['No', 'Sheet Name', 'Data Count']);
141+
142+
createdSheetNames.forEach((obj, idx) => {
143+
const row = tocOnly.addRow([idx + 1, obj.displayName, createdSheetCounts[idx]]);
144+
row.getCell(2).font = { color: { argb: '0563C1' }, underline: true };
145+
row.getCell(3).font = { color: { argb: '0563C1' }, underline: true };
146+
});
147+
148+
tocOnly.getRow(1).font = { bold: true };
149+
tocOnly.columns = [
150+
{ header: 'No', key: 'no', width: 6 },
151+
{ header: 'Sheet Name', key: 'name', width: 30 },
152+
{ header: 'Data Count', key: 'count', width: 12 }
153+
];
154+
155+
const tocExt = FileUtils.getExtension(outputPath);
156+
const tocBase = outputPath.slice(0, -tocExt.length);
157+
const tocFile = `${tocBase}_목차_${FileUtils.getNowTimestampStr()}${tocExt}`;
158+
159+
await tocWb.xlsx.writeFile(tocFile);
160+
console.log(`[목차] 별도 엑셀 파일 생성: ${tocFile}`);
161+
}
162+
163+
/**
164+
* 시트가 활성화되어 있는지 확인
165+
* @param {Object} sheetDef - 시트 정의 객체
166+
* @returns {boolean} 활성화 여부
167+
*/
168+
isSheetEnabled(sheetDef) {
169+
let use = true;
170+
// JSON: use 속성
171+
if (typeof sheetDef.use !== 'undefined') {
172+
if (
173+
sheetDef.use === false ||
174+
sheetDef.use === 0 ||
175+
sheetDef.use === 'false' ||
176+
sheetDef.use === '0' ||
177+
sheetDef.use === '' ||
178+
sheetDef.use === null
179+
) use = false;
180+
}
181+
// XML: $.use 속성
182+
else if (sheetDef.hasOwnProperty('$') && typeof sheetDef.$.use !== 'undefined') {
183+
const val = sheetDef.$.use;
184+
if (
185+
val === false ||
186+
val === 0 ||
187+
val === 'false' ||
188+
val === '0' ||
189+
val === '' ||
190+
val === null
191+
) use = false;
192+
}
193+
return use;
194+
}
195+
196+
/**
197+
* 집계 데이터 계산
198+
* @param {string} aggregateColumn - 집계 컬럼명
199+
* @param {Array} data - 데이터 배열
200+
* @returns {Array} 집계 결과
201+
*/
202+
calculateAggregateData(aggregateColumn, data) {
203+
if (!data || data.length === 0) return [];
204+
205+
const aggregateMap = {};
206+
207+
data.forEach(row => {
208+
const value = row[aggregateColumn];
209+
if (value !== null && value !== undefined) {
210+
const key = String(value).trim();
211+
aggregateMap[key] = (aggregateMap[key] || 0) + 1;
212+
}
213+
});
214+
215+
// 집계 결과를 배열로 변환 (건수가 많은 순으로 정렬)
216+
return Object.entries(aggregateMap)
217+
.map(([key, count]) => ({ key, count }))
218+
.sort((a, b) => b.count - a.count);
219+
}
220+
221+
/**
222+
* 엑셀 파일 경로 생성
223+
* @param {string} basePath - 기본 경로
224+
* @param {string} timestamp - 타임스탬프
225+
* @returns {string} 생성된 파일 경로
226+
*/
227+
generateOutputPath(basePath, timestamp) {
228+
const ext = FileUtils.getExtension(basePath);
229+
const base = basePath.slice(0, -ext.length);
230+
return `${base}_${timestamp}${ext}`;
231+
}
232+
233+
/**
234+
* 엑셀 파일 검증
235+
* @param {string} filePath - 파일 경로
236+
* @returns {boolean} 유효성 여부
237+
*/
238+
validateExcelFile(filePath) {
239+
const ext = this.fileUtils.getExtension(filePath).toLowerCase();
240+
return ext === '.xlsx' || ext === '.xls';
241+
}
242+
243+
/**
244+
* 엑셀 파일 크기 확인
245+
* @param {string} filePath - 파일 경로
246+
* @returns {number} 파일 크기 (바이트)
247+
*/
248+
getExcelFileSize(filePath) {
249+
return FileUtils.getFileSize(filePath);
250+
}
251+
252+
/**
253+
* 엑셀 파일 생성 시간 확인
254+
* @param {string} filePath - 파일 경로
255+
* @returns {Date} 생성 시간
256+
*/
257+
getExcelFileCreatedTime(filePath) {
258+
return FileUtils.getModifiedTime(filePath);
259+
}
260+
}
261+
262+
module.exports = ExcelGenerator;

0 commit comments

Comments
 (0)