"use client"; import React, { useState, useEffect } from "react"; import { ChartDataSource, KeyValuePair } from "@/components/admin/dashboard/types"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { Plus, Trash2, Loader2, CheckCircle, XCircle } from "lucide-react"; import { Switch } from "@/components/ui/switch"; import { ExternalDbConnectionAPI, ExternalApiConnection } from "@/lib/api/externalDbConnection"; import { getApiUrl } from "@/lib/utils/apiUrl"; interface MultiApiConfigProps { dataSource: ChartDataSource; onChange: (updates: Partial) => void; onTestResult?: (data: any) => void; // 테스트 결과 데이터 전달 } export default function MultiApiConfig({ dataSource, onChange, onTestResult }: MultiApiConfigProps) { const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); const [apiConnections, setApiConnections] = useState([]); const [selectedConnectionId, setSelectedConnectionId] = useState(dataSource.externalConnectionId || ""); const [availableColumns, setAvailableColumns] = useState([]); // API 테스트 후 발견된 컬럼 목록 const [columnTypes, setColumnTypes] = useState>({}); // 컬럼 타입 정보 const [sampleData, setSampleData] = useState([]); // 샘플 데이터 (최대 3개) const [columnSearchTerm, setColumnSearchTerm] = useState(""); // 컬럼 검색어 // 외부 API 커넥션 목록 로드 useEffect(() => { const loadApiConnections = async () => { const connections = await ExternalDbConnectionAPI.getApiConnections({ is_active: "Y" }); setApiConnections(connections); }; loadApiConnections(); }, []); // dataSource.externalConnectionId가 변경되면 selectedConnectionId 업데이트 useEffect(() => { if (dataSource.externalConnectionId) { setSelectedConnectionId(dataSource.externalConnectionId); } }, [dataSource.externalConnectionId]); // 외부 커넥션 선택 핸들러 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; } // base_url과 endpoint_path를 조합하여 전체 URL 생성 const fullEndpoint = connection.endpoint_path ? `${connection.base_url}${connection.endpoint_path}` : connection.base_url; const updates: Partial = { endpoint: fullEndpoint, externalConnectionId: connectionId, // 외부 연결 ID 저장 }; const headers: KeyValuePair[] = []; const queryParams: KeyValuePair[] = []; // 기본 메서드/바디가 있으면 적용 if (connection.default_method) { updates.method = connection.default_method as ChartDataSource["method"]; } if (connection.default_body) { updates.body = connection.default_body; } // 기본 헤더가 있으면 적용 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, }); }); } // 인증 설정이 있으면 헤더 또는 쿼리 파라미터에 추가 if (connection.auth_type && connection.auth_type !== "none" && connection.auth_config) { const authConfig = connection.auth_config; switch (connection.auth_type) { case "api-key": if (authConfig.keyLocation === "header" && authConfig.keyName && authConfig.keyValue) { headers.push({ id: `auth_header_${Date.now()}`, key: authConfig.keyName, value: authConfig.keyValue, }); } else if (authConfig.keyLocation === "query" && authConfig.keyName && authConfig.keyValue) { // UTIC API는 'key'를 사용하므로, 'apiKey'를 'key'로 변환 const actualKeyName = authConfig.keyName === "apiKey" ? "key" : authConfig.keyName; queryParams.push({ id: `auth_query_${Date.now()}`, key: actualKeyName, value: authConfig.keyValue, }); } break; case "bearer": if (authConfig.token) { headers.push({ id: `auth_bearer_${Date.now()}`, key: "Authorization", value: `Bearer ${authConfig.token}`, }); } break; case "basic": if (authConfig.username && authConfig.password) { const credentials = btoa(`${authConfig.username}:${authConfig.password}`); headers.push({ id: `auth_basic_${Date.now()}`, key: "Authorization", value: `Basic ${credentials}`, }); } break; case "oauth2": if (authConfig.accessToken) { headers.push({ id: `auth_oauth_${Date.now()}`, key: "Authorization", value: `Bearer ${authConfig.accessToken}`, }); } break; } } // 헤더와 쿼리 파라미터 적용 if (headers.length > 0) { updates.headers = headers; } if (queryParams.length > 0) { updates.queryParams = queryParams; } onChange(updates); }; // 헤더 추가 const handleAddHeader = () => { const headers = dataSource.headers || []; onChange({ headers: [...headers, { id: Date.now().toString(), key: "", value: "" }], }); }; // 헤더 삭제 const handleDeleteHeader = (id: string) => { const headers = (dataSource.headers || []).filter((h) => h.id !== id); onChange({ headers }); }; // 헤더 업데이트 const handleUpdateHeader = (id: string, field: "key" | "value", value: string) => { const headers = (dataSource.headers || []).map((h) => h.id === id ? { ...h, [field]: value } : h ); onChange({ headers }); }; // 쿼리 파라미터 추가 const handleAddQueryParam = () => { const queryParams = dataSource.queryParams || []; onChange({ queryParams: [...queryParams, { id: Date.now().toString(), key: "", value: "" }], }); }; // 쿼리 파라미터 삭제 const handleDeleteQueryParam = (id: string) => { const queryParams = (dataSource.queryParams || []).filter((q) => q.id !== id); onChange({ queryParams }); }; // 쿼리 파라미터 업데이트 const handleUpdateQueryParam = (id: string, field: "key" | "value", value: string) => { const queryParams = (dataSource.queryParams || []).map((q) => q.id === id ? { ...q, [field]: value } : q ); onChange({ queryParams }); }; // API 테스트 const handleTestApi = async () => { if (!dataSource.endpoint) { setTestResult({ success: false, message: "API URL을 입력해주세요" }); return; } setTesting(true); setTestResult(null); try { const queryParams: Record = {}; (dataSource.queryParams || []).forEach((param) => { if (param.key && param.value) { queryParams[param.key] = param.value; } }); const headers: Record = {}; (dataSource.headers || []).forEach((header) => { if (header.key && header.value) { headers[header.key] = header.value; } }); const bodyPayload = dataSource.body && dataSource.body.trim().length > 0 ? dataSource.body : undefined; const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ url: dataSource.endpoint, method: dataSource.method || "GET", headers, queryParams, body: bodyPayload, externalConnectionId: dataSource.externalConnectionId, // 외부 연결 ID 전달 }), }); const result = await response.json(); console.log("🌐 [API 테스트 결과]", result.data); if (result.success) { // 텍스트 데이터 파싱 함수 (MapTestWidgetV2와 동일) const parseTextData = (text: string): any[] => { try { const lines = text.split('\n').filter(line => { const trimmed = line.trim(); return trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('=') && !trimmed.startsWith('---'); }); if (lines.length === 0) return []; const result: any[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const values = line.split(',').map(v => v.trim().replace(/,=$/g, '')); // 기상특보 형식: 지역코드, 지역명, 하위코드, 하위지역명, 발표시각, 특보종류, 등급, 발표상태, 설명 if (values.length >= 4) { const obj: any = { code: values[0] || '', // 지역 코드 (예: L1070000) region: values[1] || '', // 지역명 (예: 경상북도) subCode: values[2] || '', // 하위 코드 (예: L1071600) subRegion: values[3] || '', // 하위 지역명 (예: 영주시) tmFc: values[4] || '', // 발표시각 type: values[5] || '', // 특보종류 (강풍, 호우 등) level: values[6] || '', // 등급 (주의, 경보) status: values[7] || '', // 발표상태 description: values.slice(8).join(', ').trim() || '', name: values[3] || values[1] || values[0], // 하위 지역명 우선 }; result.push(obj); } } return result; } catch (error) { console.error("❌ 텍스트 파싱 오류:", error); return []; } }; // JSON Path로 데이터 추출 let data = result.data; // 텍스트 데이터 체크 (기상청 API 등) if (data && typeof data === 'object' && data.text && typeof data.text === 'string') { const parsedData = parseTextData(data.text); if (parsedData.length > 0) { data = parsedData; } } else if (dataSource.jsonPath) { const pathParts = dataSource.jsonPath.split("."); for (const part of pathParts) { data = data?.[part]; } } const rows = Array.isArray(data) ? data : [data]; console.log("📊 [최종 파싱된 데이터]", rows); // 컬럼 목록 및 타입 추출 if (rows.length > 0) { const columns = Object.keys(rows[0]); setAvailableColumns(columns); // 컬럼 타입 분석 (첫 번째 행 기준) const types: Record = {}; columns.forEach(col => { const value = rows[0][col]; if (value === null || value === undefined) { types[col] = "unknown"; } else if (typeof value === "number") { types[col] = "number"; } else if (typeof value === "boolean") { types[col] = "boolean"; } else if (typeof value === "string") { // 날짜 형식 체크 if (/^\d{4}-\d{2}-\d{2}/.test(value)) { types[col] = "date"; } else { types[col] = "string"; } } else { types[col] = "object"; } }); setColumnTypes(types); // 샘플 데이터 저장 (최대 3개) setSampleData(rows.slice(0, 3)); } // 위도/경도 또는 coordinates 필드 또는 지역 코드 체크 const hasLocationData = rows.some((row) => { const hasLatLng = (row.lat || row.latitude) && (row.lng || row.longitude); const hasCoordinates = row.coordinates && Array.isArray(row.coordinates); const hasRegionCode = row.code || row.areaCode || row.regionCode; return hasLatLng || hasCoordinates || hasRegionCode; }); if (hasLocationData) { const markerCount = rows.filter(r => ((r.lat || r.latitude) && (r.lng || r.longitude)) || r.code || r.areaCode || r.regionCode ).length; const polygonCount = rows.filter(r => r.coordinates && Array.isArray(r.coordinates)).length; setTestResult({ success: true, message: `API 연결 성공 - 마커 ${markerCount}개, 영역 ${polygonCount}개 발견` }); // 부모에게 테스트 결과 전달 (지도 미리보기용) if (onTestResult) { onTestResult(rows); } } else { setTestResult({ success: true, message: `API 연결 성공 - ${rows.length}개 데이터 (위치 정보 없음)` }); } } else { setTestResult({ success: false, message: result.message || "API 호출 실패" }); } } catch (error: any) { setTestResult({ success: false, message: error.message || "네트워크 오류" }); } finally { setTesting(false); } }; return (
REST API 설정
{/* 외부 연결 선택 */}

외부 연결을 선택하면 API URL이 자동으로 입력됩니다

{/* API URL (직접 입력 또는 수정) */}
{ onChange({ endpoint: e.target.value }); }} placeholder="https://api.example.com/data" className="h-8 text-xs" />

외부 연결을 선택하거나 직접 입력할 수 있습니다

{/* HTTP 메서드 */}
{/* Request Body (POST/PUT/PATCH 일 때만) */} {(dataSource.method === "POST" || dataSource.method === "PUT" || dataSource.method === "PATCH") && (