조건 그룹핑 구현
This commit is contained in:
parent
dbad9bbc0c
commit
41f40ac216
|
|
@ -5,19 +5,21 @@ const prisma = new PrismaClient();
|
||||||
|
|
||||||
// 조건 노드 타입 정의
|
// 조건 노드 타입 정의
|
||||||
interface ConditionNode {
|
interface ConditionNode {
|
||||||
type: "group" | "condition";
|
id: string; // 고유 ID
|
||||||
operator?: "AND" | "OR";
|
type: "condition" | "group-start" | "group-end";
|
||||||
children?: ConditionNode[];
|
|
||||||
field?: string;
|
field?: string;
|
||||||
operator_type?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
|
operator_type?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
|
||||||
value?: any;
|
value?: any;
|
||||||
dataType?: string;
|
dataType?: string;
|
||||||
|
logicalOperator?: "AND" | "OR"; // 다음 조건과의 논리 연산자
|
||||||
|
groupId?: string; // 그룹 ID (group-start와 group-end가 같은 groupId를 가짐)
|
||||||
|
groupLevel?: number; // 중첩 레벨 (0, 1, 2, ...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 조건 제어 정보
|
// 조건 제어 정보
|
||||||
interface ConditionControl {
|
interface ConditionControl {
|
||||||
triggerType: "insert" | "update" | "delete" | "insert_update";
|
triggerType: "insert" | "update" | "delete" | "insert_update";
|
||||||
conditionTree: ConditionNode | null;
|
conditionTree: ConditionNode | ConditionNode[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 연결 카테고리 정보
|
// 연결 카테고리 정보
|
||||||
|
|
@ -237,34 +239,105 @@ export class EventTriggerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 조건 평가
|
* 조건 평가 (플랫 구조 + 그룹핑 지원)
|
||||||
*/
|
*/
|
||||||
private static async evaluateCondition(
|
private static async evaluateCondition(
|
||||||
condition: ConditionNode,
|
condition: ConditionNode | ConditionNode[],
|
||||||
data: Record<string, any>
|
data: Record<string, any>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (condition.type === "group") {
|
// 단일 조건인 경우 (하위 호환성)
|
||||||
if (!condition.children || condition.children.length === 0) {
|
if (!Array.isArray(condition)) {
|
||||||
|
if (condition.type === "condition") {
|
||||||
|
return this.evaluateSingleCondition(condition, data);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(
|
// 조건 배열인 경우 (새로운 그룹핑 시스템)
|
||||||
condition.children.map((child) => this.evaluateCondition(child, data))
|
return this.evaluateConditionList(condition, data);
|
||||||
);
|
|
||||||
|
|
||||||
if (condition.operator === "OR") {
|
|
||||||
return results.some((result) => result);
|
|
||||||
} else {
|
|
||||||
// AND
|
|
||||||
return results.every((result) => result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건 리스트 평가 (괄호 그룹핑 지원)
|
||||||
|
*/
|
||||||
|
private static async evaluateConditionList(
|
||||||
|
conditions: ConditionNode[],
|
||||||
|
data: Record<string, any>
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (conditions.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 조건을 평가 가능한 표현식으로 변환
|
||||||
|
const expression = await this.buildConditionExpression(conditions, data);
|
||||||
|
|
||||||
|
// 표현식 평가
|
||||||
|
return this.evaluateExpression(expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건들을 평가 가능한 표현식으로 변환
|
||||||
|
*/
|
||||||
|
private static async buildConditionExpression(
|
||||||
|
conditions: ConditionNode[],
|
||||||
|
data: Record<string, any>
|
||||||
|
): Promise<string> {
|
||||||
|
const tokens: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < conditions.length; i++) {
|
||||||
|
const condition = conditions[i];
|
||||||
|
|
||||||
|
if (condition.type === "group-start") {
|
||||||
|
// 이전 조건과의 논리 연산자 추가
|
||||||
|
if (i > 0 && condition.logicalOperator) {
|
||||||
|
tokens.push(condition.logicalOperator);
|
||||||
|
}
|
||||||
|
tokens.push("(");
|
||||||
|
} else if (condition.type === "group-end") {
|
||||||
|
tokens.push(")");
|
||||||
} else if (condition.type === "condition") {
|
} else if (condition.type === "condition") {
|
||||||
return this.evaluateSingleCondition(condition, data);
|
// 이전 조건과의 논리 연산자 추가
|
||||||
|
if (i > 0 && condition.logicalOperator) {
|
||||||
|
tokens.push(condition.logicalOperator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 조건 평가 결과를 토큰으로 추가
|
||||||
|
const result = await this.evaluateSingleCondition(condition, data);
|
||||||
|
tokens.push(result.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 논리 표현식 평가 (괄호 우선순위 지원)
|
||||||
|
*/
|
||||||
|
private static evaluateExpression(expression: string): boolean {
|
||||||
|
try {
|
||||||
|
// 안전한 논리 표현식 평가
|
||||||
|
// true/false와 AND/OR/괄호만 포함된 표현식을 평가
|
||||||
|
const sanitizedExpression = expression
|
||||||
|
.replace(/\bAND\b/g, "&&")
|
||||||
|
.replace(/\bOR\b/g, "||")
|
||||||
|
.replace(/\btrue\b/g, "true")
|
||||||
|
.replace(/\bfalse\b/g, "false");
|
||||||
|
|
||||||
|
// 보안을 위해 허용된 문자만 확인
|
||||||
|
if (!/^[true|false|\s|&|\||\(|\)]+$/.test(sanitizedExpression)) {
|
||||||
|
logger.warn(`Invalid expression: ${expression}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function constructor를 사용한 안전한 평가
|
||||||
|
const result = new Function(`return ${sanitizedExpression}`)();
|
||||||
|
return Boolean(result);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error evaluating expression: ${expression}`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 액션별 조건들 평가 (AND/OR 연산자 지원)
|
* 액션별 조건들 평가 (AND/OR 연산자 지원)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,7 @@ interface DataSaveSettings {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
actionType: "insert" | "update" | "delete" | "upsert";
|
actionType: "insert" | "update" | "delete" | "upsert";
|
||||||
conditions?: Array<{
|
conditions?: ConditionNode[];
|
||||||
field: string;
|
|
||||||
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
|
|
||||||
value: string;
|
|
||||||
logicalOperator?: "AND" | "OR";
|
|
||||||
}>;
|
|
||||||
fieldMappings: Array<{
|
fieldMappings: Array<{
|
||||||
sourceTable?: string;
|
sourceTable?: string;
|
||||||
sourceField: string;
|
sourceField: string;
|
||||||
|
|
@ -353,14 +348,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
? {
|
? {
|
||||||
control: {
|
control: {
|
||||||
triggerType: "insert",
|
triggerType: "insert",
|
||||||
conditionTree:
|
conditionTree: conditions.length > 0 ? conditions : null,
|
||||||
conditions.length > 0
|
|
||||||
? {
|
|
||||||
type: "group" as const,
|
|
||||||
operator: "AND" as const,
|
|
||||||
children: conditions,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
type: config.connectionType,
|
type: config.connectionType,
|
||||||
|
|
@ -446,19 +434,87 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
return config.connectionType === "data-save" || config.connectionType === "external-call";
|
return config.connectionType === "data-save" || config.connectionType === "external-call";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 고유 ID 생성 헬퍼
|
||||||
|
const generateId = () => `cond_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
// 조건 관리 헬퍼 함수들
|
// 조건 관리 헬퍼 함수들
|
||||||
const addCondition = () => {
|
const addCondition = () => {
|
||||||
const newCondition: ConditionNode = {
|
const newCondition: ConditionNode = {
|
||||||
|
id: generateId(),
|
||||||
type: "condition",
|
type: "condition",
|
||||||
field: "",
|
field: "",
|
||||||
operator_type: "=",
|
operator_type: "=",
|
||||||
value: "",
|
value: "",
|
||||||
dataType: "string",
|
dataType: "string",
|
||||||
operator: "AND", // 기본값으로 AND 설정
|
logicalOperator: "AND", // 기본값으로 AND 설정
|
||||||
};
|
};
|
||||||
setConditions([...conditions, newCondition]);
|
setConditions([...conditions, newCondition]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 그룹 시작 추가
|
||||||
|
const addGroupStart = () => {
|
||||||
|
const groupId = `group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const groupLevel = getNextGroupLevel();
|
||||||
|
|
||||||
|
const groupStart: ConditionNode = {
|
||||||
|
id: generateId(),
|
||||||
|
type: "group-start",
|
||||||
|
groupId,
|
||||||
|
groupLevel,
|
||||||
|
logicalOperator: conditions.length > 0 ? "AND" : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
setConditions([...conditions, groupStart]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 그룹 끝 추가
|
||||||
|
const addGroupEnd = () => {
|
||||||
|
// 가장 최근에 열린 그룹 찾기
|
||||||
|
const openGroups = findOpenGroups();
|
||||||
|
if (openGroups.length === 0) {
|
||||||
|
toast.error("닫을 그룹이 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastOpenGroup = openGroups[openGroups.length - 1];
|
||||||
|
const groupEnd: ConditionNode = {
|
||||||
|
id: generateId(),
|
||||||
|
type: "group-end",
|
||||||
|
groupId: lastOpenGroup.groupId,
|
||||||
|
groupLevel: lastOpenGroup.groupLevel,
|
||||||
|
};
|
||||||
|
|
||||||
|
setConditions([...conditions, groupEnd]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 다음 그룹 레벨 계산
|
||||||
|
const getNextGroupLevel = (): number => {
|
||||||
|
const openGroups = findOpenGroups();
|
||||||
|
return openGroups.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 열린 그룹 찾기
|
||||||
|
const findOpenGroups = () => {
|
||||||
|
const openGroups: Array<{ groupId: string; groupLevel: number }> = [];
|
||||||
|
|
||||||
|
for (const condition of conditions) {
|
||||||
|
if (condition.type === "group-start") {
|
||||||
|
openGroups.push({
|
||||||
|
groupId: condition.groupId!,
|
||||||
|
groupLevel: condition.groupLevel!,
|
||||||
|
});
|
||||||
|
} else if (condition.type === "group-end") {
|
||||||
|
// 해당 그룹 제거
|
||||||
|
const groupIndex = openGroups.findIndex((g) => g.groupId === condition.groupId);
|
||||||
|
if (groupIndex !== -1) {
|
||||||
|
openGroups.splice(groupIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return openGroups;
|
||||||
|
};
|
||||||
|
|
||||||
const updateCondition = (index: number, field: keyof ConditionNode, value: string) => {
|
const updateCondition = (index: number, field: keyof ConditionNode, value: string) => {
|
||||||
const updatedConditions = [...conditions];
|
const updatedConditions = [...conditions];
|
||||||
updatedConditions[index] = { ...updatedConditions[index], [field]: value };
|
updatedConditions[index] = { ...updatedConditions[index], [field]: value };
|
||||||
|
|
@ -466,8 +522,379 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeCondition = (index: number) => {
|
const removeCondition = (index: number) => {
|
||||||
|
const conditionToRemove = conditions[index];
|
||||||
|
|
||||||
|
// 그룹 시작/끝을 삭제하는 경우 해당 그룹 전체 삭제
|
||||||
|
if (conditionToRemove.type === "group-start" || conditionToRemove.type === "group-end") {
|
||||||
|
removeGroup(conditionToRemove.groupId!);
|
||||||
|
} else {
|
||||||
const updatedConditions = conditions.filter((_, i) => i !== index);
|
const updatedConditions = conditions.filter((_, i) => i !== index);
|
||||||
setConditions(updatedConditions);
|
setConditions(updatedConditions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 그룹 전체 삭제
|
||||||
|
const removeGroup = (groupId: string) => {
|
||||||
|
const updatedConditions = conditions.filter((c) => c.groupId !== groupId);
|
||||||
|
setConditions(updatedConditions);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 현재 조건의 그룹 레벨 계산
|
||||||
|
const getCurrentGroupLevel = (conditionIndex: number): number => {
|
||||||
|
let level = 0;
|
||||||
|
for (let i = 0; i < conditionIndex; i++) {
|
||||||
|
const condition = conditions[i];
|
||||||
|
if (condition.type === "group-start") {
|
||||||
|
level++;
|
||||||
|
} else if (condition.type === "group-end") {
|
||||||
|
level--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 액션별 조건 그룹 관리 함수들
|
||||||
|
const addActionGroupStart = (actionIndex: number) => {
|
||||||
|
const groupId = `action_group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const currentConditions = dataSaveSettings.actions[actionIndex].conditions || [];
|
||||||
|
const groupLevel = getActionNextGroupLevel(currentConditions);
|
||||||
|
|
||||||
|
const groupStart: ConditionNode = {
|
||||||
|
id: generateId(),
|
||||||
|
type: "group-start",
|
||||||
|
groupId,
|
||||||
|
groupLevel,
|
||||||
|
logicalOperator: currentConditions.length > 0 ? "AND" : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions = [...currentConditions, groupStart];
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
};
|
||||||
|
|
||||||
|
const addActionGroupEnd = (actionIndex: number) => {
|
||||||
|
const currentConditions = dataSaveSettings.actions[actionIndex].conditions || [];
|
||||||
|
const openGroups = findActionOpenGroups(currentConditions);
|
||||||
|
|
||||||
|
if (openGroups.length === 0) {
|
||||||
|
toast.error("닫을 그룹이 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastOpenGroup = openGroups[openGroups.length - 1];
|
||||||
|
const groupEnd: ConditionNode = {
|
||||||
|
id: generateId(),
|
||||||
|
type: "group-end",
|
||||||
|
groupId: lastOpenGroup.groupId,
|
||||||
|
groupLevel: lastOpenGroup.groupLevel,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions = [...currentConditions, groupEnd];
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 액션별 다음 그룹 레벨 계산
|
||||||
|
const getActionNextGroupLevel = (conditions: ConditionNode[]): number => {
|
||||||
|
const openGroups = findActionOpenGroups(conditions);
|
||||||
|
return openGroups.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 액션별 열린 그룹 찾기
|
||||||
|
const findActionOpenGroups = (conditions: ConditionNode[]) => {
|
||||||
|
const openGroups: Array<{ groupId: string; groupLevel: number }> = [];
|
||||||
|
|
||||||
|
for (const condition of conditions) {
|
||||||
|
if (condition.type === "group-start") {
|
||||||
|
openGroups.push({
|
||||||
|
groupId: condition.groupId!,
|
||||||
|
groupLevel: condition.groupLevel!,
|
||||||
|
});
|
||||||
|
} else if (condition.type === "group-end") {
|
||||||
|
const groupIndex = openGroups.findIndex((g) => g.groupId === condition.groupId);
|
||||||
|
if (groupIndex !== -1) {
|
||||||
|
openGroups.splice(groupIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return openGroups;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 액션별 현재 조건의 그룹 레벨 계산
|
||||||
|
const getActionCurrentGroupLevel = (conditions: ConditionNode[], conditionIndex: number): number => {
|
||||||
|
let level = 0;
|
||||||
|
for (let i = 0; i < conditionIndex; i++) {
|
||||||
|
const condition = conditions[i];
|
||||||
|
if (condition.type === "group-start") {
|
||||||
|
level++;
|
||||||
|
} else if (condition.type === "group-end") {
|
||||||
|
level--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 액션별 조건 렌더링 함수
|
||||||
|
const renderActionCondition = (condition: ConditionNode, condIndex: number, actionIndex: number) => {
|
||||||
|
// 그룹 시작 렌더링
|
||||||
|
if (condition.type === "group-start") {
|
||||||
|
return (
|
||||||
|
<div key={condition.id} className="flex items-center gap-2">
|
||||||
|
{condIndex > 0 && (
|
||||||
|
<Select
|
||||||
|
value={dataSaveSettings.actions[actionIndex].conditions![condIndex - 1]?.logicalOperator || "AND"}
|
||||||
|
onValueChange={(value: "AND" | "OR") => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions![condIndex - 1].logicalOperator = value;
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 w-24 border-green-200 bg-green-50 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="AND">AND</SelectItem>
|
||||||
|
<SelectItem value="OR">OR</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 rounded border-2 border-dashed border-green-300 bg-green-50/50 p-1"
|
||||||
|
style={{ marginLeft: `${(condition.groupLevel || 0) * 15}px` }}
|
||||||
|
>
|
||||||
|
<span className="font-mono text-xs text-green-600">(</span>
|
||||||
|
<span className="text-xs text-green-600">그룹 시작</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions = newActions[actionIndex].conditions!.filter(
|
||||||
|
(c) => c.groupId !== condition.groupId,
|
||||||
|
);
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
className="h-4 w-4 p-0"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-2 w-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 그룹 끝 렌더링
|
||||||
|
if (condition.type === "group-end") {
|
||||||
|
return (
|
||||||
|
<div key={condition.id} className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 rounded border-2 border-dashed border-green-300 bg-green-50/50 p-1"
|
||||||
|
style={{ marginLeft: `${(condition.groupLevel || 0) * 15}px` }}
|
||||||
|
>
|
||||||
|
<span className="font-mono text-xs text-green-600">)</span>
|
||||||
|
<span className="text-xs text-green-600">그룹 끝</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions = newActions[actionIndex].conditions!.filter(
|
||||||
|
(c) => c.groupId !== condition.groupId,
|
||||||
|
);
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
className="h-4 w-4 p-0"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-2 w-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 일반 조건 렌더링 (기존 로직 간소화)
|
||||||
|
return (
|
||||||
|
<div key={condition.id} className="flex items-center gap-2">
|
||||||
|
{condIndex > 0 && (
|
||||||
|
<Select
|
||||||
|
value={dataSaveSettings.actions[actionIndex].conditions![condIndex - 1]?.logicalOperator || "AND"}
|
||||||
|
onValueChange={(value: "AND" | "OR") => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions![condIndex - 1].logicalOperator = value;
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 w-24 border-green-200 bg-green-50 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="AND">AND</SelectItem>
|
||||||
|
<SelectItem value="OR">OR</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="flex flex-1 items-center gap-2 rounded border bg-white p-1"
|
||||||
|
style={{
|
||||||
|
marginLeft: `${getActionCurrentGroupLevel(dataSaveSettings.actions[actionIndex].conditions || [], condIndex) * 15}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={condition.field || ""}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions![condIndex].field = value;
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 flex-1 text-xs">
|
||||||
|
<SelectValue placeholder="필드" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{fromTableColumns.map((column) => (
|
||||||
|
<SelectItem key={column.columnName} value={column.columnName}>
|
||||||
|
{column.columnName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={condition.operator_type || "="}
|
||||||
|
onValueChange={(value: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE") => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions![condIndex].operator_type = value;
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 w-20 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="=">=</SelectItem>
|
||||||
|
<SelectItem value="!=">!=</SelectItem>
|
||||||
|
<SelectItem value=">">></SelectItem>
|
||||||
|
<SelectItem value="<"><</SelectItem>
|
||||||
|
<SelectItem value=">=">>=</SelectItem>
|
||||||
|
<SelectItem value="<="><=</SelectItem>
|
||||||
|
<SelectItem value="LIKE">LIKE</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{/* 데이터 타입에 따른 동적 입력 컴포넌트 */}
|
||||||
|
{(() => {
|
||||||
|
const selectedColumn = fromTableColumns.find((col) => col.columnName === condition.field);
|
||||||
|
const dataType = selectedColumn?.dataType?.toLowerCase() || "string";
|
||||||
|
|
||||||
|
if (dataType.includes("timestamp") || dataType.includes("datetime") || dataType.includes("date")) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
value={condition.value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
className="h-6 flex-1 text-xs"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (dataType.includes("time")) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
value={condition.value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
className="h-6 flex-1 text-xs"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (dataType.includes("date")) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={condition.value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
className="h-6 flex-1 text-xs"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
dataType.includes("int") ||
|
||||||
|
dataType.includes("numeric") ||
|
||||||
|
dataType.includes("decimal") ||
|
||||||
|
dataType.includes("float") ||
|
||||||
|
dataType.includes("double")
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="숫자"
|
||||||
|
value={condition.value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
className="h-6 flex-1 text-xs"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (dataType.includes("bool")) {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={condition.value || ""}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions![condIndex].value = value;
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 flex-1 text-xs">
|
||||||
|
<SelectValue placeholder="선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="true">TRUE</SelectItem>
|
||||||
|
<SelectItem value="false">FALSE</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
placeholder="값"
|
||||||
|
value={condition.value || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
className="h-6 flex-1 text-xs"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
const newActions = [...dataSaveSettings.actions];
|
||||||
|
newActions[actionIndex].conditions = newActions[actionIndex].conditions!.filter(
|
||||||
|
(_, i) => i !== condIndex,
|
||||||
|
);
|
||||||
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
|
}}
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-2 w-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 조건부 연결 설정 UI 렌더링
|
// 조건부 연결 설정 UI 렌더링
|
||||||
|
|
@ -483,10 +910,18 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<Label className="text-sm font-medium">실행 조건</Label>
|
<Label className="text-sm font-medium">실행 조건</Label>
|
||||||
|
<div className="flex gap-1">
|
||||||
<Button size="sm" variant="outline" onClick={() => addCondition()} className="h-7 text-xs">
|
<Button size="sm" variant="outline" onClick={() => addCondition()} className="h-7 text-xs">
|
||||||
<Plus className="mr-1 h-3 w-3" />
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
조건 추가
|
조건 추가
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => addGroupStart()} className="h-7 text-xs">
|
||||||
|
그룹 시작 (
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => addGroupEnd()} className="h-7 text-xs">
|
||||||
|
그룹 끝 )
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 조건 목록 */}
|
{/* 조건 목록 */}
|
||||||
|
|
@ -498,13 +933,16 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
조건이 없으면 항상 실행됩니다.
|
조건이 없으면 항상 실행됩니다.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
conditions.map((condition, index) => (
|
conditions.map((condition, index) => {
|
||||||
<div key={index} className="flex items-center gap-2 rounded border bg-white p-2">
|
// 그룹 시작 렌더링
|
||||||
{/* 첫 번째 조건이 아닐 때 AND/OR 연산자를 앞에 표시 */}
|
if (condition.type === "group-start") {
|
||||||
|
return (
|
||||||
|
<div key={condition.id} className="flex items-center gap-2">
|
||||||
|
{/* 이전 조건과의 논리 연산자 */}
|
||||||
{index > 0 && (
|
{index > 0 && (
|
||||||
<Select
|
<Select
|
||||||
value={conditions[index - 1]?.operator || "AND"}
|
value={conditions[index - 1]?.logicalOperator || "AND"}
|
||||||
onValueChange={(value: "AND" | "OR") => updateCondition(index - 1, "operator", value)}
|
onValueChange={(value: "AND" | "OR") => updateCondition(index - 1, "logicalOperator", value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 w-24 border-blue-200 bg-blue-50 text-xs">
|
<SelectTrigger className="h-8 w-24 border-blue-200 bg-blue-50 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
|
@ -516,7 +954,74 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 조건 필드들 */}
|
{/* 그룹 레벨에 따른 들여쓰기 */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 rounded border-2 border-dashed border-blue-300 bg-blue-50/50 p-2"
|
||||||
|
style={{ marginLeft: `${(condition.groupLevel || 0) * 20}px` }}
|
||||||
|
>
|
||||||
|
<span className="font-mono text-sm text-blue-600">(</span>
|
||||||
|
<span className="text-xs text-blue-600">그룹 시작</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => removeCondition(index)}
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 그룹 끝 렌더링
|
||||||
|
if (condition.type === "group-end") {
|
||||||
|
return (
|
||||||
|
<div key={condition.id} className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 rounded border-2 border-dashed border-blue-300 bg-blue-50/50 p-2"
|
||||||
|
style={{ marginLeft: `${(condition.groupLevel || 0) * 20}px` }}
|
||||||
|
>
|
||||||
|
<span className="font-mono text-sm text-blue-600">)</span>
|
||||||
|
<span className="text-xs text-blue-600">그룹 끝</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => removeCondition(index)}
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 일반 조건 렌더링
|
||||||
|
return (
|
||||||
|
<div key={condition.id} className="flex items-center gap-2">
|
||||||
|
{/* 이전 조건과의 논리 연산자 */}
|
||||||
|
{index > 0 && (
|
||||||
|
<Select
|
||||||
|
value={conditions[index - 1]?.logicalOperator || "AND"}
|
||||||
|
onValueChange={(value: "AND" | "OR") => updateCondition(index - 1, "logicalOperator", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 w-24 border-blue-200 bg-blue-50 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="AND">AND</SelectItem>
|
||||||
|
<SelectItem value="OR">OR</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 그룹 레벨에 따른 들여쓰기와 조건 필드들 */}
|
||||||
|
<div
|
||||||
|
className="flex flex-1 items-center gap-2 rounded border bg-white p-2"
|
||||||
|
style={{ marginLeft: `${getCurrentGroupLevel(index) * 20}px` }}
|
||||||
|
>
|
||||||
|
{/* 조건 필드 선택 */}
|
||||||
<Select
|
<Select
|
||||||
value={condition.field || ""}
|
value={condition.field || ""}
|
||||||
onValueChange={(value) => updateCondition(index, "field", value)}
|
onValueChange={(value) => updateCondition(index, "field", value)}
|
||||||
|
|
@ -533,6 +1038,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
{/* 연산자 선택 */}
|
||||||
<Select
|
<Select
|
||||||
value={condition.operator_type || "="}
|
value={condition.operator_type || "="}
|
||||||
onValueChange={(value) => updateCondition(index, "operator_type", value)}
|
onValueChange={(value) => updateCondition(index, "operator_type", value)}
|
||||||
|
|
@ -551,12 +1057,16 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{/* 데이터 타입에 따른 적절한 입력 컴포넌트 */}
|
{/* 데이터 타입에 따른 동적 입력 컴포넌트 */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const selectedColumn = fromTableColumns.find((col) => col.columnName === condition.field);
|
const selectedColumn = fromTableColumns.find((col) => col.columnName === condition.field);
|
||||||
const dataType = selectedColumn?.dataType?.toLowerCase() || "string";
|
const dataType = selectedColumn?.dataType?.toLowerCase() || "string";
|
||||||
|
|
||||||
if (dataType.includes("timestamp") || dataType.includes("datetime") || dataType.includes("date")) {
|
if (
|
||||||
|
dataType.includes("timestamp") ||
|
||||||
|
dataType.includes("datetime") ||
|
||||||
|
dataType.includes("date")
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
|
|
@ -626,11 +1136,14 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
{/* 삭제 버튼 */}
|
||||||
<Button size="sm" variant="ghost" onClick={() => removeCondition(index)} className="h-8 w-8 p-0">
|
<Button size="sm" variant="ghost" onClick={() => removeCondition(index)} className="h-8 w-8 p-0">
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -795,6 +1308,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
</summary>
|
</summary>
|
||||||
<div className="mt-2 space-y-2 border-l-2 border-gray-100 pl-4">
|
<div className="mt-2 space-y-2 border-l-2 border-gray-100 pl-4">
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -805,7 +1319,15 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
}
|
}
|
||||||
newActions[actionIndex].conditions = [
|
newActions[actionIndex].conditions = [
|
||||||
...(newActions[actionIndex].conditions || []),
|
...(newActions[actionIndex].conditions || []),
|
||||||
{ field: "", operator: "=", value: "", logicalOperator: "AND" },
|
{
|
||||||
|
id: generateId(),
|
||||||
|
type: "condition",
|
||||||
|
field: "",
|
||||||
|
operator_type: "=",
|
||||||
|
value: "",
|
||||||
|
dataType: "string",
|
||||||
|
logicalOperator: "AND",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
||||||
}}
|
}}
|
||||||
|
|
@ -814,192 +1336,29 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
<Plus className="mr-1 h-2 w-2" />
|
<Plus className="mr-1 h-2 w-2" />
|
||||||
조건 추가
|
조건 추가
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => addActionGroupStart(actionIndex)}
|
||||||
|
className="h-6 text-xs"
|
||||||
|
>
|
||||||
|
그룹 시작 (
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => addActionGroupEnd(actionIndex)}
|
||||||
|
className="h-6 text-xs"
|
||||||
|
>
|
||||||
|
그룹 끝 )
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{action.conditions && action.conditions.length > 0 && (
|
{action.conditions && action.conditions.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{action.conditions.map((condition, condIndex) => (
|
{action.conditions.map((condition, condIndex) =>
|
||||||
<div key={condIndex} className="flex items-center gap-2">
|
renderActionCondition(condition, condIndex, actionIndex),
|
||||||
{/* 첫 번째 조건이 아닐 때 AND/OR 연산자를 앞에 표시 */}
|
|
||||||
{condIndex > 0 && (
|
|
||||||
<Select
|
|
||||||
value={action.conditions![condIndex - 1].logicalOperator || "AND"}
|
|
||||||
onValueChange={(value: "AND" | "OR") => {
|
|
||||||
const newActions = [...dataSaveSettings.actions];
|
|
||||||
newActions[actionIndex].conditions![condIndex - 1].logicalOperator = value;
|
|
||||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-6 w-20 border-green-200 bg-green-50 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="AND">AND</SelectItem>
|
|
||||||
<SelectItem value="OR">OR</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 조건 필드들 */}
|
|
||||||
<Select
|
|
||||||
value={condition.field}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const newActions = [...dataSaveSettings.actions];
|
|
||||||
newActions[actionIndex].conditions![condIndex].field = value;
|
|
||||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-6 flex-1 text-xs">
|
|
||||||
<SelectValue placeholder="필드" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{fromTableColumns.map((column) => (
|
|
||||||
<SelectItem key={column.columnName} value={column.columnName}>
|
|
||||||
{column.columnName}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Select
|
|
||||||
value={condition.operator}
|
|
||||||
onValueChange={(value: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE") => {
|
|
||||||
const newActions = [...dataSaveSettings.actions];
|
|
||||||
newActions[actionIndex].conditions![condIndex].operator = value;
|
|
||||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-6 w-16 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="=">=</SelectItem>
|
|
||||||
<SelectItem value="!=">!=</SelectItem>
|
|
||||||
<SelectItem value=">">></SelectItem>
|
|
||||||
<SelectItem value="<"><</SelectItem>
|
|
||||||
<SelectItem value=">=">>=</SelectItem>
|
|
||||||
<SelectItem value="<="><=</SelectItem>
|
|
||||||
<SelectItem value="LIKE">LIKE</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{/* 데이터 타입에 따른 적절한 입력 컴포넌트 */}
|
|
||||||
{(() => {
|
|
||||||
const selectedColumn = fromTableColumns.find(
|
|
||||||
(col) => col.columnName === condition.field,
|
|
||||||
);
|
|
||||||
const dataType = selectedColumn?.dataType?.toLowerCase() || "string";
|
|
||||||
|
|
||||||
if (
|
|
||||||
dataType.includes("timestamp") ||
|
|
||||||
dataType.includes("datetime") ||
|
|
||||||
dataType.includes("date")
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
type="datetime-local"
|
|
||||||
value={condition.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newActions = [...dataSaveSettings.actions];
|
|
||||||
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
|
||||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
||||||
}}
|
|
||||||
className="h-6 flex-1 text-xs"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (dataType.includes("time")) {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
type="time"
|
|
||||||
value={condition.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newActions = [...dataSaveSettings.actions];
|
|
||||||
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
|
||||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
||||||
}}
|
|
||||||
className="h-6 flex-1 text-xs"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (dataType.includes("date")) {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={condition.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newActions = [...dataSaveSettings.actions];
|
|
||||||
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
|
||||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
||||||
}}
|
|
||||||
className="h-6 flex-1 text-xs"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
dataType.includes("int") ||
|
|
||||||
dataType.includes("numeric") ||
|
|
||||||
dataType.includes("decimal") ||
|
|
||||||
dataType.includes("float") ||
|
|
||||||
dataType.includes("double")
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="숫자"
|
|
||||||
value={condition.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newActions = [...dataSaveSettings.actions];
|
|
||||||
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
|
||||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
||||||
}}
|
|
||||||
className="h-6 flex-1 text-xs"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (dataType.includes("bool")) {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
value={condition.value}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const newActions = [...dataSaveSettings.actions];
|
|
||||||
newActions[actionIndex].conditions![condIndex].value = value;
|
|
||||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-6 flex-1 text-xs">
|
|
||||||
<SelectValue placeholder="선택" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="true">TRUE</SelectItem>
|
|
||||||
<SelectItem value="false">FALSE</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
placeholder="값"
|
|
||||||
value={condition.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newActions = [...dataSaveSettings.actions];
|
|
||||||
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
|
||||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
||||||
}}
|
|
||||||
className="h-6 flex-1 text-xs"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
const newActions = [...dataSaveSettings.actions];
|
|
||||||
newActions[actionIndex].conditions = newActions[
|
|
||||||
actionIndex
|
|
||||||
].conditions!.filter((_, i) => i !== condIndex);
|
|
||||||
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
||||||
}}
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-2 w-2" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,19 @@ import { apiClient, ApiResponse } from "./client";
|
||||||
// 조건부 연결 관련 타입들
|
// 조건부 연결 관련 타입들
|
||||||
export interface ConditionControl {
|
export interface ConditionControl {
|
||||||
triggerType: "insert" | "update" | "delete" | "insert_update";
|
triggerType: "insert" | "update" | "delete" | "insert_update";
|
||||||
conditionTree: ConditionNode;
|
conditionTree: ConditionNode | ConditionNode[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConditionNode {
|
export interface ConditionNode {
|
||||||
type: "group" | "condition";
|
id: string; // 고유 ID
|
||||||
operator?: "AND" | "OR";
|
type: "condition" | "group-start" | "group-end";
|
||||||
children?: ConditionNode[];
|
|
||||||
field?: string;
|
field?: string;
|
||||||
operator_type?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
|
operator_type?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
|
||||||
value?: any;
|
value?: any;
|
||||||
dataType?: string;
|
dataType?: string;
|
||||||
|
logicalOperator?: "AND" | "OR"; // 다음 조건과의 논리 연산자
|
||||||
|
groupId?: string; // 그룹 ID (group-start와 group-end가 같은 groupId를 가짐)
|
||||||
|
groupLevel?: number; // 중첩 레벨 (0, 1, 2, ...)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConnectionCategory {
|
export interface ConnectionCategory {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue