261 lines
7.2 KiB
TypeScript
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;
|
|
}
|