rest api 설정 오류 해결

This commit is contained in:
dohyeons 2025-10-15 14:01:01 +09:00
parent c8f66a9f18
commit 8799568cc6
2 changed files with 137 additions and 90 deletions

View File

@ -1,7 +1,7 @@
"use client";
import React, { useState } from "react";
import { ChartDataSource, QueryResult, ApiResponse } from "../types";
import { ChartDataSource, QueryResult, KeyValuePair } from "../types";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
@ -25,48 +25,72 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
const [testResult, setTestResult] = useState<QueryResult | null>(null);
const [testError, setTestError] = useState<string | null>(null);
// 헤더를 배열로 정규화 (객체 형식 호환)
const normalizeHeaders = (): KeyValuePair[] => {
if (!dataSource.headers) return [];
if (Array.isArray(dataSource.headers)) return dataSource.headers;
// 객체 형식이면 배열로 변환
return Object.entries(dataSource.headers as Record<string, string>).map(([key, value]) => ({
id: `header_${Date.now()}_${Math.random()}`,
key,
value,
}));
};
// 헤더 추가
const addHeader = () => {
const headers = dataSource.headers || {};
const newKey = `header_${Object.keys(headers).length + 1}`;
onChange({ headers: { ...headers, [newKey]: "" } });
const headers = normalizeHeaders();
onChange({
headers: [...headers, { id: `header_${Date.now()}`, key: "", value: "" }],
});
};
// 헤더 제거
const removeHeader = (key: string) => {
const headers = { ...dataSource.headers };
delete headers[key];
onChange({ headers });
const removeHeader = (id: string) => {
const headers = normalizeHeaders();
onChange({ headers: headers.filter((h) => h.id !== id) });
};
// 헤더 업데이트
const updateHeader = (oldKey: string, newKey: string, value: string) => {
const headers = { ...dataSource.headers };
delete headers[oldKey];
headers[newKey] = value;
onChange({ headers });
const updateHeader = (id: string, updates: Partial<KeyValuePair>) => {
const headers = normalizeHeaders();
onChange({
headers: headers.map((h) => (h.id === id ? { ...h, ...updates } : h)),
});
};
// 쿼리 파라미터를 배열로 정규화 (객체 형식 호환)
const normalizeQueryParams = (): KeyValuePair[] => {
if (!dataSource.queryParams) return [];
if (Array.isArray(dataSource.queryParams)) return dataSource.queryParams;
// 객체 형식이면 배열로 변환
return Object.entries(dataSource.queryParams as Record<string, string>).map(([key, value]) => ({
id: `param_${Date.now()}_${Math.random()}`,
key,
value,
}));
};
// 쿼리 파라미터 추가
const addQueryParam = () => {
const queryParams = dataSource.queryParams || {};
const newKey = `param_${Object.keys(queryParams).length + 1}`;
onChange({ queryParams: { ...queryParams, [newKey]: "" } });
const queryParams = normalizeQueryParams();
onChange({
queryParams: [...queryParams, { id: `param_${Date.now()}`, key: "", value: "" }],
});
};
// 쿼리 파라미터 제거
const removeQueryParam = (key: string) => {
const queryParams = { ...dataSource.queryParams };
delete queryParams[key];
onChange({ queryParams });
const removeQueryParam = (id: string) => {
const queryParams = normalizeQueryParams();
onChange({ queryParams: queryParams.filter((p) => p.id !== id) });
};
// 쿼리 파라미터 업데이트
const updateQueryParam = (oldKey: string, newKey: string, value: string) => {
const queryParams = { ...dataSource.queryParams };
delete queryParams[oldKey];
queryParams[newKey] = value;
onChange({ queryParams });
const updateQueryParam = (id: string, updates: Partial<KeyValuePair>) => {
const queryParams = normalizeQueryParams();
onChange({
queryParams: queryParams.map((p) => (p.id === id ? { ...p, ...updates } : p)),
});
};
// API 테스트
@ -82,14 +106,22 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
try {
// 쿼리 파라미터 구성
const params = new URLSearchParams();
if (dataSource.queryParams) {
Object.entries(dataSource.queryParams).forEach(([key, value]) => {
if (key && value) {
params.append(key, value);
}
});
}
const params: Record<string, string> = {};
const normalizedQueryParams = normalizeQueryParams();
normalizedQueryParams.forEach(({ key, value }) => {
if (key && value) {
params[key] = value;
}
});
// 헤더 구성
const headers: Record<string, string> = {};
const normalizedHeaders = normalizeHeaders();
normalizedHeaders.forEach(({ key, value }) => {
if (key && value) {
headers[key] = value;
}
});
// 백엔드 프록시를 통한 외부 API 호출 (CORS 우회)
const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", {
@ -100,8 +132,8 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
body: JSON.stringify({
url: dataSource.endpoint,
method: "GET",
headers: dataSource.headers || {},
queryParams: Object.fromEntries(params),
headers: headers,
queryParams: params,
}),
});
@ -217,31 +249,34 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
</Button>
</div>
{dataSource.queryParams && Object.keys(dataSource.queryParams).length > 0 ? (
<div className="space-y-2">
{Object.entries(dataSource.queryParams).map(([key, value]) => (
<div key={key} className="flex gap-2">
<Input
placeholder="key"
value={key}
onChange={(e) => updateQueryParam(key, e.target.value, value)}
className="flex-1"
/>
<Input
placeholder="value"
value={value}
onChange={(e) => updateQueryParam(key, key, e.target.value)}
className="flex-1"
/>
<Button variant="ghost" size="icon" onClick={() => removeQueryParam(key)}>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
) : (
<p className="py-2 text-center text-sm text-gray-500"> </p>
)}
{(() => {
const params = normalizeQueryParams();
return params.length > 0 ? (
<div className="space-y-2">
{params.map((param) => (
<div key={param.id} className="flex gap-2">
<Input
placeholder="key"
value={param.key}
onChange={(e) => updateQueryParam(param.id, { key: e.target.value })}
className="flex-1"
/>
<Input
placeholder="value"
value={param.value}
onChange={(e) => updateQueryParam(param.id, { value: e.target.value })}
className="flex-1"
/>
<Button variant="ghost" size="icon" onClick={() => removeQueryParam(param.id)}>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
) : (
<p className="py-2 text-center text-sm text-gray-500"> </p>
);
})()}
<p className="text-xs text-gray-500">: category=electronics, limit=10</p>
</Card>
@ -262,8 +297,9 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
variant="outline"
size="sm"
onClick={() => {
const headers = normalizeHeaders();
onChange({
headers: { ...dataSource.headers, Authorization: "Bearer YOUR_TOKEN" },
headers: [...headers, { id: `header_${Date.now()}`, key: "Authorization", value: "Bearer YOUR_TOKEN" }],
});
}}
>
@ -273,8 +309,9 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
variant="outline"
size="sm"
onClick={() => {
const headers = normalizeHeaders();
onChange({
headers: { ...dataSource.headers, "Content-Type": "application/json" },
headers: [...headers, { id: `header_${Date.now()}`, key: "Content-Type", value: "application/json" }],
});
}}
>
@ -282,32 +319,35 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
</Button>
</div>
{dataSource.headers && Object.keys(dataSource.headers).length > 0 ? (
<div className="space-y-2">
{Object.entries(dataSource.headers).map(([key, value]) => (
<div key={key} className="flex gap-2">
<Input
placeholder="Header Name"
value={key}
onChange={(e) => updateHeader(key, e.target.value, value)}
className="flex-1"
/>
<Input
placeholder="Header Value"
value={value}
onChange={(e) => updateHeader(key, key, e.target.value)}
className="flex-1"
type={key.toLowerCase().includes("auth") ? "password" : "text"}
/>
<Button variant="ghost" size="icon" onClick={() => removeHeader(key)}>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
) : (
<p className="py-2 text-center text-sm text-gray-500"> </p>
)}
{(() => {
const headers = normalizeHeaders();
return headers.length > 0 ? (
<div className="space-y-2">
{headers.map((header) => (
<div key={header.id} className="flex gap-2">
<Input
placeholder="Header Name"
value={header.key}
onChange={(e) => updateHeader(header.id, { key: e.target.value })}
className="flex-1"
/>
<Input
placeholder="Header Value"
value={header.value}
onChange={(e) => updateHeader(header.id, { value: e.target.value })}
className="flex-1"
type={header.key.toLowerCase().includes("auth") ? "password" : "text"}
/>
<Button variant="ghost" size="icon" onClick={() => removeHeader(header.id)}>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
) : (
<p className="py-2 text-center text-sm text-gray-500"> </p>
);
})()}
</Card>
{/* JSON Path */}

View File

@ -66,6 +66,13 @@ export interface ResizeHandle {
cursor: string;
}
// 키-값 쌍 인터페이스
export interface KeyValuePair {
id: string; // 고유 ID
key: string; // 키
value: string; // 값
}
export interface ChartDataSource {
type: "database" | "api"; // 데이터 소스 타입
@ -77,8 +84,8 @@ export interface ChartDataSource {
// API 관련
endpoint?: string; // API URL
method?: "GET"; // HTTP 메서드 (GET만 지원)
headers?: Record<string, string>; // 커스텀 헤더
queryParams?: Record<string, string>; // URL 쿼리 파라미터
headers?: KeyValuePair[]; // 커스텀 헤더 (배열)
queryParams?: KeyValuePair[]; // URL 쿼리 파라미터 (배열)
jsonPath?: string; // JSON 응답에서 데이터 추출 경로 (예: "data.results")
// 공통