diff --git a/backend-node/src/routes/dataflow/node-flows.ts b/backend-node/src/routes/dataflow/node-flows.ts
index 6de84866..177b4304 100644
--- a/backend-node/src/routes/dataflow/node-flows.ts
+++ b/backend-node/src/routes/dataflow/node-flows.ts
@@ -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
diff --git a/frontend/app/(main)/admin/systemMng/dataflow/page.tsx b/frontend/app/(main)/admin/systemMng/dataflow/page.tsx
index 87d937ec..d55a6cf1 100644
--- a/frontend/app/(main)/admin/systemMng/dataflow/page.tsx
+++ b/frontend/app/(main)/admin/systemMng/dataflow/page.tsx
@@ -51,17 +51,17 @@ export default function DataFlowPage() {
// 에디터 모드일 때는 레이아웃 없이 전체 화면 사용
if (isEditorMode) {
return (
-
+
{/* 에디터 헤더 */}
-
+
노드 플로우 에디터
-
+
드래그 앤 드롭으로 데이터 제어 플로우를 시각적으로 설계합니다
@@ -77,12 +77,12 @@ export default function DataFlowPage() {
}
return (
-
+
{/* 페이지 헤더 */}
제어 관리
-
노드 기반 데이터 플로우를 시각적으로 설계하고 관리합니다
+
노드 기반 데이터 플로우를 시각적으로 설계하고 관리합니다
{/* 플로우 목록 */}
diff --git a/frontend/lib/api/nodeFlows.ts b/frontend/lib/api/nodeFlows.ts
index b42340d7..27bb1b96 100644
--- a/frontend/lib/api/nodeFlows.ts
+++ b/frontend/lib/api/nodeFlows.ts
@@ -120,3 +120,41 @@ export interface NodeExecutionSummary {
duration?: number;
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
{
+ try {
+ const response = await apiClient.get>(
+ `/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 호출 중 오류가 발생했습니다.",
+ };
+ }
+}
diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts
index 9a6a606e..327cb87f 100644
--- a/frontend/lib/utils/buttonActions.ts
+++ b/frontend/lib/utils/buttonActions.ts
@@ -1608,6 +1608,66 @@ export class ButtonActionExecutor {
return { handled: false, success: false };
}
+ // 🎯 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가)
+ console.log("🔍 [handleUniversalFormModalTableSectionSave] 채번 규칙 할당 체크 시작");
+
+ const fieldsWithNumbering: Record = {};
+
+ // 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 {
// 사용자 정보 추가
if (!context.userId) {
@@ -1804,6 +1864,84 @@ export class ButtonActionExecutor {
console.log(`✅ [handleUniversalFormModalTableSectionSave] 완료: ${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("refreshTable"));