"use client"; import React, { useState } from "react"; import { ChartDataSource, QueryResult, ApiResponse } from "../types"; import { Card } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Plus, X, Play, AlertCircle } from "lucide-react"; interface ApiConfigProps { dataSource: ChartDataSource; onChange: (updates: Partial) => void; onTestResult?: (result: QueryResult) => void; } /** * REST API 설정 컴포넌트 * - API 엔드포인트 설정 * - 헤더 및 쿼리 파라미터 추가 * - JSON Path 설정 */ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps) { const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState(null); const [testError, setTestError] = useState(null); // 헤더 추가 const addHeader = () => { const headers = dataSource.headers || {}; const newKey = `header_${Object.keys(headers).length + 1}`; onChange({ headers: { ...headers, [newKey]: "" } }); }; // 헤더 제거 const removeHeader = (key: string) => { const headers = { ...dataSource.headers }; delete headers[key]; onChange({ headers }); }; // 헤더 업데이트 const updateHeader = (oldKey: string, newKey: string, value: string) => { const headers = { ...dataSource.headers }; delete headers[oldKey]; headers[newKey] = value; onChange({ headers }); }; // 쿼리 파라미터 추가 const addQueryParam = () => { const queryParams = dataSource.queryParams || {}; const newKey = `param_${Object.keys(queryParams).length + 1}`; onChange({ queryParams: { ...queryParams, [newKey]: "" } }); }; // 쿼리 파라미터 제거 const removeQueryParam = (key: string) => { const queryParams = { ...dataSource.queryParams }; delete queryParams[key]; onChange({ queryParams }); }; // 쿼리 파라미터 업데이트 const updateQueryParam = (oldKey: string, newKey: string, value: string) => { const queryParams = { ...dataSource.queryParams }; delete queryParams[oldKey]; queryParams[newKey] = value; onChange({ queryParams }); }; // API 테스트 const testApi = async () => { if (!dataSource.endpoint) { setTestError("API URL을 입력하세요"); return; } setTesting(true); setTestError(null); setTestResult(null); try { // 쿼리 파라미터 구성 const params = new URLSearchParams(); if (dataSource.queryParams) { Object.entries(dataSource.queryParams).forEach(([key, value]) => { if (key && value) { params.append(key, value); } }); } // 백엔드 프록시를 통한 외부 API 호출 (CORS 우회) const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ url: dataSource.endpoint, method: "GET", headers: dataSource.headers || {}, queryParams: Object.fromEntries(params), }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const apiResponse = await response.json(); if (!apiResponse.success) { throw new Error(apiResponse.message || "외부 API 호출 실패"); } const apiData = apiResponse.data; // JSON Path 처리 let data = apiData; if (dataSource.jsonPath) { const paths = dataSource.jsonPath.split("."); for (const path of paths) { if (data && typeof data === "object" && path in data) { data = data[path]; } else { throw new Error(`JSON Path "${dataSource.jsonPath}"에서 데이터를 찾을 수 없습니다`); } } } // 배열이 아니면 배열로 변환 const rows = Array.isArray(data) ? data : [data]; if (rows.length === 0) { throw new Error("API 응답에 데이터가 없습니다"); } // 컬럼 추출 및 타입 분석 const firstRow = rows[0]; const columns = Object.keys(firstRow); // 각 컬럼의 타입 분석 const columnTypes: Record = {}; columns.forEach((col) => { const value = firstRow[col]; if (value === null || value === undefined) { columnTypes[col] = "null"; } else if (Array.isArray(value)) { columnTypes[col] = "array"; } else if (typeof value === "object") { columnTypes[col] = "object"; } else if (typeof value === "number") { columnTypes[col] = "number"; } else if (typeof value === "boolean") { columnTypes[col] = "boolean"; } else { columnTypes[col] = "string"; } }); const queryResult: QueryResult = { columns, rows, totalRows: rows.length, executionTime: 0, columnTypes, // 타입 정보 추가 }; setTestResult(queryResult); onTestResult?.(queryResult); } catch (err) { const errorMessage = err instanceof Error ? err.message : "알 수 없는 오류가 발생했습니다"; setTestError(errorMessage); } finally { setTesting(false); } }; return (

2단계: REST API 설정

외부 API에서 데이터를 가져올 설정을 입력하세요

{/* API URL */}
onChange({ endpoint: e.target.value })} className="mt-2" />

GET 요청을 보낼 API 엔드포인트

{/* HTTP 메서드 (고정) */}
GET (고정)

데이터 조회는 GET 메서드만 지원합니다

{/* 쿼리 파라미터 */}
{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" />
))}
) : (

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

)}

예: category=electronics, limit=10

{/* 헤더 */}
{/* 빠른 헤더 템플릿 */}
{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"} />
))}
) : (

추가된 헤더가 없습니다

)}
{/* JSON Path */} onChange({ jsonPath: e.target.value })} />

JSON 응답에서 데이터 배열의 경로 (예: data.results, items, response.data)
비워두면 전체 응답을 사용합니다

{/* 테스트 버튼 */}
{/* 테스트 오류 */} {testError && (
API 호출 실패
{testError}
)} {/* 테스트 결과 */} {testResult && (
✅ API 호출 성공
총 {testResult.rows.length}개의 데이터를 불러왔습니다
컬럼: {testResult.columns.join(", ")}
)}
); }