diff --git a/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx b/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx index 2784def6..26e4b588 100644 --- a/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx @@ -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(null); const [testError, setTestError] = useState(null); + // 헤더를 배열로 정규화 (객체 형식 호환) + const normalizeHeaders = (): KeyValuePair[] => { + if (!dataSource.headers) return []; + if (Array.isArray(dataSource.headers)) return dataSource.headers; + // 객체 형식이면 배열로 변환 + return Object.entries(dataSource.headers as Record).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) => { + 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).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) => { + 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 = {}; + const normalizedQueryParams = normalizeQueryParams(); + normalizedQueryParams.forEach(({ key, value }) => { + if (key && value) { + params[key] = value; + } + }); + + // 헤더 구성 + const headers: Record = {}; + 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 - {dataSource.queryParams && Object.keys(dataSource.queryParams).length > 0 ? ( -
- {Object.entries(dataSource.queryParams).map(([key, value]) => ( -
- updateQueryParam(key, e.target.value, value)} - className="flex-1" - /> - updateQueryParam(key, key, e.target.value)} - className="flex-1" - /> - -
- ))} -
- ) : ( -

추가된 파라미터가 없습니다

- )} + {(() => { + const params = normalizeQueryParams(); + return params.length > 0 ? ( +
+ {params.map((param) => ( +
+ updateQueryParam(param.id, { key: e.target.value })} + className="flex-1" + /> + updateQueryParam(param.id, { value: e.target.value })} + className="flex-1" + /> + +
+ ))} +
+ ) : ( +

추가된 파라미터가 없습니다

+ ); + })()}

예: category=electronics, limit=10

@@ -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 - {dataSource.headers && Object.keys(dataSource.headers).length > 0 ? ( -
- {Object.entries(dataSource.headers).map(([key, value]) => ( -
- updateHeader(key, e.target.value, value)} - className="flex-1" - /> - updateHeader(key, key, e.target.value)} - className="flex-1" - type={key.toLowerCase().includes("auth") ? "password" : "text"} - /> - -
- ))} -
- ) : ( -

추가된 헤더가 없습니다

- )} + {(() => { + const headers = normalizeHeaders(); + return headers.length > 0 ? ( +
+ {headers.map((header) => ( +
+ updateHeader(header.id, { key: e.target.value })} + className="flex-1" + /> + updateHeader(header.id, { value: e.target.value })} + className="flex-1" + type={header.key.toLowerCase().includes("auth") ? "password" : "text"} + /> + +
+ ))} +
+ ) : ( +

추가된 헤더가 없습니다

+ ); + })()} {/* JSON Path */} diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index 6d08fa14..337da311 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -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; // 커스텀 헤더 - queryParams?: Record; // URL 쿼리 파라미터 + headers?: KeyValuePair[]; // 커스텀 헤더 (배열) + queryParams?: KeyValuePair[]; // URL 쿼리 파라미터 (배열) jsonPath?: string; // JSON 응답에서 데이터 추출 경로 (예: "data.results") // 공통