Skip to content

Commit 7f50b86

Browse files
committed
Added improvements
1. Sheet name verification logic is also applied to file verification logic. 2. Added XML element and attribute name validation logic
1 parent 39241b0 commit 7f50b86

File tree

4 files changed

+252
-37
lines changed

4 files changed

+252
-37
lines changed

app.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,7 @@ function question(query) {
174174
function pause() {
175175
return new Promise(resolve => {
176176
console.log(`\n${msg.pressAnyKey}`);
177-
process.stdin.setRawMode(true);
178-
process.stdin.resume();
179-
process.stdin.once('data', () => {
180-
process.stdin.setRawMode(false);
181-
process.stdin.pause();
177+
rl.question('', () => {
182178
resolve();
183179
});
184180
});

src/excel-cli.js

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,46 @@ const yargs = require('yargs');
77
const args = process.argv.slice(2);
88
const command = args[0];
99

10+
/**
11+
* 시트명 유효성 검증
12+
* @param {string} sheetName - 검증할 시트명
13+
* @param {boolean} skipLengthCheck - 길이 검증 건너뛰기 (변수 포함 시)
14+
* @returns {Object} { valid: boolean, errors: string[] }
15+
*/
16+
function validateSheetName(sheetName, skipLengthCheck = false) {
17+
const errors = [];
18+
19+
// Excel 시트명에 사용할 수 없는 문자
20+
const invalidChars = ['\\', '/', '*', '?', '[', ']', ':'];
21+
22+
// 1. 빈 문자열 체크
23+
if (!sheetName || sheetName.trim() === '') {
24+
errors.push('시트명이 비어있습니다.');
25+
return { valid: false, errors };
26+
}
27+
28+
// 2. 최대 길이 체크 (31자) - 변수 포함 시 건너뛰기
29+
if (!skipLengthCheck && sheetName.length > 31) {
30+
errors.push(`시트명이 너무 깁니다 (최대 31자, 현재: ${sheetName.length}자)`);
31+
}
32+
33+
// 3. 허용되지 않는 문자 체크
34+
const foundInvalidChars = invalidChars.filter(char => sheetName.includes(char));
35+
if (foundInvalidChars.length > 0) {
36+
errors.push(`허용되지 않는 문자 포함: ${foundInvalidChars.join(', ')}`);
37+
}
38+
39+
// 4. 시트명 시작/끝 공백 체크
40+
if (sheetName !== sheetName.trim()) {
41+
errors.push('시트명 앞뒤에 공백이 있습니다.');
42+
}
43+
44+
return {
45+
valid: errors.length === 0,
46+
errors
47+
};
48+
}
49+
1050
// 도움말 표시
1151
function showHelp() {
1252
console.log(`
@@ -271,18 +311,39 @@ async function validateQueryFile(options) {
271311
// 쿼리 정의 수집
272312
const queryDefArray = Array.isArray(parsed.queries.queryDefs[0].queryDef) ? parsed.queries.queryDefs[0].queryDef : [parsed.queries.queryDefs[0].queryDef];
273313
queryDefArray.forEach(def => {
274-
if (def.$ && def.$.name) {
275-
queryDefs[def.$.name] = true;
314+
if (def.$ && (def.$.id || def.$.name)) {
315+
const queryId = def.$.id || def.$.name;
316+
queryDefs[queryId] = true;
317+
console.log(` [DEBUG] queryDef 발견: ${queryId}`);
276318
}
277319
});
320+
console.log(` [DEBUG] 총 ${Object.keys(queryDefs).length}개의 queryDef: ${Object.keys(queryDefs).join(', ')}`);
278321

279-
// 쿼리 참조 검증
322+
// 쿼리 참조 검증 및 시트명 검증
280323
for (const sheet of sheets) {
281-
if (sheet.$ && sheet.$.queryRef) {
282-
if (!queryDefs[sheet.$.queryRef]) {
283-
throw new Error(`시트 "${sheet.$.name}"에서 참조하는 쿼리 정의 "${sheet.$.queryRef}"를 찾을 수 없습니다.`);
324+
if (sheet.$) {
325+
// 시트명 검증 (변수 치환 전이므로 변수 포함 가능)
326+
const sheetName = sheet.$.name || '';
327+
328+
// 시트명 검증 (변수 포함 시 길이 검증만 건너뛰기)
329+
const hasVariables = sheetName.includes('${');
330+
const sheetNameValidation = validateSheetName(sheetName, hasVariables);
331+
if (!sheetNameValidation.valid) {
332+
console.error(`\n❌ 시트명 검증 실패:`);
333+
console.error(` 시트명: "${sheetName}"`);
334+
sheetNameValidation.errors.forEach(error => {
335+
console.error(` - ${error}`);
336+
});
337+
throw new Error(`시트명 검증 실패: "${sheetName}"`);
338+
}
339+
340+
// 쿼리 참조 검증
341+
if (sheet.$.queryRef) {
342+
if (!queryDefs[sheet.$.queryRef]) {
343+
throw new Error(`시트 "${sheetName}"에서 참조하는 쿼리 정의 "${sheet.$.queryRef}"를 찾을 수 없습니다.`);
344+
}
345+
console.log(` ✅ 시트 "${sheetName}" -> 쿼리 정의 "${sheet.$.queryRef}" 참조 확인`);
284346
}
285-
console.log(` ✅ 시트 "${sheet.$.name}" -> 쿼리 정의 "${sheet.$.queryRef}" 참조 확인`);
286347
}
287348
}
288349
}
@@ -303,13 +364,28 @@ async function validateQueryFile(options) {
303364
const queryDefCount = Object.keys(parsed.queryDefs).length;
304365
console.log(` 쿼리 정의 개수: ${queryDefCount}개`);
305366

306-
// 쿼리 참조 검증
367+
// 쿼리 참조 검증 및 시트명 검증
307368
for (const sheet of parsed.sheets) {
369+
const sheetName = sheet.name || '';
370+
371+
// 시트명 검증 (변수 포함 시 길이 검증만 건너뛰기)
372+
const hasVariables = sheetName.includes('${');
373+
const sheetNameValidation = validateSheetName(sheetName, hasVariables);
374+
if (!sheetNameValidation.valid) {
375+
console.error(`\n❌ 시트명 검증 실패:`);
376+
console.error(` 시트명: "${sheetName}"`);
377+
sheetNameValidation.errors.forEach(error => {
378+
console.error(` - ${error}`);
379+
});
380+
throw new Error(`시트명 검증 실패: "${sheetName}"`);
381+
}
382+
383+
// 쿼리 참조 검증
308384
if (sheet.queryRef) {
309385
if (!parsed.queryDefs[sheet.queryRef]) {
310-
throw new Error(`시트 "${sheet.name}"에서 참조하는 쿼리 정의 "${sheet.queryRef}"를 찾을 수 없습니다.`);
386+
throw new Error(`시트 "${sheetName}"에서 참조하는 쿼리 정의 "${sheet.queryRef}"를 찾을 수 없습니다.`);
311387
}
312-
console.log(` ✅ 시트 "${sheet.name}" -> 쿼리 정의 "${sheet.queryRef}" 참조 확인`);
388+
console.log(` ✅ 시트 "${sheetName}" -> 쿼리 정의 "${sheet.queryRef}" 참조 확인`);
313389
}
314390
}
315391
}
@@ -332,7 +408,10 @@ async function validateQueryFile(options) {
332408
// main 함수
333409
async function main() {
334410
try {
411+
console.log('[DEBUG] args:', args);
412+
console.log('[DEBUG] command:', command);
335413
const options = parseOptions(args.slice(1));
414+
console.log('[DEBUG] options:', options);
336415

337416
// 명령어 정보 출력
338417
if (command !== 'list-dbs') {

src/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,17 @@ async function main() {
188188
let sql = variableProcessor.substituteVars(sheetDef.query, mergedVars, sheetDef.params || {});
189189
const sheetName = variableProcessor.substituteVars(sheetDef.name, mergedVars, sheetDef.params || {});
190190

191+
// 시트명 검증 (변수 치환 후)
192+
const sheetNameValidation = queryParser.validateSheetName(sheetName, i);
193+
if (!sheetNameValidation.valid) {
194+
console.error(`\n❌ 시트명 검증 실패 (시트 #${i + 1}):`);
195+
console.error(` 시트명: "${sheetName}"`);
196+
sheetNameValidation.errors.forEach(error => {
197+
console.error(` - ${error}`);
198+
});
199+
throw new Error(`시트명 검증 실패: "${sheetName}"`);
200+
}
201+
191202
// maxRows 제한 적용 (개별 시트 설정 > 전역 설정 우선)
192203
const effectiveMaxRows = sheetDef.maxRows || globalMaxRows;
193204
if (effectiveMaxRows && effectiveMaxRows > 0) {

src/query-parser.js

Lines changed: 151 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,141 @@ class QueryParser {
5151
};
5252
}
5353

54+
/**
55+
* XML 구조 검증 (element명과 속성명)
56+
* @param {Object} parsed - 파싱된 XML 객체
57+
* @returns {Object} { valid: boolean, errors: string[] }
58+
*/
59+
validateXMLStructure(parsed) {
60+
const errors = [];
61+
62+
// 허용되는 최상위 element
63+
const allowedRootElements = ['queries'];
64+
65+
// 허용되는 element와 그 속성 정의
66+
const allowedElements = {
67+
queries: ['excel', 'vars', 'dynamicVars', 'queryDefs', 'sheet'],
68+
excel: [], // 자식 element 없음
69+
vars: ['var'],
70+
var: [], // 자식 element 없음
71+
dynamicVars: ['dynamicVar'],
72+
dynamicVar: [], // 자식 element 없음
73+
queryDefs: ['queryDef'],
74+
queryDef: [], // 자식 element 없음
75+
sheet: ['params'],
76+
params: ['param'],
77+
param: [] // 자식 element 없음
78+
};
79+
80+
// 허용되는 속성 정의
81+
const allowedAttributes = {
82+
excel: ['db', 'output', 'maxRows', 'style', 'aggregateInfoTemplate'],
83+
var: ['name'],
84+
dynamicVar: ['name', 'description', 'type', 'database'],
85+
queryDef: ['id', 'description'],
86+
sheet: ['name', 'use', 'queryRef', 'aggregateColumn', 'aggregateInfoTemplate', 'maxRows', 'db', 'style'],
87+
param: ['name']
88+
};
89+
90+
// 최상위 element 검증
91+
const rootElementNames = Object.keys(parsed);
92+
const invalidRootElements = rootElementNames.filter(name => !allowedRootElements.includes(name));
93+
if (invalidRootElements.length > 0) {
94+
errors.push(`허용되지 않는 최상위 element: ${invalidRootElements.join(', ')}`);
95+
}
96+
97+
// queries element 검증
98+
if (parsed.queries) {
99+
const queries = parsed.queries;
100+
const queryKeys = Object.keys(queries);
101+
const invalidElements = queryKeys.filter(key => !allowedElements.queries.includes(key));
102+
if (invalidElements.length > 0) {
103+
errors.push(`queries 내 허용되지 않는 element: ${invalidElements.join(', ')}`);
104+
}
105+
106+
// excel element 속성 검증
107+
if (queries.excel && queries.excel[0] && queries.excel[0].$) {
108+
const excelAttrs = Object.keys(queries.excel[0].$);
109+
const invalidAttrs = excelAttrs.filter(attr => !allowedAttributes.excel.includes(attr));
110+
if (invalidAttrs.length > 0) {
111+
errors.push(`excel element의 허용되지 않는 속성: ${invalidAttrs.join(', ')}`);
112+
}
113+
}
114+
115+
// var elements 검증
116+
if (queries.vars && queries.vars[0] && queries.vars[0].var) {
117+
queries.vars[0].var.forEach((v, i) => {
118+
if (v.$) {
119+
const attrs = Object.keys(v.$);
120+
const invalidAttrs = attrs.filter(attr => !allowedAttributes.var.includes(attr));
121+
if (invalidAttrs.length > 0) {
122+
errors.push(`var element #${i + 1}의 허용되지 않는 속성: ${invalidAttrs.join(', ')}`);
123+
}
124+
}
125+
});
126+
}
127+
128+
// dynamicVar elements 검증
129+
if (queries.dynamicVars && queries.dynamicVars[0] && queries.dynamicVars[0].dynamicVar) {
130+
queries.dynamicVars[0].dynamicVar.forEach((dv, i) => {
131+
if (dv.$) {
132+
const attrs = Object.keys(dv.$);
133+
const invalidAttrs = attrs.filter(attr => !allowedAttributes.dynamicVar.includes(attr));
134+
if (invalidAttrs.length > 0) {
135+
errors.push(`dynamicVar element #${i + 1}의 허용되지 않는 속성: ${invalidAttrs.join(', ')}`);
136+
}
137+
}
138+
});
139+
}
140+
141+
// queryDef elements 검증
142+
if (queries.queryDefs && queries.queryDefs[0] && queries.queryDefs[0].queryDef) {
143+
queries.queryDefs[0].queryDef.forEach((qd, i) => {
144+
if (qd.$) {
145+
const attrs = Object.keys(qd.$);
146+
const invalidAttrs = attrs.filter(attr => !allowedAttributes.queryDef.includes(attr));
147+
if (invalidAttrs.length > 0) {
148+
errors.push(`queryDef element #${i + 1}의 허용되지 않는 속성: ${invalidAttrs.join(', ')}`);
149+
}
150+
}
151+
});
152+
}
153+
154+
// sheet elements 검증
155+
if (queries.sheet) {
156+
const sheets = Array.isArray(queries.sheet) ? queries.sheet : [queries.sheet];
157+
sheets.forEach((sheet, i) => {
158+
if (sheet.$) {
159+
const attrs = Object.keys(sheet.$);
160+
const invalidAttrs = attrs.filter(attr => !allowedAttributes.sheet.includes(attr));
161+
if (invalidAttrs.length > 0) {
162+
errors.push(`sheet element #${i + 1}의 허용되지 않는 속성: ${invalidAttrs.join(', ')}`);
163+
}
164+
}
165+
166+
// params 검증
167+
if (sheet.params && sheet.params[0] && sheet.params[0].param) {
168+
const params = Array.isArray(sheet.params[0].param) ? sheet.params[0].param : [sheet.params[0].param];
169+
params.forEach((param, j) => {
170+
if (param.$) {
171+
const attrs = Object.keys(param.$);
172+
const invalidAttrs = attrs.filter(attr => !allowedAttributes.param.includes(attr));
173+
if (invalidAttrs.length > 0) {
174+
errors.push(`sheet #${i + 1}의 param element #${j + 1}의 허용되지 않는 속성: ${invalidAttrs.join(', ')}`);
175+
}
176+
}
177+
});
178+
}
179+
});
180+
}
181+
}
182+
183+
return {
184+
valid: errors.length === 0,
185+
errors
186+
};
187+
}
188+
54189
/**
55190
* XML 파일에서 쿼리 로드
56191
* @param {string} xmlPath - XML 파일 경로
@@ -62,6 +197,16 @@ class QueryParser {
62197
const parsed = await xml2js.parseStringPromise(xml, { trim: true });
63198
if (!parsed.queries || !parsed.queries.sheet) throw new Error('Invalid XML format');
64199

200+
// XML 구조 검증
201+
const structureValidation = this.validateXMLStructure(parsed);
202+
if (!structureValidation.valid) {
203+
console.error('\n❌ XML 구조 검증 실패:');
204+
structureValidation.errors.forEach(error => {
205+
console.error(` - ${error}`);
206+
});
207+
throw new Error('XML 구조 검증 실패');
208+
}
209+
65210
// 쿼리 정의 파싱
66211
let queryDefs = {};
67212
if (parsed.queries.queryDefs && parsed.queries.queryDefs[0] && parsed.queries.queryDefs[0].queryDef) {
@@ -70,16 +215,22 @@ class QueryParser {
70215
const queryName = queryDef.$.id || queryDef.$.name;
71216
const queryText = (queryDef._ || queryDef['#text'] || queryDef.__cdata || '').toString().trim();
72217

218+
console.log(`[DEBUG] queryDef 파싱: id="${queryName}", queryText 길이=${queryText.length}`);
219+
73220
if (queryText) {
74221
queryDefs[queryName] = {
75222
name: queryName,
76223
description: queryDef.$.description || '',
77224
query: queryText
78225
};
226+
console.log(`[DEBUG] queryDef 추가됨: ${queryName}`);
227+
} else {
228+
console.warn(`[WARN] queryDef "${queryName}"의 쿼리 텍스트가 비어있습니다.`);
79229
}
80230
}
81231
}
82232
}
233+
console.log(`[DEBUG] 총 ${Object.keys(queryDefs).length}개의 queryDef 로드됨: ${Object.keys(queryDefs).join(', ')}`);
83234

84235
// 전역 변수 파싱
85236
let globalVars = {};
@@ -188,17 +339,6 @@ class QueryParser {
188339
}
189340
}
190341

191-
// 시트명 검증
192-
const sheetNameValidation = this.validateSheetName(s.$.name, i);
193-
if (!sheetNameValidation.valid) {
194-
console.error(`\n❌ 시트명 검증 실패 (시트 #${i + 1}):`);
195-
console.error(` 시트명: "${s.$.name}"`);
196-
sheetNameValidation.errors.forEach(error => {
197-
console.error(` - ${error}`);
198-
});
199-
throw new Error(`시트명 검증 실패: "${s.$.name}"`);
200-
}
201-
202342
// queryRef 속성이 있으면 쿼리 정의에서 참조
203343
if (s.$.queryRef) {
204344
const queryRef = s.$.queryRef;
@@ -257,17 +397,6 @@ class QueryParser {
257397
let query = sheet.query || '';
258398
let sheetParams = sheet.params || {};
259399

260-
// 시트명 검증
261-
const sheetNameValidation = this.validateSheetName(sheet.name, i);
262-
if (!sheetNameValidation.valid) {
263-
console.error(`\n❌ 시트명 검증 실패 (시트 #${i + 1}):`);
264-
console.error(` 시트명: "${sheet.name}"`);
265-
sheetNameValidation.errors.forEach(error => {
266-
console.error(` - ${error}`);
267-
});
268-
throw new Error(`시트명 검증 실패: "${sheet.name}"`);
269-
}
270-
271400
// queryRef가 있으면 쿼리 정의에서 참조
272401
if (sheet.queryRef) {
273402
if (queryDefs[sheet.queryRef]) {

0 commit comments

Comments
 (0)