207 lines
7.9 KiB
TypeScript
207 lines
7.9 KiB
TypeScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import React, { useState, useCallback } from 'react';
|
|||
|
|
import { ChartConfig, QueryResult } from './types';
|
|||
|
|
|
|||
|
|
interface ChartConfigPanelProps {
|
|||
|
|
config?: ChartConfig;
|
|||
|
|
queryResult?: QueryResult;
|
|||
|
|
onConfigChange: (config: ChartConfig) => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 차트 설정 패널 컴포넌트
|
|||
|
|
* - 데이터 필드 매핑 설정
|
|||
|
|
* - 차트 스타일 설정
|
|||
|
|
* - 실시간 미리보기
|
|||
|
|
*/
|
|||
|
|
export function ChartConfigPanel({ config, queryResult, onConfigChange }: ChartConfigPanelProps) {
|
|||
|
|
const [currentConfig, setCurrentConfig] = useState<ChartConfig>(config || {});
|
|||
|
|
|
|||
|
|
// 설정 업데이트
|
|||
|
|
const updateConfig = useCallback((updates: Partial<ChartConfig>) => {
|
|||
|
|
const newConfig = { ...currentConfig, ...updates };
|
|||
|
|
setCurrentConfig(newConfig);
|
|||
|
|
onConfigChange(newConfig);
|
|||
|
|
}, [currentConfig, onConfigChange]);
|
|||
|
|
|
|||
|
|
// 사용 가능한 컬럼 목록
|
|||
|
|
const availableColumns = queryResult?.columns || [];
|
|||
|
|
const sampleData = queryResult?.rows?.[0] || {};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<h4 className="text-lg font-semibold text-gray-800">⚙️ 차트 설정</h4>
|
|||
|
|
|
|||
|
|
{/* 쿼리 결과가 없을 때 */}
|
|||
|
|
{!queryResult && (
|
|||
|
|
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|||
|
|
<div className="text-yellow-800 text-sm">
|
|||
|
|
💡 먼저 SQL 쿼리를 실행하여 데이터를 가져온 후 차트를 설정할 수 있습니다.
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 데이터 필드 매핑 */}
|
|||
|
|
{queryResult && (
|
|||
|
|
<>
|
|||
|
|
{/* 차트 제목 */}
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<label className="block text-sm font-medium text-gray-700">차트 제목</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={currentConfig.title || ''}
|
|||
|
|
onChange={(e) => updateConfig({ title: e.target.value })}
|
|||
|
|
placeholder="차트 제목을 입력하세요"
|
|||
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* X축 설정 */}
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<label className="block text-sm font-medium text-gray-700">
|
|||
|
|
X축 (카테고리)
|
|||
|
|
<span className="text-red-500 ml-1">*</span>
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
value={currentConfig.xAxis || ''}
|
|||
|
|
onChange={(e) => updateConfig({ xAxis: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="">선택하세요</option>
|
|||
|
|
{availableColumns.map((col) => (
|
|||
|
|
<option key={col} value={col}>
|
|||
|
|
{col} {sampleData[col] && `(예: ${sampleData[col]})`}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Y축 설정 */}
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<label className="block text-sm font-medium text-gray-700">
|
|||
|
|
Y축 (값)
|
|||
|
|
<span className="text-red-500 ml-1">*</span>
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
value={currentConfig.yAxis || ''}
|
|||
|
|
onChange={(e) => updateConfig({ yAxis: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="">선택하세요</option>
|
|||
|
|
{availableColumns.map((col) => (
|
|||
|
|
<option key={col} value={col}>
|
|||
|
|
{col} {sampleData[col] && `(예: ${sampleData[col]})`}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 집계 함수 */}
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<label className="block text-sm font-medium text-gray-700">집계 함수</label>
|
|||
|
|
<select
|
|||
|
|
value={currentConfig.aggregation || 'sum'}
|
|||
|
|
onChange={(e) => updateConfig({ aggregation: e.target.value as any })}
|
|||
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="sum">합계 (SUM)</option>
|
|||
|
|
<option value="avg">평균 (AVG)</option>
|
|||
|
|
<option value="count">개수 (COUNT)</option>
|
|||
|
|
<option value="max">최대값 (MAX)</option>
|
|||
|
|
<option value="min">최소값 (MIN)</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 그룹핑 필드 (선택사항) */}
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<label className="block text-sm font-medium text-gray-700">
|
|||
|
|
그룹핑 필드 (선택사항)
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
value={currentConfig.groupBy || ''}
|
|||
|
|
onChange={(e) => updateConfig({ groupBy: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="">없음</option>
|
|||
|
|
{availableColumns.map((col) => (
|
|||
|
|
<option key={col} value={col}>
|
|||
|
|
{col}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 차트 색상 */}
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<label className="block text-sm font-medium text-gray-700">차트 색상</label>
|
|||
|
|
<div className="grid grid-cols-4 gap-2">
|
|||
|
|
{[
|
|||
|
|
['#3B82F6', '#EF4444', '#10B981', '#F59E0B'], // 기본
|
|||
|
|
['#8B5CF6', '#EC4899', '#06B6D4', '#84CC16'], // 밝은
|
|||
|
|
['#1F2937', '#374151', '#6B7280', '#9CA3AF'], // 회색
|
|||
|
|
['#DC2626', '#EA580C', '#CA8A04', '#65A30D'], // 따뜻한
|
|||
|
|
].map((colorSet, setIdx) => (
|
|||
|
|
<button
|
|||
|
|
key={setIdx}
|
|||
|
|
onClick={() => updateConfig({ colors: colorSet })}
|
|||
|
|
className={`
|
|||
|
|
h-8 rounded border-2 flex
|
|||
|
|
${JSON.stringify(currentConfig.colors) === JSON.stringify(colorSet)
|
|||
|
|
? 'border-gray-800' : 'border-gray-300'}
|
|||
|
|
`}
|
|||
|
|
>
|
|||
|
|
{colorSet.map((color, idx) => (
|
|||
|
|
<div
|
|||
|
|
key={idx}
|
|||
|
|
className="flex-1 first:rounded-l last:rounded-r"
|
|||
|
|
style={{ backgroundColor: color }}
|
|||
|
|
/>
|
|||
|
|
))}
|
|||
|
|
</button>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 범례 표시 */}
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
id="showLegend"
|
|||
|
|
checked={currentConfig.showLegend !== false}
|
|||
|
|
onChange={(e) => updateConfig({ showLegend: e.target.checked })}
|
|||
|
|
className="rounded"
|
|||
|
|
/>
|
|||
|
|
<label htmlFor="showLegend" className="text-sm text-gray-700">
|
|||
|
|
범례 표시
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 설정 미리보기 */}
|
|||
|
|
<div className="p-3 bg-gray-50 rounded-lg">
|
|||
|
|
<div className="text-sm font-medium text-gray-700 mb-2">📋 설정 미리보기</div>
|
|||
|
|
<div className="text-xs text-gray-600 space-y-1">
|
|||
|
|
<div><strong>X축:</strong> {currentConfig.xAxis || '미설정'}</div>
|
|||
|
|
<div><strong>Y축:</strong> {currentConfig.yAxis || '미설정'}</div>
|
|||
|
|
<div><strong>집계:</strong> {currentConfig.aggregation || 'sum'}</div>
|
|||
|
|
{currentConfig.groupBy && (
|
|||
|
|
<div><strong>그룹핑:</strong> {currentConfig.groupBy}</div>
|
|||
|
|
)}
|
|||
|
|
<div><strong>데이터 행 수:</strong> {queryResult.rows.length}개</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 필수 필드 확인 */}
|
|||
|
|
{(!currentConfig.xAxis || !currentConfig.yAxis) && (
|
|||
|
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
|||
|
|
<div className="text-red-800 text-sm">
|
|||
|
|
⚠️ X축과 Y축을 모두 설정해야 차트가 표시됩니다.
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|