리스트 위젯 REST API 기능 개선
This commit is contained in:
@@ -137,9 +137,18 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
||||
}
|
||||
|
||||
updates.type = "api"; // ⭐ 중요: type을 api로 명시
|
||||
updates.method = "GET"; // 기본 메서드
|
||||
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);
|
||||
@@ -254,6 +263,19 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
||||
}
|
||||
});
|
||||
|
||||
// 요청 메서드 결정
|
||||
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",
|
||||
@@ -262,9 +284,11 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: dataSource.endpoint,
|
||||
method: "GET",
|
||||
method: requestMethod,
|
||||
headers: headers,
|
||||
queryParams: params,
|
||||
body: requestBody,
|
||||
externalConnectionId: dataSource.externalConnectionId, // DB 토큰 등 인증 정보 조회용
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -314,10 +338,23 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
||||
if (dataSource.jsonPath) {
|
||||
const paths = dataSource.jsonPath.split(".");
|
||||
for (const path of paths) {
|
||||
if (data && typeof data === "object" && path in data) {
|
||||
data = data[path];
|
||||
// 배열인 경우 인덱스 접근, 객체인 경우 키 접근
|
||||
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<string, any>)[path];
|
||||
} else {
|
||||
throw new Error(`JSON Path "${dataSource.jsonPath}"에서 데이터를 찾을 수 없습니다`);
|
||||
throw new Error(`JSON Path "${dataSource.jsonPath}"에서 "${path}" 키를 찾을 수 없습니다`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,6 +368,16 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
||||
|
||||
// 컬럼 추출 및 타입 분석
|
||||
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);
|
||||
|
||||
// 각 컬럼의 타입 분석
|
||||
@@ -400,21 +447,54 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
||||
<p className="text-[11px] text-muted-foreground">저장한 REST API 설정을 불러올 수 있습니다</p>
|
||||
</div>
|
||||
|
||||
{/* API URL */}
|
||||
{/* HTTP 메서드 및 API URL */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-medium text-foreground">API URL *</Label>
|
||||
<Input
|
||||
type="url"
|
||||
placeholder="https://api.example.com/data 또는 /api/typ01/url/wrn_now_data.php"
|
||||
value={dataSource.endpoint || ""}
|
||||
onChange={(e) => onChange({ endpoint: e.target.value })}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={dataSource.method || "GET"}
|
||||
onValueChange={(value) => onChange({ method: value as "GET" | "POST" | "PUT" | "PATCH" | "DELETE" })}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-24 text-xs">
|
||||
<SelectValue placeholder="GET" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[9999]">
|
||||
<SelectItem value="GET" className="text-xs">GET</SelectItem>
|
||||
<SelectItem value="POST" className="text-xs">POST</SelectItem>
|
||||
<SelectItem value="PUT" className="text-xs">PUT</SelectItem>
|
||||
<SelectItem value="PATCH" className="text-xs">PATCH</SelectItem>
|
||||
<SelectItem value="DELETE" className="text-xs">DELETE</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
type="url"
|
||||
placeholder="https://api.example.com/data"
|
||||
value={dataSource.endpoint || ""}
|
||||
onChange={(e) => onChange({ endpoint: e.target.value })}
|
||||
className="h-8 flex-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
전체 URL 또는 base_url 이후 경로를 입력하세요 (외부 커넥션 선택 시 base_url 자동 입력)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Request Body (POST, PUT, PATCH인 경우) */}
|
||||
{["POST", "PUT", "PATCH"].includes(dataSource.method || "") && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-medium text-foreground">Request Body (JSON)</Label>
|
||||
<textarea
|
||||
placeholder='{"key": "value"}'
|
||||
value={dataSource.body || ""}
|
||||
onChange={(e) => onChange({ body: e.target.value })}
|
||||
className="h-24 w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-xs ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
JSON 형식으로 요청 본문을 입력하세요
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 쿼리 파라미터 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -544,6 +624,30 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 자동 새로고침 (HTTP Polling) */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-medium text-foreground">자동 새로고침 간격</Label>
|
||||
<Select
|
||||
value={(dataSource.refreshInterval || 0).toString()}
|
||||
onValueChange={(value) => onChange({ refreshInterval: parseInt(value) })}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="간격 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[9999]">
|
||||
<SelectItem value="0" className="text-xs">없음 (수동)</SelectItem>
|
||||
<SelectItem value="5" className="text-xs">5초</SelectItem>
|
||||
<SelectItem value="10" className="text-xs">10초</SelectItem>
|
||||
<SelectItem value="30" className="text-xs">30초</SelectItem>
|
||||
<SelectItem value="60" className="text-xs">1분</SelectItem>
|
||||
<SelectItem value="300" className="text-xs">5분</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
설정된 간격마다 자동으로 API를 호출하여 데이터를 갱신합니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 테스트 버튼 */}
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={testApi} disabled={!dataSource.endpoint || testing}>
|
||||
|
||||
Reference in New Issue
Block a user