restapi도 가능하게 구현
This commit is contained in:
@@ -34,6 +34,7 @@ import { formatErrorMessage } from "@/lib/utils/errorUtils";
|
||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
||||
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
|
||||
import { ExternalRestApiConnectionAPI, ExternalRestApiConnection } from "@/lib/api/externalRestApiConnection";
|
||||
|
||||
export default function FlowManagementPage() {
|
||||
const router = useRouter();
|
||||
@@ -52,13 +53,19 @@ export default function FlowManagementPage() {
|
||||
);
|
||||
const [loadingTables, setLoadingTables] = useState(false);
|
||||
const [openTableCombobox, setOpenTableCombobox] = useState(false);
|
||||
const [selectedDbSource, setSelectedDbSource] = useState<"internal" | number>("internal"); // "internal" 또는 외부 DB connection ID
|
||||
// 데이터 소스 타입: "internal" (내부 DB), "external_db_숫자" (외부 DB), "restapi_숫자" (REST API)
|
||||
const [selectedDbSource, setSelectedDbSource] = useState<string>("internal");
|
||||
const [externalConnections, setExternalConnections] = useState<
|
||||
Array<{ id: number; connection_name: string; db_type: string }>
|
||||
>([]);
|
||||
const [externalTableList, setExternalTableList] = useState<string[]>([]);
|
||||
const [loadingExternalTables, setLoadingExternalTables] = useState(false);
|
||||
|
||||
// REST API 연결 관련 상태
|
||||
const [restApiConnections, setRestApiConnections] = useState<ExternalRestApiConnection[]>([]);
|
||||
const [restApiEndpoint, setRestApiEndpoint] = useState("");
|
||||
const [restApiJsonPath, setRestApiJsonPath] = useState("data");
|
||||
|
||||
// 생성 폼 상태
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
@@ -135,75 +142,132 @@ export default function FlowManagementPage() {
|
||||
loadConnections();
|
||||
}, []);
|
||||
|
||||
// REST API 연결 목록 로드
|
||||
useEffect(() => {
|
||||
const loadRestApiConnections = async () => {
|
||||
try {
|
||||
const connections = await ExternalRestApiConnectionAPI.getConnections({ is_active: "Y" });
|
||||
setRestApiConnections(connections);
|
||||
} catch (error) {
|
||||
console.error("Failed to load REST API connections:", error);
|
||||
setRestApiConnections([]);
|
||||
}
|
||||
};
|
||||
loadRestApiConnections();
|
||||
}, []);
|
||||
|
||||
// 외부 DB 테이블 목록 로드
|
||||
useEffect(() => {
|
||||
if (selectedDbSource === "internal" || !selectedDbSource) {
|
||||
// REST API인 경우 테이블 목록 로드 불필요
|
||||
if (selectedDbSource === "internal" || !selectedDbSource || selectedDbSource.startsWith("restapi_")) {
|
||||
setExternalTableList([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadExternalTables = async () => {
|
||||
try {
|
||||
setLoadingExternalTables(true);
|
||||
const token = localStorage.getItem("authToken");
|
||||
// 외부 DB인 경우
|
||||
if (selectedDbSource.startsWith("external_db_")) {
|
||||
const connectionId = selectedDbSource.replace("external_db_", "");
|
||||
|
||||
const loadExternalTables = async () => {
|
||||
try {
|
||||
setLoadingExternalTables(true);
|
||||
const token = localStorage.getItem("authToken");
|
||||
|
||||
const response = await fetch(`/api/multi-connection/connections/${selectedDbSource}/tables`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const response = await fetch(`/api/multi-connection/connections/${connectionId}/tables`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response && response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.data) {
|
||||
const tables = Array.isArray(data.data) ? data.data : [];
|
||||
const tableNames = tables
|
||||
.map((t: string | { tableName?: string; table_name?: string; tablename?: string; name?: string }) =>
|
||||
typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name,
|
||||
)
|
||||
.filter(Boolean);
|
||||
setExternalTableList(tableNames);
|
||||
if (response && response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.data) {
|
||||
const tables = Array.isArray(data.data) ? data.data : [];
|
||||
const tableNames = tables
|
||||
.map((t: string | { tableName?: string; table_name?: string; tablename?: string; name?: string }) =>
|
||||
typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name,
|
||||
)
|
||||
.filter(Boolean);
|
||||
setExternalTableList(tableNames);
|
||||
} else {
|
||||
setExternalTableList([]);
|
||||
}
|
||||
} else {
|
||||
setExternalTableList([]);
|
||||
}
|
||||
} else {
|
||||
} catch (error) {
|
||||
console.error("외부 DB 테이블 목록 조회 오류:", error);
|
||||
setExternalTableList([]);
|
||||
} finally {
|
||||
setLoadingExternalTables(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("외부 DB 테이블 목록 조회 오류:", error);
|
||||
setExternalTableList([]);
|
||||
} finally {
|
||||
setLoadingExternalTables(false);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
loadExternalTables();
|
||||
loadExternalTables();
|
||||
}
|
||||
}, [selectedDbSource]);
|
||||
|
||||
// 플로우 생성
|
||||
const handleCreate = async () => {
|
||||
console.log("🚀 handleCreate called with formData:", formData);
|
||||
|
||||
if (!formData.name || !formData.tableName) {
|
||||
console.log("❌ Validation failed:", { name: formData.name, tableName: formData.tableName });
|
||||
// REST API인 경우 테이블 이름 검증 스킵
|
||||
const isRestApi = selectedDbSource.startsWith("restapi_");
|
||||
|
||||
if (!formData.name || (!isRestApi && !formData.tableName)) {
|
||||
console.log("❌ Validation failed:", { name: formData.name, tableName: formData.tableName, isRestApi });
|
||||
toast({
|
||||
title: "입력 오류",
|
||||
description: "플로우 이름과 테이블 이름은 필수입니다.",
|
||||
description: isRestApi ? "플로우 이름은 필수입니다." : "플로우 이름과 테이블 이름은 필수입니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// REST API인 경우 엔드포인트 검증
|
||||
if (isRestApi && !restApiEndpoint) {
|
||||
toast({
|
||||
title: "입력 오류",
|
||||
description: "REST API 엔드포인트는 필수입니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// DB 소스 정보 추가
|
||||
const requestData = {
|
||||
// 데이터 소스 타입 및 ID 파싱
|
||||
let dbSourceType: "internal" | "external" | "restapi" = "internal";
|
||||
let dbConnectionId: number | undefined = undefined;
|
||||
let restApiConnectionId: number | undefined = undefined;
|
||||
|
||||
if (selectedDbSource === "internal") {
|
||||
dbSourceType = "internal";
|
||||
} else if (selectedDbSource.startsWith("external_db_")) {
|
||||
dbSourceType = "external";
|
||||
dbConnectionId = parseInt(selectedDbSource.replace("external_db_", ""));
|
||||
} else if (selectedDbSource.startsWith("restapi_")) {
|
||||
dbSourceType = "restapi";
|
||||
restApiConnectionId = parseInt(selectedDbSource.replace("restapi_", ""));
|
||||
}
|
||||
|
||||
// 요청 데이터 구성
|
||||
const requestData: Record<string, unknown> = {
|
||||
...formData,
|
||||
dbSourceType: selectedDbSource === "internal" ? "internal" : "external",
|
||||
dbConnectionId: selectedDbSource === "internal" ? undefined : Number(selectedDbSource),
|
||||
dbSourceType,
|
||||
dbConnectionId,
|
||||
};
|
||||
|
||||
// REST API인 경우 추가 정보
|
||||
if (dbSourceType === "restapi") {
|
||||
requestData.restApiConnectionId = restApiConnectionId;
|
||||
requestData.restApiEndpoint = restApiEndpoint;
|
||||
requestData.restApiJsonPath = restApiJsonPath || "data";
|
||||
// REST API는 가상 테이블명 사용
|
||||
requestData.tableName = `_restapi_${restApiConnectionId}`;
|
||||
}
|
||||
|
||||
console.log("✅ Calling createFlowDefinition with:", requestData);
|
||||
const response = await createFlowDefinition(requestData);
|
||||
const response = await createFlowDefinition(requestData as Parameters<typeof createFlowDefinition>[0]);
|
||||
if (response.success && response.data) {
|
||||
toast({
|
||||
title: "생성 완료",
|
||||
@@ -212,6 +276,8 @@ export default function FlowManagementPage() {
|
||||
setIsCreateDialogOpen(false);
|
||||
setFormData({ name: "", description: "", tableName: "" });
|
||||
setSelectedDbSource("internal");
|
||||
setRestApiEndpoint("");
|
||||
setRestApiJsonPath("data");
|
||||
loadFlows();
|
||||
} else {
|
||||
toast({
|
||||
@@ -415,125 +481,186 @@ export default function FlowManagementPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* DB 소스 선택 */}
|
||||
{/* 데이터 소스 선택 */}
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">데이터베이스 소스</Label>
|
||||
<Label className="text-xs sm:text-sm">데이터 소스</Label>
|
||||
<Select
|
||||
value={selectedDbSource.toString()}
|
||||
value={selectedDbSource}
|
||||
onValueChange={(value) => {
|
||||
const dbSource = value === "internal" ? "internal" : parseInt(value);
|
||||
setSelectedDbSource(dbSource);
|
||||
// DB 소스 변경 시 테이블 선택 초기화
|
||||
setSelectedDbSource(value);
|
||||
// 소스 변경 시 테이블 선택 및 REST API 설정 초기화
|
||||
setFormData({ ...formData, tableName: "" });
|
||||
setRestApiEndpoint("");
|
||||
setRestApiJsonPath("data");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||
<SelectValue placeholder="데이터베이스 선택" />
|
||||
<SelectValue placeholder="데이터 소스 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* 내부 DB */}
|
||||
<SelectItem value="internal">내부 데이터베이스</SelectItem>
|
||||
{externalConnections.map((conn) => (
|
||||
<SelectItem key={conn.id} value={conn.id.toString()}>
|
||||
{conn.connection_name} ({conn.db_type?.toUpperCase()})
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
{/* 외부 DB 연결 */}
|
||||
{externalConnections.length > 0 && (
|
||||
<>
|
||||
<SelectItem value="__divider_db__" disabled className="text-xs text-muted-foreground">
|
||||
-- 외부 데이터베이스 --
|
||||
</SelectItem>
|
||||
{externalConnections.map((conn) => (
|
||||
<SelectItem key={`db_${conn.id}`} value={`external_db_${conn.id}`}>
|
||||
{conn.connection_name} ({conn.db_type?.toUpperCase()})
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* REST API 연결 */}
|
||||
{restApiConnections.length > 0 && (
|
||||
<>
|
||||
<SelectItem value="__divider_api__" disabled className="text-xs text-muted-foreground">
|
||||
-- REST API --
|
||||
</SelectItem>
|
||||
{restApiConnections.map((conn) => (
|
||||
<SelectItem key={`api_${conn.id}`} value={`restapi_${conn.id}`}>
|
||||
{conn.connection_name} (REST API)
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||
플로우에서 사용할 데이터베이스를 선택합니다
|
||||
플로우에서 사용할 데이터 소스를 선택합니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 테이블 선택 */}
|
||||
<div>
|
||||
<Label htmlFor="tableName" className="text-xs sm:text-sm">
|
||||
연결 테이블 *
|
||||
</Label>
|
||||
<Popover open={openTableCombobox} onOpenChange={setOpenTableCombobox}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openTableCombobox}
|
||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
||||
disabled={loadingTables || (selectedDbSource !== "internal" && loadingExternalTables)}
|
||||
>
|
||||
{formData.tableName
|
||||
? selectedDbSource === "internal"
|
||||
? tableList.find((table) => table.tableName === formData.tableName)?.displayName ||
|
||||
formData.tableName
|
||||
: formData.tableName
|
||||
: loadingTables || loadingExternalTables
|
||||
? "로딩 중..."
|
||||
: "테이블 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-xs sm:text-sm">테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{selectedDbSource === "internal"
|
||||
? // 내부 DB 테이블 목록
|
||||
tableList.map((table) => (
|
||||
<CommandItem
|
||||
key={table.tableName}
|
||||
value={table.tableName}
|
||||
onSelect={(currentValue) => {
|
||||
console.log("📝 Internal table selected:", {
|
||||
tableName: table.tableName,
|
||||
currentValue,
|
||||
});
|
||||
setFormData({ ...formData, tableName: currentValue });
|
||||
setOpenTableCombobox(false);
|
||||
}}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
formData.tableName === table.tableName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{table.displayName || table.tableName}</span>
|
||||
{table.description && (
|
||||
<span className="text-[10px] text-gray-500">{table.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))
|
||||
: // 외부 DB 테이블 목록
|
||||
externalTableList.map((tableName, index) => (
|
||||
<CommandItem
|
||||
key={`external-${selectedDbSource}-${tableName}-${index}`}
|
||||
value={tableName}
|
||||
onSelect={(currentValue) => {
|
||||
setFormData({ ...formData, tableName: currentValue });
|
||||
setOpenTableCombobox(false);
|
||||
}}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
formData.tableName === tableName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div>{tableName}</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||
플로우의 모든 단계에서 사용할 기본 테이블입니다 (단계마다 상태 컬럼만 지정합니다)
|
||||
</p>
|
||||
</div>
|
||||
{/* REST API인 경우 엔드포인트 설정 */}
|
||||
{selectedDbSource.startsWith("restapi_") ? (
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="restApiEndpoint" className="text-xs sm:text-sm">
|
||||
API 엔드포인트 *
|
||||
</Label>
|
||||
<Input
|
||||
id="restApiEndpoint"
|
||||
value={restApiEndpoint}
|
||||
onChange={(e) => setRestApiEndpoint(e.target.value)}
|
||||
placeholder="예: /api/data/list"
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||
데이터를 조회할 API 엔드포인트 경로입니다
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="restApiJsonPath" className="text-xs sm:text-sm">
|
||||
JSON 경로
|
||||
</Label>
|
||||
<Input
|
||||
id="restApiJsonPath"
|
||||
value={restApiJsonPath}
|
||||
onChange={(e) => setRestApiJsonPath(e.target.value)}
|
||||
placeholder="예: data 또는 result.items"
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||
응답 JSON에서 데이터 배열의 경로입니다 (기본: data)
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* 테이블 선택 (내부 DB 또는 외부 DB) */
|
||||
<div>
|
||||
<Label htmlFor="tableName" className="text-xs sm:text-sm">
|
||||
연결 테이블 *
|
||||
</Label>
|
||||
<Popover open={openTableCombobox} onOpenChange={setOpenTableCombobox}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openTableCombobox}
|
||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
||||
disabled={loadingTables || (selectedDbSource !== "internal" && loadingExternalTables)}
|
||||
>
|
||||
{formData.tableName
|
||||
? selectedDbSource === "internal"
|
||||
? tableList.find((table) => table.tableName === formData.tableName)?.displayName ||
|
||||
formData.tableName
|
||||
: formData.tableName
|
||||
: loadingTables || loadingExternalTables
|
||||
? "로딩 중..."
|
||||
: "테이블 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-xs sm:text-sm">테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{selectedDbSource === "internal"
|
||||
? // 내부 DB 테이블 목록
|
||||
tableList.map((table) => (
|
||||
<CommandItem
|
||||
key={table.tableName}
|
||||
value={table.tableName}
|
||||
onSelect={(currentValue) => {
|
||||
console.log("📝 Internal table selected:", {
|
||||
tableName: table.tableName,
|
||||
currentValue,
|
||||
});
|
||||
setFormData({ ...formData, tableName: currentValue });
|
||||
setOpenTableCombobox(false);
|
||||
}}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
formData.tableName === table.tableName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{table.displayName || table.tableName}</span>
|
||||
{table.description && (
|
||||
<span className="text-[10px] text-gray-500">{table.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))
|
||||
: // 외부 DB 테이블 목록
|
||||
externalTableList.map((tableName, index) => (
|
||||
<CommandItem
|
||||
key={`external-${selectedDbSource}-${tableName}-${index}`}
|
||||
value={tableName}
|
||||
onSelect={(currentValue) => {
|
||||
setFormData({ ...formData, tableName: currentValue });
|
||||
setOpenTableCombobox(false);
|
||||
}}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
formData.tableName === tableName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div>{tableName}</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||
플로우의 모든 단계에서 사용할 기본 테이블입니다 (단계마다 상태 컬럼만 지정합니다)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description" className="text-xs sm:text-sm">
|
||||
|
||||
Reference in New Issue
Block a user