"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 = (connection.default_method as "GET" | "POST" | "PUT" | "PATCH" | "DELETE") || "GET"; // 커넥션에 설정된 메서드 사용 updates.headers = headers; updates.queryParams = queryParams; // Request Body가 있으면 적용 if (connection.default_body) { updates.body = connection.default_body; } // 외부 커넥션 ID 저장 (백엔드에서 인증 정보 조회용) updates.externalConnectionId = connection.id; 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; } }); // 요청 메서드 결정 const requestMethod = dataSource.method || "GET"; // Request Body 파싱 (POST, PUT, PATCH인 경우) let requestBody: any = undefined; if (["POST", "PUT", "PATCH"].includes(requestMethod) && dataSource.body) { try { requestBody = JSON.parse(dataSource.body); } catch { throw new Error("Request Body가 올바른 JSON 형식이 아닙니다"); } } // 백엔드 프록시를 통한 외부 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: requestMethod, headers: headers, queryParams: params, body: requestBody, externalConnectionId: dataSource.externalConnectionId, // DB 토큰 등 인증 정보 조회용 }), }); 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 === null || data === undefined) { throw new Error(`JSON Path "${dataSource.jsonPath}"에서 데이터를 찾을 수 없습니다 (null/undefined)`); } if (Array.isArray(data)) { // 배열인 경우 숫자 인덱스로 접근 시도 const index = parseInt(path); if (!isNaN(index) && index >= 0 && index < data.length) { data = data[index]; } else { throw new Error(`JSON Path "${dataSource.jsonPath}"에서 배열 인덱스 "${path}"를 찾을 수 없습니다`); } } else if (typeof data === "object" && path in data) { data = (data as Record)[path]; } else { throw new Error(`JSON Path "${dataSource.jsonPath}"에서 "${path}" 키를 찾을 수 없습니다`); } } } // 배열이 아니면 배열로 변환 const rows = Array.isArray(data) ? data : [data]; if (rows.length === 0) { throw new Error("API 응답에 데이터가 없습니다"); } // 컬럼 추출 및 타입 분석 const firstRow = rows[0]; // firstRow가 null이거나 객체가 아닌 경우 처리 if (firstRow === null || firstRow === undefined) { throw new Error("API 응답의 첫 번째 행이 비어있습니다"); } if (typeof firstRow !== "object" || Array.isArray(firstRow)) { throw new Error("API 응답 데이터가 올바른 객체 형식이 아닙니다"); } 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 설정을 불러올 수 있습니다

{/* HTTP 메서드 및 API URL */}
onChange({ endpoint: e.target.value })} className="h-8 flex-1 text-xs" />

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

{/* Request Body (POST, PUT, PATCH인 경우) */} {["POST", "PUT", "PATCH"].includes(dataSource.method || "") && (