feat: 저장 테이블 정보 및 애니메이션 기능 추가

- 화면 서브 테이블에서 저장 테이블 정보를 추출하는 쿼리 추가
- 저장 테이블 정보 구조를 TableNodeData 인터페이스에 통합
- 저장 테이블의 시각적 표현을 위한 애니메이션 효과 추가
- 필터링 및 참조 관계 뱃지 레이아웃 개선
- 테이블 높이 부드러운 애니메이션 및 스크롤 기능 구현
This commit is contained in:
DDD1542
2026-01-09 11:19:30 +09:00
parent b8c8b31033
commit af4072cef1
6 changed files with 602 additions and 59 deletions
@@ -39,7 +39,7 @@ const RELATION_COLORS: Record<VisualRelationType, { stroke: string; strokeLight:
hierarchy: { stroke: '#06b6d4', strokeLight: '#a5f3fc', label: '계층 구조' }, // 시안색
lookup: { stroke: '#f59e0b', strokeLight: '#fcd34d', label: '코드 참조' }, // 주황색 (기존)
mapping: { stroke: '#10b981', strokeLight: '#6ee7b7', label: '데이터 매핑' }, // 녹색
join: { stroke: '#ea580c', strokeLight: '#fdba74', label: '엔티티 조인' }, // 주황색 (진한)
join: { stroke: '#f97316', strokeLight: '#fdba74', label: '엔티티 조인' }, // orange-500 (기존 주황색)
};
// 노드 타입 등록
@@ -51,7 +51,7 @@ const nodeTypes = {
// 레이아웃 상수
const SCREEN_Y = 50; // 화면 노드 Y 위치 (상단)
const TABLE_Y = 420; // 메인 테이블 노드 Y 위치 (중단)
const SUB_TABLE_Y = 690; // 서브 테이블 노드 Y 위치 (하단) - 메인과 270px 간격
const SUB_TABLE_Y = 740; // 서브 테이블 노드 Y 위치 (하단) - 메인과 320px 간격
const NODE_WIDTH = 260; // 노드 너비
const NODE_GAP = 40; // 노드 간격
@@ -493,7 +493,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
subLabel: subLabel,
isMain: true, // mainTableSet의 모든 테이블은 메인
columns: formattedColumns,
// referencedBy, filterColumns는 styledNodes에서 포커스 상태에 따라 동적으로 설정
// referencedBy, filterColumns, saveInfos는 styledNodes에서 포커스 상태에 따라 동적으로 설정
},
});
}
@@ -547,7 +547,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
isMain: false,
columns: formattedColumns,
isFaded: true, // 기본적으로 흐리게 표시 (포커스 시에만 활성화)
// referencedBy, filterColumns는 styledNodes에서 포커스 상태에 따라 동적으로 설정
// referencedBy, filterColumns, saveInfos는 styledNodes에서 포커스 상태에 따라 동적으로 설정
},
});
}
@@ -599,6 +599,104 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
});
}
});
// 필터링 관계일 때 화면 → 필터 대상 테이블 연결선 추가 (점선)
// rightPanelRelation (split-panel-layout의 마스터-디테일) 관계일 때
// + 필터 대상 테이블의 조인 관계도 함께 표시
const filterJoinEdgeSet = new Set<string>(); // 필터 테이블의 조인선 중복 방지
Object.entries(subTablesData).forEach(([screenIdStr, screenSubData]) => {
const sourceScreenId = parseInt(screenIdStr);
screenSubData.subTables.forEach((subTable) => {
// rightPanelRelation (필터 관계)이고, 해당 테이블이 존재하는 경우
// 메인 테이블이든 서브 테이블이든 상관없이 연결선 추가
if (subTable.relationType === 'rightPanelRelation') {
// 테이블 노드 ID 결정: 메인 테이블 영역 또는 서브 테이블 영역
const isFilterTargetMainTable = mainTableSet.has(subTable.tableName);
const isFilterTargetSubTable = subTableSet.has(subTable.tableName);
if (!isFilterTargetMainTable && !isFilterTargetSubTable) return; // 노드가 없으면 스킵
const targetNodeId = isFilterTargetMainTable
? `table-${subTable.tableName}`
: `subtable-${subTable.tableName}`;
// 화면 → 필터 대상 테이블 연결선
newEdges.push({
id: `edge-screen-filter-${sourceScreenId}-${subTable.tableName}`,
source: `screen-${sourceScreenId}`,
target: targetNodeId,
sourceHandle: "bottom",
targetHandle: "top",
type: "smoothstep",
animated: true,
style: {
stroke: "#3b82f6",
strokeWidth: 2,
strokeDasharray: "5,5", // 점선으로 필터 관계 표시
},
data: {
sourceScreenId,
},
});
// 필터 대상 테이블의 조인 관계 (joinColumnRefs)도 조인선으로 표시
// 예: customer_item_mapping → item_info (품목 ID가 item_info.item_number 참조)
if (subTable.joinColumnRefs && subTable.joinColumnRefs.length > 0) {
subTable.joinColumnRefs.forEach((joinRef) => {
const refTable = joinRef.refTable;
if (!refTable) return;
// 참조 테이블이 메인 테이블 또는 서브 테이블에 있는지 확인
const isRefMainTable = mainTableSet.has(refTable);
const isRefSubTable = subTableSet.has(refTable);
if (!isRefMainTable && !isRefSubTable) return;
// 중복 체크 (같은 화면에서 같은 조인 관계 중복 방지)
const joinKey = `${sourceScreenId}-${subTable.tableName}-${refTable}`;
if (filterJoinEdgeSet.has(joinKey)) return;
filterJoinEdgeSet.add(joinKey);
// 소스/타겟 노드 ID 결정
const sourceNodeId = isFilterTargetMainTable
? `table-${subTable.tableName}`
: `subtable-${subTable.tableName}`;
const refTargetNodeId = isRefMainTable
? `table-${refTable}`
: `subtable-${refTable}`;
// 조인선 추가 (초기 스타일 - styledEdges에서 포커싱에 따라 스타일 결정)
newEdges.push({
id: `edge-filter-join-${sourceScreenId}-${subTable.tableName}-${refTable}`,
source: sourceNodeId,
target: refTargetNodeId,
sourceHandle: "bottom",
targetHandle: "bottom_target",
type: "smoothstep",
animated: false,
style: {
stroke: RELATION_COLORS.join.strokeLight, // 초기값 (연한색)
strokeWidth: 1.5,
strokeDasharray: "6,4",
opacity: 0.3,
},
markerEnd: {
type: MarkerType.ArrowClosed,
color: RELATION_COLORS.join.strokeLight
},
data: {
sourceScreenId,
isFilterJoin: true,
visualRelationType: 'join',
},
});
});
}
}
});
});
// 메인 테이블 → 서브 테이블 연결선 생성 (점선)
// 메인 테이블 → 메인 테이블 연결선도 생성 (점선, 연한 주황색)
@@ -978,6 +1076,24 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
}
});
}
// 3. 필터 대상 테이블의 joinColumnRefs가 있으면 해당 참조 테이블도 활성화
// 예: customer_item_mapping → item_info (품목 ID → item_info.item_number)
if (subTable.relationType === 'rightPanelRelation' && subTable.joinColumnRefs) {
subTable.joinColumnRefs.forEach((joinRef) => {
const refTable = joinRef.refTable;
if (refTable && allMainTableSet.has(refTable) && refTable !== focusedSubTablesData.mainTable) {
if (!relatedMainTables[refTable]) {
relatedMainTables[refTable] = { columns: [], displayNames: [] };
}
// 참조 테이블의 컬럼도 추가 (조인 관계 표시용)
if (joinRef.refColumn && !relatedMainTables[refTable].columns.includes(joinRef.refColumn)) {
relatedMainTables[refTable].columns.push(joinRef.refColumn);
relatedMainTables[refTable].displayNames.push(joinRef.columnLabel || joinRef.refColumn);
}
}
});
}
});
}
@@ -1181,6 +1297,9 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
// 조인 컬럼 참조 정보 수집
let focusedJoinColumnRefs: Array<{ column: string; refTable: string; refColumn: string }> = [];
// 포커싱된 화면 기준 저장 정보
let focusedSaveInfos: Array<{ saveType: string; componentType: string; isMainTable: boolean; sourceScreenId?: number }> = [];
if (focusedScreenId !== null && focusedSubTablesData) {
// 포커스된 화면에서 이 테이블이 rightPanelRelation의 서브테이블인 경우
focusedSubTablesData.subTables.forEach((subTable) => {
@@ -1251,6 +1370,20 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
}
});
}
// 포커싱된 화면 기준 저장 정보 추출
if (focusedSubTablesData.saveTables) {
focusedSubTablesData.saveTables.forEach((st) => {
if (st.tableName === tableName) {
focusedSaveInfos.push({
saveType: st.saveType,
componentType: st.componentType,
isMainTable: st.isMainTable,
sourceScreenId: focusedSubTablesData.screenId,
});
}
});
}
}
return {
@@ -1265,6 +1398,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
joinColumnRefs: focusedJoinColumnRefs.length > 0 ? focusedJoinColumnRefs : undefined, // 조인 컬럼 참조 정보
filterColumns: focusedFilterColumns, // 포커스 상태에서만 표시
referencedBy: focusedReferencedBy.length > 0 ? focusedReferencedBy : undefined, // 포커스 상태에서만 표시
saveInfos: focusedSaveInfos.length > 0 ? focusedSaveInfos : undefined, // 포커스 상태에서만 표시
fieldMappings: isFocusedTable ? mainTableFieldMappings : (isRelatedTable ? relatedTableFieldMappings : []),
},
};
@@ -1317,6 +1451,10 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
// reference, source, parentMapping, rightPanelRelation 타입: sourceField = 메인테이블 컬럼, targetField = 서브테이블 컬럼
// lookup 타입: sourceField = 서브테이블 컬럼, targetField = 메인테이블 컬럼 (swap 필요)
let displayFieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> = [];
// 포커싱된 화면 기준 저장 정보 (서브 테이블)
let subTableSaveInfos: Array<{ saveType: string; componentType: string; isMainTable: boolean; sourceScreenId?: number }> = [];
if (isActiveSubTable && focusedSubTablesData) {
const subTableInfo = focusedSubTablesData.subTables.find(st => st.tableName === subTableName);
if (subTableInfo?.fieldMappings) {
@@ -1345,6 +1483,20 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
}
});
}
// 서브 테이블에 대한 저장 정보 추출
if (focusedSubTablesData.saveTables) {
focusedSubTablesData.saveTables.forEach((st) => {
if (st.tableName === subTableName) {
subTableSaveInfos.push({
saveType: st.saveType,
componentType: st.componentType,
isMainTable: st.isMainTable,
sourceScreenId: focusedSubTablesData.screenId,
});
}
});
}
}
return {
@@ -1361,6 +1513,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
highlightedColumns: isActiveSubTable ? subTableHighlightedColumns : [],
joinColumns: isActiveSubTable ? subTableJoinColumns : [],
fieldMappings: isActiveSubTable ? displayFieldMappings : [],
saveInfos: subTableSaveInfos.length > 0 ? subTableSaveInfos : undefined, // 포커스 상태에서만 표시
},
};
}
@@ -1600,8 +1753,8 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
animated: isActive, // 활성화된 것만 애니메이션
style: {
...edge.style,
stroke: isActive ? "#f97316" : "#d1d5db",
strokeWidth: isActive ? 2 : 1,
stroke: isActive ? RELATION_COLORS.join.stroke : "#d1d5db", // 상수 사용
strokeWidth: isActive ? 2.5 : 1,
strokeDasharray: "6,4", // 항상 점선
opacity: isActive ? 1 : 0.2,
},
@@ -1612,6 +1765,59 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId
};
}
// 필터 조인 엣지 (필터 대상 테이블 → 조인 참조 테이블)
// 규격: 해당 화면이 포커싱됐을 때만 활성화
if (edge.id.startsWith("edge-filter-join-")) {
const edgeSourceScreenId = (edge.data as any)?.sourceScreenId;
// 포커스가 없으면 흐리게 표시
if (focusedScreenId === null) {
return {
...edge,
animated: false,
style: {
...edge.style,
stroke: RELATION_COLORS.join.strokeLight,
strokeWidth: 1.5,
strokeDasharray: "6,4",
opacity: 0.3,
},
markerEnd: {
type: MarkerType.ArrowClosed,
color: RELATION_COLORS.join.strokeLight,
},
};
}
// 포커스된 화면과 일치하는지 확인
const isMyConnection = edgeSourceScreenId === focusedScreenId;
if (!isMyConnection) {
// 다른 화면의 필터 조인 엣지는 숨김
return {
...edge,
hidden: true,
};
}
// 내 화면의 필터 조인 엣지는 활성화
return {
...edge,
animated: true,
style: {
...edge.style,
stroke: RELATION_COLORS.join.stroke,
strokeWidth: 2,
strokeDasharray: "6,4",
opacity: 1,
},
markerEnd: {
type: MarkerType.ArrowClosed,
color: RELATION_COLORS.join.stroke,
},
};
}
// 메인 테이블 → 메인 테이블 연결선 (서브테이블 구간 통과)
// 규격: bottom → bottom_target 고정 (아래쪽 서브테이블 선 구간을 통해 연결)
if (edge.source.startsWith("table-") && edge.target.startsWith("table-") && edge.id.startsWith("edge-main-main-")) {