Skip to content

Commit c7f15ee

Browse files
committed
조회 최대 제한 건수 설정 기능 추가
1 parent 746eace commit c7f15ee

File tree

4 files changed

+136
-10
lines changed

4 files changed

+136
-10
lines changed

README.md

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@
3939
<var name="endDate">2024-06-30</var>
4040
<var name="regionList">'서울','부산'</var>
4141
</vars>
42-
<sheet name="Orders" use="true">
42+
<sheet name="Orders" use="true" aggregateColumn="OrderStatus" maxRows="1000">
4343
<![CDATA[
4444
SELECT * FROM Orders
4545
WHERE OrderDate >= '${startDate}' AND OrderDate <= '${endDate}'
4646
]]>
4747
</sheet>
48-
<sheet name="Customers" use="false">
48+
<sheet name="Customers" use="false" aggregateColumn="Region" maxRows="500">
4949
<![CDATA[
5050
SELECT * FROM Customers
5151
WHERE region IN (${regionList})
@@ -79,11 +79,15 @@
7979
{
8080
"name": "Orders",
8181
"use": true,
82+
"aggregateColumn": "OrderStatus",
83+
"maxRows": 1000,
8284
"query": "SELECT * FROM Orders WHERE OrderDate >= '${startDate}' AND OrderDate <= '${endDate}'"
8385
},
8486
{
8587
"name": "Customers",
8688
"use": false,
89+
"aggregateColumn": "Region",
90+
"maxRows": 500,
8791
"query": "SELECT * FROM Customers WHERE region IN (${regionList})"
8892
}
8993
]
@@ -130,6 +134,9 @@ node src/index.js -q resources/queries-sample.json --db main --out output/result
130134
- 헤더/데이터 각각 스타일 적용
131135
- 실행 시 XML 쿼리파일 목록 자동 안내
132136
- 결과 엑셀 파일 경로의 폴더가 없으면 자동 생성
137+
- **자동 목차 시트 생성** (하이퍼링크 포함)
138+
- **컬럼별 집계 데이터 표시** (목차 시트에서 한눈에 확인)
139+
- **조회 건수 제한 기능** (대용량 데이터 처리 시 안전장치)
133140

134141
---
135142

@@ -188,17 +195,97 @@ node src/index.js -q resources/queries-sample.json --db main --out output/result
188195
|--------|--------|---------------------|--------|
189196
| name | sheet | 시트명(변수 사용 가능)| "매출_${startDate}_~_${endDate}" |
190197
| use | sheet | 사용여부 | true/false |
198+
| aggregateColumn | sheet | 집계할 컬럼명 (목차 시트에 표시) | "주문상태", "지역" |
199+
| maxRows | sheet | 최대 조회 건수 제한 | 1000, 5000 |
191200

192201
---
193202

194-
## 5. 기타 참고
203+
## 5. 목차 시트 및 집계 기능
204+
205+
### 자동 목차 시트
206+
- 모든 엑셀 파일에 자동으로 **'목차'** 시트가 첫 번째 시트로 생성됩니다
207+
- 목차 시트는 파란색 탭으로 구분되며, 다음 정보를 포함합니다:
208+
209+
| 컬럼 | 설명 | 하이퍼링크 |
210+
|------|------|------------|
211+
| No | 시트 순번 ||
212+
| Sheet Name | 실제 시트명 (변수 치환됨) | ✅ 클릭 시 해당 시트로 이동 |
213+
| Records | 데이터 건수 (천 단위 구분자) | ✅ 클릭 시 해당 시트로 이동 |
214+
| Aggregate Info | 집계 정보 | ✅ 클릭 시 해당 시트로 이동 |
215+
| Note | 비고 (시트명 잘림 등) ||
216+
217+
### 컬럼별 집계 기능
218+
각 시트에서 특정 컬럼의 값별 건수를 자동으로 집계하여 목차에 표시합니다.
219+
220+
#### XML에서 집계 컬럼 및 조회 제한 지정
221+
```xml
222+
<sheet name="주문_목록" use="true" aggregateColumn="주문상태" maxRows="1000">
223+
<![CDATA[
224+
SELECT OrderID, OrderStatus, CustomerName, OrderDate
225+
FROM Orders
226+
WHERE OrderDate >= '${startDate}'
227+
]]>
228+
</sheet>
229+
```
230+
231+
#### JSON에서 집계 컬럼 및 조회 제한 지정
232+
```json
233+
{
234+
"name": "주문_목록",
235+
"use": true,
236+
"aggregateColumn": "주문상태",
237+
"maxRows": 1000,
238+
"query": "SELECT OrderID, OrderStatus, CustomerName, OrderDate FROM Orders WHERE OrderDate >= '${startDate}'"
239+
}
240+
```
241+
242+
#### 집계 결과 표시 예시
243+
```
244+
[주문상태] Shipped:15, Processing:8, Cancelled:3 외 2개
245+
[지역] 서울:25, 부산:12, 대구:8
246+
[카테고리] 전자제품:45, 의류:32, 도서:18 외 5개
247+
```
248+
249+
### 조회 건수 제한 기능 (maxRows)
250+
대용량 데이터 처리 시 시스템 부하를 줄이고 안전하게 작업할 수 있도록 조회 건수를 제한합니다.
251+
252+
#### 작동 원리
253+
- SQL 쿼리에 `TOP N` 절을 자동으로 추가하여 조회 건수를 제한
254+
- 쿼리에 이미 `TOP` 절이 있는 경우 maxRows 설정을 무시하고 경고 메시지 출력
255+
- 제한이 적용되면 콘솔에 `[제한] 최대 N건으로 제한됨` 메시지 표시
256+
257+
#### 사용 예시
258+
```xml
259+
<!-- 최대 5000건까지만 조회 -->
260+
<sheet name="대용량_주문데이터" maxRows="5000">
261+
<![CDATA[
262+
SELECT * FROM Orders WHERE OrderDate >= '2024-01-01'
263+
]]>
264+
</sheet>
265+
```
266+
267+
#### 주의사항
268+
- **제한 없음**: maxRows 미설정 또는 0 이하의 값
269+
- **기존 TOP 절**: 쿼리에 이미 TOP이 있으면 maxRows 무시됨
270+
- **대용량 처리**: 수십만 건 이상의 데이터는 적절한 제한 권장
271+
272+
### 목차 시트 특징
273+
- **하이퍼링크**: 시트명, 데이터 건수, 집계 정보 클릭 시 해당 시트로 즉시 이동
274+
- **시트명 자동 처리**: 31자 초과 시 자동 잘림 및 원본명 주석 표시
275+
- **집계 정보**: 상위 3개 항목 표시, 나머지는 "외 N개"로 요약
276+
- **정렬**: 집계 항목은 건수가 많은 순으로 정렬
277+
- **스타일**: 파란색 링크, 천 단위 구분자, 적절한 컬럼 너비 자동 설정
278+
279+
---
280+
281+
## 6. 기타 참고
195282
- 쿼리문 내 `${변수명}` 형태로 변수 사용 가능
196283
- 시트별로 `use="false"` 또는 `"use": false`로 비활성화 가능
197284
- 쿼리파일 구조/옵션은 필요에 따라 확장 가능
198285

199286
---
200287

201-
## 6. 모듈 구조
288+
## 7. 모듈 구조
202289

203290
### 핵심 파일 구조
204291
```
@@ -227,7 +314,8 @@ test/
227314
| `applyBodyStyle(sheet, columns, dataRowCount, bodyStyle)` | 데이터 행들에 스타일 적용 | 데이터 스타일링 |
228315
| `calculateColumnWidths(columns, data, colwidths)` | 컬럼 너비 자동 계산 | 자동 너비 조정 |
229316
| `applySheetStyle(sheet, data, excelStyle)` | 시트 전체에 데이터와 스타일 적용 | 통합 시트 처리 |
230-
| `createTableOfContents(workbook, sheetNames)` | 목차 시트 생성 | 목차 생성 |
317+
| `createTableOfContents(workbook, sheetNames)` | 새로운 목차 시트 생성 | 별도 파일용 목차 생성 |
318+
| `populateTableOfContents(tocSheet, sheetNames)` | 기존 목차 시트에 내용 채우기 | 메인 파일 목차 업데이트 |
231319

232320
#### 사용 예시
233321
```javascript
@@ -239,8 +327,21 @@ excelStyleHelper.applySheetStyle(sheet, data, {
239327
body: { font: { size: 11 }, fill: { color: 'FFFFCC' } }
240328
});
241329

242-
// 목차 시트 생성
243-
const tocSheet = excelStyleHelper.createTableOfContents(workbook, sheetNames);
330+
// 기존 목차 시트에 내용 채우기 (집계 정보 및 하이퍼링크 포함)
331+
const sheetInfo = [
332+
{
333+
displayName: '주문_목록',
334+
tabName: '주문_목록',
335+
recordCount: 150,
336+
aggregateColumn: '주문상태',
337+
aggregateData: [
338+
{ key: 'Shipped', count: 89 },
339+
{ key: 'Processing', count: 45 },
340+
{ key: 'Cancelled', count: 16 }
341+
]
342+
}
343+
];
344+
excelStyleHelper.populateTableOfContents(tocSheet, sheetInfo);
244345
```
245346

246347
### 테스트 실행

queries/queries-sample.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"excel": {
33
"db": "sampleDB",
44
"output": "output/매출집계_2024.xlsx",
5+
"maxRows": 1500,
56
"header": {
67
"font": { "name": "맑은 고딕", "size": 12, "color": "FFFFFF", "bold": true },
78
"fill": { "color": "4F81BD" },
@@ -25,11 +26,15 @@
2526
{
2627
"name": "Orders",
2728
"use": true,
29+
"maxRows": 1000,
30+
"aggregateColumn": "OrderStatus",
2831
"query": "SELECT * FROM Orders WHERE OrderDate >= '${startDate}' AND OrderDate <= '${endDate}'"
2932
},
3033
{
3134
"name": "Customers",
3235
"use": false,
36+
"maxRows": 500,
37+
"aggregateColumn": "Region",
3338
"query": "SELECT * FROM Customers WHERE region IN (${regionList})"
3439
}
3540
]

queries/queries-sample.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<queries>
2-
<excel db="sampleDB" output="d:/temp/매출집계_2024.xlsx" separateToc="false">
2+
<excel db="sampleDB" output="d:/temp/매출집계_2024.xlsx" separateToc="false" maxRows="2000">
33
<header>
44
<font name="맑은 고딕" size="12" color="FFFFFF" bold="true"/>
55
<fill color="4F81BD"/>
@@ -24,7 +24,7 @@
2424
<var name="endDate">2024-06-30</var>
2525
<var name="regionList">'서울','부산'</var>
2626
</vars>
27-
<sheet name="${envType}_주문_목록" use="true" aggregateColumn="주문상태">
27+
<sheet name="${envType}_주문_목록" use="true" aggregateColumn="결제방법" maxRows="1000">
2828
<![CDATA[
2929
SELECT
3030
OrderNumber as 주문번호,

src/index.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ async function loadQueriesFromXML(xmlPath) {
3939
name: s.$.name,
4040
use: s.$.use,
4141
aggregateColumn: s.$.aggregateColumn || null,
42+
maxRows: s.$.maxRows ? parseInt(s.$.maxRows) : null,
4243
query: (s._ || (s["_"] ? s["_"] : (s["$"] ? s["$"] : '')) || (s["__cdata"] ? s["__cdata"] : '') || (s["cdata"] ? s["cdata"] : '') || (s["#cdata-section"] ? s["#cdata-section"] : '') || (s["__text"] ? s["__text"] : '') || (s["#text"] ? s["#text"] : '') || (s["$text"] ? s["$text"] : '') || (s["$value"] ? s["$value"] : '') || (s["value"] ? s["value"] : '') || '').toString().trim()
4344
}));
4445
return { globalVars, sheets, dbId, outputPath };
@@ -195,6 +196,7 @@ async function main() {
195196
let excelDb = undefined;
196197
let excelOutput = undefined;
197198
let createSeparateToc = false; // 별도 목차 파일 생성 여부
199+
let globalMaxRows = null; // 전역 최대 조회 건수
198200

199201
if (argv.xml && fs.existsSync(resolvePath(argv.xml))) {
200202
const xml = fs.readFileSync(resolvePath(argv.xml), 'utf8');
@@ -211,6 +213,8 @@ async function main() {
211213
if (excel.$ && excel.$.output) excelOutput = excel.$.output;
212214
// excel 엘리먼트의 separateToc가 있으면 우선적용 (덮어쓰기)
213215
if (excel.$ && excel.$.separateToc) createSeparateToc = excel.$.separateToc === 'true';
216+
// excel 엘리먼트의 maxRows 읽기
217+
if (excel.$ && excel.$.maxRows) globalMaxRows = parseInt(excel.$.maxRows);
214218

215219
excelStyle.header = {};
216220
excelStyle.body = {};
@@ -245,6 +249,7 @@ async function main() {
245249
if (queries.excel.db) excelDb = queries.excel.db;
246250
if (queries.excel.output) excelOutput = queries.excel.output;
247251
if (queries.excel.separateToc !== undefined) createSeparateToc = queries.excel.separateToc;
252+
if (queries.excel.maxRows !== undefined) globalMaxRows = parseInt(queries.excel.maxRows);
248253
}
249254
}
250255

@@ -294,8 +299,23 @@ async function main() {
294299
console.log(`[목차] 맨 첫 번째 시트로 생성됨`);
295300
}
296301

297-
const sql = substituteVars(sheetDef.query, mergedVars);
302+
let sql = substituteVars(sheetDef.query, mergedVars);
298303
const sheetName = substituteVars(sheetDef.name, mergedVars);
304+
305+
// maxRows 제한 적용 (개별 시트 설정 > 전역 설정 우선)
306+
const effectiveMaxRows = sheetDef.maxRows || globalMaxRows;
307+
if (effectiveMaxRows && effectiveMaxRows > 0) {
308+
// SQL에 TOP 절이 없는 경우에만 추가
309+
if (!sql.trim().toUpperCase().includes('TOP ')) {
310+
// SELECT 다음에 TOP N을 삽입
311+
sql = sql.replace(/^\s*SELECT\s+/i, `SELECT TOP ${effectiveMaxRows} `);
312+
const source = sheetDef.maxRows ? '시트별' : '전역';
313+
console.log(`\t[제한] 최대 ${effectiveMaxRows}건으로 제한됨 (${source} 설정)`);
314+
} else {
315+
console.log(`\t[제한] 쿼리에 이미 TOP 절이 존재하여 maxRows 설정 무시됨`);
316+
}
317+
}
318+
299319
console.log(`[INFO] Executing for sheet '${sheetName}'`);
300320
try {
301321
const result = await pool.request().query(sql);

0 commit comments

Comments
 (0)