ERP-node/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx

345 lines
11 KiB
TypeScript

"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<ChartDataSource>) => 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<QueryResult | null>(null);
const [testError, setTestError] = useState<string | null>(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);
}
});
}
// URL 구성
let url = dataSource.endpoint;
const queryString = params.toString();
if (queryString) {
url += (url.includes("?") ? "&" : "?") + queryString;
}
// 헤더 구성
const headers: Record<string, string> = {
"Content-Type": "application/json",
...dataSource.headers,
};
// 외부 API 직접 호출
const response = await fetch(url, {
method: "GET",
headers,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const apiData = await response.json();
// 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];
// 컬럼 추출
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
const result: QueryResult = {
columns,
rows,
totalRows: rows.length,
executionTime: 0,
};
setTestResult(result);
onTestResult?.(result);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "알 수 없는 오류가 발생했습니다";
setTestError(errorMessage);
} finally {
setTesting(false);
}
};
return (
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold text-gray-800">2단계: REST API </h3>
<p className="mt-1 text-sm text-gray-600"> API에서 </p>
</div>
{/* API URL */}
<Card className="space-y-4 p-4">
<div>
<Label className="text-sm font-medium text-gray-700">API URL *</Label>
<Input
type="url"
placeholder="https://api.example.com/data"
value={dataSource.endpoint || ""}
onChange={(e) => onChange({ endpoint: e.target.value })}
className="mt-2"
/>
<p className="mt-1 text-xs text-gray-500">GET API </p>
</div>
{/* HTTP 메서드 (고정) */}
<div>
<Label className="text-sm font-medium text-gray-700">HTTP </Label>
<div className="mt-2 rounded border border-gray-300 bg-gray-100 p-2 text-sm text-gray-700">GET ()</div>
<p className="mt-1 text-xs text-gray-500"> GET </p>
</div>
</Card>
{/* 쿼리 파라미터 */}
<Card className="space-y-4 p-4">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-gray-700">URL </Label>
<Button variant="outline" size="sm" onClick={addQueryParam}>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{dataSource.queryParams && Object.keys(dataSource.queryParams).length > 0 ? (
<div className="space-y-2">
{Object.entries(dataSource.queryParams).map(([key, value]) => (
<div key={key} className="flex gap-2">
<Input
placeholder="key"
value={key}
onChange={(e) => updateQueryParam(key, e.target.value, value)}
className="flex-1"
/>
<Input
placeholder="value"
value={value}
onChange={(e) => updateQueryParam(key, key, e.target.value)}
className="flex-1"
/>
<Button variant="ghost" size="icon" onClick={() => removeQueryParam(key)}>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
) : (
<p className="py-2 text-center text-sm text-gray-500"> </p>
)}
<p className="text-xs text-gray-500">: category=electronics, limit=10</p>
</Card>
{/* 헤더 */}
<Card className="space-y-4 p-4">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-gray-700"> </Label>
<Button variant="outline" size="sm" onClick={addHeader}>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{/* 빠른 헤더 템플릿 */}
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
onChange({
headers: { ...dataSource.headers, Authorization: "Bearer YOUR_TOKEN" },
});
}}
>
+ Authorization
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
onChange({
headers: { ...dataSource.headers, "Content-Type": "application/json" },
});
}}
>
+ Content-Type
</Button>
</div>
{dataSource.headers && Object.keys(dataSource.headers).length > 0 ? (
<div className="space-y-2">
{Object.entries(dataSource.headers).map(([key, value]) => (
<div key={key} className="flex gap-2">
<Input
placeholder="Header Name"
value={key}
onChange={(e) => updateHeader(key, e.target.value, value)}
className="flex-1"
/>
<Input
placeholder="Header Value"
value={value}
onChange={(e) => updateHeader(key, key, e.target.value)}
className="flex-1"
type={key.toLowerCase().includes("auth") ? "password" : "text"}
/>
<Button variant="ghost" size="icon" onClick={() => removeHeader(key)}>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
) : (
<p className="py-2 text-center text-sm text-gray-500"> </p>
)}
</Card>
{/* JSON Path */}
<Card className="space-y-2 p-4">
<Label className="text-sm font-medium text-gray-700">JSON Path ()</Label>
<Input
placeholder="data.results"
value={dataSource.jsonPath || ""}
onChange={(e) => onChange({ jsonPath: e.target.value })}
/>
<p className="text-xs text-gray-500">
JSON (: data.results, items, response.data)
<br />
</p>
</Card>
{/* 테스트 버튼 */}
<div className="flex justify-end">
<Button onClick={testApi} disabled={!dataSource.endpoint || testing}>
{testing ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
...
</>
) : (
<>
<Play className="mr-2 h-4 w-4" />
API
</>
)}
</Button>
</div>
{/* 테스트 오류 */}
{testError && (
<Card className="border-red-200 bg-red-50 p-4">
<div className="flex items-start gap-2">
<AlertCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-red-600" />
<div>
<div className="text-sm font-medium text-red-800">API </div>
<div className="mt-1 text-sm text-red-700">{testError}</div>
</div>
</div>
</Card>
)}
{/* 테스트 결과 */}
{testResult && (
<Card className="border-green-200 bg-green-50 p-4">
<div className="mb-2 text-sm font-medium text-green-800"> API </div>
<div className="space-y-1 text-xs text-green-700">
<div> {testResult.rows.length} </div>
<div>: {testResult.columns.join(", ")}</div>
</div>
</Card>
)}
</div>
);
}