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}`; 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[] { 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 { let processedQuery = query; // 1. 날짜 필터 적용 if (config?.dateFilter?.enabled && config.dateFilter.dateColumn) { processedQuery = applyDateFilter( processedQuery, config.dateFilter.dateColumn, config.dateFilter.startDate, config.dateFilter.endDate, ); } // 2. 안전장치 LIMIT 적용 const limit = config?.autoLimit ?? 1000; processedQuery = applySafetyLimit(processedQuery, limit); return processedQuery; }