Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
8337d7ee60
|
|
@ -214,6 +214,73 @@ router.delete("/:flowId", async (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 플로우 소스 테이블 조회
|
||||||
|
* GET /api/dataflow/node-flows/:flowId/source-table
|
||||||
|
* 플로우의 첫 번째 소스 노드(tableSource, externalDBSource)에서 테이블명 추출
|
||||||
|
*/
|
||||||
|
router.get("/:flowId/source-table", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { flowId } = req.params;
|
||||||
|
|
||||||
|
const flow = await queryOne<{ flow_data: any }>(
|
||||||
|
`SELECT flow_data FROM node_flows WHERE flow_id = $1`,
|
||||||
|
[flowId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!flow) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: "플로우를 찾을 수 없습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const flowData =
|
||||||
|
typeof flow.flow_data === "string"
|
||||||
|
? JSON.parse(flow.flow_data)
|
||||||
|
: flow.flow_data;
|
||||||
|
|
||||||
|
const nodes = flowData.nodes || [];
|
||||||
|
|
||||||
|
// 소스 노드 찾기 (tableSource, externalDBSource 타입)
|
||||||
|
const sourceNode = nodes.find(
|
||||||
|
(node: any) =>
|
||||||
|
node.type === "tableSource" || node.type === "externalDBSource"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sourceNode || !sourceNode.data?.tableName) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
sourceTable: null,
|
||||||
|
sourceNodeType: null,
|
||||||
|
message: "소스 노드가 없거나 테이블명이 설정되지 않았습니다.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`플로우 소스 테이블 조회: flowId=${flowId}, table=${sourceNode.data.tableName}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
sourceTable: sourceNode.data.tableName,
|
||||||
|
sourceNodeType: sourceNode.type,
|
||||||
|
sourceNodeId: sourceNode.id,
|
||||||
|
displayName: sourceNode.data.displayName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("플로우 소스 테이블 조회 실패:", error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "플로우 소스 테이블을 조회하지 못했습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 플로우 실행
|
* 플로우 실행
|
||||||
* POST /api/dataflow/node-flows/:flowId/execute
|
* POST /api/dataflow/node-flows/:flowId/execute
|
||||||
|
|
|
||||||
|
|
@ -51,17 +51,17 @@ export default function DataFlowPage() {
|
||||||
// 에디터 모드일 때는 레이아웃 없이 전체 화면 사용
|
// 에디터 모드일 때는 레이아웃 없이 전체 화면 사용
|
||||||
if (isEditorMode) {
|
if (isEditorMode) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 bg-background">
|
<div className="bg-background fixed inset-0 z-50">
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{/* 에디터 헤더 */}
|
{/* 에디터 헤더 */}
|
||||||
<div className="flex items-center gap-4 border-b bg-background p-4">
|
<div className="bg-background flex items-center gap-4 border-b p-4">
|
||||||
<Button variant="outline" size="sm" onClick={handleBackToList} className="flex items-center gap-2">
|
<Button variant="outline" size="sm" onClick={handleBackToList} className="flex items-center gap-2">
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
목록으로
|
목록으로
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold tracking-tight">노드 플로우 에디터</h1>
|
<h1 className="text-2xl font-bold tracking-tight">노드 플로우 에디터</h1>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="text-muted-foreground mt-1 text-sm">
|
||||||
드래그 앤 드롭으로 데이터 제어 플로우를 시각적으로 설계합니다
|
드래그 앤 드롭으로 데이터 제어 플로우를 시각적으로 설계합니다
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -77,12 +77,12 @@ export default function DataFlowPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col bg-background">
|
<div className="bg-background flex min-h-screen flex-col">
|
||||||
<div className="space-y-6 p-4 sm:p-6">
|
<div className="space-y-6 p-4 sm:p-6">
|
||||||
{/* 페이지 헤더 */}
|
{/* 페이지 헤더 */}
|
||||||
<div className="space-y-2 border-b pb-4">
|
<div className="space-y-2 border-b pb-4">
|
||||||
<h1 className="text-3xl font-bold tracking-tight">제어 관리</h1>
|
<h1 className="text-3xl font-bold tracking-tight">제어 관리</h1>
|
||||||
<p className="text-sm text-muted-foreground">노드 기반 데이터 플로우를 시각적으로 설계하고 관리합니다</p>
|
<p className="text-muted-foreground text-sm">노드 기반 데이터 플로우를 시각적으로 설계하고 관리합니다</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 플로우 목록 */}
|
{/* 플로우 목록 */}
|
||||||
|
|
|
||||||
|
|
@ -120,3 +120,41 @@ export interface NodeExecutionSummary {
|
||||||
duration?: number;
|
duration?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 플로우 소스 테이블 정보 인터페이스
|
||||||
|
*/
|
||||||
|
export interface FlowSourceTableInfo {
|
||||||
|
sourceTable: string | null;
|
||||||
|
sourceNodeType: string | null;
|
||||||
|
sourceNodeId?: string;
|
||||||
|
displayName?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 플로우 소스 테이블 조회
|
||||||
|
* 플로우의 첫 번째 소스 노드(tableSource, externalDBSource)에서 테이블명 추출
|
||||||
|
*/
|
||||||
|
export async function getFlowSourceTable(flowId: number): Promise<FlowSourceTableInfo> {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<ApiResponse<FlowSourceTableInfo>>(
|
||||||
|
`/dataflow/node-flows/${flowId}/source-table`,
|
||||||
|
);
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
sourceTable: null,
|
||||||
|
sourceNodeType: null,
|
||||||
|
message: response.data.message || "소스 테이블 정보를 가져올 수 없습니다.",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("플로우 소스 테이블 조회 실패:", error);
|
||||||
|
return {
|
||||||
|
sourceTable: null,
|
||||||
|
sourceNodeType: null,
|
||||||
|
message: "API 호출 중 오류가 발생했습니다.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1608,6 +1608,66 @@ export class ButtonActionExecutor {
|
||||||
return { handled: false, success: false };
|
return { handled: false, success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🎯 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가)
|
||||||
|
console.log("🔍 [handleUniversalFormModalTableSectionSave] 채번 규칙 할당 체크 시작");
|
||||||
|
|
||||||
|
const fieldsWithNumbering: Record<string, string> = {};
|
||||||
|
|
||||||
|
// commonFieldsData와 modalData에서 채번 규칙이 설정된 필드 찾기
|
||||||
|
for (const [key, value] of Object.entries(modalData)) {
|
||||||
|
if (key.endsWith("_numberingRuleId") && value) {
|
||||||
|
const fieldName = key.replace("_numberingRuleId", "");
|
||||||
|
fieldsWithNumbering[fieldName] = value as string;
|
||||||
|
console.log(`🎯 [handleUniversalFormModalTableSectionSave] 채번 필드 발견: ${fieldName} → 규칙 ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formData에서도 확인 (모달 외부에 있을 수 있음)
|
||||||
|
for (const [key, value] of Object.entries(formData)) {
|
||||||
|
if (key.endsWith("_numberingRuleId") && value && !fieldsWithNumbering[key.replace("_numberingRuleId", "")]) {
|
||||||
|
const fieldName = key.replace("_numberingRuleId", "");
|
||||||
|
fieldsWithNumbering[fieldName] = value as string;
|
||||||
|
console.log(
|
||||||
|
`🎯 [handleUniversalFormModalTableSectionSave] 채번 필드 발견 (formData): ${fieldName} → 규칙 ${value}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📋 [handleUniversalFormModalTableSectionSave] 채번 규칙이 설정된 필드:", fieldsWithNumbering);
|
||||||
|
|
||||||
|
// 🔥 저장 시점에 allocateCode 호출하여 실제 순번 증가
|
||||||
|
if (Object.keys(fieldsWithNumbering).length > 0) {
|
||||||
|
console.log("🎯 [handleUniversalFormModalTableSectionSave] 채번 규칙 할당 시작 (allocateCode 호출)");
|
||||||
|
const { allocateNumberingCode } = await import("@/lib/api/numberingRule");
|
||||||
|
|
||||||
|
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
|
||||||
|
try {
|
||||||
|
console.log(
|
||||||
|
`🔄 [handleUniversalFormModalTableSectionSave] ${fieldName} 필드에 대해 allocateCode 호출: ${ruleId}`,
|
||||||
|
);
|
||||||
|
const allocateResult = await allocateNumberingCode(ruleId);
|
||||||
|
|
||||||
|
if (allocateResult.success && allocateResult.data?.generatedCode) {
|
||||||
|
const newCode = allocateResult.data.generatedCode;
|
||||||
|
console.log(
|
||||||
|
`✅ [handleUniversalFormModalTableSectionSave] ${fieldName} 새 코드 할당: ${commonFieldsData[fieldName]} → ${newCode}`,
|
||||||
|
);
|
||||||
|
commonFieldsData[fieldName] = newCode;
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ [handleUniversalFormModalTableSectionSave] ${fieldName} 코드 할당 실패, 기존 값 유지:`,
|
||||||
|
allocateResult.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (allocateError) {
|
||||||
|
console.error(`❌ [handleUniversalFormModalTableSectionSave] ${fieldName} 코드 할당 오류:`, allocateError);
|
||||||
|
// 오류 시 기존 값 유지
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ [handleUniversalFormModalTableSectionSave] 채번 규칙 할당 완료");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 사용자 정보 추가
|
// 사용자 정보 추가
|
||||||
if (!context.userId) {
|
if (!context.userId) {
|
||||||
|
|
@ -1804,6 +1864,84 @@ export class ButtonActionExecutor {
|
||||||
console.log(`✅ [handleUniversalFormModalTableSectionSave] 완료: ${resultMessage}`);
|
console.log(`✅ [handleUniversalFormModalTableSectionSave] 완료: ${resultMessage}`);
|
||||||
toast.success(`저장 완료: ${resultMessage}`);
|
toast.success(`저장 완료: ${resultMessage}`);
|
||||||
|
|
||||||
|
// 🆕 저장 성공 후 제어 관리 실행 (다중 테이블 저장 시 소스 테이블과 일치하는 섹션만 실행)
|
||||||
|
if (config.enableDataflowControl && config.dataflowConfig?.flowConfig?.flowId) {
|
||||||
|
const flowId = config.dataflowConfig.flowConfig.flowId;
|
||||||
|
console.log("🎯 [handleUniversalFormModalTableSectionSave] 제어 관리 실행 시작:", { flowId });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 플로우 소스 테이블 조회
|
||||||
|
const { getFlowSourceTable } = await import("@/lib/api/nodeFlows");
|
||||||
|
const flowSourceInfo = await getFlowSourceTable(flowId);
|
||||||
|
|
||||||
|
console.log("📊 [handleUniversalFormModalTableSectionSave] 플로우 소스 테이블:", flowSourceInfo);
|
||||||
|
|
||||||
|
if (flowSourceInfo.sourceTable) {
|
||||||
|
// 각 섹션 확인하여 소스 테이블과 일치하는 섹션 찾기
|
||||||
|
let controlExecuted = false;
|
||||||
|
|
||||||
|
for (const [sectionId, sectionItems] of Object.entries(tableSectionData)) {
|
||||||
|
const sectionConfig = sections.find((s: any) => s.id === sectionId);
|
||||||
|
const sectionTargetTable = sectionConfig?.tableConfig?.saveConfig?.targetTable || tableName;
|
||||||
|
|
||||||
|
console.log(`🔍 [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} 테이블 비교:`, {
|
||||||
|
sectionTargetTable,
|
||||||
|
flowSourceTable: flowSourceInfo.sourceTable,
|
||||||
|
isMatch: sectionTargetTable === flowSourceInfo.sourceTable,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 소스 테이블과 일치하는 섹션만 제어 실행
|
||||||
|
if (sectionTargetTable === flowSourceInfo.sourceTable && sectionItems.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`✅ [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} → 플로우 소스 테이블 일치! 제어 실행`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 공통 필드 + 해당 섹션 데이터 병합하여 sourceData 생성
|
||||||
|
const sourceData = sectionItems.map((item: any) => ({
|
||||||
|
...commonFieldsData,
|
||||||
|
...item,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`📦 [handleUniversalFormModalTableSectionSave] 제어 전달 데이터: ${sourceData.length}건`,
|
||||||
|
sourceData[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 제어 관리용 컨텍스트 생성
|
||||||
|
const controlContext: ButtonActionContext = {
|
||||||
|
...context,
|
||||||
|
selectedRowsData: sourceData,
|
||||||
|
formData: commonFieldsData,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 제어 관리 실행
|
||||||
|
await this.executeAfterSaveControl(config, controlContext);
|
||||||
|
controlExecuted = true;
|
||||||
|
break; // 첫 번째 매칭 섹션만 실행
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 매칭되는 섹션이 없으면 메인 테이블 확인
|
||||||
|
if (!controlExecuted && tableName === flowSourceInfo.sourceTable) {
|
||||||
|
console.log("✅ [handleUniversalFormModalTableSectionSave] 메인 테이블 일치! 공통 필드로 제어 실행");
|
||||||
|
|
||||||
|
const controlContext: ButtonActionContext = {
|
||||||
|
...context,
|
||||||
|
selectedRowsData: [commonFieldsData],
|
||||||
|
formData: commonFieldsData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.executeAfterSaveControl(config, controlContext);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("⚠️ [handleUniversalFormModalTableSectionSave] 플로우 소스 테이블 없음 - 제어 스킵");
|
||||||
|
}
|
||||||
|
} catch (controlError) {
|
||||||
|
console.error("❌ [handleUniversalFormModalTableSectionSave] 제어 관리 실행 오류:", controlError);
|
||||||
|
// 제어 관리 실패는 저장 성공에 영향주지 않음
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 저장 성공 이벤트 발생
|
// 저장 성공 이벤트 발생
|
||||||
window.dispatchEvent(new CustomEvent("saveSuccess"));
|
window.dispatchEvent(new CustomEvent("saveSuccess"));
|
||||||
window.dispatchEvent(new CustomEvent("refreshTable"));
|
window.dispatchEvent(new CustomEvent("refreshTable"));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue