Files
vexplor/frontend/components/admin/dashboard/ElementConfigModal.tsx
hjjeong d8f73c1136 feat: 대시보드 관리 시스템 구현
 새로운 기능:
- 드래그 앤 드롭 대시보드 설계 도구
- SQL 쿼리 에디터 및 실시간 실행
- Recharts 기반 차트 컴포넌트 (Bar, Pie, Line)
- 차트 데이터 매핑 및 설정 UI
- 요소 이동, 크기 조절, 삭제 기능
- 레이아웃 저장 기능

📦 추가된 컴포넌트:
- DashboardDesigner: 메인 설계 도구
- QueryEditor: SQL 쿼리 작성 및 실행
- ChartConfigPanel: 차트 설정 패널
- ChartRenderer: 실제 차트 렌더링
- CanvasElement: 드래그 가능한 캔버스 요소

🔧 기술 스택:
- Recharts 라이브러리 추가
- TypeScript 타입 정의 완비
- 독립적 컴포넌트 구조로 설계

🎯 접속 경로: /admin/dashboard
2025-09-30 13:23:22 +09:00

170 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useState, useCallback } from 'react';
import { DashboardElement, ChartDataSource, ChartConfig, QueryResult } from './types';
import { QueryEditor } from './QueryEditor';
import { ChartConfigPanel } from './ChartConfigPanel';
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>(
element.dataSource || { type: 'database', refreshInterval: 30000 }
);
const [chartConfig, setChartConfig] = useState<ChartConfig>(
element.chartConfig || {}
);
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
const [activeTab, setActiveTab] = useState<'query' | 'chart'>('query');
// 데이터 소스 변경 처리
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) {
setActiveTab('chart');
}
}, []);
// 저장 처리
const handleSave = useCallback(() => {
const updatedElement: DashboardElement = {
...element,
dataSource,
chartConfig,
};
onSave(updatedElement);
onClose();
}, [element, dataSource, chartConfig, onSave, onClose]);
// 모달이 열려있지 않으면 렌더링하지 않음
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-4xl h-[80vh] flex flex-col">
{/* 모달 헤더 */}
<div className="flex justify-between items-center p-6 border-b border-gray-200">
<div>
<h2 className="text-xl font-semibold text-gray-800">
{element.title}
</h2>
<p className="text-sm text-gray-600 mt-1">
</p>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-2xl"
>
×
</button>
</div>
{/* 탭 네비게이션 */}
<div className="flex border-b border-gray-200">
<button
onClick={() => setActiveTab('query')}
className={`
px-6 py-3 text-sm font-medium border-b-2 transition-colors
${activeTab === 'query'
? 'border-blue-500 text-blue-600 bg-blue-50'
: 'border-transparent text-gray-500 hover:text-gray-700'}
`}
>
📝 &
</button>
<button
onClick={() => setActiveTab('chart')}
className={`
px-6 py-3 text-sm font-medium border-b-2 transition-colors
${activeTab === 'chart'
? 'border-blue-500 text-blue-600 bg-blue-50'
: 'border-transparent text-gray-500 hover:text-gray-700'}
`}
>
📊
{queryResult && (
<span className="ml-2 px-2 py-0.5 bg-green-100 text-green-800 text-xs rounded-full">
{queryResult.rows.length}
</span>
)}
</button>
</div>
{/* 탭 내용 */}
<div className="flex-1 overflow-auto p-6">
{activeTab === 'query' && (
<QueryEditor
dataSource={dataSource}
onDataSourceChange={handleDataSourceChange}
onQueryTest={handleQueryTest}
/>
)}
{activeTab === 'chart' && (
<ChartConfigPanel
config={chartConfig}
queryResult={queryResult}
onConfigChange={handleChartConfigChange}
/>
)}
</div>
{/* 모달 푸터 */}
<div className="flex justify-between items-center p-6 border-t border-gray-200">
<div className="text-sm text-gray-500">
{dataSource.query && (
<>
💾 : {dataSource.query.length > 50
? `${dataSource.query.substring(0, 50)}...`
: dataSource.query}
</>
)}
</div>
<div className="flex gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50"
>
</button>
<button
onClick={handleSave}
disabled={!dataSource.query || (!chartConfig.xAxis || !chartConfig.yAxis)}
className="
px-4 py-2 bg-blue-500 text-white rounded-lg
hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed
"
>
</button>
</div>
</div>
</div>
</div>
);
}