"use client"; import { useState, useEffect, useCallback, useMemo } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Database, Columns3, FunctionSquare, Play, Plus, Trash2, Code, Loader2 } from "lucide-react"; import { reportApi } from "@/lib/api/reportApi"; import { useReportDesigner } from "@/contexts/ReportDesignerContext"; import type { ComponentConfig, VisualQuery, VisualQueryFormulaColumn } from "@/types/report"; // ─── 타입 ────────────────────────────────────────────────────────────────────── interface SchemaTable { table_name: string; table_type: string; } interface SchemaColumn { column_name: string; data_type: string; is_nullable: string; } interface Props { component: ComponentConfig; } // ─── 컴포넌트 ────────────────────────────────────────────────────────────────── export function VisualQueryBuilder({ component }: Props) { const { updateComponent, setQueryResult } = useReportDesigner(); const vq: VisualQuery = component.visualQuery ?? { tableName: "", limit: 100, columns: [], formulaColumns: [], }; // ─── 스키마 상태 ─────────────────────────────────────────────────────────────── const [tables, setTables] = useState([]); const [columns, setColumns] = useState([]); const [tablesLoading, setTablesLoading] = useState(false); const [columnsLoading, setColumnsLoading] = useState(false); const [previewLoading, setPreviewLoading] = useState(false); const [generatedSql, setGeneratedSql] = useState(""); const [previewError, setPreviewError] = useState(""); // ─── 테이블 목록 로드 ────────────────────────────────────────────────────────── useEffect(() => { let cancelled = false; const loadTables = async () => { setTablesLoading(true); try { const res = await reportApi.getSchemaTableList(); if (!cancelled && res.success) setTables(res.data); } catch { /* 무시 */ } finally { if (!cancelled) setTablesLoading(false); } }; loadTables(); return () => { cancelled = true; }; }, []); // ─── 테이블 변경 시 컬럼 로드 ────────────────────────────────────────────────── useEffect(() => { if (!vq.tableName) { setColumns([]); return; } let cancelled = false; const loadColumns = async () => { setColumnsLoading(true); try { const res = await reportApi.getSchemaTableColumns(vq.tableName); if (!cancelled && res.success) setColumns(res.data); } catch { /* 무시 */ } finally { if (!cancelled) setColumnsLoading(false); } }; loadColumns(); return () => { cancelled = true; }; }, [vq.tableName]); // ─── visualQuery 업데이트 헬퍼 ──────────────────────────────────────────────── const updateVQ = useCallback( (patch: Partial) => { updateComponent(component.id, { visualQuery: { ...vq, ...patch } }); }, [component.id, vq, updateComponent], ); // ─── 테이블 선택 ─────────────────────────────────────────────────────────────── const handleTableChange = useCallback( (tableName: string) => { updateComponent(component.id, { visualQuery: { tableName: tableName === "none" ? "" : tableName, limit: vq.limit ?? 100, columns: [], formulaColumns: [], }, }); }, [component.id, vq.limit, updateComponent], ); // ─── 컬럼 토글 ───────────────────────────────────────────────────────────────── const toggleColumn = useCallback( (colName: string) => { const selected = vq.columns.includes(colName) ? vq.columns.filter((c) => c !== colName) : [...vq.columns, colName]; updateVQ({ columns: selected }); }, [vq.columns, updateVQ], ); const selectAllColumns = useCallback(() => { updateVQ({ columns: columns.map((c) => c.column_name) }); }, [columns, updateVQ]); const deselectAllColumns = useCallback(() => { updateVQ({ columns: [] }); }, [updateVQ]); // ─── 수식 컬럼 관리 ──────────────────────────────────────────────────────────── const addFormulaColumn = useCallback(() => { const fc: VisualQueryFormulaColumn = { alias: `calc_${(vq.formulaColumns.length + 1)}`, header: `수식 ${vq.formulaColumns.length + 1}`, expression: "", }; updateVQ({ formulaColumns: [...vq.formulaColumns, fc] }); }, [vq.formulaColumns, updateVQ]); const updateFormulaColumn = useCallback( (idx: number, patch: Partial) => { const updated = vq.formulaColumns.map((fc, i) => (i === idx ? { ...fc, ...patch } : fc)); updateVQ({ formulaColumns: updated }); }, [vq.formulaColumns, updateVQ], ); const removeFormulaColumn = useCallback( (idx: number) => { updateVQ({ formulaColumns: vq.formulaColumns.filter((_, i) => i !== idx) }); }, [vq.formulaColumns, updateVQ], ); // ─── SQL 미리보기 빌드 ──────────────────────────────────────────────────────── const previewSql = useMemo(() => { if (!vq.tableName || (vq.columns.length === 0 && vq.formulaColumns.length === 0)) return ""; const parts: string[] = []; for (const col of vq.columns) parts.push(`"${col}"`); for (const fc of vq.formulaColumns) { if (fc.expression && fc.alias) parts.push(`(${fc.expression}) AS "${fc.alias}"`); } if (parts.length === 0) return ""; const limit = Math.min(Math.max(vq.limit ?? 100, 1), 10000); return `SELECT ${parts.join(", ")} FROM "${vq.tableName}" LIMIT ${limit}`; }, [vq]); // ─── 미리보기 실행 ──────────────────────────────────────────────────────────── const resultKey = `visual_${component.id}`; const handlePreview = useCallback(async () => { if (!vq.tableName || (vq.columns.length === 0 && vq.formulaColumns.length === 0)) return; setPreviewLoading(true); setPreviewError(""); try { const res = await reportApi.previewVisualQuery(vq); if (res.success) { setQueryResult(resultKey, res.data.fields, res.data.rows); setGeneratedSql(res.data.sql); } else { setPreviewError("쿼리 실행에 실패했습니다."); } } catch (err: any) { setPreviewError(err?.response?.data?.message || err.message || "오류가 발생했습니다."); } finally { setPreviewLoading(false); } }, [vq, resultKey, setQueryResult]); const hasSelection = vq.columns.length > 0 || vq.formulaColumns.length > 0; // ─── 렌더링 ──────────────────────────────────────────────────────────────────── return (
{/* [1] 데이터 소스 */}
데이터 소스
updateVQ({ limit: parseInt(e.target.value) || 100 })} className="h-8 bg-white text-xs" />
{/* [2] 컬럼 선택 */} {vq.tableName && (
컬럼 선택 {vq.columns.length > 0 && ( ({vq.columns.length}/{columns.length}) )}
{columnsLoading ? (
컬럼 로딩 중...
) : columns.length > 0 ? (
{columns.map((col) => { const checked = vq.columns.includes(col.column_name); return ( ); })}
) : (

컬럼 정보가 없습니다.

)}
)} {/* [3] 수식 컬럼 */} {vq.tableName && (
수식 컬럼 {vq.formulaColumns.length > 0 && ( ({vq.formulaColumns.length}) )}
{vq.formulaColumns.length > 0 ? ( vq.formulaColumns.map((fc, idx) => (
updateFormulaColumn(idx, { alias: e.target.value })} placeholder="필드명" className="h-7 flex-1 font-mono text-xs" /> updateFormulaColumn(idx, { header: e.target.value })} placeholder="표시 헤더" className="h-7 flex-1 text-xs" />
updateFormulaColumn(idx, { expression: e.target.value })} placeholder='SQL 표현식 (예: "price" * "quantity")' className="h-7 font-mono text-xs" />
)) ) : (

계산이 필요한 컬럼을 추가하세요. (예: price * quantity)

)}
)} {/* [4] SQL 미리보기 + 실행 */} {vq.tableName && hasSelection && (
생성 SQL
              {generatedSql || previewSql || "컬럼을 선택하세요."}
            
{previewError && (

{previewError}

)}
)}
); }