"use client"; import React, { useState, useEffect } from "react"; import { ChartDataSource, QueryResult, KeyValuePair } from "../types"; 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"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { ExternalDbConnectionAPI, ExternalApiConnection } from "@/lib/api/externalDbConnection"; import { getApiUrl } from "@/lib/utils/apiUrl"; // 개별 API 소스 인터페이스 interface ApiSource { id: string; endpoint: string; headers: KeyValuePair[]; queryParams: KeyValuePair[]; jsonPath?: string; } 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 [apiConnections, setApiConnections] = useState([]); const [selectedConnectionId, setSelectedConnectionId] = useState(""); // 외부 API 커넥션 목록 로드 useEffect(() => { const loadApiConnections = async () => { const connections = await ExternalDbConnectionAPI.getApiConnections({ is_active: "Y" }); setApiConnections(connections); }; loadApiConnections(); }, []); // 외부 커넥션 선택 핸들러 const handleConnectionSelect = async (connectionId: string) => { setSelectedConnectionId(connectionId); if (!connectionId || connectionId === "manual") return; const connection = await ExternalDbConnectionAPI.getApiConnectionById(Number(connectionId)); if (!connection) { console.error("커넥션을 찾을 수 없습니다:", connectionId); return; } console.log("불러온 커넥션:", connection); // 커넥션 설정을 API 설정에 자동 적용 // base_url과 endpoint_path를 조합하여 전체 URL 생성 const fullEndpoint = connection.endpoint_path ? `${connection.base_url}${connection.endpoint_path}` : connection.base_url; console.log("전체 엔드포인트:", fullEndpoint); const updates: Partial = { endpoint: fullEndpoint, }; const headers: KeyValuePair[] = []; const queryParams: KeyValuePair[] = []; // 기본 헤더가 있으면 적용 if (connection.default_headers && Object.keys(connection.default_headers).length > 0) { Object.entries(connection.default_headers).forEach(([key, value]) => { headers.push({ id: `header_${Date.now()}_${Math.random()}`, key, value, }); }); console.log("기본 헤더 적용:", headers); } // 인증 설정이 있으면 헤더 또는 쿼리 파라미터에 추가 if (connection.auth_type && connection.auth_type !== "none" && connection.auth_config) { console.log("인증 설정:", connection.auth_type, connection.auth_config); if (connection.auth_type === "bearer" && connection.auth_config.token) { headers.push({ id: `header_${Date.now()}_auth`, key: "Authorization", value: `Bearer ${connection.auth_config.token}`, }); console.log("Bearer 토큰 추가"); } else if (connection.auth_type === "api-key") { console.log("API Key 설정:", connection.auth_config); if (connection.auth_config.keyName && connection.auth_config.keyValue) { if (connection.auth_config.keyLocation === "header") { headers.push({ id: `header_${Date.now()}_apikey`, key: connection.auth_config.keyName, value: connection.auth_config.keyValue, }); console.log(`API Key 헤더 추가: ${connection.auth_config.keyName}=${connection.auth_config.keyValue}`); } else if (connection.auth_config.keyLocation === "query") { queryParams.push({ id: `param_${Date.now()}_apikey`, key: connection.auth_config.keyName, value: connection.auth_config.keyValue, }); console.log( `API Key 쿼리 파라미터 추가: ${connection.auth_config.keyName}=${connection.auth_config.keyValue}`, ); } } } else if ( connection.auth_type === "basic" && connection.auth_config.username && connection.auth_config.password ) { const basicAuth = btoa(`${connection.auth_config.username}:${connection.auth_config.password}`); headers.push({ id: `header_${Date.now()}_basic`, key: "Authorization", value: `Basic ${basicAuth}`, }); console.log("Basic Auth 추가"); } } updates.type = "api"; // ⭐ 중요: type을 api로 명시 updates.method = "GET"; // 기본 메서드 updates.headers = headers; updates.queryParams = queryParams; console.log("최종 업데이트:", updates); onChange(updates); }; // 헤더를 배열로 정규화 (객체 형식 호환) 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 = normalizeHeaders(); onChange({ headers: [...headers, { id: `header_${Date.now()}`, key: "", value: "" }], }); }; // 헤더 제거 const removeHeader = (id: string) => { const headers = normalizeHeaders(); onChange({ headers: headers.filter((h) => h.id !== id) }); }; // 헤더 업데이트 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 = normalizeQueryParams(); onChange({ queryParams: [...queryParams, { id: `param_${Date.now()}`, key: "", value: "" }], }); }; // 쿼리 파라미터 제거 const removeQueryParam = (id: string) => { const queryParams = normalizeQueryParams(); onChange({ queryParams: queryParams.filter((p) => p.id !== id) }); }; // 쿼리 파라미터 업데이트 const updateQueryParam = (id: string, updates: Partial) => { const queryParams = normalizeQueryParams(); onChange({ queryParams: queryParams.map((p) => (p.id === id ? { ...p, ...updates } : p)), }); }; // API 테스트 const testApi = async () => { if (!dataSource.endpoint) { setTestError("API URL을 입력하세요"); return; } // 타일맵 URL 감지 (이미지 파일이므로 테스트 불가) const isTilemapUrl = dataSource.endpoint.includes('{z}') && dataSource.endpoint.includes('{y}') && dataSource.endpoint.includes('{x}'); if (isTilemapUrl) { setTestError("타일맵 URL은 테스트할 수 없습니다. 지도 위젯에서 직접 확인하세요."); return; } setTesting(true); setTestError(null); setTestResult(null); try { // 쿼리 파라미터 구성 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(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ url: dataSource.endpoint, method: "GET", headers: headers, queryParams: 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 호출 실패"); } let apiData = apiResponse.data; // 텍스트 응답인 경우 파싱 if (apiData && typeof apiData === "object" && "text" in apiData && typeof apiData.text === "string") { const textData = apiData.text; // CSV 형식 파싱 (기상청 API) if (textData.includes("#START7777") || textData.includes(",")) { const lines = textData.split("\n").filter((line) => line.trim() && !line.startsWith("#")); const parsedRows = lines.map((line) => { const values = line.split(",").map((v) => v.trim()); return { reg_up: values[0] || "", reg_up_ko: values[1] || "", reg_id: values[2] || "", reg_ko: values[3] || "", tm_fc: values[4] || "", tm_ef: values[5] || "", wrn: values[6] || "", lvl: values[7] || "", cmd: values[8] || "", ed_tm: values[9] || "", }; }); apiData = parsedRows; } else { // 일반 텍스트는 그대로 반환 apiData = [{ text: textData }]; } } // 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 (
{/* 외부 커넥션 선택 - 항상 표시 */}

저장한 REST API 설정을 불러올 수 있습니다

{/* API URL */}
onChange({ endpoint: e.target.value })} className="h-8 text-xs" />

전체 URL 또는 base_url 이후 경로를 입력하세요 (외부 커넥션 선택 시 base_url 자동 입력)

{/* 쿼리 파라미터 */}
{(() => { const params = normalizeQueryParams(); return params.length > 0 ? (
{params.map((param) => (
updateQueryParam(param.id, { key: e.target.value })} className="h-7 flex-1 text-xs" /> updateQueryParam(param.id, { value: e.target.value })} className="h-7 flex-1 text-xs" />
))}
) : (

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

); })()}

예: category=electronics, limit=10

{/* 헤더 */}
{/* 빠른 헤더 템플릿 */}
{(() => { 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 */}
onChange({ jsonPath: e.target.value })} />

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

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