ERP-node/frontend/components/dataflow/external-call/ExternalCallPanel.tsx

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;