2025-10-14 13:59:54 +09:00
|
|
|
"use client";
|
2025-09-30 13:23:22 +09:00
|
|
|
|
2025-10-14 13:59:54 +09:00
|
|
|
import React, { useState, useCallback } from "react";
|
2025-10-15 15:05:20 +09:00
|
|
|
import { ChartDataSource, QueryResult, ChartConfig } from "./types";
|
2025-10-14 14:10:49 +09:00
|
|
|
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
|
|
|
|
|
import { dashboardApi } from "@/lib/api/dashboard";
|
2025-10-14 13:59:54 +09:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Card } from "@/components/ui/card";
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
|
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
2025-10-22 12:48:17 +09:00
|
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
|
|
|
import { Play, Loader2, Database, Code, ChevronDown, ChevronRight } from "lucide-react";
|
2025-10-15 15:05:20 +09:00
|
|
|
import { applyQueryFilters } from "./utils/queryHelpers";
|
2025-09-30 13:23:22 +09:00
|
|
|
|
|
|
|
|
interface QueryEditorProps {
|
|
|
|
|
dataSource?: ChartDataSource;
|
|
|
|
|
onDataSourceChange: (dataSource: ChartDataSource) => void;
|
|
|
|
|
onQueryTest?: (result: QueryResult) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SQL 쿼리 에디터 컴포넌트
|
|
|
|
|
* - SQL 쿼리 작성 및 편집
|
|
|
|
|
* - 쿼리 실행 및 결과 미리보기
|
2025-10-14 14:10:49 +09:00
|
|
|
* - 현재 DB / 외부 DB 분기 처리
|
2025-09-30 13:23:22 +09:00
|
|
|
*/
|
|
|
|
|
export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: QueryEditorProps) {
|
2025-10-14 13:59:54 +09:00
|
|
|
const [query, setQuery] = useState(dataSource?.query || "");
|
2025-09-30 13:23:22 +09:00
|
|
|
const [isExecuting, setIsExecuting] = useState(false);
|
|
|
|
|
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
2025-10-22 12:48:17 +09:00
|
|
|
const [sampleQueryOpen, setSampleQueryOpen] = useState(false);
|
2025-09-30 13:23:22 +09:00
|
|
|
|
2025-10-23 13:17:21 +09:00
|
|
|
// dataSource.query가 변경되면 query state 업데이트 (저장된 쿼리 불러오기)
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (dataSource?.query) {
|
|
|
|
|
setQuery(dataSource.query);
|
|
|
|
|
}
|
|
|
|
|
}, [dataSource?.query]);
|
|
|
|
|
|
2025-09-30 13:23:22 +09:00
|
|
|
// 쿼리 실행
|
|
|
|
|
const executeQuery = useCallback(async () => {
|
2025-10-17 10:38:22 +09:00
|
|
|
// console.log("🚀 executeQuery 호출됨!");
|
|
|
|
|
// console.log("📝 현재 쿼리:", query);
|
|
|
|
|
// console.log("✅ query.trim():", query.trim());
|
2025-10-15 11:56:36 +09:00
|
|
|
|
2025-09-30 13:23:22 +09:00
|
|
|
if (!query.trim()) {
|
2025-10-14 13:59:54 +09:00
|
|
|
setError("쿼리를 입력해주세요.");
|
2025-09-30 13:23:22 +09:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-14 14:10:49 +09:00
|
|
|
// 외부 DB인 경우 커넥션 ID 확인
|
|
|
|
|
if (dataSource?.connectionType === "external" && !dataSource?.externalConnectionId) {
|
|
|
|
|
setError("외부 DB 커넥션을 선택해주세요.");
|
2025-10-17 10:38:22 +09:00
|
|
|
// console.log("❌ 쿼리가 비어있음!");
|
2025-10-14 14:10:49 +09:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 13:23:22 +09:00
|
|
|
setIsExecuting(true);
|
|
|
|
|
setError(null);
|
2025-10-17 10:38:22 +09:00
|
|
|
// console.log("🔄 쿼리 실행 시작...");
|
2025-09-30 13:23:22 +09:00
|
|
|
|
|
|
|
|
try {
|
2025-10-14 14:10:49 +09:00
|
|
|
let apiResult: { columns: string[]; rows: any[]; rowCount: number };
|
2025-10-01 12:06:24 +09:00
|
|
|
|
2025-10-14 14:10:49 +09:00
|
|
|
// 현재 DB vs 외부 DB 분기
|
|
|
|
|
if (dataSource?.connectionType === "external" && dataSource?.externalConnectionId) {
|
|
|
|
|
// 외부 DB 쿼리 실행
|
|
|
|
|
const result = await ExternalDbConnectionAPI.executeQuery(
|
|
|
|
|
parseInt(dataSource.externalConnectionId),
|
|
|
|
|
query.trim(),
|
|
|
|
|
);
|
2025-10-01 12:06:24 +09:00
|
|
|
|
2025-10-14 14:10:49 +09:00
|
|
|
if (!result.success) {
|
|
|
|
|
throw new Error(result.message || "외부 DB 쿼리 실행에 실패했습니다.");
|
|
|
|
|
}
|
2025-10-14 13:59:54 +09:00
|
|
|
|
2025-10-14 14:10:49 +09:00
|
|
|
// ExternalDbConnectionAPI의 응답을 통일된 형식으로 변환
|
|
|
|
|
apiResult = {
|
|
|
|
|
columns: result.data?.[0] ? Object.keys(result.data[0]) : [],
|
|
|
|
|
rows: result.data || [],
|
|
|
|
|
rowCount: result.data?.length || 0,
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
// 현재 DB 쿼리 실행
|
|
|
|
|
apiResult = await dashboardApi.executeQuery(query.trim());
|
2025-10-01 12:06:24 +09:00
|
|
|
}
|
|
|
|
|
|
2025-10-14 14:10:49 +09:00
|
|
|
// 결과를 QueryResult 형식으로 변환
|
2025-10-01 12:06:24 +09:00
|
|
|
const result: QueryResult = {
|
2025-10-14 14:10:49 +09:00
|
|
|
columns: apiResult.columns,
|
|
|
|
|
rows: apiResult.rows,
|
|
|
|
|
totalRows: apiResult.rowCount,
|
|
|
|
|
executionTime: 0,
|
2025-10-01 12:06:24 +09:00
|
|
|
};
|
2025-10-14 13:59:54 +09:00
|
|
|
|
2025-09-30 13:23:22 +09:00
|
|
|
setQueryResult(result);
|
|
|
|
|
onQueryTest?.(result);
|
|
|
|
|
|
|
|
|
|
// 데이터 소스 업데이트
|
|
|
|
|
onDataSourceChange({
|
2025-10-14 14:10:49 +09:00
|
|
|
...dataSource,
|
2025-10-14 13:59:54 +09:00
|
|
|
type: "database",
|
2025-09-30 13:23:22 +09:00
|
|
|
query: query.trim(),
|
2025-10-14 15:25:11 +09:00
|
|
|
refreshInterval: dataSource?.refreshInterval ?? 0,
|
2025-10-14 13:59:54 +09:00
|
|
|
lastExecuted: new Date().toISOString(),
|
2025-09-30 13:23:22 +09:00
|
|
|
});
|
|
|
|
|
} catch (err) {
|
2025-10-14 13:59:54 +09:00
|
|
|
const errorMessage = err instanceof Error ? err.message : "쿼리 실행 중 오류가 발생했습니다.";
|
2025-09-30 13:23:22 +09:00
|
|
|
setError(errorMessage);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsExecuting(false);
|
|
|
|
|
}
|
2025-10-14 14:10:49 +09:00
|
|
|
}, [query, dataSource, onDataSourceChange, onQueryTest]);
|
2025-09-30 13:23:22 +09:00
|
|
|
|
|
|
|
|
// 샘플 쿼리 삽입
|
|
|
|
|
const insertSampleQuery = useCallback((sampleType: string) => {
|
|
|
|
|
const samples = {
|
2025-10-17 18:14:13 +09:00
|
|
|
users: `SELECT
|
|
|
|
|
dept_name as 부서명,
|
|
|
|
|
COUNT(*) as 회원수
|
|
|
|
|
FROM user_info
|
|
|
|
|
WHERE dept_name IS NOT NULL
|
|
|
|
|
GROUP BY dept_name
|
|
|
|
|
ORDER BY 회원수 DESC`,
|
2025-10-01 12:06:24 +09:00
|
|
|
|
2025-10-17 18:14:13 +09:00
|
|
|
dept: `SELECT
|
|
|
|
|
dept_code as 부서코드,
|
|
|
|
|
dept_name as 부서명,
|
|
|
|
|
location_name as 위치,
|
|
|
|
|
TO_CHAR(regdate, 'YYYY-MM-DD') as 등록일
|
|
|
|
|
FROM dept_info
|
|
|
|
|
ORDER BY dept_code`,
|
2025-10-14 13:59:54 +09:00
|
|
|
|
2025-10-17 18:14:13 +09:00
|
|
|
usersByDate: `SELECT
|
|
|
|
|
DATE_TRUNC('month', regdate)::date as 월,
|
|
|
|
|
COUNT(*) as 신규사용자수
|
|
|
|
|
FROM user_info
|
|
|
|
|
WHERE regdate >= CURRENT_DATE - INTERVAL '12 months'
|
|
|
|
|
GROUP BY DATE_TRUNC('month', regdate)
|
|
|
|
|
ORDER BY 월`,
|
2025-10-14 13:59:54 +09:00
|
|
|
|
2025-10-17 18:14:13 +09:00
|
|
|
usersByPosition: `SELECT
|
|
|
|
|
position_name as 직급,
|
|
|
|
|
COUNT(*) as 인원수
|
|
|
|
|
FROM user_info
|
|
|
|
|
WHERE position_name IS NOT NULL
|
|
|
|
|
GROUP BY position_name
|
|
|
|
|
ORDER BY 인원수 DESC`,
|
2025-10-01 12:06:24 +09:00
|
|
|
|
2025-10-17 18:14:13 +09:00
|
|
|
deptHierarchy: `SELECT
|
|
|
|
|
COALESCE(parent_dept_code, '최상위') as 상위부서코드,
|
|
|
|
|
COUNT(*) as 하위부서수
|
|
|
|
|
FROM dept_info
|
|
|
|
|
GROUP BY parent_dept_code
|
|
|
|
|
ORDER BY 하위부서수 DESC`,
|
2025-09-30 13:23:22 +09:00
|
|
|
};
|
|
|
|
|
|
2025-10-14 13:59:54 +09:00
|
|
|
setQuery(samples[sampleType as keyof typeof samples] || "");
|
2025-09-30 13:23:22 +09:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return (
|
2025-10-22 12:48:17 +09:00
|
|
|
<div className="space-y-3">
|
2025-09-30 13:23:22 +09:00
|
|
|
{/* 쿼리 에디터 헤더 */}
|
2025-10-14 13:59:54 +09:00
|
|
|
<div className="flex items-center justify-between">
|
2025-10-22 12:48:17 +09:00
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<Database className="h-3.5 w-3.5 text-blue-600" />
|
|
|
|
|
<h4 className="text-xs font-semibold text-gray-800">SQL 쿼리 에디터</h4>
|
2025-09-30 13:23:22 +09:00
|
|
|
</div>
|
2025-10-22 12:48:17 +09:00
|
|
|
<Button onClick={executeQuery} disabled={isExecuting || !query.trim()} size="sm" className="h-7 text-xs">
|
2025-10-14 13:59:54 +09:00
|
|
|
{isExecuting ? (
|
|
|
|
|
<>
|
2025-10-22 12:48:17 +09:00
|
|
|
<Loader2 className="mr-1.5 h-3 w-3 animate-spin" />
|
|
|
|
|
실행 중
|
2025-10-14 13:59:54 +09:00
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
2025-10-22 12:48:17 +09:00
|
|
|
<Play className="mr-1.5 h-3 w-3" />
|
2025-10-14 13:59:54 +09:00
|
|
|
실행
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
2025-09-30 13:23:22 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-10-22 12:48:17 +09:00
|
|
|
{/* 샘플 쿼리 아코디언 */}
|
|
|
|
|
<Collapsible open={sampleQueryOpen} onOpenChange={setSampleQueryOpen}>
|
|
|
|
|
<CollapsibleTrigger className="flex w-full items-center gap-1.5 rounded border border-gray-200 bg-gray-50 px-2 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-100">
|
|
|
|
|
{sampleQueryOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
|
|
|
|
샘플 쿼리
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
<CollapsibleContent className="mt-2">
|
|
|
|
|
<div className="flex flex-wrap gap-1.5">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => insertSampleQuery("users")}
|
|
|
|
|
className="flex items-center gap-1 rounded border border-gray-200 bg-white px-2 py-1 text-[11px] transition-colors hover:bg-gray-50"
|
|
|
|
|
>
|
|
|
|
|
<Code className="h-3 w-3" />
|
|
|
|
|
부서별 사용자
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => insertSampleQuery("dept")}
|
|
|
|
|
className="flex items-center gap-1 rounded border border-gray-200 bg-white px-2 py-1 text-[11px] transition-colors hover:bg-gray-50"
|
|
|
|
|
>
|
|
|
|
|
<Code className="h-3 w-3" />
|
|
|
|
|
부서 정보
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => insertSampleQuery("usersByDate")}
|
|
|
|
|
className="rounded border border-gray-200 bg-white px-2 py-1 text-[11px] transition-colors hover:bg-gray-50"
|
|
|
|
|
>
|
|
|
|
|
월별 가입 추이
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => insertSampleQuery("usersByPosition")}
|
|
|
|
|
className="rounded border border-gray-200 bg-white px-2 py-1 text-[11px] transition-colors hover:bg-gray-50"
|
|
|
|
|
>
|
|
|
|
|
직급별 분포
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => insertSampleQuery("deptHierarchy")}
|
|
|
|
|
className="rounded border border-gray-200 bg-white px-2 py-1 text-[11px] transition-colors hover:bg-gray-50"
|
|
|
|
|
>
|
|
|
|
|
부서 계층
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
</Collapsible>
|
2025-09-30 13:23:22 +09:00
|
|
|
|
|
|
|
|
{/* SQL 쿼리 입력 영역 */}
|
2025-10-22 12:48:17 +09:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label className="text-xs">SQL 쿼리</Label>
|
2025-10-14 13:59:54 +09:00
|
|
|
<div className="relative">
|
|
|
|
|
<Textarea
|
|
|
|
|
value={query}
|
|
|
|
|
onChange={(e) => setQuery(e.target.value)}
|
2025-10-20 14:07:08 +09:00
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
// 모든 키보드 이벤트를 textarea 내부에서만 처리
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
}}
|
2025-10-14 13:59:54 +09:00
|
|
|
placeholder="SELECT * FROM your_table WHERE condition = 'value';"
|
2025-10-22 12:48:17 +09:00
|
|
|
className="h-32 resize-none font-mono text-[11px]"
|
2025-10-14 13:59:54 +09:00
|
|
|
/>
|
2025-09-30 13:23:22 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 새로고침 간격 설정 */}
|
2025-10-22 12:48:17 +09:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Label className="text-xs">자동 새로고침:</Label>
|
2025-10-14 13:59:54 +09:00
|
|
|
<Select
|
2025-10-14 15:25:11 +09:00
|
|
|
value={String(dataSource?.refreshInterval ?? 0)}
|
2025-10-14 13:59:54 +09:00
|
|
|
onValueChange={(value) =>
|
|
|
|
|
onDataSourceChange({
|
|
|
|
|
...dataSource,
|
|
|
|
|
type: "database",
|
|
|
|
|
query,
|
|
|
|
|
refreshInterval: parseInt(value),
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-09-30 13:23:22 +09:00
|
|
|
>
|
2025-10-22 12:48:17 +09:00
|
|
|
<SelectTrigger className="h-7 w-24 text-xs">
|
2025-10-14 13:59:54 +09:00
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
2025-10-17 14:52:08 +09:00
|
|
|
<SelectContent className="z-[99999]">
|
2025-10-22 12:48:17 +09:00
|
|
|
<SelectItem value="0" className="text-xs">
|
|
|
|
|
수동
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="10000" className="text-xs">
|
|
|
|
|
10초
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="30000" className="text-xs">
|
|
|
|
|
30초
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="60000" className="text-xs">
|
|
|
|
|
1분
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="300000" className="text-xs">
|
|
|
|
|
5분
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="600000" className="text-xs">
|
|
|
|
|
10분
|
|
|
|
|
</SelectItem>
|
2025-10-14 13:59:54 +09:00
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-09-30 13:23:22 +09:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 오류 메시지 */}
|
|
|
|
|
{error && (
|
2025-10-22 12:48:17 +09:00
|
|
|
<Alert variant="destructive" className="py-2">
|
2025-10-14 13:59:54 +09:00
|
|
|
<AlertDescription>
|
2025-10-22 12:48:17 +09:00
|
|
|
<div className="text-xs font-medium">오류</div>
|
|
|
|
|
<div className="mt-0.5 text-xs">{error}</div>
|
2025-10-14 13:59:54 +09:00
|
|
|
</AlertDescription>
|
|
|
|
|
</Alert>
|
2025-09-30 13:23:22 +09:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 쿼리 결과 미리보기 */}
|
|
|
|
|
{queryResult && (
|
2025-10-14 13:59:54 +09:00
|
|
|
<Card>
|
2025-10-22 12:48:17 +09:00
|
|
|
<div className="border-b border-gray-200 bg-gray-50 px-2 py-1.5">
|
2025-10-14 13:59:54 +09:00
|
|
|
<div className="flex items-center justify-between">
|
2025-10-22 12:48:17 +09:00
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<span className="text-xs font-medium text-gray-700">쿼리 결과</span>
|
|
|
|
|
<Badge variant="secondary" className="h-4 text-[10px]">
|
|
|
|
|
{queryResult.rows.length}행
|
|
|
|
|
</Badge>
|
2025-10-14 13:59:54 +09:00
|
|
|
</div>
|
2025-10-22 12:48:17 +09:00
|
|
|
<span className="text-[10px] text-gray-500">실행 시간: {queryResult.executionTime}ms</span>
|
2025-09-30 13:23:22 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-14 13:59:54 +09:00
|
|
|
|
2025-10-22 12:48:17 +09:00
|
|
|
<div className="p-2">
|
2025-09-30 13:23:22 +09:00
|
|
|
{queryResult.rows.length > 0 ? (
|
2025-10-22 12:48:17 +09:00
|
|
|
<div className="max-h-48 overflow-auto">
|
2025-10-14 13:59:54 +09:00
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow>
|
|
|
|
|
{queryResult.columns.map((col, idx) => (
|
2025-10-22 12:48:17 +09:00
|
|
|
<TableHead key={idx} className="h-7 text-[11px]">
|
|
|
|
|
{col}
|
|
|
|
|
</TableHead>
|
2025-09-30 13:23:22 +09:00
|
|
|
))}
|
2025-10-14 13:59:54 +09:00
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{queryResult.rows.slice(0, 10).map((row, idx) => (
|
|
|
|
|
<TableRow key={idx}>
|
|
|
|
|
{queryResult.columns.map((col, colIdx) => (
|
2025-10-22 12:48:17 +09:00
|
|
|
<TableCell key={colIdx} className="py-1 text-[11px]">
|
|
|
|
|
{String(row[col] ?? "")}
|
|
|
|
|
</TableCell>
|
2025-10-14 13:59:54 +09:00
|
|
|
))}
|
|
|
|
|
</TableRow>
|
|
|
|
|
))}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
|
|
|
|
|
{queryResult.rows.length > 10 && (
|
2025-10-22 12:48:17 +09:00
|
|
|
<div className="mt-2 text-center text-[10px] text-gray-500">
|
2025-10-14 13:59:54 +09:00
|
|
|
... 및 {queryResult.rows.length - 10}개 더 (미리보기는 10행까지만 표시)
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-30 13:23:22 +09:00
|
|
|
</div>
|
2025-10-14 13:59:54 +09:00
|
|
|
) : (
|
2025-10-22 12:48:17 +09:00
|
|
|
<div className="py-6 text-center text-xs text-gray-500">결과가 없습니다.</div>
|
2025-09-30 13:23:22 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
2025-10-14 13:59:54 +09:00
|
|
|
</Card>
|
2025-09-30 13:23:22 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 샘플 쿼리 결과 생성 함수
|
|
|
|
|
*/
|
|
|
|
|
function generateSampleQueryResult(query: string): QueryResult {
|
|
|
|
|
// 쿼리에서 키워드 추출하여 적절한 샘플 데이터 생성
|
2025-10-01 12:06:24 +09:00
|
|
|
const queryLower = query.toLowerCase();
|
2025-10-14 13:59:54 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
// 디버깅용 로그
|
|
|
|
|
// console.log('generateSampleQueryResult called with query:', query.substring(0, 100));
|
2025-10-14 13:59:54 +09:00
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
// 가장 구체적인 조건부터 먼저 체크 (순서 중요!)
|
2025-10-14 13:59:54 +09:00
|
|
|
const isComparison =
|
|
|
|
|
queryLower.includes("galaxy") ||
|
|
|
|
|
queryLower.includes("갤럭시") ||
|
|
|
|
|
queryLower.includes("아이폰") ||
|
|
|
|
|
queryLower.includes("iphone");
|
|
|
|
|
const isRegional = queryLower.includes("region") || queryLower.includes("지역");
|
|
|
|
|
const isMonthly = queryLower.includes("month");
|
|
|
|
|
const isSales = queryLower.includes("sales") || queryLower.includes("매출");
|
|
|
|
|
const isUsers = queryLower.includes("users") || queryLower.includes("사용자");
|
|
|
|
|
const isProducts = queryLower.includes("product") || queryLower.includes("상품");
|
|
|
|
|
const isWeekly = queryLower.includes("week");
|
2025-10-01 12:06:24 +09:00
|
|
|
|
|
|
|
|
// console.log('Sample data type detection:', {
|
|
|
|
|
// isComparison,
|
|
|
|
|
// isRegional,
|
|
|
|
|
// isWeekly,
|
|
|
|
|
// isProducts,
|
|
|
|
|
// isMonthly,
|
|
|
|
|
// isSales,
|
|
|
|
|
// isUsers,
|
|
|
|
|
// querySnippet: query.substring(0, 200)
|
|
|
|
|
// });
|
2025-09-30 13:23:22 +09:00
|
|
|
|
|
|
|
|
let columns: string[];
|
|
|
|
|
let rows: Record<string, any>[];
|
|
|
|
|
|
2025-10-01 12:06:24 +09:00
|
|
|
// 더 구체적인 조건부터 먼저 체크 (순서 중요!)
|
|
|
|
|
if (isComparison) {
|
|
|
|
|
// console.log('✅ Using COMPARISON data');
|
|
|
|
|
// 제품 비교 데이터 (다중 시리즈)
|
2025-10-14 13:59:54 +09:00
|
|
|
columns = ["month", "galaxy_sales", "iphone_sales", "other_sales"];
|
2025-09-30 13:23:22 +09:00
|
|
|
rows = [
|
2025-10-14 13:59:54 +09:00
|
|
|
{ month: "2024-01", galaxy_sales: 450000, iphone_sales: 620000, other_sales: 130000 },
|
|
|
|
|
{ month: "2024-02", galaxy_sales: 520000, iphone_sales: 680000, other_sales: 150000 },
|
|
|
|
|
{ month: "2024-03", galaxy_sales: 480000, iphone_sales: 590000, other_sales: 110000 },
|
|
|
|
|
{ month: "2024-04", galaxy_sales: 610000, iphone_sales: 650000, other_sales: 160000 },
|
|
|
|
|
{ month: "2024-05", galaxy_sales: 720000, iphone_sales: 780000, other_sales: 180000 },
|
|
|
|
|
{ month: "2024-06", galaxy_sales: 680000, iphone_sales: 690000, other_sales: 170000 },
|
|
|
|
|
{ month: "2024-07", galaxy_sales: 750000, iphone_sales: 800000, other_sales: 170000 },
|
|
|
|
|
{ month: "2024-08", galaxy_sales: 690000, iphone_sales: 720000, other_sales: 170000 },
|
|
|
|
|
{ month: "2024-09", galaxy_sales: 730000, iphone_sales: 750000, other_sales: 170000 },
|
|
|
|
|
{ month: "2024-10", galaxy_sales: 800000, iphone_sales: 810000, other_sales: 170000 },
|
|
|
|
|
{ month: "2024-11", galaxy_sales: 870000, iphone_sales: 880000, other_sales: 170000 },
|
|
|
|
|
{ month: "2024-12", galaxy_sales: 950000, iphone_sales: 990000, other_sales: 160000 },
|
2025-10-01 12:06:24 +09:00
|
|
|
];
|
|
|
|
|
// COMPARISON 데이터를 반환하고 함수 종료
|
2025-10-14 13:59:54 +09:00
|
|
|
// console.log('COMPARISON data generated:', {
|
|
|
|
|
// columns,
|
|
|
|
|
// rowCount: rows.length,
|
2025-10-01 12:06:24 +09:00
|
|
|
// sampleRow: rows[0],
|
|
|
|
|
// allRows: rows,
|
2025-10-01 13:36:04 +09:00
|
|
|
// fieldTypes: {
|
|
|
|
|
// month: typeof rows[0].month,
|
|
|
|
|
// galaxy_sales: typeof rows[0].galaxy_sales,
|
|
|
|
|
// iphone_sales: typeof rows[0].iphone_sales,
|
|
|
|
|
// other_sales: typeof rows[0].other_sales
|
|
|
|
|
// },
|
|
|
|
|
// firstFewRows: rows.slice(0, 3),
|
|
|
|
|
// lastFewRows: rows.slice(-3)
|
2025-10-01 12:06:24 +09:00
|
|
|
// });
|
|
|
|
|
return {
|
|
|
|
|
columns,
|
|
|
|
|
rows,
|
|
|
|
|
totalRows: rows.length,
|
|
|
|
|
executionTime: Math.floor(Math.random() * 200) + 100,
|
|
|
|
|
};
|
|
|
|
|
} else if (isRegional) {
|
|
|
|
|
// console.log('✅ Using REGIONAL data');
|
|
|
|
|
// 지역별 분기별 매출
|
2025-10-14 13:59:54 +09:00
|
|
|
columns = ["지역", "Q1", "Q2", "Q3", "Q4"];
|
2025-10-01 12:06:24 +09:00
|
|
|
rows = [
|
2025-10-14 13:59:54 +09:00
|
|
|
{ 지역: "서울", Q1: 1200000, Q2: 1350000, Q3: 1420000, Q4: 1580000 },
|
|
|
|
|
{ 지역: "경기", Q1: 980000, Q2: 1120000, Q3: 1180000, Q4: 1290000 },
|
|
|
|
|
{ 지역: "부산", Q1: 650000, Q2: 720000, Q3: 780000, Q4: 850000 },
|
|
|
|
|
{ 지역: "대구", Q1: 450000, Q2: 490000, Q3: 520000, Q4: 580000 },
|
|
|
|
|
{ 지역: "인천", Q1: 520000, Q2: 580000, Q3: 620000, Q4: 690000 },
|
|
|
|
|
{ 지역: "광주", Q1: 380000, Q2: 420000, Q3: 450000, Q4: 490000 },
|
|
|
|
|
{ 지역: "대전", Q1: 410000, Q2: 460000, Q3: 490000, Q4: 530000 },
|
2025-09-30 13:23:22 +09:00
|
|
|
];
|
|
|
|
|
} else if (isWeekly && isUsers) {
|
2025-10-01 12:06:24 +09:00
|
|
|
// console.log('✅ Using USERS data');
|
2025-09-30 13:23:22 +09:00
|
|
|
// 사용자 가입 추이
|
2025-10-14 13:59:54 +09:00
|
|
|
columns = ["week", "new_users"];
|
2025-09-30 13:23:22 +09:00
|
|
|
rows = [
|
2025-10-14 13:59:54 +09:00
|
|
|
{ week: "2024-W10", new_users: 23 },
|
|
|
|
|
{ week: "2024-W11", new_users: 31 },
|
|
|
|
|
{ week: "2024-W12", new_users: 28 },
|
|
|
|
|
{ week: "2024-W13", new_users: 35 },
|
|
|
|
|
{ week: "2024-W14", new_users: 42 },
|
|
|
|
|
{ week: "2024-W15", new_users: 38 },
|
|
|
|
|
{ week: "2024-W16", new_users: 45 },
|
|
|
|
|
{ week: "2024-W17", new_users: 52 },
|
|
|
|
|
{ week: "2024-W18", new_users: 48 },
|
|
|
|
|
{ week: "2024-W19", new_users: 55 },
|
|
|
|
|
{ week: "2024-W20", new_users: 61 },
|
|
|
|
|
{ week: "2024-W21", new_users: 58 },
|
2025-09-30 13:23:22 +09:00
|
|
|
];
|
2025-10-01 12:06:24 +09:00
|
|
|
} else if (isProducts && !isComparison) {
|
|
|
|
|
// console.log('✅ Using PRODUCTS data');
|
2025-09-30 13:23:22 +09:00
|
|
|
// 상품별 판매량
|
2025-10-14 13:59:54 +09:00
|
|
|
columns = ["product_name", "total_sold", "revenue"];
|
2025-09-30 13:23:22 +09:00
|
|
|
rows = [
|
2025-10-14 13:59:54 +09:00
|
|
|
{ product_name: "스마트폰", total_sold: 156, revenue: 234000000 },
|
|
|
|
|
{ product_name: "노트북", total_sold: 89, revenue: 178000000 },
|
|
|
|
|
{ product_name: "태블릿", total_sold: 134, revenue: 67000000 },
|
|
|
|
|
{ product_name: "이어폰", total_sold: 267, revenue: 26700000 },
|
|
|
|
|
{ product_name: "스마트워치", total_sold: 98, revenue: 49000000 },
|
|
|
|
|
{ product_name: "키보드", total_sold: 78, revenue: 15600000 },
|
|
|
|
|
{ product_name: "마우스", total_sold: 145, revenue: 8700000 },
|
|
|
|
|
{ product_name: "모니터", total_sold: 67, revenue: 134000000 },
|
|
|
|
|
{ product_name: "프린터", total_sold: 34, revenue: 17000000 },
|
|
|
|
|
{ product_name: "웹캠", total_sold: 89, revenue: 8900000 },
|
2025-09-30 13:23:22 +09:00
|
|
|
];
|
2025-10-01 12:06:24 +09:00
|
|
|
} else if (isMonthly && isSales && !isComparison) {
|
|
|
|
|
// console.log('✅ Using MONTHLY SALES data');
|
|
|
|
|
// 월별 매출 데이터
|
2025-10-14 13:59:54 +09:00
|
|
|
columns = ["month", "sales", "order_count"];
|
2025-10-01 12:06:24 +09:00
|
|
|
rows = [
|
2025-10-14 13:59:54 +09:00
|
|
|
{ month: "2024-01", sales: 1200000, order_count: 45 },
|
|
|
|
|
{ month: "2024-02", sales: 1350000, order_count: 52 },
|
|
|
|
|
{ month: "2024-03", sales: 1180000, order_count: 41 },
|
|
|
|
|
{ month: "2024-04", sales: 1420000, order_count: 58 },
|
|
|
|
|
{ month: "2024-05", sales: 1680000, order_count: 67 },
|
|
|
|
|
{ month: "2024-06", sales: 1540000, order_count: 61 },
|
|
|
|
|
{ month: "2024-07", sales: 1720000, order_count: 71 },
|
|
|
|
|
{ month: "2024-08", sales: 1580000, order_count: 63 },
|
|
|
|
|
{ month: "2024-09", sales: 1650000, order_count: 68 },
|
|
|
|
|
{ month: "2024-10", sales: 1780000, order_count: 75 },
|
|
|
|
|
{ month: "2024-11", sales: 1920000, order_count: 82 },
|
|
|
|
|
{ month: "2024-12", sales: 2100000, order_count: 89 },
|
2025-10-01 12:06:24 +09:00
|
|
|
];
|
2025-09-30 13:23:22 +09:00
|
|
|
} else {
|
2025-10-01 12:06:24 +09:00
|
|
|
// console.log('⚠️ Using DEFAULT data');
|
2025-09-30 13:23:22 +09:00
|
|
|
// 기본 샘플 데이터
|
2025-10-14 13:59:54 +09:00
|
|
|
columns = ["category", "value", "count"];
|
2025-09-30 13:23:22 +09:00
|
|
|
rows = [
|
2025-10-14 13:59:54 +09:00
|
|
|
{ category: "A", value: 100, count: 10 },
|
|
|
|
|
{ category: "B", value: 150, count: 15 },
|
|
|
|
|
{ category: "C", value: 120, count: 12 },
|
|
|
|
|
{ category: "D", value: 180, count: 18 },
|
|
|
|
|
{ category: "E", value: 90, count: 9 },
|
|
|
|
|
{ category: "F", value: 200, count: 20 },
|
|
|
|
|
{ category: "G", value: 110, count: 11 },
|
|
|
|
|
{ category: "H", value: 160, count: 16 },
|
2025-09-30 13:23:22 +09:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
columns,
|
|
|
|
|
rows,
|
|
|
|
|
totalRows: rows.length,
|
|
|
|
|
executionTime: Math.floor(Math.random() * 200) + 100, // 100-300ms
|
|
|
|
|
};
|
|
|
|
|
}
|