사이드바 디자인 다듬기

This commit is contained in:
dohyeons
2025-10-22 12:48:17 +09:00
parent 8a421cfced
commit 85987af65e
5 changed files with 414 additions and 370 deletions

View File

@@ -2,7 +2,6 @@
import React, { useState, useEffect } from "react";
import { ChartDataSource, QueryResult, KeyValuePair } from "../types";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@@ -314,55 +313,48 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
return (
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold text-gray-800">2단계: REST API </h3>
<p className="mt-1 text-sm text-gray-600"> API에서 </p>
</div>
{/* 외부 커넥션 선택 */}
{apiConnections.length > 0 && (
<Card className="space-y-4 p-4">
<div>
<Label className="text-sm font-medium text-gray-700"> ()</Label>
<Select value={selectedConnectionId} onValueChange={handleConnectionSelect}>
<SelectTrigger className="mt-2">
<SelectValue placeholder="저장된 커넥션 선택" />
</SelectTrigger>
<SelectContent className="z-[9999]">
<SelectItem value="manual"> </SelectItem>
{apiConnections.map((conn) => (
<SelectItem key={conn.id} value={String(conn.id)}>
{conn.connection_name}
{conn.description && <span className="ml-2 text-xs text-gray-500">({conn.description})</span>}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="mt-1 text-xs text-gray-500"> REST API </p>
</div>
</Card>
<div className="space-y-2">
<Label className="text-xs font-medium text-gray-700"> ()</Label>
<Select value={selectedConnectionId} onValueChange={handleConnectionSelect}>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="저장된 커넥션 선택" />
</SelectTrigger>
<SelectContent className="z-[9999]">
<SelectItem value="manual" className="text-xs">
</SelectItem>
{apiConnections.map((conn) => (
<SelectItem key={conn.id} value={String(conn.id)} className="text-xs">
{conn.connection_name}
{conn.description && <span className="ml-1.5 text-[10px] text-gray-500">({conn.description})</span>}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[11px] text-gray-500"> REST API </p>
</div>
)}
{/* API URL */}
<Card className="space-y-4 p-4">
<div>
<Label className="text-sm font-medium text-gray-700">API URL *</Label>
<Input
type="url"
placeholder="https://api.example.com/data"
value={dataSource.endpoint || ""}
onChange={(e) => onChange({ endpoint: e.target.value })}
className="mt-2"
/>
<p className="mt-1 text-xs text-gray-500">GET API </p>
</div>
</Card>
<div className="space-y-1.5">
<Label className="text-xs font-medium text-gray-700">API URL *</Label>
<Input
type="url"
placeholder="https://api.example.com/data"
value={dataSource.endpoint || ""}
onChange={(e) => onChange({ endpoint: e.target.value })}
className="h-8 text-xs"
/>
<p className="text-[11px] text-gray-500">GET API </p>
</div>
{/* 쿼리 파라미터 */}
<Card className="space-y-4 p-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-gray-700">URL </Label>
<Button variant="outline" size="sm" onClick={addQueryParam}>
<Label className="text-xs font-medium text-gray-700">URL </Label>
<Button variant="outline" size="sm" onClick={addQueryParam} className="h-6 text-[11px]">
<Plus className="mr-1 h-3 w-3" />
</Button>
@@ -371,39 +363,42 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
{(() => {
const params = normalizeQueryParams();
return params.length > 0 ? (
<div className="space-y-2">
<div className="space-y-1.5">
{params.map((param) => (
<div key={param.id} className="flex gap-2">
<div key={param.id} className="flex gap-1.5">
<Input
placeholder="key"
value={param.key}
onChange={(e) => updateQueryParam(param.id, { key: e.target.value })}
className="flex-1"
className="h-7 flex-1 text-xs"
/>
<Input
placeholder="value"
value={param.value}
onChange={(e) => updateQueryParam(param.id, { value: e.target.value })}
className="flex-1"
className="h-7 flex-1 text-xs"
/>
<Button variant="ghost" size="icon" onClick={() => removeQueryParam(param.id)}>
<X className="h-4 w-4" />
</Button>
<button
onClick={() => removeQueryParam(param.id)}
className="flex h-7 w-7 items-center justify-center rounded hover:bg-gray-100"
>
<X className="h-3 w-3" />
</button>
</div>
))}
</div>
) : (
<p className="py-2 text-center text-sm text-gray-500"> </p>
<p className="py-2 text-center text-[11px] text-gray-500"> </p>
);
})()}
<p className="text-xs text-gray-500">: category=electronics, limit=10</p>
</Card>
<p className="text-[11px] text-gray-500">: category=electronics, limit=10</p>
</div>
{/* 헤더 */}
<Card className="space-y-4 p-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-gray-700"> </Label>
<Label className="text-xs font-medium text-gray-700"> </Label>
<Button variant="outline" size="sm" onClick={addHeader}>
<Plus className="mr-1 h-3 w-3" />
@@ -467,22 +462,22 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
<p className="py-2 text-center text-sm text-gray-500"> </p>
);
})()}
</Card>
</div>
{/* JSON Path */}
<Card className="space-y-2 p-4">
<Label className="text-sm font-medium text-gray-700">JSON Path ()</Label>
<div className="space-y-2">
<Label className="text-xs font-medium text-gray-700">JSON Path ()</Label>
<Input
placeholder="data.results"
value={dataSource.jsonPath || ""}
onChange={(e) => onChange({ jsonPath: e.target.value })}
/>
<p className="text-xs text-gray-500">
<p className="text-[11px] text-gray-500">
JSON (: data.results, items, response.data)
<br />
</p>
</Card>
</div>
{/* 테스트 버튼 */}
<div className="flex justify-end">
@@ -503,7 +498,7 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
{/* 테스트 오류 */}
{testError && (
<Card className="border-red-200 bg-red-50 p-4">
<div className="rounded bg-red-50 px-2 py-2">
<div className="flex items-start gap-2">
<AlertCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-red-600" />
<div>
@@ -511,18 +506,18 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
<div className="mt-1 text-sm text-red-700">{testError}</div>
</div>
</div>
</Card>
</div>
)}
{/* 테스트 결과 */}
{testResult && (
<Card className="border-green-200 bg-green-50 p-4">
<div className="rounded bg-green-50 px-2 py-2">
<div className="mb-2 text-sm font-medium text-green-800">API </div>
<div className="space-y-1 text-xs text-green-700">
<div> {testResult.rows.length} </div>
<div>: {testResult.columns.join(", ")}</div>
</div>
</Card>
</div>
)}
</div>
);

View File

@@ -1,12 +1,11 @@
"use client";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { ChartDataSource } from "../types";
import { ExternalDbConnectionAPI, ExternalDbConnection } from "@/lib/api/externalDbConnection";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { ExternalLink, Database, Server } from "lucide-react";
interface DatabaseConfigProps {
@@ -20,6 +19,7 @@ interface DatabaseConfigProps {
* - 외부 커넥션 목록 불러오기
*/
export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
const router = useRouter();
const [connections, setConnections] = useState<ExternalDbConnection[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -49,93 +49,87 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
const selectedConnection = connections.find((conn) => String(conn.id) === dataSource.externalConnectionId);
return (
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold text-gray-800">2단계: 데이터베이스 </h3>
<p className="mt-1 text-sm text-gray-600"> </p>
</div>
<div className="space-y-3">
{/* 현재 DB vs 외부 DB 선택 */}
<Card className="p-4">
<Label className="mb-3 block text-sm font-medium text-gray-700"> </Label>
<div className="grid grid-cols-2 gap-3">
<Button
variant={dataSource.connectionType === "current" ? "default" : "outline"}
className="h-auto justify-start py-3"
<div>
<Label className="mb-2 block text-xs font-medium text-gray-700"> </Label>
<div className="flex gap-2">
<button
onClick={() => {
onChange({ connectionType: "current", externalConnectionId: undefined });
}}
className={`flex flex-1 items-center gap-1.5 rounded border px-2 py-1.5 text-xs transition-colors ${
dataSource.connectionType === "current"
? "bg-primary border-primary text-white"
: "border-gray-200 bg-white hover:bg-gray-50"
}`}
>
<Database className="mr-2 h-4 w-4" />
<div className="text-left">
<div className="font-medium"> DB</div>
<div className="text-xs opacity-80"> DB</div>
</div>
</Button>
<Database className="h-3 w-3" />
DB
</button>
<Button
variant={dataSource.connectionType === "external" ? "default" : "outline"}
className="h-auto justify-start py-3"
<button
onClick={() => {
onChange({ connectionType: "external" });
}}
className={`flex flex-1 items-center gap-1.5 rounded border px-2 py-1.5 text-xs transition-colors ${
dataSource.connectionType === "external"
? "bg-primary border-primary text-white"
: "border-gray-200 bg-white hover:bg-gray-50"
}`}
>
<Server className="mr-2 h-4 w-4" />
<div className="text-left">
<div className="font-medium"> DB</div>
<div className="text-xs opacity-80"> </div>
</div>
</Button>
<Server className="h-3 w-3" />
DB
</button>
</div>
</Card>
</div>
{/* 외부 DB 선택 시 커넥션 목록 */}
{dataSource.connectionType === "external" && (
<Card className="space-y-4 p-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-gray-700"> </Label>
<Button
variant="ghost"
size="sm"
<Label className="text-xs font-medium text-gray-700"> </Label>
<button
onClick={() => {
window.open("/admin/external-connections", "_blank");
router.push("/admin/external-connections");
}}
className="text-xs"
className="flex items-center gap-1 text-[11px] text-blue-600 transition-colors hover:text-blue-700"
>
<ExternalLink className="mr-1 h-3 w-3" />
<ExternalLink className="h-3 w-3" />
</Button>
</button>
</div>
{loading && (
<div className="flex items-center justify-center py-4">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600" />
<span className="ml-2 text-sm text-gray-600"> ...</span>
<div className="flex items-center justify-center py-3">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600" />
<span className="ml-2 text-xs text-gray-600"> ...</span>
</div>
)}
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
<div className="text-sm text-red-800"> {error}</div>
<Button variant="ghost" size="sm" onClick={loadExternalConnections} className="mt-2 text-xs">
<div className="rounded bg-red-50 px-2 py-1.5">
<div className="text-xs text-red-800">{error}</div>
<button
onClick={loadExternalConnections}
className="mt-1 text-[11px] text-red-600 underline hover:no-underline"
>
</Button>
</button>
</div>
)}
{!loading && !error && connections.length === 0 && (
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4 text-center">
<div className="mb-2 text-sm text-yellow-800"> </div>
<Button
variant="outline"
size="sm"
<div className="rounded bg-yellow-50 px-2 py-2 text-center">
<div className="mb-1 text-xs text-yellow-800"> </div>
<button
onClick={() => {
window.open("/admin/external-connections", "_blank");
router.push("/admin/external-connections");
}}
className="text-[11px] text-yellow-700 underline hover:no-underline"
>
<ExternalLink className="mr-1 h-3 w-3" />
</Button>
</button>
</div>
)}
@@ -147,15 +141,15 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
onChange({ externalConnectionId: value });
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="커넥션 선택하세요" />
<SelectTrigger className="h-8 w-full text-xs">
<SelectValue placeholder="커넥션 선택" />
</SelectTrigger>
<SelectContent className="z-[9999]">
{connections.map((conn) => (
<SelectItem key={conn.id} value={String(conn.id)}>
<div className="flex items-center gap-2">
<SelectItem key={conn.id} value={String(conn.id)} className="text-xs">
<div className="flex items-center gap-1.5">
<span className="font-medium">{conn.connection_name}</span>
<span className="text-xs text-gray-500">({conn.db_type.toUpperCase()})</span>
<span className="text-[10px] text-gray-500">({conn.db_type.toUpperCase()})</span>
</div>
</SelectItem>
))}
@@ -163,31 +157,17 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
</Select>
{selectedConnection && (
<div className="rounded-lg border border-gray-200 bg-gray-50 p-3">
<div className="space-y-1 text-xs text-gray-600">
<div>
<span className="font-medium">:</span> {selectedConnection.connection_name}
</div>
<div>
<span className="font-medium">:</span> {selectedConnection.db_type.toUpperCase()}
</div>
<div className="space-y-0.5 rounded bg-gray-50 px-2 py-1.5 text-[11px] text-gray-600">
<div>
<span className="font-medium">:</span> {selectedConnection.connection_name}
</div>
<div>
<span className="font-medium">:</span> {selectedConnection.db_type.toUpperCase()}
</div>
</div>
)}
</>
)}
</Card>
)}
{/* 다음 단계 안내 */}
{(dataSource.connectionType === "current" ||
(dataSource.connectionType === "external" && dataSource.externalConnectionId)) && (
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3">
<div className="text-sm text-blue-800">
.
<br />
SQL .
</div>
</div>
)}
</div>