ERP-node/frontend/components/admin/dashboard/utils/queryHelpers.ts

261 lines
7.2 KiB
TypeScript

import { ChartConfig } from "../types";
/**
* 쿼리에 안전장치 LIMIT 추가
*/
export function applySafetyLimit(query: string, limit: number = 1000): string {
const trimmedQuery = query.trim();
// 이미 LIMIT이 있으면 그대로 반환
if (/\bLIMIT\b/i.test(trimmedQuery)) {
return trimmedQuery;
}
return `${trimmedQuery} LIMIT ${limit}`;
}
/**
* 날짜 필터를 쿼리에 적용
*/
export function applyDateFilter(query: string, dateColumn: string, startDate?: string, endDate?: string): string {
if (!dateColumn || (!startDate && !endDate)) {
return query;
}
const conditions: string[] = [];
// NULL 값 제외 조건 추가 (필수)
conditions.push(`${dateColumn} IS NOT NULL`);
if (startDate) {
conditions.push(`${dateColumn} >= '${startDate}'`);
}
if (endDate) {
// 종료일은 해당 날짜의 23:59:59까지 포함
conditions.push(`${dateColumn} <= '${endDate} 23:59:59'`);
}
if (conditions.length === 0) {
return query;
}
// FROM 절 이후의 WHERE, GROUP BY, ORDER BY, LIMIT 위치 파악
// 줄바꿈 제거하여 한 줄로 만들기 (정규식 매칭을 위해)
let baseQuery = query.trim().replace(/\s+/g, " ");
let whereClause = "";
let groupByClause = "";
let orderByClause = "";
let limitClause = "";
// LIMIT 추출
const limitMatch = baseQuery.match(/\s+LIMIT\s+\d+\s*$/i);
if (limitMatch) {
limitClause = limitMatch[0];
baseQuery = baseQuery.substring(0, limitMatch.index);
}
// ORDER BY 추출
const orderByMatch = baseQuery.match(/\s+ORDER\s+BY\s+.+$/i);
if (orderByMatch) {
orderByClause = orderByMatch[0];
baseQuery = baseQuery.substring(0, orderByMatch.index);
}
// GROUP BY 추출
const groupByMatch = baseQuery.match(/\s+GROUP\s+BY\s+.+$/i);
if (groupByMatch) {
groupByClause = groupByMatch[0];
baseQuery = baseQuery.substring(0, groupByMatch.index);
}
// WHERE 추출 (있으면)
const whereMatch = baseQuery.match(/\s+WHERE\s+.+$/i);
if (whereMatch) {
whereClause = whereMatch[0];
baseQuery = baseQuery.substring(0, whereMatch.index);
}
// 날짜 필터 조건 추가
const filterCondition = conditions.join(" AND ");
if (whereClause) {
// 기존 WHERE 절이 있으면 AND로 연결
whereClause = `${whereClause} AND ${filterCondition}`;
} else {
// WHERE 절이 없으면 새로 생성
whereClause = ` WHERE ${filterCondition}`;
}
// 쿼리 재조립
const finalQuery = `${baseQuery}${whereClause}${groupByClause}${orderByClause}${limitClause}`;
console.log("🔧 날짜 필터 적용:");
console.log(" 원본 쿼리:", query);
console.log(" 최종 쿼리:", finalQuery);
return finalQuery;
}
/**
* 빠른 날짜 범위 계산
*/
export function getQuickDateRange(range: "today" | "week" | "month" | "year"): {
startDate: string;
endDate: string;
} {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
switch (range) {
case "today":
return {
startDate: today.toISOString().split("T")[0],
endDate: today.toISOString().split("T")[0],
};
case "week": {
const weekStart = new Date(today);
weekStart.setDate(today.getDate() - today.getDay()); // 일요일부터
return {
startDate: weekStart.toISOString().split("T")[0],
endDate: today.toISOString().split("T")[0],
};
}
case "month": {
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
return {
startDate: monthStart.toISOString().split("T")[0],
endDate: today.toISOString().split("T")[0],
};
}
case "year": {
const yearStart = new Date(today.getFullYear(), 0, 1);
return {
startDate: yearStart.toISOString().split("T")[0],
endDate: today.toISOString().split("T")[0],
};
}
default:
return {
startDate: "",
endDate: "",
};
}
}
/**
* 쿼리에서 테이블명 추출
*/
export function extractTableNameFromQuery(query: string): string | null {
const trimmedQuery = query.trim().toLowerCase();
// FROM 절 찾기
const fromMatch = trimmedQuery.match(/\bfrom\s+([a-z_][a-z0-9_]*)/i);
if (fromMatch) {
return fromMatch[1];
}
return null;
}
/**
* 날짜 컬럼 자동 감지 (서버에서 테이블 스키마 조회 필요)
* 이 함수는 쿼리 결과가 아닌, 원본 테이블의 실제 컬럼 타입을 확인해야 합니다.
*
* 현재는 임시로 컬럼명 기반 추측만 수행합니다.
*/
export function detectDateColumns(columns: string[], rows: Record<string, any>[]): string[] {
const dateColumns: string[] = [];
// 컬럼명 기반 추측 (가장 안전한 방법)
columns.forEach((col) => {
const lowerCol = col.toLowerCase();
if (
lowerCol.includes("date") ||
lowerCol.includes("time") ||
lowerCol.includes("created") ||
lowerCol.includes("updated") ||
lowerCol.includes("modified") ||
lowerCol === "reg_date" ||
lowerCol === "regdate" ||
lowerCol === "update_date" ||
lowerCol === "updatedate" ||
lowerCol.endsWith("_at") || // created_at, updated_at
lowerCol.endsWith("_date") || // birth_date, start_date
lowerCol.endsWith("_time") // start_time, end_time
) {
dateColumns.push(col);
}
});
// 데이터가 있는 경우, 실제 값도 확인 (추가 검증)
if (rows.length > 0 && dateColumns.length > 0) {
const firstRow = rows[0];
// 컬럼명으로 감지된 것들 중에서 실제 날짜 형식인지 재확인
const validatedColumns = dateColumns.filter((col) => {
const value = firstRow[col];
// null이면 스킵 (판단 불가)
if (value == null) return true;
// Date 객체면 확실히 날짜
if (value instanceof Date) return true;
// 문자열이고 날짜 형식이면 날짜
if (typeof value === "string") {
const parsed = Date.parse(value);
if (!isNaN(parsed)) return true;
}
// 숫자면 날짜가 아님 (타임스탬프 제외)
if (typeof value === "number") {
// 타임스탬프인지 확인 (1970년 이후의 밀리초 또는 초)
if (value > 946684800000 || (value > 946684800 && value < 2147483647)) {
return true;
}
return false;
}
return false;
});
return validatedColumns;
}
return dateColumns;
}
/**
* 쿼리에 필터와 안전장치를 모두 적용
*/
export function applyQueryFilters(query: string, config?: ChartConfig): string {
console.log("🔍 applyQueryFilters 호출:");
console.log(" config:", config);
console.log(" dateFilter:", config?.dateFilter);
let processedQuery = query;
// 1. 날짜 필터 적용
if (config?.dateFilter?.enabled && config.dateFilter.dateColumn) {
console.log("✅ 날짜 필터 적용 중...");
processedQuery = applyDateFilter(
processedQuery,
config.dateFilter.dateColumn,
config.dateFilter.startDate,
config.dateFilter.endDate,
);
} else {
console.log("⚠️ 날짜 필터 비활성화 또는 설정 없음");
}
// 2. 안전장치 LIMIT 적용
const limit = config?.autoLimit ?? 1000;
processedQuery = applySafetyLimit(processedQuery, limit);
return processedQuery;
}