feat: Add table aggregation endpoint for data summarization

- Implemented a new endpoint to retrieve aggregated data (SUM/COUNT) for specified columns in a given table.
- Added validation to ensure the presence of table name and valid aggregation columns in the request.
- Integrated company code filtering to restrict data access based on user permissions.
- Updated the table management routes to include the new aggregation functionality.
- Enhanced the frontend order page to utilize the new aggregation endpoint for improved statistical reporting.
This commit is contained in:
kjs
2026-04-16 12:03:51 +09:00
parent 0e09b9e686
commit a20fd267fc
5 changed files with 341 additions and 108 deletions

View File

@@ -907,6 +907,61 @@ export async function getTableData(
}
}
/**
* 테이블 집계 조회 (SUM/COUNT)
* POST /api/table-management/tables/:tableName/aggregate
*/
export async function getTableAggregate(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName } = req.params;
const { columns, autoFilter } = req.body;
const companyCode = req.user?.companyCode;
if (!tableName || !columns || !Array.isArray(columns)) {
res.status(400).json({ success: false, message: "tableName과 columns 배열이 필요합니다." });
return;
}
const validCols = columns.filter((c: any) =>
c.column && c.func && /^[a-zA-Z0-9_]+$/.test(c.column) && ["sum", "count", "avg", "min", "max"].includes(c.func)
);
if (validCols.length === 0) {
res.status(400).json({ success: false, message: "유효한 집계 컬럼이 없습니다." });
return;
}
const selectParts = validCols.map((c: any) => {
const col = c.column.replace(/[^a-zA-Z0-9_]/g, "");
return `${c.func}(COALESCE(CAST(NULLIF(${col}, '') AS numeric), 0)) AS "${c.func}_${col}"`;
});
let whereClause = "";
const params: any[] = [];
let paramIdx = 1;
if (autoFilter !== false && companyCode && companyCode !== "*") {
whereClause = `WHERE company_code = $${paramIdx}`;
params.push(companyCode);
paramIdx++;
}
const pool = (await import("../database/db")).getPool();
const safeTable = tableName.replace(/[^a-zA-Z0-9_]/g, "");
const result = await pool.query(
`SELECT ${selectParts.join(", ")} FROM ${safeTable} ${whereClause}`,
params
);
res.json({ success: true, data: result.rows[0] || {} });
} catch (error: any) {
logger.error("테이블 집계 조회 실패:", error);
res.status(500).json({ success: false, message: error.message });
}
}
/**
* 테이블 데이터 추가
*/

View File

@@ -11,7 +11,8 @@ import {
updateColumnInputType,
updateTableLabel,
getTableData,
getTableRecord, // 🆕 단일 레코드 조회
getTableRecord,
getTableAggregate,
addTableData,
editTableData,
deleteTableData,
@@ -193,6 +194,7 @@ router.get("/health", checkDatabaseConnection);
* POST /api/table-management/tables/:tableName/data
*/
router.post("/tables/:tableName/data", getTableData);
router.post("/tables/:tableName/aggregate", getTableAggregate);
/**
* 단일 레코드 조회 (자동 입력용)

View File

@@ -2367,26 +2367,24 @@ export class TableManagementService {
const total = parseInt(countResult[0].count);
// 데이터 조회 (main 별칭 추가)
const dataQuery = `
SELECT main.* FROM ${safeTableName} main
${whereClause}
${orderClause}
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`;
// size=0 이면 LIMIT 없이 전체 반환 (마스터 참조 데이터 조회용)
const usePaging = size > 0;
const dataQuery = usePaging
? `SELECT main.* FROM ${safeTableName} main ${whereClause} ${orderClause} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`
: `SELECT main.* FROM ${safeTableName} main ${whereClause} ${orderClause}`;
logger.info(`🔍 실행할 SQL: ${dataQuery}`);
logger.info(
`🔍 파라미터: ${JSON.stringify([...searchValues, size, offset])}`
);
const queryParams = usePaging ? [...searchValues, size, offset] : [...searchValues];
logger.info(`🔍 파라미터: ${JSON.stringify(queryParams)}`);
let data = await query<any>(dataQuery, [...searchValues, size, offset]);
let data = await query<any>(dataQuery, queryParams);
// 🎯 파일 컬럼이 있으면 파일 정보 보강
if (fileColumns.length > 0) {
data = await this.enrichFileData(data, fileColumns, safeTableName);
}
const totalPages = Math.ceil(total / size);
const totalPages = usePaging ? Math.ceil(total / size) : 1;
logger.info(
`테이블 데이터 조회 완료: ${tableName}, 총 ${total}건, ${data.length}개 반환`