- Added new entries to .gitignore for multi-agent MCP task queue and related rules. - Removed "즉시 저장" (quick insert) options from the ScreenSettingModal and BasicTab components to streamline button configurations. - Cleaned up unused event options in the V2ButtonConfigPanel to enhance clarity and maintainability. These changes aim to improve project organization and simplify the user interface by eliminating redundant options.
250 lines
6.8 KiB
TypeScript
250 lines
6.8 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}`;
|
|
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());
|
|
const fmtDate = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
|
|
switch (range) {
|
|
case "today":
|
|
return {
|
|
startDate: fmtDate(today),
|
|
endDate: fmtDate(today),
|
|
};
|
|
|
|
case "week": {
|
|
const weekStart = new Date(today);
|
|
weekStart.setDate(today.getDate() - today.getDay());
|
|
return {
|
|
startDate: fmtDate(weekStart),
|
|
endDate: fmtDate(today),
|
|
};
|
|
}
|
|
|
|
case "month": {
|
|
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
return {
|
|
startDate: fmtDate(monthStart),
|
|
endDate: fmtDate(today),
|
|
};
|
|
}
|
|
|
|
case "year": {
|
|
const yearStart = new Date(today.getFullYear(), 0, 1);
|
|
return {
|
|
startDate: fmtDate(yearStart),
|
|
endDate: fmtDate(today),
|
|
};
|
|
}
|
|
|
|
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 {
|
|
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;
|
|
}
|