사이드바 디자인 다듬기
This commit is contained in:
@@ -12,7 +12,8 @@ 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";
|
||||
import { Play, Loader2, Database, Code } from "lucide-react";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { Play, Loader2, Database, Code, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { applyQueryFilters } from "./utils/queryHelpers";
|
||||
|
||||
interface QueryEditorProps {
|
||||
@@ -32,6 +33,7 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [sampleQueryOpen, setSampleQueryOpen] = useState(false);
|
||||
|
||||
// 쿼리 실행
|
||||
const executeQuery = useCallback(async () => {
|
||||
@@ -155,55 +157,75 @@ ORDER BY 하위부서수 DESC`,
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
{/* 쿼리 에디터 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5 text-blue-600" />
|
||||
<h4 className="text-lg font-semibold text-gray-800">SQL 쿼리 에디터</h4>
|
||||
<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>
|
||||
</div>
|
||||
<Button onClick={executeQuery} disabled={isExecuting || !query.trim()} size="sm">
|
||||
<Button onClick={executeQuery} disabled={isExecuting || !query.trim()} size="sm" className="h-7 text-xs">
|
||||
{isExecuting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
실행 중...
|
||||
<Loader2 className="mr-1.5 h-3 w-3 animate-spin" />
|
||||
실행 중
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
<Play className="mr-1.5 h-3 w-3" />
|
||||
실행
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 샘플 쿼리 버튼들 */}
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Label className="text-sm text-gray-600">샘플 쿼리:</Label>
|
||||
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("users")}>
|
||||
<Code className="mr-2 h-3 w-3" />
|
||||
부서별 사용자
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("dept")}>
|
||||
<Code className="mr-2 h-3 w-3" />
|
||||
부서 정보
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("usersByDate")}>
|
||||
월별 가입 추이
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("usersByPosition")}>
|
||||
직급별 분포
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("deptHierarchy")}>
|
||||
부서 계층
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
{/* 샘플 쿼리 아코디언 */}
|
||||
<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>
|
||||
|
||||
{/* SQL 쿼리 입력 영역 */}
|
||||
<div className="space-y-2">
|
||||
<Label>SQL 쿼리</Label>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">SQL 쿼리</Label>
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
value={query}
|
||||
@@ -213,14 +235,14 @@ ORDER BY 하위부서수 DESC`,
|
||||
e.stopPropagation();
|
||||
}}
|
||||
placeholder="SELECT * FROM your_table WHERE condition = 'value';"
|
||||
className="h-40 resize-none font-mono text-sm"
|
||||
className="h-32 resize-none font-mono text-[11px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 새로고침 간격 설정 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="text-sm">자동 새로고침:</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs">자동 새로고침:</Label>
|
||||
<Select
|
||||
value={String(dataSource?.refreshInterval ?? 0)}
|
||||
onValueChange={(value) =>
|
||||
@@ -232,26 +254,38 @@ ORDER BY 하위부서수 DESC`,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectTrigger className="h-7 w-24 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[99999]">
|
||||
<SelectItem value="0">수동</SelectItem>
|
||||
<SelectItem value="10000">10초</SelectItem>
|
||||
<SelectItem value="30000">30초</SelectItem>
|
||||
<SelectItem value="60000">1분</SelectItem>
|
||||
<SelectItem value="300000">5분</SelectItem>
|
||||
<SelectItem value="600000">10분</SelectItem>
|
||||
<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>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 오류 메시지 */}
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<Alert variant="destructive" className="py-2">
|
||||
<AlertDescription>
|
||||
<div className="text-sm font-medium">오류</div>
|
||||
<div className="mt-1 text-sm">{error}</div>
|
||||
<div className="text-xs font-medium">오류</div>
|
||||
<div className="mt-0.5 text-xs">{error}</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -259,24 +293,28 @@ ORDER BY 하위부서수 DESC`,
|
||||
{/* 쿼리 결과 미리보기 */}
|
||||
{queryResult && (
|
||||
<Card>
|
||||
<div className="border-b border-gray-200 bg-gray-50 px-4 py-3">
|
||||
<div className="border-b border-gray-200 bg-gray-50 px-2 py-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700">쿼리 결과</span>
|
||||
<Badge variant="secondary">{queryResult.rows.length}행</Badge>
|
||||
<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>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">실행 시간: {queryResult.executionTime}ms</span>
|
||||
<span className="text-[10px] text-gray-500">실행 시간: {queryResult.executionTime}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3">
|
||||
<div className="p-2">
|
||||
{queryResult.rows.length > 0 ? (
|
||||
<div className="max-h-60 overflow-auto">
|
||||
<div className="max-h-48 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{queryResult.columns.map((col, idx) => (
|
||||
<TableHead key={idx}>{col}</TableHead>
|
||||
<TableHead key={idx} className="h-7 text-[11px]">
|
||||
{col}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -284,7 +322,9 @@ ORDER BY 하위부서수 DESC`,
|
||||
{queryResult.rows.slice(0, 10).map((row, idx) => (
|
||||
<TableRow key={idx}>
|
||||
{queryResult.columns.map((col, colIdx) => (
|
||||
<TableCell key={colIdx}>{String(row[col] ?? "")}</TableCell>
|
||||
<TableCell key={colIdx} className="py-1 text-[11px]">
|
||||
{String(row[col] ?? "")}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -292,13 +332,13 @@ ORDER BY 하위부서수 DESC`,
|
||||
</Table>
|
||||
|
||||
{queryResult.rows.length > 10 && (
|
||||
<div className="mt-3 text-center text-xs text-gray-500">
|
||||
<div className="mt-2 text-center text-[10px] text-gray-500">
|
||||
... 및 {queryResult.rows.length - 10}개 더 (미리보기는 10행까지만 표시)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-8 text-center text-gray-500">결과가 없습니다.</div>
|
||||
<div className="py-6 text-center text-xs text-gray-500">결과가 없습니다.</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user