차트 구현 phase1 완료
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback } from "react";
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { DashboardElement, ChartDataSource, ChartConfig, QueryResult } from "./types";
|
||||
import { QueryEditor } from "./QueryEditor";
|
||||
import { ChartConfigPanel } from "./ChartConfigPanel";
|
||||
import { DataSourceSelector } from "./data-sources/DataSourceSelector";
|
||||
import { DatabaseConfig } from "./data-sources/DatabaseConfig";
|
||||
import { ApiConfig } from "./data-sources/ApiConfig";
|
||||
import { validateDataSource } from "./data-sources/dataSourceUtils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { X, ChevronLeft, ChevronRight, Save } from "lucide-react";
|
||||
|
||||
interface ElementConfigModalProps {
|
||||
element: DashboardElement;
|
||||
@@ -13,22 +21,48 @@ interface ElementConfigModalProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* 요소 설정 모달 컴포넌트
|
||||
* - 차트/위젯 데이터 소스 설정
|
||||
* - 쿼리 에디터 통합
|
||||
* - 차트 설정 패널 통합
|
||||
* 요소 설정 모달 컴포넌트 (리팩토링)
|
||||
* - 3단계 플로우: 데이터 소스 선택 → 데이터 설정 → 차트 설정
|
||||
* - 새로운 데이터 소스 컴포넌트 통합
|
||||
*/
|
||||
export function ElementConfigModal({ element, isOpen, onClose, onSave }: ElementConfigModalProps) {
|
||||
const [dataSource, setDataSource] = useState<ChartDataSource>(
|
||||
element.dataSource || { type: "database", refreshInterval: 30000 },
|
||||
element.dataSource || { type: "database", connectionType: "current", refreshInterval: 30 },
|
||||
);
|
||||
const [chartConfig, setChartConfig] = useState<ChartConfig>(element.chartConfig || {});
|
||||
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<"query" | "chart">("query");
|
||||
const [currentStep, setCurrentStep] = useState<1 | 2 | 3>(1);
|
||||
|
||||
// 데이터 소스 변경 처리
|
||||
const handleDataSourceChange = useCallback((newDataSource: ChartDataSource) => {
|
||||
setDataSource(newDataSource);
|
||||
// 모달이 열릴 때 초기화
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 30 });
|
||||
setChartConfig(element.chartConfig || {});
|
||||
setQueryResult(null);
|
||||
setCurrentStep(1);
|
||||
}
|
||||
}, [isOpen, element]);
|
||||
|
||||
// 데이터 소스 타입 변경
|
||||
const handleDataSourceTypeChange = useCallback((type: "database" | "api") => {
|
||||
if (type === "database") {
|
||||
setDataSource({
|
||||
type: "database",
|
||||
connectionType: "current",
|
||||
refreshInterval: 30,
|
||||
});
|
||||
} else {
|
||||
setDataSource({
|
||||
type: "api",
|
||||
method: "GET",
|
||||
refreshInterval: 30,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 데이터 소스 업데이트
|
||||
const handleDataSourceUpdate = useCallback((updates: Partial<ChartDataSource>) => {
|
||||
setDataSource((prev) => ({ ...prev, ...updates }));
|
||||
}, []);
|
||||
|
||||
// 차트 설정 변경 처리
|
||||
@@ -39,12 +73,48 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
|
||||
// 쿼리 테스트 결과 처리
|
||||
const handleQueryTest = useCallback((result: QueryResult) => {
|
||||
setQueryResult(result);
|
||||
// 쿼리 결과가 나오면 자동으로 차트 설정 탭으로 이동
|
||||
// 쿼리 결과가 나오면 자동으로 3단계로 이동
|
||||
if (result.rows.length > 0) {
|
||||
setActiveTab("chart");
|
||||
setCurrentStep(3);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 다음 단계로 이동 (검증 포함)
|
||||
const handleNext = useCallback(() => {
|
||||
if (currentStep === 1) {
|
||||
// 1단계: 데이터 소스 타입 선택 완료
|
||||
setCurrentStep(2);
|
||||
} else if (currentStep === 2) {
|
||||
// 2단계: 데이터 설정 완료 - 검증
|
||||
const validation = validateDataSource(
|
||||
dataSource.type,
|
||||
dataSource.connectionType,
|
||||
dataSource.externalConnectionId,
|
||||
dataSource.query,
|
||||
dataSource.endpoint,
|
||||
);
|
||||
|
||||
if (!validation.valid) {
|
||||
alert(validation.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 쿼리 결과가 있으면 3단계로 이동
|
||||
if (queryResult && queryResult.rows.length > 0) {
|
||||
setCurrentStep(3);
|
||||
} else {
|
||||
alert("먼저 데이터를 테스트하여 결과를 확인하세요");
|
||||
}
|
||||
}
|
||||
}, [currentStep, dataSource, queryResult]);
|
||||
|
||||
// 이전 단계로 이동
|
||||
const handlePrev = useCallback(() => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep((prev) => (prev - 1) as 1 | 2 | 3);
|
||||
}
|
||||
}, [currentStep]);
|
||||
|
||||
// 저장 처리
|
||||
const handleSave = useCallback(() => {
|
||||
const updatedElement: DashboardElement = {
|
||||
@@ -67,88 +137,99 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
|
||||
return null;
|
||||
}
|
||||
|
||||
// 저장 가능 여부 확인
|
||||
const canSave =
|
||||
queryResult &&
|
||||
queryResult.rows.length > 0 &&
|
||||
chartConfig.xAxis &&
|
||||
(chartConfig.yAxis || (Array.isArray(chartConfig.yAxis) && chartConfig.yAxis.length > 0));
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div className="flex h-[80vh] w-full max-w-4xl flex-col rounded-lg bg-white shadow-xl">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="flex h-[85vh] w-full max-w-5xl flex-col rounded-xl border bg-white shadow-2xl">
|
||||
{/* 모달 헤더 */}
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between border-b p-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-800">{element.title} 설정</h2>
|
||||
<p className="text-muted-foreground mt-1 text-sm">데이터 소스와 차트 설정을 구성하세요</p>
|
||||
<h2 className="text-xl font-semibold text-gray-900">{element.title} 설정</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">3단계 플로우로 차트를 설정하세요</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="hover:text-muted-foreground text-2xl text-gray-400">
|
||||
×
|
||||
</button>
|
||||
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8">
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 탭 네비게이션 */}
|
||||
<div className="flex border-b border-gray-200">
|
||||
<button
|
||||
onClick={() => setActiveTab("query")}
|
||||
className={`border-b-2 px-6 py-3 text-sm font-medium transition-colors ${
|
||||
activeTab === "query"
|
||||
? "border-primary text-primary bg-accent"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700"
|
||||
} `}
|
||||
>
|
||||
📝 쿼리 & 데이터
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("chart")}
|
||||
className={`border-b-2 px-6 py-3 text-sm font-medium transition-colors ${
|
||||
activeTab === "chart"
|
||||
? "border-primary text-primary bg-accent"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700"
|
||||
} `}
|
||||
>
|
||||
📊 차트 설정
|
||||
{queryResult && (
|
||||
<span className="ml-2 rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-800">
|
||||
{queryResult.rows.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{/* 진행 상황 표시 */}
|
||||
<div className="border-b bg-gray-50 px-6 py-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
단계 {currentStep} / 3: {currentStep === 1 && "데이터 소스 선택"}
|
||||
{currentStep === 2 && "데이터 설정"}
|
||||
{currentStep === 3 && "차트 설정"}
|
||||
</div>
|
||||
<Badge variant="secondary">{Math.round((currentStep / 3) * 100)}% 완료</Badge>
|
||||
</div>
|
||||
<Progress value={(currentStep / 3) * 100} className="h-2" />
|
||||
</div>
|
||||
|
||||
{/* 탭 내용 */}
|
||||
{/* 단계별 내용 */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
{activeTab === "query" && (
|
||||
<QueryEditor
|
||||
dataSource={dataSource}
|
||||
onDataSourceChange={handleDataSourceChange}
|
||||
onQueryTest={handleQueryTest}
|
||||
/>
|
||||
{currentStep === 1 && (
|
||||
<DataSourceSelector dataSource={dataSource} onTypeChange={handleDataSourceTypeChange} />
|
||||
)}
|
||||
|
||||
{activeTab === "chart" && (
|
||||
{currentStep === 2 && (
|
||||
<div className="space-y-6">
|
||||
{dataSource.type === "database" ? (
|
||||
<>
|
||||
<DatabaseConfig dataSource={dataSource} onChange={handleDataSourceUpdate} />
|
||||
<QueryEditor
|
||||
dataSource={dataSource}
|
||||
onDataSourceChange={handleDataSourceUpdate}
|
||||
onQueryTest={handleQueryTest}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ApiConfig dataSource={dataSource} onChange={handleDataSourceUpdate} onTestResult={handleQueryTest} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 3 && (
|
||||
<ChartConfigPanel config={chartConfig} queryResult={queryResult} onConfigChange={handleChartConfigChange} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 모달 푸터 */}
|
||||
<div className="flex items-center justify-between border-t border-gray-200 p-6">
|
||||
<div className="text-sm text-gray-500">
|
||||
{dataSource.query && (
|
||||
<>
|
||||
💾 쿼리: {dataSource.query.length > 50 ? `${dataSource.query.substring(0, 50)}...` : dataSource.query}
|
||||
</>
|
||||
<div className="flex items-center justify-between border-t bg-gray-50 p-6">
|
||||
<div>
|
||||
{queryResult && (
|
||||
<Badge variant="default" className="bg-green-600">
|
||||
📊 {queryResult.rows.length}개 데이터 로드됨
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-muted-foreground rounded-lg border border-gray-300 px-4 py-2 hover:bg-gray-50"
|
||||
>
|
||||
{currentStep > 1 && (
|
||||
<Button variant="outline" onClick={handlePrev}>
|
||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||
이전
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!dataSource.query || !chartConfig.xAxis || !chartConfig.yAxis}
|
||||
className="bg-accent0 rounded-lg px-4 py-2 text-white hover:bg-blue-600 disabled:cursor-not-allowed disabled:bg-gray-300"
|
||||
>
|
||||
저장
|
||||
</button>
|
||||
</Button>
|
||||
{currentStep < 3 ? (
|
||||
<Button onClick={handleNext}>
|
||||
다음
|
||||
<ChevronRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleSave} disabled={!canSave}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
저장
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user