2025-10-14 10:12:40 +09:00
|
|
|
|
"use client";
|
2025-09-30 13:23:22 +09:00
|
|
|
|
|
2025-10-14 10:12:40 +09:00
|
|
|
|
import React, { useState, useCallback } from "react";
|
2025-10-14 10:48:17 +09:00
|
|
|
|
import { DashboardElement, ChartDataSource, ChartConfig, QueryResult } from "./types";
|
2025-10-14 10:12:40 +09:00
|
|
|
|
import { QueryEditor } from "./QueryEditor";
|
|
|
|
|
|
import { ChartConfigPanel } from "./ChartConfigPanel";
|
2025-09-30 13:23:22 +09:00
|
|
|
|
|
|
|
|
|
|
interface ElementConfigModalProps {
|
|
|
|
|
|
element: DashboardElement;
|
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
onSave: (element: DashboardElement) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 요소 설정 모달 컴포넌트
|
|
|
|
|
|
* - 차트/위젯 데이터 소스 설정
|
|
|
|
|
|
* - 쿼리 에디터 통합
|
|
|
|
|
|
* - 차트 설정 패널 통합
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function ElementConfigModal({ element, isOpen, onClose, onSave }: ElementConfigModalProps) {
|
|
|
|
|
|
const [dataSource, setDataSource] = useState<ChartDataSource>(
|
2025-10-14 10:12:40 +09:00
|
|
|
|
element.dataSource || { type: "database", refreshInterval: 30000 },
|
2025-09-30 13:23:22 +09:00
|
|
|
|
);
|
2025-10-14 10:12:40 +09:00
|
|
|
|
const [chartConfig, setChartConfig] = useState<ChartConfig>(element.chartConfig || {});
|
2025-09-30 13:23:22 +09:00
|
|
|
|
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
|
2025-10-14 10:12:40 +09:00
|
|
|
|
const [activeTab, setActiveTab] = useState<"query" | "chart">("query");
|
2025-09-30 13:23:22 +09:00
|
|
|
|
|
|
|
|
|
|
// 데이터 소스 변경 처리
|
|
|
|
|
|
const handleDataSourceChange = useCallback((newDataSource: ChartDataSource) => {
|
|
|
|
|
|
setDataSource(newDataSource);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 차트 설정 변경 처리
|
|
|
|
|
|
const handleChartConfigChange = useCallback((newConfig: ChartConfig) => {
|
|
|
|
|
|
setChartConfig(newConfig);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 쿼리 테스트 결과 처리
|
|
|
|
|
|
const handleQueryTest = useCallback((result: QueryResult) => {
|
|
|
|
|
|
setQueryResult(result);
|
|
|
|
|
|
// 쿼리 결과가 나오면 자동으로 차트 설정 탭으로 이동
|
|
|
|
|
|
if (result.rows.length > 0) {
|
2025-10-14 10:12:40 +09:00
|
|
|
|
setActiveTab("chart");
|
2025-09-30 13:23:22 +09:00
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 저장 처리
|
|
|
|
|
|
const handleSave = useCallback(() => {
|
|
|
|
|
|
const updatedElement: DashboardElement = {
|
|
|
|
|
|
...element,
|
|
|
|
|
|
dataSource,
|
|
|
|
|
|
chartConfig,
|
|
|
|
|
|
};
|
|
|
|
|
|
onSave(updatedElement);
|
|
|
|
|
|
onClose();
|
|
|
|
|
|
}, [element, dataSource, chartConfig, onSave, onClose]);
|
|
|
|
|
|
|
|
|
|
|
|
// 모달이 열려있지 않으면 렌더링하지 않음
|
|
|
|
|
|
if (!isOpen) return null;
|
|
|
|
|
|
|
2025-10-14 11:26:53 +09:00
|
|
|
|
// 시계, 달력, 기사관리 위젯은 자체 설정 UI를 가지고 있으므로 모달 표시하지 않음
|
|
|
|
|
|
if (
|
|
|
|
|
|
element.type === "widget" &&
|
|
|
|
|
|
(element.subtype === "clock" || element.subtype === "calendar" || element.subtype === "driver-management")
|
|
|
|
|
|
) {
|
2025-10-14 10:23:20 +09:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 13:23:22 +09:00
|
|
|
|
return (
|
2025-10-14 11:26:53 +09:00
|
|
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
2025-10-14 10:12:40 +09:00
|
|
|
|
<div className="flex h-[80vh] w-full max-w-4xl flex-col rounded-lg bg-white shadow-xl">
|
2025-09-30 13:23:22 +09:00
|
|
|
|
{/* 모달 헤더 */}
|
2025-10-14 10:12:40 +09:00
|
|
|
|
<div className="flex items-center justify-between border-b border-gray-200 p-6">
|
2025-09-30 13:23:22 +09:00
|
|
|
|
<div>
|
2025-10-14 10:12:40 +09:00
|
|
|
|
<h2 className="text-xl font-semibold text-gray-800">{element.title} 설정</h2>
|
|
|
|
|
|
<p className="text-muted-foreground mt-1 text-sm">데이터 소스와 차트 설정을 구성하세요</p>
|
2025-09-30 13:23:22 +09:00
|
|
|
|
</div>
|
2025-10-14 10:12:40 +09:00
|
|
|
|
<button onClick={onClose} className="hover:text-muted-foreground text-2xl text-gray-400">
|
2025-09-30 13:23:22 +09:00
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 탭 네비게이션 */}
|
|
|
|
|
|
<div className="flex border-b border-gray-200">
|
|
|
|
|
|
<button
|
2025-10-14 10:12:40 +09:00
|
|
|
|
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"
|
|
|
|
|
|
} `}
|
2025-09-30 13:23:22 +09:00
|
|
|
|
>
|
|
|
|
|
|
📝 쿼리 & 데이터
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
2025-10-14 10:12:40 +09:00
|
|
|
|
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"
|
|
|
|
|
|
} `}
|
2025-09-30 13:23:22 +09:00
|
|
|
|
>
|
|
|
|
|
|
📊 차트 설정
|
|
|
|
|
|
{queryResult && (
|
2025-10-14 10:12:40 +09:00
|
|
|
|
<span className="ml-2 rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-800">
|
2025-09-30 13:23:22 +09:00
|
|
|
|
{queryResult.rows.length}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 탭 내용 */}
|
|
|
|
|
|
<div className="flex-1 overflow-auto p-6">
|
2025-10-14 10:12:40 +09:00
|
|
|
|
{activeTab === "query" && (
|
2025-09-30 13:23:22 +09:00
|
|
|
|
<QueryEditor
|
|
|
|
|
|
dataSource={dataSource}
|
|
|
|
|
|
onDataSourceChange={handleDataSourceChange}
|
|
|
|
|
|
onQueryTest={handleQueryTest}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-14 10:12:40 +09:00
|
|
|
|
{activeTab === "chart" && (
|
|
|
|
|
|
<ChartConfigPanel config={chartConfig} queryResult={queryResult} onConfigChange={handleChartConfigChange} />
|
2025-09-30 13:23:22 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 모달 푸터 */}
|
2025-10-14 10:12:40 +09:00
|
|
|
|
<div className="flex items-center justify-between border-t border-gray-200 p-6">
|
2025-09-30 13:23:22 +09:00
|
|
|
|
<div className="text-sm text-gray-500">
|
|
|
|
|
|
{dataSource.query && (
|
|
|
|
|
|
<>
|
2025-10-14 10:12:40 +09:00
|
|
|
|
💾 쿼리: {dataSource.query.length > 50 ? `${dataSource.query.substring(0, 50)}...` : dataSource.query}
|
2025-09-30 13:23:22 +09:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-10-14 10:12:40 +09:00
|
|
|
|
|
2025-09-30 13:23:22 +09:00
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={onClose}
|
2025-10-14 10:12:40 +09:00
|
|
|
|
className="text-muted-foreground rounded-lg border border-gray-300 px-4 py-2 hover:bg-gray-50"
|
2025-09-30 13:23:22 +09:00
|
|
|
|
>
|
|
|
|
|
|
취소
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleSave}
|
2025-10-14 10:12:40 +09:00
|
|
|
|
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"
|
2025-09-30 13:23:22 +09:00
|
|
|
|
>
|
|
|
|
|
|
저장
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|