Files
vexplor_dev/frontend/components/admin/dashboard/DashboardDesigner.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

176 lines
5.5 KiB
TypeScript

'use client';
import React, { useState, useRef, useCallback } from 'react';
import { DashboardCanvas } from './DashboardCanvas';
import { DashboardSidebar } from './DashboardSidebar';
import { DashboardToolbar } from './DashboardToolbar';
import { ElementConfigModal } from './ElementConfigModal';
import { DashboardElement, ElementType, ElementSubtype } from './types';
/**
* 대시보드 설계 도구 메인 컴포넌트
* - 드래그 앤 드롭으로 차트/위젯 배치
* - 요소 이동, 크기 조절, 삭제 기능
* - 레이아웃 저장/불러오기 기능
*/
export default function DashboardDesigner() {
const [elements, setElements] = useState<DashboardElement[]>([]);
const [selectedElement, setSelectedElement] = useState<string | null>(null);
const [elementCounter, setElementCounter] = useState(0);
const [configModalElement, setConfigModalElement] = useState<DashboardElement | null>(null);
const canvasRef = useRef<HTMLDivElement>(null);
// 새로운 요소 생성
const createElement = useCallback((
type: ElementType,
subtype: ElementSubtype,
x: number,
y: number
) => {
const newElement: DashboardElement = {
id: `element-${elementCounter + 1}`,
type,
subtype,
position: { x, y },
size: { width: 250, height: 200 },
title: getElementTitle(type, subtype),
content: getElementContent(type, subtype)
};
setElements(prev => [...prev, newElement]);
setElementCounter(prev => prev + 1);
setSelectedElement(newElement.id);
}, [elementCounter]);
// 요소 업데이트
const updateElement = useCallback((id: string, updates: Partial<DashboardElement>) => {
setElements(prev => prev.map(el =>
el.id === id ? { ...el, ...updates } : el
));
}, []);
// 요소 삭제
const removeElement = useCallback((id: string) => {
setElements(prev => prev.filter(el => el.id !== id));
if (selectedElement === id) {
setSelectedElement(null);
}
}, [selectedElement]);
// 전체 삭제
const clearCanvas = useCallback(() => {
if (window.confirm('모든 요소를 삭제하시겠습니까?')) {
setElements([]);
setSelectedElement(null);
setElementCounter(0);
}
}, []);
// 요소 설정 모달 열기
const openConfigModal = useCallback((element: DashboardElement) => {
setConfigModalElement(element);
}, []);
// 요소 설정 모달 닫기
const closeConfigModal = useCallback(() => {
setConfigModalElement(null);
}, []);
// 요소 설정 저장
const saveElementConfig = useCallback((updatedElement: DashboardElement) => {
updateElement(updatedElement.id, updatedElement);
}, [updateElement]);
// 레이아웃 저장
const saveLayout = useCallback(() => {
const layoutData = {
elements: elements.map(el => ({
type: el.type,
subtype: el.subtype,
position: el.position,
size: el.size,
title: el.title,
dataSource: el.dataSource,
chartConfig: el.chartConfig
})),
timestamp: new Date().toISOString()
};
console.log('저장된 레이아웃:', JSON.stringify(layoutData, null, 2));
alert('레이아웃이 콘솔에 저장되었습니다. (F12를 눌러 확인하세요)');
}, [elements]);
return (
<div className="flex h-full bg-gray-50">
{/* 캔버스 영역 */}
<div className="flex-1 relative overflow-auto border-r-2 border-gray-300">
<DashboardToolbar
onClearCanvas={clearCanvas}
onSaveLayout={saveLayout}
/>
<DashboardCanvas
ref={canvasRef}
elements={elements}
selectedElement={selectedElement}
onCreateElement={createElement}
onUpdateElement={updateElement}
onRemoveElement={removeElement}
onSelectElement={setSelectedElement}
onConfigureElement={openConfigModal}
/>
</div>
{/* 사이드바 */}
<DashboardSidebar />
{/* 요소 설정 모달 */}
{configModalElement && (
<ElementConfigModal
element={configModalElement}
isOpen={true}
onClose={closeConfigModal}
onSave={saveElementConfig}
/>
)}
</div>
);
}
// 요소 제목 생성 헬퍼 함수
function getElementTitle(type: ElementType, subtype: ElementSubtype): string {
if (type === 'chart') {
switch (subtype) {
case 'bar': return '📊 바 차트';
case 'pie': return '🥧 원형 차트';
case 'line': return '📈 꺾은선 차트';
default: return '📊 차트';
}
} else if (type === 'widget') {
switch (subtype) {
case 'exchange': return '💱 환율 위젯';
case 'weather': return '☁️ 날씨 위젯';
default: return '🔧 위젯';
}
}
return '요소';
}
// 요소 내용 생성 헬퍼 함수
function getElementContent(type: ElementType, subtype: ElementSubtype): string {
if (type === 'chart') {
switch (subtype) {
case 'bar': return '바 차트가 여기에 표시됩니다';
case 'pie': return '원형 차트가 여기에 표시됩니다';
case 'line': return '꺾은선 차트가 여기에 표시됩니다';
default: return '차트가 여기에 표시됩니다';
}
} else if (type === 'widget') {
switch (subtype) {
case 'exchange': return 'USD: ₩1,320\nJPY: ₩900\nEUR: ₩1,450';
case 'weather': return '서울\n23°C\n구름 많음';
default: return '위젯 내용이 여기에 표시됩니다';
}
}
return '내용이 여기에 표시됩니다';
}