ERP-node/frontend/components/flow/FlowStepPanel.tsx

1005 lines
41 KiB
TypeScript
Raw Normal View History

2025-10-20 10:55:33 +09:00
/**
*
*
*/
2025-10-20 17:50:27 +09:00
import { useState, useEffect, useCallback, useRef } from "react";
2025-10-20 10:55:33 +09:00
import { X, Trash2, Save, Check, ChevronsUpDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
2025-10-20 15:53:00 +09:00
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
2025-10-20 10:55:33 +09:00
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useToast } from "@/hooks/use-toast";
import { updateFlowStep, deleteFlowStep } from "@/lib/api/flow";
import { FlowStep } from "@/types/flow";
import { FlowConditionBuilder } from "./FlowConditionBuilder";
2025-10-20 15:53:00 +09:00
import { tableManagementApi, getTableColumns } from "@/lib/api/tableManagement";
2025-10-20 10:55:33 +09:00
import { cn } from "@/lib/utils";
2025-10-20 17:50:27 +09:00
import { flowExternalDbApi } from "@/lib/api/flowExternalDb";
import {
FlowExternalDbConnection,
FlowExternalDbIntegrationConfig,
INTEGRATION_TYPE_OPTIONS,
OPERATION_OPTIONS,
} from "@/types/flowExternalDb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
2025-10-20 10:55:33 +09:00
interface FlowStepPanelProps {
step: FlowStep;
flowId: number;
2025-10-21 13:19:18 +09:00
flowTableName?: string; // 플로우 정의에서 선택한 테이블명
flowDbSourceType?: "internal" | "external"; // 플로우의 DB 소스 타입
flowDbConnectionId?: number; // 플로우의 외부 DB 연결 ID
2025-10-20 10:55:33 +09:00
onClose: () => void;
onUpdate: () => void;
}
2025-10-21 13:19:18 +09:00
export function FlowStepPanel({
step,
flowId,
flowTableName,
flowDbSourceType = "internal",
flowDbConnectionId,
onClose,
onUpdate,
}: FlowStepPanelProps) {
2025-10-20 10:55:33 +09:00
const { toast } = useToast();
2025-10-21 13:19:18 +09:00
console.log("🎯 FlowStepPanel Props:", {
stepTableName: step.tableName,
flowTableName,
flowDbSourceType,
flowDbConnectionId,
final: step.tableName || flowTableName || "",
});
2025-10-20 10:55:33 +09:00
const [formData, setFormData] = useState({
stepName: step.stepName,
2025-10-21 13:19:18 +09:00
tableName: step.tableName || flowTableName || "", // 플로우 테이블명 우선 사용 (신규 방식)
2025-10-20 10:55:33 +09:00
conditionJson: step.conditionJson,
2025-10-20 15:53:00 +09:00
// 하이브리드 모드 필드
moveType: step.moveType || "status",
statusColumn: step.statusColumn || "",
statusValue: step.statusValue || "",
targetTable: step.targetTable || "",
fieldMappings: step.fieldMappings || {},
2025-10-20 17:50:27 +09:00
// 외부 연동 필드
integrationType: step.integrationType || "internal",
integrationConfig: step.integrationConfig,
2025-10-20 10:55:33 +09:00
});
const [tableList, setTableList] = useState<any[]>([]);
const [loadingTables, setLoadingTables] = useState(true);
const [openTableCombobox, setOpenTableCombobox] = useState(false);
2025-10-20 17:50:27 +09:00
// 외부 DB 테이블 목록
const [externalTableList, setExternalTableList] = useState<string[]>([]);
const [loadingExternalTables, setLoadingExternalTables] = useState(false);
const [selectedDbSource, setSelectedDbSource] = useState<"internal" | number>("internal"); // "internal" 또는 외부 DB connection ID
2025-10-20 15:53:00 +09:00
// 컬럼 목록 (상태 컬럼 선택용)
const [columns, setColumns] = useState<any[]>([]);
const [loadingColumns, setLoadingColumns] = useState(false);
const [openStatusColumnCombobox, setOpenStatusColumnCombobox] = useState(false);
2025-10-20 17:50:27 +09:00
// 외부 DB 연결 목록
const [externalConnections, setExternalConnections] = useState<FlowExternalDbConnection[]>([]);
const [loadingConnections, setLoadingConnections] = useState(false);
2025-10-20 10:55:33 +09:00
// 테이블 목록 조회
useEffect(() => {
const loadTables = async () => {
try {
setLoadingTables(true);
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
setTableList(response.data);
}
} catch (error) {
console.error("Failed to load tables:", error);
} finally {
setLoadingTables(false);
}
};
loadTables();
}, []);
2025-10-20 17:50:27 +09:00
// 외부 DB 연결 목록 조회 (JWT 토큰 사용)
useEffect(() => {
const loadConnections = async () => {
try {
setLoadingConnections(true);
// localStorage에서 JWT 토큰 가져오기
const token = localStorage.getItem("authToken");
if (!token) {
console.warn("토큰이 없습니다. 외부 DB 연결 목록을 조회할 수 없습니다.");
setExternalConnections([]);
return;
}
const response = await fetch("/api/external-db-connections/control/active", {
credentials: "include",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
}).catch((err) => {
console.warn("외부 DB 연결 목록 fetch 실패:", err);
return null;
});
if (response && response.ok) {
const result = await response.json();
if (result.success && result.data) {
// 메인 DB 제외하고 외부 DB만 필터링
const externalOnly = result.data.filter((conn: any) => conn.id !== 0);
setExternalConnections(externalOnly);
} else {
setExternalConnections([]);
}
} else {
// 401 오류 시 빈 배열로 처리 (리다이렉트 방지)
console.warn("외부 DB 연결 목록 조회 실패:", response?.status || "네트워크 오류");
setExternalConnections([]);
}
} catch (error: any) {
console.error("Failed to load external connections:", error);
setExternalConnections([]);
} finally {
setLoadingConnections(false);
}
};
loadConnections();
}, []);
// 외부 DB 선택 시 해당 DB의 테이블 목록 조회 (JWT 토큰 사용)
useEffect(() => {
const loadExternalTables = async () => {
console.log("🔍 loadExternalTables triggered, selectedDbSource:", selectedDbSource);
if (selectedDbSource === "internal" || typeof selectedDbSource !== "number") {
console.log("⚠️ Skipping external table load (internal or not a number)");
setExternalTableList([]);
return;
}
console.log("📡 Loading external tables for connection ID:", selectedDbSource);
try {
setLoadingExternalTables(true);
// localStorage에서 JWT 토큰 가져오기
const token = localStorage.getItem("authToken");
if (!token) {
console.warn("토큰이 없습니다. 외부 DB 테이블 목록을 조회할 수 없습니다.");
setExternalTableList([]);
return;
}
// 기존 multi-connection API 사용 (JWT 토큰 포함)
const response = await fetch(`/api/multi-connection/connections/${selectedDbSource}/tables`, {
credentials: "include",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
}).catch((err) => {
console.warn("외부 DB 테이블 목록 fetch 실패:", err);
return null;
});
if (response && response.ok) {
const result = await response.json();
console.log("✅ External tables API response:", result);
console.log("📊 result.data type:", typeof result.data, "isArray:", Array.isArray(result.data));
console.log("📊 result.data:", JSON.stringify(result.data, null, 2));
if (result.success && result.data) {
// 데이터 형식이 다를 수 있으므로 변환
const tableNames = result.data.map((t: any) => {
console.log("🔍 Processing item:", t, "type:", typeof t);
// tableName (camelCase), table_name, tablename, name 모두 지원
return typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name;
});
console.log("📋 Processed table names:", tableNames);
setExternalTableList(tableNames);
} else {
console.warn("❌ No data in response or success=false");
setExternalTableList([]);
}
} else {
// 인증 오류 시에도 빈 배열로 처리 (리다이렉트 방지)
console.warn(`외부 DB 테이블 목록 조회 실패: ${response?.status || "네트워크 오류"}`);
setExternalTableList([]);
}
} catch (error) {
console.error("외부 DB 테이블 목록 조회 오류:", error);
setExternalTableList([]);
} finally {
setLoadingExternalTables(false);
}
};
loadExternalTables();
}, [selectedDbSource]);
2025-10-20 10:55:33 +09:00
useEffect(() => {
2025-10-20 17:50:27 +09:00
console.log("🔄 Initializing formData from step:", {
id: step.id,
stepName: step.stepName,
statusColumn: step.statusColumn,
statusValue: step.statusValue,
2025-10-21 13:19:18 +09:00
flowTableName, // 플로우 정의의 테이블명
2025-10-20 17:50:27 +09:00
});
const newFormData = {
2025-10-20 10:55:33 +09:00
stepName: step.stepName,
2025-10-21 13:19:18 +09:00
tableName: step.tableName || flowTableName || "", // 플로우 테이블명 우선 사용
2025-10-20 10:55:33 +09:00
conditionJson: step.conditionJson,
2025-10-20 15:53:00 +09:00
// 하이브리드 모드 필드
moveType: step.moveType || "status",
statusColumn: step.statusColumn || "",
statusValue: step.statusValue || "",
targetTable: step.targetTable || "",
fieldMappings: step.fieldMappings || {},
2025-10-20 17:50:27 +09:00
// 외부 연동 필드
integrationType: step.integrationType || "internal",
integrationConfig: step.integrationConfig,
};
console.log("✅ Setting formData:", newFormData);
setFormData(newFormData);
2025-10-21 13:19:18 +09:00
}, [step.id, flowTableName]); // flowTableName도 의존성 추가
2025-10-20 10:55:33 +09:00
2025-10-21 13:19:18 +09:00
// 테이블 선택 시 컬럼 로드 - 내부/외부 DB 모두 지원
2025-10-20 15:53:00 +09:00
useEffect(() => {
const loadColumns = async () => {
if (!formData.tableName) {
setColumns([]);
return;
}
try {
setLoadingColumns(true);
2025-10-21 13:19:18 +09:00
console.log("🔍 Loading columns for status column selector:", {
tableName: formData.tableName,
flowDbSourceType,
flowDbConnectionId,
});
2025-10-20 15:53:00 +09:00
2025-10-21 13:19:18 +09:00
// 외부 DB인 경우
if (flowDbSourceType === "external" && flowDbConnectionId) {
const token = localStorage.getItem("authToken");
if (!token) {
console.warn("토큰이 없습니다. 외부 DB 컬럼 목록을 조회할 수 없습니다.");
setColumns([]);
return;
}
// 외부 DB 컬럼 조회 API
const response = await fetch(
`/api/multi-connection/connections/${flowDbConnectionId}/tables/${formData.tableName}/columns`,
{
credentials: "include",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
},
).catch((err) => {
console.warn("외부 DB 컬럼 목록 fetch 실패:", err);
return null;
});
if (response && response.ok) {
const result = await response.json();
console.log("✅ External columns API response:", result);
if (result.success && result.data) {
// 컬럼 데이터 형식 통일
const columnList = Array.isArray(result.data)
? result.data.map((col: any) => ({
column_name: col.column_name || col.columnName || col.name,
data_type: col.data_type || col.dataType || col.type,
}))
: [];
console.log("✅ Setting external columns:", columnList);
setColumns(columnList);
} else {
console.warn("❌ No data in external columns response");
setColumns([]);
}
} else {
console.warn(`외부 DB 컬럼 목록 조회 실패: ${response?.status || "네트워크 오류"}`);
setColumns([]);
}
2025-10-20 15:53:00 +09:00
} else {
2025-10-21 13:19:18 +09:00
// 내부 DB인 경우 (기존 로직)
const response = await getTableColumns(formData.tableName);
console.log("📦 Internal columns response:", response);
if (response.success && response.data && response.data.columns) {
console.log("✅ Setting internal columns:", response.data.columns);
setColumns(response.data.columns);
} else {
console.log("❌ No columns in response");
setColumns([]);
}
2025-10-20 15:53:00 +09:00
}
} catch (error) {
console.error("Failed to load columns:", error);
setColumns([]);
} finally {
setLoadingColumns(false);
}
};
loadColumns();
2025-10-21 13:19:18 +09:00
}, [formData.tableName, flowDbSourceType, flowDbConnectionId]);
2025-10-20 15:53:00 +09:00
2025-10-20 17:50:27 +09:00
// formData의 최신 값을 항상 참조하기 위한 ref
const formDataRef = useRef(formData);
// formData가 변경될 때마다 ref 업데이트
useEffect(() => {
formDataRef.current = formData;
}, [formData]);
2025-10-20 10:55:33 +09:00
// 저장
2025-10-20 17:50:27 +09:00
const handleSave = useCallback(async () => {
const currentFormData = formDataRef.current;
console.log("🚀 handleSave called, formData:", JSON.stringify(currentFormData, null, 2));
2025-10-21 13:19:18 +09:00
// 상태 변경 방식일 때 필수 필드 검증
if (currentFormData.moveType === "status") {
if (!currentFormData.statusColumn) {
toast({
title: "입력 오류",
description: "상태 변경 방식을 사용하려면 '상태 컬럼명'을 반드시 지정해야 합니다.",
variant: "destructive",
});
return;
}
if (!currentFormData.statusValue) {
toast({
title: "입력 오류",
description: "상태 변경 방식을 사용하려면 '이 단계의 상태값'을 반드시 지정해야 합니다.",
variant: "destructive",
});
return;
}
}
2025-10-20 10:55:33 +09:00
try {
2025-10-20 17:50:27 +09:00
const response = await updateFlowStep(step.id, currentFormData);
console.log("📡 API response:", response);
2025-10-20 10:55:33 +09:00
if (response.success) {
toast({
title: "저장 완료",
description: "단계가 수정되었습니다.",
});
onUpdate();
onClose();
} else {
toast({
title: "저장 실패",
description: response.error,
variant: "destructive",
});
}
} catch (error: any) {
toast({
title: "오류 발생",
description: error.message,
variant: "destructive",
});
}
2025-10-20 17:50:27 +09:00
}, [step.id, onUpdate, onClose, toast]);
2025-10-20 10:55:33 +09:00
// 삭제
const handleDelete = async () => {
if (!confirm(`"${step.stepName}" 단계를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`)) {
return;
}
try {
const response = await deleteFlowStep(step.id);
if (response.success) {
toast({
title: "삭제 완료",
description: "단계가 삭제되었습니다.",
});
onUpdate();
onClose();
} else {
toast({
title: "삭제 실패",
description: response.error,
variant: "destructive",
});
}
} catch (error: any) {
toast({
title: "오류 발생",
description: error.message,
variant: "destructive",
});
}
};
return (
<div className="fixed top-0 right-0 z-50 h-full w-96 overflow-y-auto border-l bg-white shadow-xl">
<div className="space-y-6 p-6">
{/* 헤더 */}
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold"> </h2>
<Button variant="ghost" size="sm" onClick={onClose}>
<X className="h-4 w-4" />
</Button>
</div>
{/* 기본 정보 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label> </Label>
<Input
value={formData.stepName}
onChange={(e) => setFormData({ ...formData, stepName: e.target.value })}
placeholder="단계 이름 입력"
/>
</div>
<div>
<Label> </Label>
<Input value={step.stepOrder} disabled />
</div>
2025-10-21 13:19:18 +09:00
{/* ===== 구버전: 단계별 테이블 선택 방식 (주석처리) ===== */}
2025-10-20 17:50:27 +09:00
{/* DB 소스 선택 */}
2025-10-21 13:19:18 +09:00
{/* <div>
2025-10-20 17:50:27 +09:00
<Label> </Label>
<Select
value={selectedDbSource.toString()}
onValueChange={(value) => {
const dbSource = value === "internal" ? "internal" : parseInt(value);
setSelectedDbSource(dbSource);
// DB 소스 변경 시 테이블 선택 초기화
setFormData({ ...formData, tableName: "" });
}}
>
<SelectTrigger>
<SelectValue placeholder="데이터베이스 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="internal"> </SelectItem>
{externalConnections.map((conn: any) => (
<SelectItem key={conn.id} value={conn.id.toString()}>
{conn.connection_name} ({conn.db_type?.toUpperCase()})
</SelectItem>
))}
</SelectContent>
</Select>
<p className="mt-1 text-xs text-gray-500"> </p>
2025-10-21 13:19:18 +09:00
</div> */}
2025-10-20 17:50:27 +09:00
{/* 테이블 선택 */}
2025-10-21 13:19:18 +09:00
{/* <div>
2025-10-20 10:55:33 +09:00
<Label> </Label>
<Popover open={openTableCombobox} onOpenChange={setOpenTableCombobox}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openTableCombobox}
className="w-full justify-between"
2025-10-20 17:50:27 +09:00
disabled={loadingTables || (selectedDbSource !== "internal" && loadingExternalTables)}
2025-10-20 10:55:33 +09:00
>
{formData.tableName
2025-10-20 17:50:27 +09:00
? selectedDbSource === "internal"
? tableList.find((table) => table.tableName === formData.tableName)?.displayName ||
formData.tableName
: formData.tableName
: loadingTables || loadingExternalTables
2025-10-20 10:55:33 +09:00
? "로딩 중..."
: "테이블 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
2025-10-20 17:50:27 +09:00
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
2025-10-20 10:55:33 +09:00
<Command>
<CommandInput placeholder="테이블 검색..." />
<CommandList>
<CommandEmpty> .</CommandEmpty>
<CommandGroup>
2025-10-20 17:50:27 +09:00
{selectedDbSource === "internal"
? // 내부 DB 테이블 목록
tableList.map((table) => (
<CommandItem
key={table.tableName}
value={table.tableName}
onSelect={(currentValue) => {
setFormData({ ...formData, tableName: currentValue });
setOpenTableCombobox(false);
}}
>
<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-xs 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);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
formData.tableName === tableName ? "opacity-100" : "opacity-0",
)}
/>
<div>{tableName}</div>
</CommandItem>
))}
2025-10-20 10:55:33 +09:00
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
2025-10-20 17:50:27 +09:00
<p className="mt-1 text-xs text-gray-500">
{selectedDbSource === "internal"
? "이 단계에서 조건을 적용할 테이블을 선택합니다"
: "외부 데이터베이스의 테이블을 선택합니다"}
</p>
2025-10-21 13:19:18 +09:00
</div> */}
{/* ===== 구버전 끝 ===== */}
{/* ===== 신버전: 플로우에서 선택한 테이블 표시만 ===== */}
<div>
<Label> </Label>
<Input value={formData.tableName || "테이블이 지정되지 않았습니다"} disabled className="bg-gray-50" />
<p className="mt-1 text-xs text-gray-500"> ( )</p>
2025-10-20 10:55:33 +09:00
</div>
2025-10-21 13:19:18 +09:00
{/* ===== 신버전 끝 ===== */}
2025-10-20 10:55:33 +09:00
</CardContent>
</Card>
{/* 조건 설정 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
{!formData.tableName ? (
<div className="py-8 text-center text-gray-500"> </div>
) : (
<FlowConditionBuilder
flowId={flowId}
tableName={formData.tableName}
2025-10-21 13:19:18 +09:00
dbSourceType={flowDbSourceType}
dbConnectionId={flowDbConnectionId}
2025-10-20 10:55:33 +09:00
condition={formData.conditionJson}
onChange={(condition) => setFormData({ ...formData, conditionJson: condition })}
/>
)}
</CardContent>
</Card>
2025-10-20 15:53:00 +09:00
{/* 데이터 이동 설정 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* 이동 방식 선택 */}
<div>
<Label> </Label>
<Select
value={formData.moveType}
onValueChange={(value: "status" | "table" | "both") => setFormData({ ...formData, moveType: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="status">
<div>
<div className="font-medium"> </div>
<div className="text-xs text-gray-500"> </div>
</div>
</SelectItem>
<SelectItem value="table">
<div>
<div className="font-medium"> </div>
<div className="text-xs text-gray-500"> </div>
</div>
</SelectItem>
<SelectItem value="both">
<div>
<div className="font-medium"></div>
<div className="text-xs text-gray-500"> + </div>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 상태 변경 설정 (status 또는 both일 때) */}
{(formData.moveType === "status" || formData.moveType === "both") && (
<>
<div>
<Label> </Label>
<Popover open={openStatusColumnCombobox} onOpenChange={setOpenStatusColumnCombobox}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openStatusColumnCombobox}
className="w-full justify-between"
disabled={!formData.tableName || loadingColumns}
>
{loadingColumns
? "컬럼 로딩 중..."
: formData.statusColumn
2025-10-21 14:21:29 +09:00
? (() => {
const col = columns.find(
(c) => (c.column_name || c.columnName) === formData.statusColumn,
);
return col ? col.column_name || col.columnName : formData.statusColumn;
})()
2025-10-20 15:53:00 +09:00
: "상태 컬럼 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." />
<CommandList>
<CommandEmpty> .</CommandEmpty>
<CommandGroup>
2025-10-21 14:21:29 +09:00
{columns.map((column, idx) => {
const columnName = column.column_name || column.columnName || "";
const dataType = column.data_type || column.dataType || "";
return (
<CommandItem
key={`${columnName}-${idx}`}
value={columnName}
onSelect={() => {
setFormData({ ...formData, statusColumn: columnName });
setOpenStatusColumnCombobox(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
formData.statusColumn === columnName ? "opacity-100" : "opacity-0",
)}
/>
<div>
<div>{columnName}</div>
<div className="text-xs text-gray-500">({dataType})</div>
</div>
</CommandItem>
);
})}
2025-10-20 15:53:00 +09:00
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<p className="mt-1 text-xs text-gray-500"> </p>
</div>
<div>
<Label> </Label>
<Input
value={formData.statusValue}
2025-10-20 17:50:27 +09:00
onChange={(e) => {
const newValue = e.target.value;
console.log("💡 statusValue onChange:", newValue);
setFormData({ ...formData, statusValue: newValue });
console.log("✅ Updated formData:", { ...formData, statusValue: newValue });
}}
2025-10-20 15:53:00 +09:00
placeholder="예: approved"
/>
<p className="mt-1 text-xs text-gray-500"> </p>
</div>
</>
)}
{/* 테이블 이동 설정 (table 또는 both일 때) */}
{(formData.moveType === "table" || formData.moveType === "both") && (
<>
<div>
<Label> </Label>
<Select
value={formData.targetTable}
onValueChange={(value) => setFormData({ ...formData, targetTable: value })}
>
<SelectTrigger>
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tableList.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
{table.displayName || table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="mt-1 text-xs text-gray-500"> </p>
</div>
<div className="rounded-md bg-blue-50 p-3">
<p className="text-sm text-blue-900">
💡 . .
</p>
</div>
</>
)}
</CardContent>
</Card>
2025-10-20 17:50:27 +09:00
{/* 외부 DB 연동 설정 */}
<Card>
<CardHeader>
<CardTitle> DB </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label> </Label>
<Select
value={formData.integrationType}
onValueChange={(value: any) => {
setFormData({ ...formData, integrationType: value });
// 타입 변경 시 config 초기화
if (value === "internal") {
setFormData((prev) => ({ ...prev, integrationConfig: undefined }));
}
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{INTEGRATION_TYPE_OPTIONS.map((opt) => (
<SelectItem
key={opt.value}
value={opt.value}
disabled={opt.value !== "internal" && opt.value !== "external_db"}
>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 외부 DB 연동 설정 */}
{formData.integrationType === "external_db" && (
<div className="space-y-4 rounded-lg border p-4">
{externalConnections.length === 0 ? (
<div className="rounded-md bg-yellow-50 p-3">
<p className="text-sm text-yellow-900">
DB . DB .
</p>
</div>
) : (
<>
<div>
<Label> DB </Label>
<Select
value={formData.integrationConfig?.connectionId?.toString() || ""}
onValueChange={(value) => {
const connectionId = parseInt(value);
setFormData({
...formData,
integrationConfig: {
type: "external_db",
connectionId,
operation: "update",
tableName: "",
updateFields: {},
whereCondition: {},
},
});
}}
>
<SelectTrigger>
<SelectValue placeholder="연결 선택" />
</SelectTrigger>
<SelectContent>
{externalConnections.map((conn) => (
<SelectItem key={conn.id} value={conn.id.toString()}>
{conn.name} ({conn.dbType})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{formData.integrationConfig?.connectionId && (
<>
<div>
<Label> </Label>
<Select
value={formData.integrationConfig.operation}
onValueChange={(value: any) =>
setFormData({
...formData,
integrationConfig: {
...formData.integrationConfig!,
operation: value,
},
})
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{OPERATION_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label></Label>
<Input
value={formData.integrationConfig.tableName}
onChange={(e) =>
setFormData({
...formData,
integrationConfig: {
...formData.integrationConfig!,
tableName: e.target.value,
},
})
}
placeholder="예: orders"
/>
</div>
{formData.integrationConfig.operation === "custom" ? (
<div>
<Label> </Label>
<Textarea
value={formData.integrationConfig.customQuery || ""}
onChange={(e) =>
setFormData({
...formData,
integrationConfig: {
...formData.integrationConfig!,
customQuery: e.target.value,
},
})
}
placeholder="UPDATE orders SET status = 'approved' WHERE id = {{dataId}}"
rows={4}
className="font-mono text-sm"
/>
<p className="mt-1 text-xs text-gray-500">
릿 : {`{{dataId}}, {{currentUser}}, {{currentTimestamp}}`}
</p>
</div>
) : (
<>
{(formData.integrationConfig.operation === "update" ||
formData.integrationConfig.operation === "insert") && (
<div>
<Label> (JSON)</Label>
<Textarea
value={JSON.stringify(formData.integrationConfig.updateFields || {}, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
setFormData({
...formData,
integrationConfig: {
...formData.integrationConfig!,
updateFields: parsed,
},
});
} catch (err) {
// JSON 파싱 실패 시 무시
}
}}
placeholder='{"status": "approved", "updated_by": "{{currentUser}}"}'
rows={4}
className="font-mono text-sm"
/>
</div>
)}
{(formData.integrationConfig.operation === "update" ||
formData.integrationConfig.operation === "delete") && (
<div>
<Label>WHERE (JSON)</Label>
<Textarea
value={JSON.stringify(formData.integrationConfig.whereCondition || {}, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
setFormData({
...formData,
integrationConfig: {
...formData.integrationConfig!,
whereCondition: parsed,
},
});
} catch (err) {
// JSON 파싱 실패 시 무시
}
}}
placeholder='{"id": "{{dataId}}"}'
rows={3}
className="font-mono text-sm"
/>
</div>
)}
</>
)}
<div className="rounded-md bg-blue-50 p-3">
<p className="text-sm text-blue-900">
💡 릿 :
<br /> {`{{dataId}}`} - ID
<br /> {`{{currentUser}}`} -
<br /> {`{{currentTimestamp}}`} -
</p>
</div>
</>
)}
</>
)}
</div>
)}
</CardContent>
</Card>
2025-10-20 10:55:33 +09:00
{/* 액션 버튼 */}
<div className="flex gap-2">
<Button className="flex-1" onClick={handleSave}>
<Save className="mr-2 h-4 w-4" />
</Button>
<Button variant="destructive" onClick={handleDelete}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
}