"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 { Plus, Trash2, Loader2, CheckCircle, XCircle } from "lucide-react"; import { ExternalDbConnectionAPI, ExternalApiConnection } from "@/lib/api/externalDbConnection"; 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(""); const [availableColumns, setAvailableColumns] = useState([]); // API 테스트 후 발견된 컬럼 목록 const [columnTypes, setColumnTypes] = useState>({}); // 컬럼 타입 정보 const [sampleData, setSampleData] = useState([]); // 샘플 데이터 (최대 3개) const [columnSearchTerm, setColumnSearchTerm] = useState(""); // 컬럼 검색어 console.log("🔧 MultiApiConfig - dataSource:", dataSource); // 외부 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); // 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) { 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, }); console.log("API Key 헤더 추가:", authConfig.keyName); } 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, }); console.log("API Key 쿼리 파라미터 추가:", actualKeyName, "(원본:", authConfig.keyName, ")"); } break; case "bearer": if (authConfig.token) { headers.push({ id: `auth_bearer_${Date.now()}`, key: "Authorization", value: `Bearer ${authConfig.token}`, }); console.log("Bearer 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}`, }); console.log("Basic Auth 헤더 추가"); } break; case "oauth2": if (authConfig.accessToken) { headers.push({ id: `auth_oauth_${Date.now()}`, key: "Authorization", value: `Bearer ${authConfig.accessToken}`, }); console.log("OAuth2 Token 헤더 추가"); } break; } } // 헤더와 쿼리 파라미터 적용 if (headers.length > 0) { updates.headers = headers; } if (queryParams.length > 0) { updates.queryParams = queryParams; } console.log("최종 업데이트:", updates); 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 response = await fetch("/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, }), }); const result = await response.json(); if (result.success) { // 텍스트 데이터 파싱 함수 (MapTestWidgetV2와 동일) const parseTextData = (text: string): any[] => { try { console.log("🔍 텍스트 파싱 시작 (처음 500자):", text.substring(0, 500)); const lines = text.split('\n').filter(line => { const trimmed = line.trim(); return trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('=') && !trimmed.startsWith('---'); }); console.log(`📝 유효한 라인: ${lines.length}개`); 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); } } console.log("📊 파싱 결과:", result.length, "개"); 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') { console.log("📄 텍스트 형식 데이터 감지, CSV 파싱 시도"); const parsedData = parseTextData(data.text); if (parsedData.length > 0) { console.log(`✅ CSV 파싱 성공: ${parsedData.length}개 행`); 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]; // 컬럼 목록 및 타입 추출 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)); console.log("📊 발견된 컬럼:", columns); console.log("📊 컬럼 타입:", types); } // 위도/경도 또는 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 (직접 입력 또는 수정) */}
{ console.log("📝 API URL 변경:", e.target.value); onChange({ endpoint: e.target.value }); }} placeholder="https://api.example.com/data" className="h-8 text-xs" />

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

{/* JSON Path */}
onChange({ jsonPath: e.target.value })} placeholder="예: data.results" className="h-8 text-xs" />

응답 JSON에서 데이터를 추출할 경로

{/* 쿼리 파라미터 */}
{(dataSource.queryParams || []).map((param) => (
handleUpdateQueryParam(param.id, "key", e.target.value)} placeholder="키" className="h-8 text-xs" /> handleUpdateQueryParam(param.id, "value", e.target.value)} placeholder="값" className="h-8 text-xs" />
))}
{/* 헤더 */}
{(dataSource.headers || []).map((header) => (
handleUpdateHeader(header.id, "key", e.target.value)} placeholder="키" className="h-8 text-xs" /> handleUpdateHeader(header.id, "value", e.target.value)} placeholder="값" className="h-8 text-xs" />
))}
{/* 자동 새로고침 설정 */}

설정한 간격마다 자동으로 데이터를 다시 불러옵니다

{/* 테스트 버튼 */}
{testResult && (
{testResult.success ? ( ) : ( )} {testResult.message}
)}
{/* 컬럼 선택 (메트릭 위젯용) - 개선된 UI */} {availableColumns.length > 0 && (

{dataSource.selectedColumns && dataSource.selectedColumns.length > 0 ? `${dataSource.selectedColumns.length}개 컬럼 선택됨` : "모든 컬럼 표시"}

{/* 검색 */} {availableColumns.length > 5 && ( setColumnSearchTerm(e.target.value)} className="h-8 text-xs" /> )} {/* 컬럼 카드 그리드 */}
{availableColumns .filter(col => !columnSearchTerm || col.toLowerCase().includes(columnSearchTerm.toLowerCase()) ) .map((col) => { const isSelected = !dataSource.selectedColumns || dataSource.selectedColumns.length === 0 || dataSource.selectedColumns.includes(col); const type = columnTypes[col] || "unknown"; const typeIcon = { number: "🔢", string: "📝", date: "📅", boolean: "✓", object: "📦", unknown: "❓" }[type]; const typeColor = { number: "text-blue-600 bg-blue-50", string: "text-gray-600 bg-gray-50", date: "text-purple-600 bg-purple-50", boolean: "text-green-600 bg-green-50", object: "text-orange-600 bg-orange-50", unknown: "text-gray-400 bg-gray-50" }[type]; return (
{ const currentSelected = dataSource.selectedColumns && dataSource.selectedColumns.length > 0 ? dataSource.selectedColumns : availableColumns; const newSelected = isSelected ? currentSelected.filter(c => c !== col) : [...currentSelected, col]; onChange({ selectedColumns: newSelected }); }} className={` relative flex items-start gap-3 rounded-lg border p-3 cursor-pointer transition-all ${isSelected ? "border-primary bg-primary/5 shadow-sm" : "border-border bg-card hover:border-primary/50 hover:bg-muted/50" } `} > {/* 체크박스 */}
{isSelected && ( )}
{/* 컬럼 정보 */}
{col} {typeIcon} {type}
{/* 샘플 데이터 */} {sampleData.length > 0 && (
예시:{" "} {sampleData.slice(0, 2).map((row, i) => ( {String(row[col]).substring(0, 20)} {String(row[col]).length > 20 && "..."} {i < Math.min(sampleData.length - 1, 1) && ", "} ))}
)}
); })}
{/* 검색 결과 없음 */} {columnSearchTerm && availableColumns.filter(col => col.toLowerCase().includes(columnSearchTerm.toLowerCase()) ).length === 0 && (
"{columnSearchTerm}"에 대한 컬럼을 찾을 수 없습니다
)}
)}
); }