Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node
This commit is contained in:
parent
4e12f93da4
commit
0512a3214c
|
|
@ -2830,12 +2830,12 @@ export class NodeFlowExecutionService {
|
|||
inputData: any,
|
||||
context: ExecutionContext
|
||||
): Promise<any> {
|
||||
const { conditions, logic } = node.data;
|
||||
const { conditions, logic, targetLookup } = node.data;
|
||||
|
||||
logger.info(
|
||||
`🔍 조건 노드 실행 - inputData 타입: ${typeof inputData}, 배열 여부: ${Array.isArray(inputData)}, 길이: ${Array.isArray(inputData) ? inputData.length : "N/A"}`
|
||||
);
|
||||
logger.info(`🔍 조건 개수: ${conditions?.length || 0}, 로직: ${logic}`);
|
||||
logger.info(`🔍 조건 개수: ${conditions?.length || 0}, 로직: ${logic}, 타겟조회: ${targetLookup ? targetLookup.tableName : "없음"}`);
|
||||
|
||||
if (inputData) {
|
||||
console.log(
|
||||
|
|
@ -2865,6 +2865,9 @@ export class NodeFlowExecutionService {
|
|||
|
||||
// 배열의 각 항목에 대해 조건 평가 (EXISTS 조건은 비동기)
|
||||
for (const item of inputData) {
|
||||
// 타겟 테이블 조회 (DB 기존값 비교용)
|
||||
const targetRow = await this.lookupTargetRow(targetLookup, item, context);
|
||||
|
||||
const results: boolean[] = [];
|
||||
|
||||
for (const condition of conditions) {
|
||||
|
|
@ -2887,9 +2890,14 @@ export class NodeFlowExecutionService {
|
|||
`🔍 EXISTS 조건: ${condition.field} (${fieldValue}) ${condition.operator} ${condition.lookupTable}.${condition.lookupField} => ${existsResult}`
|
||||
);
|
||||
} else {
|
||||
// 일반 연산자 처리
|
||||
// 비교값 결정: static(고정값) / field(같은 데이터 내 필드) / target(DB 기존값)
|
||||
let compareValue = condition.value;
|
||||
if (condition.valueType === "field") {
|
||||
if (condition.valueType === "target" && targetRow) {
|
||||
compareValue = targetRow[condition.value];
|
||||
logger.info(
|
||||
`🎯 타겟(DB) 비교: ${condition.field} (${fieldValue}) vs DB.${condition.value} (${compareValue})`
|
||||
);
|
||||
} else if (condition.valueType === "field") {
|
||||
compareValue = item[condition.value];
|
||||
logger.info(
|
||||
`🔄 필드 참조 비교: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})`
|
||||
|
|
@ -2931,6 +2939,9 @@ export class NodeFlowExecutionService {
|
|||
}
|
||||
|
||||
// 단일 객체인 경우
|
||||
// 타겟 테이블 조회 (DB 기존값 비교용)
|
||||
const targetRow = await this.lookupTargetRow(targetLookup, inputData, context);
|
||||
|
||||
const results: boolean[] = [];
|
||||
|
||||
for (const condition of conditions) {
|
||||
|
|
@ -2953,9 +2964,14 @@ export class NodeFlowExecutionService {
|
|||
`🔍 EXISTS 조건: ${condition.field} (${fieldValue}) ${condition.operator} ${condition.lookupTable}.${condition.lookupField} => ${existsResult}`
|
||||
);
|
||||
} else {
|
||||
// 일반 연산자 처리
|
||||
// 비교값 결정: static(고정값) / field(같은 데이터 내 필드) / target(DB 기존값)
|
||||
let compareValue = condition.value;
|
||||
if (condition.valueType === "field") {
|
||||
if (condition.valueType === "target" && targetRow) {
|
||||
compareValue = targetRow[condition.value];
|
||||
logger.info(
|
||||
`🎯 타겟(DB) 비교: ${condition.field} (${fieldValue}) vs DB.${condition.value} (${compareValue})`
|
||||
);
|
||||
} else if (condition.valueType === "field") {
|
||||
compareValue = inputData[condition.value];
|
||||
logger.info(
|
||||
`🔄 필드 참조 비교: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})`
|
||||
|
|
@ -2990,6 +3006,63 @@ export class NodeFlowExecutionService {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 노드의 타겟 테이블 조회 (DB 기존값 비교용)
|
||||
* targetLookup 설정이 있을 때, 소스 데이터의 키값으로 DB에서 기존 레코드를 조회
|
||||
*/
|
||||
private static async lookupTargetRow(
|
||||
targetLookup: any,
|
||||
sourceRow: any,
|
||||
context: ExecutionContext
|
||||
): Promise<any | null> {
|
||||
if (!targetLookup?.tableName || !targetLookup?.lookupKeys?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const whereConditions = targetLookup.lookupKeys
|
||||
.map((key: any, idx: number) => `"${key.targetField}" = $${idx + 1}`)
|
||||
.join(" AND ");
|
||||
|
||||
const lookupValues = targetLookup.lookupKeys.map(
|
||||
(key: any) => sourceRow[key.sourceField]
|
||||
);
|
||||
|
||||
// 키값이 비어있으면 조회 불필요
|
||||
if (lookupValues.some((v: any) => v === null || v === undefined || v === "")) {
|
||||
logger.info(`⚠️ 조건 노드 타겟 조회: 키값이 비어있어 스킵`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// company_code 필터링 (멀티테넌시)
|
||||
const companyCode = context.buttonContext?.companyCode || sourceRow.company_code;
|
||||
let sql = `SELECT * FROM "${targetLookup.tableName}" WHERE ${whereConditions}`;
|
||||
const params = [...lookupValues];
|
||||
|
||||
if (companyCode && companyCode !== "*") {
|
||||
sql += ` AND company_code = $${params.length + 1}`;
|
||||
params.push(companyCode);
|
||||
}
|
||||
|
||||
sql += " LIMIT 1";
|
||||
|
||||
logger.info(`🎯 조건 노드 타겟 조회: ${targetLookup.tableName}, 조건: ${whereConditions}, 값: ${JSON.stringify(lookupValues)}`);
|
||||
|
||||
const targetRow = await queryOne(sql, params);
|
||||
|
||||
if (targetRow) {
|
||||
logger.info(`🎯 타겟 데이터 조회 성공`);
|
||||
} else {
|
||||
logger.info(`🎯 타겟 데이터 없음 (신규 레코드)`);
|
||||
}
|
||||
|
||||
return targetRow;
|
||||
} catch (error: any) {
|
||||
logger.warn(`⚠️ 조건 노드 타겟 조회 실패: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EXISTS_IN / NOT_EXISTS_IN 조건 평가
|
||||
* 다른 테이블에 값이 존재하는지 확인
|
||||
|
|
|
|||
|
|
@ -251,6 +251,14 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
|
|||
const [logic, setLogic] = useState<"AND" | "OR">(data.logic || "AND");
|
||||
const [availableFields, setAvailableFields] = useState<FieldDefinition[]>([]);
|
||||
|
||||
// 타겟 조회 설정 (DB 기존값 비교용)
|
||||
const [targetLookup, setTargetLookup] = useState<{
|
||||
tableName: string;
|
||||
tableLabel?: string;
|
||||
lookupKeys: Array<{ sourceField: string; targetField: string; sourceFieldLabel?: string }>;
|
||||
} | undefined>(data.targetLookup);
|
||||
const [targetLookupColumns, setTargetLookupColumns] = useState<ColumnInfo[]>([]);
|
||||
|
||||
// EXISTS 연산자용 상태
|
||||
const [allTables, setAllTables] = useState<TableInfo[]>([]);
|
||||
const [tableColumnsCache, setTableColumnsCache] = useState<Record<string, ColumnInfo[]>>({});
|
||||
|
|
@ -262,8 +270,20 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
|
|||
setDisplayName(data.displayName || "조건 분기");
|
||||
setConditions(data.conditions || []);
|
||||
setLogic(data.logic || "AND");
|
||||
setTargetLookup(data.targetLookup);
|
||||
}, [data]);
|
||||
|
||||
// targetLookup 테이블 변경 시 컬럼 목록 로드
|
||||
useEffect(() => {
|
||||
if (targetLookup?.tableName) {
|
||||
loadTableColumns(targetLookup.tableName).then((cols) => {
|
||||
setTargetLookupColumns(cols);
|
||||
});
|
||||
} else {
|
||||
setTargetLookupColumns([]);
|
||||
}
|
||||
}, [targetLookup?.tableName]);
|
||||
|
||||
// 전체 테이블 목록 로드 (EXISTS 연산자용)
|
||||
useEffect(() => {
|
||||
const loadAllTables = async () => {
|
||||
|
|
@ -559,6 +579,47 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
|
|||
});
|
||||
};
|
||||
|
||||
// 타겟 조회 테이블 변경
|
||||
const handleTargetLookupTableChange = async (tableName: string) => {
|
||||
await ensureTablesLoaded();
|
||||
const tableInfo = allTables.find((t) => t.tableName === tableName);
|
||||
const newLookup = {
|
||||
tableName,
|
||||
tableLabel: tableInfo?.tableLabel || tableName,
|
||||
lookupKeys: targetLookup?.lookupKeys || [],
|
||||
};
|
||||
setTargetLookup(newLookup);
|
||||
updateNode(nodeId, { targetLookup: newLookup });
|
||||
|
||||
// 컬럼 로드
|
||||
const cols = await loadTableColumns(tableName);
|
||||
setTargetLookupColumns(cols);
|
||||
};
|
||||
|
||||
// 타겟 조회 키 필드 변경
|
||||
const handleTargetLookupKeyChange = (sourceField: string, targetField: string) => {
|
||||
if (!targetLookup) return;
|
||||
const sourceFieldInfo = availableFields.find((f) => f.name === sourceField);
|
||||
const newLookup = {
|
||||
...targetLookup,
|
||||
lookupKeys: [{ sourceField, targetField, sourceFieldLabel: sourceFieldInfo?.label || sourceField }],
|
||||
};
|
||||
setTargetLookup(newLookup);
|
||||
updateNode(nodeId, { targetLookup: newLookup });
|
||||
};
|
||||
|
||||
// 타겟 조회 제거
|
||||
const handleRemoveTargetLookup = () => {
|
||||
setTargetLookup(undefined);
|
||||
updateNode(nodeId, { targetLookup: undefined });
|
||||
// target 타입 조건들을 field로 변경
|
||||
const newConditions = conditions.map((c) =>
|
||||
(c as any).valueType === "target" ? { ...c, valueType: "field" } : c
|
||||
);
|
||||
setConditions(newConditions);
|
||||
updateNode(nodeId, { conditions: newConditions });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-4 p-4 pb-8">
|
||||
|
|
@ -597,6 +658,119 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* 타겟 조회 (DB 기존값 비교) */}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold">
|
||||
<Database className="mr-1 inline h-3.5 w-3.5" />
|
||||
타겟 조회 (DB 기존값)
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{!targetLookup ? (
|
||||
<div className="space-y-2">
|
||||
<div className="rounded border border-dashed p-3 text-center text-xs text-gray-400">
|
||||
DB의 기존값과 비교하려면 타겟 테이블을 설정하세요.
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 w-full text-xs"
|
||||
onClick={async () => {
|
||||
await ensureTablesLoaded();
|
||||
setTargetLookup({ tableName: "", lookupKeys: [] });
|
||||
}}
|
||||
>
|
||||
<Database className="mr-1 h-3 w-3" />
|
||||
타겟 조회 설정
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 rounded border bg-orange-50 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-orange-700">타겟 테이블</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleRemoveTargetLookup}
|
||||
className="h-5 px-1 text-xs text-orange-500 hover:text-orange-700"
|
||||
>
|
||||
제거
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 테이블 선택 */}
|
||||
{allTables.length > 0 ? (
|
||||
<TableCombobox
|
||||
tables={allTables}
|
||||
value={targetLookup.tableName}
|
||||
onSelect={handleTargetLookupTableChange}
|
||||
placeholder="비교할 테이블 검색..."
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded border border-dashed bg-gray-50 p-2 text-center text-xs text-gray-400">
|
||||
테이블 로딩 중...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 키 필드 매핑 */}
|
||||
{targetLookup.tableName && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-orange-600">조회 키 (소스 → 타겟)</Label>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Select
|
||||
value={targetLookup.lookupKeys?.[0]?.sourceField || ""}
|
||||
onValueChange={(val) => {
|
||||
const targetField = targetLookup.lookupKeys?.[0]?.targetField || "";
|
||||
handleTargetLookupKeyChange(val, targetField);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 flex-1 text-xs">
|
||||
<SelectValue placeholder="소스 필드" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableFields.map((f) => (
|
||||
<SelectItem key={f.name} value={f.name} className="text-xs">
|
||||
{f.label || f.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-xs text-gray-400">=</span>
|
||||
{targetLookupColumns.length > 0 ? (
|
||||
<Select
|
||||
value={targetLookup.lookupKeys?.[0]?.targetField || ""}
|
||||
onValueChange={(val) => {
|
||||
const sourceField = targetLookup.lookupKeys?.[0]?.sourceField || "";
|
||||
handleTargetLookupKeyChange(sourceField, val);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 flex-1 text-xs">
|
||||
<SelectValue placeholder="타겟 필드" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{targetLookupColumns.map((c) => (
|
||||
<SelectItem key={c.columnName} value={c.columnName} className="text-xs">
|
||||
{c.columnLabel || c.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="flex-1 rounded border border-dashed bg-gray-50 p-1 text-center text-[10px] text-gray-400">
|
||||
컬럼 로딩 중...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded bg-orange-100 p-1.5 text-[10px] text-orange-600">
|
||||
비교 값 타입에서 "타겟 필드 (DB 기존값)"을 선택하면 이 테이블의 기존값과 비교합니다.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 조건식 */}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
|
|
@ -738,15 +912,46 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
|
|||
<SelectContent>
|
||||
<SelectItem value="static">고정값</SelectItem>
|
||||
<SelectItem value="field">필드 참조</SelectItem>
|
||||
{targetLookup?.tableName && (
|
||||
<SelectItem value="target">타겟 필드 (DB 기존값)</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs text-gray-600">
|
||||
{(condition as any).valueType === "field" ? "비교 필드" : "비교 값"}
|
||||
{(condition as any).valueType === "target"
|
||||
? "타겟 필드 (DB 기존값)"
|
||||
: (condition as any).valueType === "field"
|
||||
? "비교 필드"
|
||||
: "비교 값"}
|
||||
</Label>
|
||||
{(condition as any).valueType === "field" ? (
|
||||
{(condition as any).valueType === "target" ? (
|
||||
// 타겟 필드 (DB 기존값): 타겟 테이블 컬럼에서 선택
|
||||
targetLookupColumns.length > 0 ? (
|
||||
<Select
|
||||
value={condition.value as string}
|
||||
onValueChange={(value) => handleConditionChange(index, "value", value)}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-8 text-xs">
|
||||
<SelectValue placeholder="DB 필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{targetLookupColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
<span className="ml-2 text-xs text-gray-400">({col.dataType})</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="mt-1 rounded border border-dashed bg-gray-50 p-2 text-center text-xs text-gray-400">
|
||||
타겟 조회를 먼저 설정하세요
|
||||
</div>
|
||||
)
|
||||
) : (condition as any).valueType === "field" ? (
|
||||
// 필드 참조: 드롭다운으로 선택
|
||||
availableFields.length > 0 ? (
|
||||
<Select
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export interface ConditionNodeData {
|
|||
field: string;
|
||||
operator: ConditionOperator;
|
||||
value: any;
|
||||
valueType?: "static" | "field"; // 비교 값 타입
|
||||
valueType?: "static" | "field" | "target"; // 비교 값 타입 (target: DB 기존값 비교)
|
||||
// EXISTS_IN / NOT_EXISTS_IN 전용 필드
|
||||
lookupTable?: string; // 조회할 테이블명
|
||||
lookupTableLabel?: string; // 조회할 테이블 라벨
|
||||
|
|
@ -127,6 +127,16 @@ export interface ConditionNodeData {
|
|||
}>;
|
||||
logic: "AND" | "OR";
|
||||
displayName?: string;
|
||||
// 타겟 테이블 조회 (DB 기존값과 비교할 때 사용)
|
||||
targetLookup?: {
|
||||
tableName: string;
|
||||
tableLabel?: string;
|
||||
lookupKeys: Array<{
|
||||
sourceField: string; // 소스(폼) 데이터의 키 필드
|
||||
targetField: string; // 타겟(DB) 테이블의 키 필드
|
||||
sourceFieldLabel?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
// 필드 매핑 노드
|
||||
|
|
|
|||
Loading…
Reference in New Issue