Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node

This commit is contained in:
DDD1542 2026-02-11 18:05:32 +09:00
parent 4e12f93da4
commit 0512a3214c
3 changed files with 297 additions and 9 deletions

View File

@ -2830,12 +2830,12 @@ export class NodeFlowExecutionService {
inputData: any, inputData: any,
context: ExecutionContext context: ExecutionContext
): Promise<any> { ): Promise<any> {
const { conditions, logic } = node.data; const { conditions, logic, targetLookup } = node.data;
logger.info( logger.info(
`🔍 조건 노드 실행 - inputData 타입: ${typeof inputData}, 배열 여부: ${Array.isArray(inputData)}, 길이: ${Array.isArray(inputData) ? inputData.length : "N/A"}` `🔍 조건 노드 실행 - 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) { if (inputData) {
console.log( console.log(
@ -2865,6 +2865,9 @@ export class NodeFlowExecutionService {
// 배열의 각 항목에 대해 조건 평가 (EXISTS 조건은 비동기) // 배열의 각 항목에 대해 조건 평가 (EXISTS 조건은 비동기)
for (const item of inputData) { for (const item of inputData) {
// 타겟 테이블 조회 (DB 기존값 비교용)
const targetRow = await this.lookupTargetRow(targetLookup, item, context);
const results: boolean[] = []; const results: boolean[] = [];
for (const condition of conditions) { for (const condition of conditions) {
@ -2887,9 +2890,14 @@ export class NodeFlowExecutionService {
`🔍 EXISTS 조건: ${condition.field} (${fieldValue}) ${condition.operator} ${condition.lookupTable}.${condition.lookupField} => ${existsResult}` `🔍 EXISTS 조건: ${condition.field} (${fieldValue}) ${condition.operator} ${condition.lookupTable}.${condition.lookupField} => ${existsResult}`
); );
} else { } else {
// 일반 연산자 처리 // 비교값 결정: static(고정값) / field(같은 데이터 내 필드) / target(DB 기존값)
let compareValue = condition.value; 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]; compareValue = item[condition.value];
logger.info( logger.info(
`🔄 필드 참조 비교: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})` `🔄 필드 참조 비교: ${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[] = []; const results: boolean[] = [];
for (const condition of conditions) { for (const condition of conditions) {
@ -2953,9 +2964,14 @@ export class NodeFlowExecutionService {
`🔍 EXISTS 조건: ${condition.field} (${fieldValue}) ${condition.operator} ${condition.lookupTable}.${condition.lookupField} => ${existsResult}` `🔍 EXISTS 조건: ${condition.field} (${fieldValue}) ${condition.operator} ${condition.lookupTable}.${condition.lookupField} => ${existsResult}`
); );
} else { } else {
// 일반 연산자 처리 // 비교값 결정: static(고정값) / field(같은 데이터 내 필드) / target(DB 기존값)
let compareValue = condition.value; 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]; compareValue = inputData[condition.value];
logger.info( logger.info(
`🔄 필드 참조 비교: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})` `🔄 필드 참조 비교: ${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 * EXISTS_IN / NOT_EXISTS_IN
* *

View File

@ -251,6 +251,14 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
const [logic, setLogic] = useState<"AND" | "OR">(data.logic || "AND"); const [logic, setLogic] = useState<"AND" | "OR">(data.logic || "AND");
const [availableFields, setAvailableFields] = useState<FieldDefinition[]>([]); 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 연산자용 상태 // EXISTS 연산자용 상태
const [allTables, setAllTables] = useState<TableInfo[]>([]); const [allTables, setAllTables] = useState<TableInfo[]>([]);
const [tableColumnsCache, setTableColumnsCache] = useState<Record<string, ColumnInfo[]>>({}); const [tableColumnsCache, setTableColumnsCache] = useState<Record<string, ColumnInfo[]>>({});
@ -262,8 +270,20 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
setDisplayName(data.displayName || "조건 분기"); setDisplayName(data.displayName || "조건 분기");
setConditions(data.conditions || []); setConditions(data.conditions || []);
setLogic(data.logic || "AND"); setLogic(data.logic || "AND");
setTargetLookup(data.targetLookup);
}, [data]); }, [data]);
// targetLookup 테이블 변경 시 컬럼 목록 로드
useEffect(() => {
if (targetLookup?.tableName) {
loadTableColumns(targetLookup.tableName).then((cols) => {
setTargetLookupColumns(cols);
});
} else {
setTargetLookupColumns([]);
}
}, [targetLookup?.tableName]);
// 전체 테이블 목록 로드 (EXISTS 연산자용) // 전체 테이블 목록 로드 (EXISTS 연산자용)
useEffect(() => { useEffect(() => {
const loadAllTables = async () => { 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 ( return (
<div> <div>
<div className="space-y-4 p-4 pb-8"> <div className="space-y-4 p-4 pb-8">
@ -597,6 +658,119 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
</div> </div>
</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>
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
@ -738,15 +912,46 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps)
<SelectContent> <SelectContent>
<SelectItem value="static"></SelectItem> <SelectItem value="static"></SelectItem>
<SelectItem value="field"> </SelectItem> <SelectItem value="field"> </SelectItem>
{targetLookup?.tableName && (
<SelectItem value="target"> (DB )</SelectItem>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div> <div>
<Label className="text-xs text-gray-600"> <Label className="text-xs text-gray-600">
{(condition as any).valueType === "field" ? "비교 필드" : "비교 값"} {(condition as any).valueType === "target"
? "타겟 필드 (DB 기존값)"
: (condition as any).valueType === "field"
? "비교 필드"
: "비교 값"}
</Label> </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 ? ( availableFields.length > 0 ? (
<Select <Select

View File

@ -118,7 +118,7 @@ export interface ConditionNodeData {
field: string; field: string;
operator: ConditionOperator; operator: ConditionOperator;
value: any; value: any;
valueType?: "static" | "field"; // 비교 값 타입 valueType?: "static" | "field" | "target"; // 비교 값 타입 (target: DB 기존값 비교)
// EXISTS_IN / NOT_EXISTS_IN 전용 필드 // EXISTS_IN / NOT_EXISTS_IN 전용 필드
lookupTable?: string; // 조회할 테이블명 lookupTable?: string; // 조회할 테이블명
lookupTableLabel?: string; // 조회할 테이블 라벨 lookupTableLabel?: string; // 조회할 테이블 라벨
@ -127,6 +127,16 @@ export interface ConditionNodeData {
}>; }>;
logic: "AND" | "OR"; logic: "AND" | "OR";
displayName?: string; displayName?: string;
// 타겟 테이블 조회 (DB 기존값과 비교할 때 사용)
targetLookup?: {
tableName: string;
tableLabel?: string;
lookupKeys: Array<{
sourceField: string; // 소스(폼) 데이터의 키 필드
targetField: string; // 타겟(DB) 테이블의 키 필드
sourceFieldLabel?: string;
}>;
};
} }
// 필드 매핑 노드 // 필드 매핑 노드