사이드바에 탭 방식 적용

This commit is contained in:
dohyeons 2025-10-22 10:58:21 +09:00
parent bdf9bd0075
commit 2433658e01
2 changed files with 114 additions and 138 deletions

View File

@ -5,13 +5,13 @@ import { DashboardElement, ChartDataSource, ChartConfig, QueryResult } from "./t
import { QueryEditor } from "./QueryEditor"; import { QueryEditor } from "./QueryEditor";
import { ChartConfigPanel } from "./ChartConfigPanel"; import { ChartConfigPanel } from "./ChartConfigPanel";
import { VehicleMapConfigPanel } from "./VehicleMapConfigPanel"; import { VehicleMapConfigPanel } from "./VehicleMapConfigPanel";
import { DataSourceSelector } from "./data-sources/DataSourceSelector";
import { DatabaseConfig } from "./data-sources/DatabaseConfig"; import { DatabaseConfig } from "./data-sources/DatabaseConfig";
import { ApiConfig } from "./data-sources/ApiConfig"; import { ApiConfig } from "./data-sources/ApiConfig";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { X, ChevronLeft, ChevronRight } from "lucide-react"; import { X } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface ElementConfigSidebarProps { interface ElementConfigSidebarProps {
element: DashboardElement | null; element: DashboardElement | null;
@ -34,7 +34,6 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
}); });
const [chartConfig, setChartConfig] = useState<ChartConfig>({}); const [chartConfig, setChartConfig] = useState<ChartConfig>({});
const [queryResult, setQueryResult] = useState<QueryResult | null>(null); const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
const [currentStep, setCurrentStep] = useState<1 | 2>(1);
const [customTitle, setCustomTitle] = useState<string>(""); const [customTitle, setCustomTitle] = useState<string>("");
const [showHeader, setShowHeader] = useState<boolean>(true); const [showHeader, setShowHeader] = useState<boolean>(true);
@ -44,7 +43,6 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 }); setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 });
setChartConfig(element.chartConfig || {}); setChartConfig(element.chartConfig || {});
setQueryResult(null); setQueryResult(null);
setCurrentStep(1);
setCustomTitle(element.customTitle || ""); setCustomTitle(element.customTitle || "");
setShowHeader(element.showHeader !== false); setShowHeader(element.showHeader !== false);
} }
@ -100,25 +98,6 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
setChartConfig({}); setChartConfig({});
}, []); }, []);
// 다음 단계로 이동
const handleNext = useCallback(() => {
if (currentStep === 1) {
setCurrentStep(2);
}
}, [currentStep]);
// 이전 단계로 이동
const handlePrev = useCallback(() => {
if (currentStep > 1) {
setCurrentStep((prev) => (prev - 1) as 1 | 2);
}
}, [currentStep]);
// 취소 처리
const handleCancel = useCallback(() => {
onClose();
}, [onClose]);
// 적용 처리 // 적용 처리
const handleApply = useCallback(() => { const handleApply = useCallback(() => {
if (!element) return; if (!element) return;
@ -184,15 +163,10 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
isTitleChanged || isTitleChanged ||
isHeaderChanged || isHeaderChanged ||
(isSimpleWidget (isSimpleWidget
? currentStep === 2 && queryResult && queryResult.rows.length > 0 ? queryResult && queryResult.rows.length > 0
: isMapWidget : isMapWidget
? currentStep === 2 && ? queryResult && queryResult.rows.length > 0 && chartConfig.latitudeColumn && chartConfig.longitudeColumn
queryResult && : queryResult &&
queryResult.rows.length > 0 &&
chartConfig.latitudeColumn &&
chartConfig.longitudeColumn
: currentStep === 2 &&
queryResult &&
queryResult.rows.length > 0 && queryResult.rows.length > 0 &&
chartConfig.xAxis && chartConfig.xAxis &&
(isPieChart || isApiSource ? (chartConfig.aggregation === "count" ? true : hasYAxis) : hasYAxis)); (isPieChart || isApiSource ? (chartConfig.aggregation === "count" ? true : hasYAxis) : hasYAxis));
@ -214,89 +188,105 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
{/* 본문: 스크롤 가능 영역 */} {/* 본문: 스크롤 가능 영역 */}
<div className="flex-1 overflow-y-auto p-4"> <div className="flex-1 overflow-y-auto p-4">
{/* 커스텀 제목 입력 */} {/* 기본 설정 */}
<div className="mb-4"> <div className="mb-4 space-y-3">
<label className="mb-1 block text-sm font-medium text-gray-700"> </label> {/* 커스텀 제목 입력 */}
<input <div>
type="text" <label className="mb-1 block text-sm font-medium text-gray-700"> </label>
value={customTitle} <input
onChange={(e) => setCustomTitle(e.target.value)} type="text"
onKeyDown={(e) => e.stopPropagation()} value={customTitle}
placeholder="비워두면 자동 생성" onChange={(e) => setCustomTitle(e.target.value)}
className="focus:border-primary focus:ring-primary w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-1 focus:outline-none" onKeyDown={(e) => e.stopPropagation()}
/> placeholder="비워두면 자동 생성"
</div> className="focus:border-primary focus:ring-primary w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-1 focus:outline-none"
/>
{/* 헤더 표시 옵션 */}
<div className="mb-4 flex items-center gap-2">
<input
type="checkbox"
id="showHeader"
checked={showHeader}
onChange={(e) => setShowHeader(e.target.checked)}
className="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300"
/>
<label htmlFor="showHeader" className="text-sm font-medium text-gray-700">
</label>
</div>
{/* 진행 상황 표시 */}
{!isSimpleWidget && !isHeaderOnlyWidget && (
<div className="mb-4 rounded-md bg-gray-50 p-3">
<div className="text-sm font-medium text-gray-700">
{currentStep} / 2: {currentStep === 1 ? "데이터 소스 선택" : "데이터 설정"}
</div>
</div> </div>
)}
{/* 단계별 내용 */} {/* 헤더 표시 옵션 */}
<div className="flex items-center gap-2">
<input
type="checkbox"
id="showHeader"
checked={showHeader}
onChange={(e) => setShowHeader(e.target.checked)}
className="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300"
/>
<label htmlFor="showHeader" className="text-sm font-medium text-gray-700">
</label>
</div>
</div>
{/* 헤더 전용 위젯이 아닐 때만 데이터 소스 탭 표시 */}
{!isHeaderOnlyWidget && ( {!isHeaderOnlyWidget && (
<div className="space-y-4"> <Tabs
{currentStep === 1 && ( defaultValue={dataSource.type}
<DataSourceSelector dataSource={dataSource} onTypeChange={handleDataSourceTypeChange} /> onValueChange={(value) => handleDataSourceTypeChange(value as "database" | "api")}
)} className="w-full"
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="database"></TabsTrigger>
<TabsTrigger value="api">REST API</TabsTrigger>
</TabsList>
{currentStep === 2 && ( <TabsContent value="database" className="mt-4 space-y-4">
<div className="space-y-4"> <DatabaseConfig dataSource={dataSource} onChange={handleDataSourceUpdate} />
{/* 데이터 설정 */} <QueryEditor
{dataSource.type === "database" ? ( dataSource={dataSource}
<> onDataSourceChange={handleDataSourceUpdate}
<DatabaseConfig dataSource={dataSource} onChange={handleDataSourceUpdate} /> onQueryTest={handleQueryTest}
<QueryEditor />
dataSource={dataSource}
onDataSourceChange={handleDataSourceUpdate} {/* 차트/지도 설정 */}
onQueryTest={handleQueryTest} {!isSimpleWidget && queryResult && queryResult.rows.length > 0 && (
<div className="mt-4">
{isMapWidget ? (
<VehicleMapConfigPanel
config={chartConfig}
queryResult={queryResult}
onConfigChange={handleChartConfigChange}
/> />
</> ) : (
) : ( <ChartConfigPanel
<ApiConfig dataSource={dataSource} onChange={handleDataSourceUpdate} onTestResult={handleQueryTest} /> config={chartConfig}
)} queryResult={queryResult}
onConfigChange={handleChartConfigChange}
chartType={element.subtype}
dataSourceType={dataSource.type}
query={dataSource.query}
/>
)}
</div>
)}
</TabsContent>
{/* 차트/지도 설정 */} <TabsContent value="api" className="mt-4 space-y-4">
{!isSimpleWidget && queryResult && queryResult.rows.length > 0 && ( <ApiConfig dataSource={dataSource} onChange={handleDataSourceUpdate} onTestResult={handleQueryTest} />
<div className="mt-4">
{isMapWidget ? ( {/* 차트/지도 설정 */}
<VehicleMapConfigPanel {!isSimpleWidget && queryResult && queryResult.rows.length > 0 && (
config={chartConfig} <div className="mt-4">
queryResult={queryResult} {isMapWidget ? (
onConfigChange={handleChartConfigChange} <VehicleMapConfigPanel
/> config={chartConfig}
) : ( queryResult={queryResult}
<ChartConfigPanel onConfigChange={handleChartConfigChange}
config={chartConfig} />
queryResult={queryResult} ) : (
onConfigChange={handleChartConfigChange} <ChartConfigPanel
chartType={element.subtype} config={chartConfig}
dataSourceType={dataSource.type} queryResult={queryResult}
query={dataSource.query} onConfigChange={handleChartConfigChange}
/> chartType={element.subtype}
)} dataSourceType={dataSource.type}
</div> query={dataSource.query}
)} />
</div> )}
)} </div>
</div> )}
</TabsContent>
</Tabs>
)} )}
{/* 데이터 로드 상태 */} {/* 데이터 로드 상태 */}
@ -307,32 +297,14 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
)} )}
</div> </div>
{/* 푸터: 단계 이동 및 적용 버튼 */} {/* 푸터: 적용 버튼 */}
<div className="flex items-center justify-between border-t p-4"> <div className="flex items-center justify-end gap-2 border-t p-4">
<div className="flex gap-2"> <Button variant="outline" size="sm" onClick={onClose}>
{!isSimpleWidget && !isHeaderOnlyWidget && currentStep > 1 && (
<Button variant="outline" size="sm" onClick={handlePrev}> </Button>
<ChevronLeft className="mr-1 h-4 w-4" /> <Button size="sm" onClick={handleApply} disabled={isHeaderOnlyWidget ? false : !canApply}>
</Button> </Button>
)}
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={handleCancel}>
</Button>
{isHeaderOnlyWidget || currentStep === 1 ? (
<Button size="sm" onClick={isHeaderOnlyWidget ? handleApply : handleNext}>
{isHeaderOnlyWidget ? "적용" : "다음"}
{!isHeaderOnlyWidget && <ChevronRight className="ml-1 h-4 w-4" />}
</Button>
) : (
<Button size="sm" onClick={handleApply} disabled={!canApply}>
</Button>
)}
</div>
</div> </div>
</div> </div>
); );

View File

@ -68,8 +68,8 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
> >
<Database className="mr-2 h-4 w-4" /> <Database className="mr-2 h-4 w-4" />
<div className="text-left"> <div className="text-left">
<div className="font-medium"> </div> <div className="font-medium"> DB</div>
<div className="text-xs opacity-80"> DB</div> <div className="text-xs opacity-80"> DB</div>
</div> </div>
</Button> </Button>
@ -82,7 +82,7 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
> >
<Server className="mr-2 h-4 w-4" /> <Server className="mr-2 h-4 w-4" />
<div className="text-left"> <div className="text-left">
<div className="font-medium"> </div> <div className="font-medium"> DB</div>
<div className="text-xs opacity-80"> </div> <div className="text-xs opacity-80"> </div>
</div> </div>
</Button> </Button>
@ -183,7 +183,11 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
{(dataSource.connectionType === "current" || {(dataSource.connectionType === "current" ||
(dataSource.connectionType === "external" && dataSource.externalConnectionId)) && ( (dataSource.connectionType === "external" && dataSource.externalConnectionId)) && (
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3"> <div className="rounded-lg border border-blue-200 bg-blue-50 p-3">
<div className="text-sm text-blue-800"> . SQL .</div> <div className="text-sm text-blue-800">
.
<br />
SQL .
</div>
</div> </div>
)} )}
</div> </div>