rest api 기능 구현

This commit is contained in:
dohyeons
2025-10-15 10:02:32 +09:00
parent 2e84c4272f
commit 593983d6ee
10 changed files with 747 additions and 226 deletions

View File

@@ -17,6 +17,7 @@ interface ChartConfigPanelProps {
queryResult?: QueryResult;
onConfigChange: (config: ChartConfig) => void;
chartType?: string;
dataSourceType?: "database" | "api"; // 데이터 소스 타입
}
/**
@@ -25,11 +26,18 @@ interface ChartConfigPanelProps {
* - 차트 스타일 설정
* - 실시간 미리보기
*/
export function ChartConfigPanel({ config, queryResult, onConfigChange, chartType }: ChartConfigPanelProps) {
export function ChartConfigPanel({
config,
queryResult,
onConfigChange,
chartType,
dataSourceType,
}: ChartConfigPanelProps) {
const [currentConfig, setCurrentConfig] = useState<ChartConfig>(config || {});
// 원형/도넛 차트는 Y축이 필수가 아님
// 원형/도넛 차트 또는 REST API는 Y축이 필수가 아님
const isPieChart = chartType === "pie" || chartType === "donut";
const isApiSource = dataSourceType === "api";
// 설정 업데이트
const updateConfig = useCallback(
@@ -41,15 +49,91 @@ export function ChartConfigPanel({ config, queryResult, onConfigChange, chartTyp
[currentConfig, onConfigChange],
);
// 사용 가능한 컬럼 목록
// 사용 가능한 컬럼 목록 및 타입 정보
const availableColumns = queryResult?.columns || [];
const columnTypes = queryResult?.columnTypes || {};
const sampleData = queryResult?.rows?.[0] || {};
// 차트에 사용 가능한 컬럼 필터링
const simpleColumns = availableColumns.filter((col) => {
const type = columnTypes[col];
// number, string, boolean만 허용 (object, array는 제외)
return !type || type === "number" || type === "string" || type === "boolean";
});
// 숫자 타입 컬럼만 필터링 (Y축용)
const numericColumns = availableColumns.filter((col) => columnTypes[col] === "number");
// 복잡한 타입의 컬럼 (경고 표시용)
const complexColumns = availableColumns.filter((col) => {
const type = columnTypes[col];
return type === "object" || type === "array";
});
return (
<div className="space-y-6">
{/* 데이터 필드 매핑 */}
{queryResult && (
<>
{/* API 응답 미리보기 */}
{queryResult.rows && queryResult.rows.length > 0 && (
<Card className="border-blue-200 bg-blue-50 p-4">
<div className="mb-2 flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-blue-600" />
<h4 className="font-semibold text-blue-900">📋 API </h4>
</div>
<div className="rounded bg-white p-3 text-xs">
<div className="mb-2 text-gray-600"> {queryResult.totalRows} :</div>
<pre className="overflow-x-auto text-gray-800">{JSON.stringify(sampleData, null, 2)}</pre>
</div>
{/* 컬럼 타입 정보 */}
{Object.keys(columnTypes).length > 0 && (
<div className="mt-3">
<div className="mb-2 text-xs font-medium text-gray-700"> :</div>
<div className="flex flex-wrap gap-2">
{availableColumns.map((col) => {
const type = columnTypes[col];
const isComplex = type === "object" || type === "array";
return (
<Badge key={col} variant={isComplex ? "destructive" : "secondary"} className="text-xs">
{col}: {type}
{isComplex && " ⚠️"}
</Badge>
);
})}
</div>
</div>
)}
</Card>
)}
{/* 복잡한 타입 경고 */}
{complexColumns.length > 0 && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<div className="font-semibold"> </div>
<div className="mt-1 text-sm">
:
<div className="mt-1 flex flex-wrap gap-1">
{complexColumns.map((col) => (
<Badge key={col} variant="outline" className="bg-red-50">
{col} ({columnTypes[col]})
</Badge>
))}
</div>
</div>
<div className="mt-2 text-xs text-gray-600">
💡 <strong> :</strong> JSON Path를 .
<br />
: <code className="rounded bg-gray-100 px-1">main</code> {" "}
<code className="rounded bg-gray-100 px-1">data.items</code>
</div>
</AlertDescription>
</Alert>
)}
{/* 차트 제목 */}
<div className="space-y-2">
<Label> </Label>
@@ -74,64 +158,157 @@ export function ChartConfigPanel({ config, queryResult, onConfigChange, chartTyp
<SelectValue placeholder="선택하세요" />
</SelectTrigger>
<SelectContent>
{availableColumns.map((col) => (
<SelectItem key={col} value={col}>
{col} {sampleData[col] && `(예: ${sampleData[col]})`}
</SelectItem>
))}
{simpleColumns.map((col) => {
const type = columnTypes[col] || "unknown";
const preview = sampleData[col];
const previewText =
preview !== undefined && preview !== null
? typeof preview === "object"
? JSON.stringify(preview).substring(0, 30)
: String(preview).substring(0, 30)
: "";
return (
<SelectItem key={col} value={col}>
<span className="flex items-center gap-2">
<span>{col}</span>
<Badge variant="outline" className="text-xs">
{type}
</Badge>
{previewText && <span className="text-xs text-gray-500">(: {previewText})</span>}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
{simpleColumns.length === 0 && (
<p className="text-xs text-red-500"> . JSON Path를 .</p>
)}
</div>
{/* Y축 설정 (다중 선택 가능) */}
<div className="space-y-2">
<Label>
Y축 () -
{!isPieChart && <span className="ml-1 text-red-500">*</span>}
{isPieChart && <span className="ml-2 text-xs text-gray-500">()</span>}
{!isPieChart && !isApiSource && <span className="ml-1 text-red-500">*</span>}
{(isPieChart || isApiSource) && (
<span className="ml-2 text-xs text-gray-500">( - + )</span>
)}
</Label>
<Card className="max-h-60 overflow-y-auto p-3">
<div className="space-y-2">
{availableColumns.map((col) => {
const isSelected = Array.isArray(currentConfig.yAxis)
? currentConfig.yAxis.includes(col)
: currentConfig.yAxis === col;
{/* 숫자 타입 우선 표시 */}
{numericColumns.length > 0 && (
<>
<div className="mb-2 text-xs font-medium text-green-700"> ()</div>
{numericColumns.map((col) => {
const isSelected = Array.isArray(currentConfig.yAxis)
? currentConfig.yAxis.includes(col)
: currentConfig.yAxis === col;
return (
<div key={col} className="flex items-center gap-2 rounded p-2 hover:bg-gray-50">
<Checkbox
checked={isSelected}
onCheckedChange={(checked) => {
const currentYAxis = Array.isArray(currentConfig.yAxis)
? currentConfig.yAxis
: currentConfig.yAxis
? [currentConfig.yAxis]
: [];
return (
<div
key={col}
className="flex items-center gap-2 rounded border-l-2 border-green-500 bg-green-50 p-2"
>
<Checkbox
checked={isSelected}
onCheckedChange={(checked) => {
const currentYAxis = Array.isArray(currentConfig.yAxis)
? currentConfig.yAxis
: currentConfig.yAxis
? [currentConfig.yAxis]
: [];
let newYAxis: string | string[];
if (checked) {
newYAxis = [...currentYAxis, col];
} else {
newYAxis = currentYAxis.filter((c) => c !== col);
}
let newYAxis: string | string[];
if (checked) {
newYAxis = [...currentYAxis, col];
} else {
newYAxis = currentYAxis.filter((c) => c !== col);
}
// 단일 값이면 문자열로, 다중 값이면 배열로
if (newYAxis.length === 1) {
newYAxis = newYAxis[0];
}
if (newYAxis.length === 1) {
newYAxis = newYAxis[0];
}
updateConfig({ yAxis: newYAxis });
}}
/>
<Label className="flex-1 cursor-pointer text-sm font-normal">
{col}
{sampleData[col] && <span className="ml-2 text-xs text-gray-500">(: {sampleData[col]})</span>}
</Label>
</div>
);
})}
updateConfig({ yAxis: newYAxis });
}}
/>
<Label className="flex-1 cursor-pointer text-sm font-normal">
<span className="font-medium">{col}</span>
<Badge variant="outline" className="ml-2 bg-green-100 text-xs">
number
</Badge>
{sampleData[col] !== undefined && (
<span className="ml-2 text-xs text-gray-600">(: {sampleData[col]})</span>
)}
</Label>
</div>
);
})}
</>
)}
{/* 기타 간단한 타입 */}
{simpleColumns.filter((col) => !numericColumns.includes(col)).length > 0 && (
<>
{numericColumns.length > 0 && <div className="my-2 border-t"></div>}
<div className="mb-2 text-xs font-medium text-gray-600">📝 </div>
{simpleColumns
.filter((col) => !numericColumns.includes(col))
.map((col) => {
const isSelected = Array.isArray(currentConfig.yAxis)
? currentConfig.yAxis.includes(col)
: currentConfig.yAxis === col;
const type = columnTypes[col];
return (
<div key={col} className="flex items-center gap-2 rounded p-2 hover:bg-gray-50">
<Checkbox
checked={isSelected}
onCheckedChange={(checked) => {
const currentYAxis = Array.isArray(currentConfig.yAxis)
? currentConfig.yAxis
: currentConfig.yAxis
? [currentConfig.yAxis]
: [];
let newYAxis: string | string[];
if (checked) {
newYAxis = [...currentYAxis, col];
} else {
newYAxis = currentYAxis.filter((c) => c !== col);
}
if (newYAxis.length === 1) {
newYAxis = newYAxis[0];
}
updateConfig({ yAxis: newYAxis });
}}
/>
<Label className="flex-1 cursor-pointer text-sm font-normal">
{col}
<Badge variant="outline" className="ml-2 text-xs">
{type}
</Badge>
{sampleData[col] !== undefined && (
<span className="ml-2 text-xs text-gray-500">
(: {String(sampleData[col]).substring(0, 30)})
</span>
)}
</Label>
</div>
);
})}
</>
)}
</div>
</Card>
{simpleColumns.length === 0 && (
<p className="text-xs text-red-500"> . JSON Path를 .</p>
)}
<p className="text-xs text-gray-500">
: 여러 (: 갤럭시 vs )
</p>
@@ -279,10 +456,22 @@ export function ChartConfigPanel({ config, queryResult, onConfigChange, chartTyp
</Card>
{/* 필수 필드 확인 */}
{(!currentConfig.xAxis || !currentConfig.yAxis) && (
{!currentConfig.xAxis && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>X축 Y축을 .</AlertDescription>
<AlertDescription>X축 .</AlertDescription>
</Alert>
)}
{!isPieChart && !isApiSource && !currentConfig.yAxis && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>Y축을 .</AlertDescription>
</Alert>
)}
{(isPieChart || isApiSource) && !currentConfig.yAxis && !currentConfig.aggregation && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>Y축 (COUNT ) .</AlertDescription>
</Alert>
)}
</>