435 lines
16 KiB
TypeScript
435 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useCallback, useEffect } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { Globe, Settings, TestTube, History, Info } from "lucide-react";
|
|
|
|
// 타입 import
|
|
import {
|
|
ExternalCallConfig,
|
|
ExternalCallPanelProps,
|
|
RestApiSettings as RestApiSettingsType,
|
|
ApiTestResult,
|
|
} from "@/types/external-call/ExternalCallTypes";
|
|
import { DataMappingConfig, TableInfo } from "@/types/external-call/DataMappingTypes";
|
|
|
|
// API import
|
|
import { DataFlowAPI } from "@/lib/api/dataflow";
|
|
import { toast } from "sonner";
|
|
|
|
// 하위 컴포넌트 import
|
|
import RestApiSettings from "./RestApiSettings";
|
|
import ExternalCallTestPanel from "./ExternalCallTestPanel";
|
|
import { DataMappingSettings } from "./DataMappingSettings";
|
|
|
|
/**
|
|
* 🌐 외부호출 메인 패널 컴포넌트
|
|
*
|
|
* 데이터 저장 기능과 완전히 분리된 독립적인 외부호출 전용 패널
|
|
* REST API 설정, 테스트, 실행 이력 등을 통합 관리
|
|
*/
|
|
const ExternalCallPanel: React.FC<ExternalCallPanelProps> = ({
|
|
relationshipId,
|
|
onSettingsChange,
|
|
initialSettings,
|
|
readonly = false,
|
|
}) => {
|
|
console.log("🌐 [ExternalCallPanel] Component mounted with props:", {
|
|
relationshipId,
|
|
initialSettings,
|
|
readonly,
|
|
});
|
|
// 상태 관리
|
|
const [config, setConfig] = useState<ExternalCallConfig>(
|
|
() => {
|
|
if (initialSettings) {
|
|
console.log("🔄 [ExternalCallPanel] 기존 설정 로드:", initialSettings);
|
|
return initialSettings;
|
|
}
|
|
|
|
console.log("🔄 [ExternalCallPanel] 기본 설정 사용");
|
|
return {
|
|
callType: "rest-api",
|
|
restApiSettings: {
|
|
apiUrl: "",
|
|
httpMethod: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
bodyTemplate: `{
|
|
"message": "데이터가 업데이트되었습니다",
|
|
"data": {{sourceData}},
|
|
"timestamp": "{{timestamp}}",
|
|
"relationshipId": "{{relationshipId}}"
|
|
}`,
|
|
authentication: {
|
|
type: "none",
|
|
},
|
|
timeout: 30000, // 30초
|
|
retryCount: 3,
|
|
},
|
|
};
|
|
},
|
|
);
|
|
|
|
const [activeTab, setActiveTab] = useState<string>("settings");
|
|
const [lastTestResult, setLastTestResult] = useState<ApiTestResult | null>(null);
|
|
const [isConfigValid, setIsConfigValid] = useState<boolean>(false);
|
|
|
|
// 데이터 매핑 상태
|
|
const [dataMappingConfig, setDataMappingConfig] = useState<DataMappingConfig>(() => {
|
|
// initialSettings에서 데이터 매핑 정보 불러오기
|
|
if (initialSettings?.dataMappingConfig) {
|
|
console.log("🔄 [ExternalCallPanel] 기존 데이터 매핑 설정 로드:", initialSettings.dataMappingConfig);
|
|
return initialSettings.dataMappingConfig;
|
|
}
|
|
|
|
console.log("🔄 [ExternalCallPanel] 기본 데이터 매핑 설정 사용");
|
|
return {
|
|
direction: "none",
|
|
};
|
|
});
|
|
|
|
// 사용 가능한 테이블 목록 (실제 API에서 로드)
|
|
const [availableTables, setAvailableTables] = useState<TableInfo[]>([]);
|
|
const [tablesLoading, setTablesLoading] = useState(false);
|
|
|
|
// 테이블 목록 로드
|
|
useEffect(() => {
|
|
const loadTables = async () => {
|
|
try {
|
|
setTablesLoading(true);
|
|
const tables = await DataFlowAPI.getTables();
|
|
|
|
// 테이블 정보를 TableInfo 형식으로 변환
|
|
const tableInfos: TableInfo[] = await Promise.all(
|
|
tables.map(async (table) => {
|
|
try {
|
|
const columns = await DataFlowAPI.getTableColumns(table.tableName);
|
|
return {
|
|
name: table.tableName,
|
|
displayName: table.displayName || table.tableName,
|
|
fields: columns.map((col) => ({
|
|
name: col.columnName,
|
|
dataType: col.dataType,
|
|
nullable: col.nullable,
|
|
isPrimaryKey: col.isPrimaryKey || false,
|
|
})),
|
|
};
|
|
} catch (error) {
|
|
console.warn(`테이블 ${table.tableName} 컬럼 정보 로드 실패:`, error);
|
|
return {
|
|
name: table.tableName,
|
|
displayName: table.displayName || table.tableName,
|
|
fields: [],
|
|
};
|
|
}
|
|
})
|
|
);
|
|
|
|
setAvailableTables(tableInfos);
|
|
} catch (error) {
|
|
console.error("테이블 목록 로드 실패:", error);
|
|
toast.error("테이블 목록을 불러오는데 실패했습니다.");
|
|
// 실패 시 빈 배열로 설정
|
|
setAvailableTables([]);
|
|
} finally {
|
|
setTablesLoading(false);
|
|
}
|
|
};
|
|
|
|
loadTables();
|
|
}, []);
|
|
|
|
// 설정 변경 핸들러
|
|
const handleRestApiSettingsChange = useCallback(
|
|
(newSettings: RestApiSettingsType) => {
|
|
const updatedConfig: ExternalCallConfig = {
|
|
...config,
|
|
restApiSettings: newSettings,
|
|
metadata: {
|
|
...config.metadata,
|
|
updatedAt: new Date().toISOString(),
|
|
version: "1.0",
|
|
},
|
|
};
|
|
|
|
setConfig(updatedConfig);
|
|
onSettingsChange({
|
|
...updatedConfig,
|
|
dataMappingConfig,
|
|
});
|
|
},
|
|
[config, onSettingsChange, dataMappingConfig],
|
|
);
|
|
|
|
// 데이터 매핑 설정 변경 핸들러
|
|
const handleDataMappingConfigChange = useCallback(
|
|
(newMappingConfig: DataMappingConfig) => {
|
|
console.log("🔄 [ExternalCallPanel] 데이터 매핑 설정 변경:", newMappingConfig);
|
|
|
|
setDataMappingConfig(newMappingConfig);
|
|
|
|
// 전체 설정에 데이터 매핑 정보 포함하여 상위로 전달
|
|
onSettingsChange({
|
|
...config,
|
|
dataMappingConfig: newMappingConfig,
|
|
});
|
|
},
|
|
[config, onSettingsChange],
|
|
);
|
|
|
|
// 테스트 결과 핸들러
|
|
const handleTestResult = useCallback((result: ApiTestResult) => {
|
|
setLastTestResult(result);
|
|
|
|
// 테스트 탭에 머물러서 응답 정보를 바로 확인할 수 있도록 함
|
|
// (이전에는 성공 시 자동으로 history 탭으로 이동했음)
|
|
}, []);
|
|
|
|
// 설정 유효성 검사
|
|
const validateConfig = useCallback(() => {
|
|
const { restApiSettings } = config;
|
|
|
|
// HTTP 메서드에 따라 바디 필요 여부 결정
|
|
const methodNeedsBody = !["GET", "HEAD", "DELETE"].includes(restApiSettings.httpMethod?.toUpperCase());
|
|
|
|
const isValid = !!(
|
|
restApiSettings.apiUrl &&
|
|
restApiSettings.apiUrl.startsWith("http") &&
|
|
restApiSettings.httpMethod &&
|
|
(methodNeedsBody ? restApiSettings.bodyTemplate : true) // GET/HEAD/DELETE는 바디 불필요
|
|
);
|
|
|
|
setIsConfigValid(isValid);
|
|
return isValid;
|
|
}, [config]);
|
|
|
|
// 설정 변경 시 유효성 검사 실행
|
|
useEffect(() => {
|
|
validateConfig();
|
|
}, [validateConfig]);
|
|
|
|
return (
|
|
<div className="flex h-full max-h-full flex-col space-y-2">
|
|
{/* 헤더 */}
|
|
<Card>
|
|
<CardHeader className="pt-3 pb-2">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Globe className="h-5 w-5 text-blue-500" />
|
|
<CardTitle className="text-lg">외부 호출 설정</CardTitle>
|
|
<Badge variant={isConfigValid ? "default" : "secondary"}>
|
|
{isConfigValid ? "설정 완료" : "설정 필요"}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-muted-foreground text-sm">
|
|
관계 실행 시 외부 API를 호출하여 데이터를 전송하거나 알림을 보낼 수 있습니다.
|
|
</div>
|
|
</CardHeader>
|
|
</Card>
|
|
|
|
{/* 메인 탭 컨텐츠 */}
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex w-full flex-1 flex-col overflow-hidden">
|
|
<TabsList className="grid w-full grid-cols-5">
|
|
<TabsTrigger value="settings" className="flex items-center gap-2">
|
|
<Settings className="h-4 w-4" />
|
|
API 설정
|
|
</TabsTrigger>
|
|
<TabsTrigger value="mapping" className="flex items-center gap-2">
|
|
🔄 데이터 매핑
|
|
</TabsTrigger>
|
|
<TabsTrigger value="test" className="flex items-center gap-2">
|
|
<TestTube className="h-4 w-4" />
|
|
테스트
|
|
</TabsTrigger>
|
|
<TabsTrigger value="history" className="flex items-center gap-2">
|
|
<History className="h-4 w-4" />
|
|
이력
|
|
</TabsTrigger>
|
|
<TabsTrigger value="info" className="flex items-center gap-2">
|
|
<Info className="h-4 w-4" />
|
|
정보
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* API 설정 탭 */}
|
|
<TabsContent value="settings" className="flex-1 space-y-2 overflow-y-auto">
|
|
<RestApiSettings
|
|
settings={config.restApiSettings}
|
|
onSettingsChange={handleRestApiSettingsChange}
|
|
readonly={readonly}
|
|
/>
|
|
</TabsContent>
|
|
|
|
{/* 데이터 매핑 탭 */}
|
|
<TabsContent value="mapping" className="flex-1 space-y-2 overflow-y-auto">
|
|
<DataMappingSettings
|
|
config={dataMappingConfig}
|
|
onConfigChange={handleDataMappingConfigChange}
|
|
httpMethod={config.restApiSettings?.httpMethod || "GET"}
|
|
availableTables={availableTables}
|
|
readonly={readonly}
|
|
tablesLoading={tablesLoading}
|
|
/>
|
|
</TabsContent>
|
|
|
|
{/* 테스트 탭 */}
|
|
<TabsContent value="test" className="flex-1 space-y-4 overflow-y-auto">
|
|
{isConfigValid ? (
|
|
<ExternalCallTestPanel
|
|
settings={config.restApiSettings}
|
|
context={{
|
|
relationshipId,
|
|
diagramId: "test-diagram",
|
|
userId: "current-user",
|
|
executionId: "test-execution",
|
|
sourceData: { test: "data" },
|
|
timestamp: new Date().toISOString(),
|
|
}}
|
|
onTestResult={handleTestResult}
|
|
disabled={readonly}
|
|
/>
|
|
) : (
|
|
<Alert>
|
|
<Info className="h-4 w-4" />
|
|
<AlertDescription>API 테스트를 실행하려면 먼저 설정 탭에서 필수 정보를 입력해주세요.</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</TabsContent>
|
|
|
|
{/* 이력 탭 */}
|
|
<TabsContent value="history" className="flex-1 space-y-4 overflow-y-auto">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">실행 이력</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{lastTestResult ? (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-medium">최근 테스트 결과</span>
|
|
<Badge variant={lastTestResult.success ? "default" : "destructive"}>
|
|
{lastTestResult.success ? "성공" : "실패"}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-muted-foreground">상태 코드:</span>
|
|
<span className="ml-2 font-mono">{lastTestResult.statusCode || "N/A"}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">응답 시간:</span>
|
|
<span className="ml-2 font-mono">{lastTestResult.responseTime}ms</span>
|
|
</div>
|
|
</div>
|
|
|
|
{lastTestResult.error && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription className="text-sm">{lastTestResult.error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{lastTestResult.responseData && (
|
|
<div>
|
|
<span className="text-muted-foreground text-sm">응답 데이터:</span>
|
|
<pre className="bg-muted mt-1 max-h-32 overflow-auto rounded p-2 text-xs">
|
|
{JSON.stringify(lastTestResult.responseData, null, 2)}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-muted-foreground py-8 text-center">
|
|
<TestTube className="mx-auto mb-2 h-8 w-8 opacity-50" />
|
|
<p>아직 실행 이력이 없습니다.</p>
|
|
<p className="text-sm">테스트 탭에서 API 호출을 테스트해보세요.</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
{/* 정보 탭 */}
|
|
<TabsContent value="info" className="flex-1 space-y-4 overflow-y-auto">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">템플릿 변수 가이드</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="text-muted-foreground text-sm">요청 바디에서 사용할 수 있는 템플릿 변수들입니다:</div>
|
|
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<code className="bg-muted rounded px-2 py-1">{"{{sourceData}}"}</code>
|
|
<span className="text-muted-foreground">소스 노드의 전체 데이터</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<code className="bg-muted rounded px-2 py-1">{"{{timestamp}}"}</code>
|
|
<span className="text-muted-foreground">현재 타임스탬프</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<code className="bg-muted rounded px-2 py-1">{"{{relationshipId}}"}</code>
|
|
<span className="text-muted-foreground">관계 ID</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<code className="bg-muted rounded px-2 py-1">{"{{userId}}"}</code>
|
|
<span className="text-muted-foreground">현재 사용자 ID</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<code className="bg-muted rounded px-2 py-1">{"{{executionId}}"}</code>
|
|
<span className="text-muted-foreground">실행 ID</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Alert>
|
|
<Info className="h-4 w-4" />
|
|
<AlertDescription className="text-sm">
|
|
템플릿 변수는 실제 실행 시 해당 값으로 자동 치환됩니다. JSON 형식의 데이터는 따옴표 없이 사용하세요.
|
|
(예: {"{{sourceData}}"})
|
|
</AlertDescription>
|
|
</Alert>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">설정 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">관계 ID:</span>
|
|
<code className="bg-muted rounded px-2 py-1 text-xs">{relationshipId}</code>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">호출 타입:</span>
|
|
<Badge variant="outline">{config.callType.toUpperCase()}</Badge>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">설정 상태:</span>
|
|
<Badge variant={isConfigValid ? "default" : "secondary"}>{isConfigValid ? "완료" : "미완료"}</Badge>
|
|
</div>
|
|
{config.metadata?.updatedAt && (
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">마지막 수정:</span>
|
|
<span className="text-xs">{new Date(config.metadata.updatedAt).toLocaleString()}</span>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ExternalCallPanel;
|