feat: 저장 테이블 정보 및 애니메이션 기능 추가
- 화면 서브 테이블에서 저장 테이블 정보를 추출하는 쿼리 추가 - 저장 테이블 정보 구조를 TableNodeData 인터페이스에 통합 - 저장 테이블의 시각적 표현을 위한 애니메이션 효과 추가 - 필터링 및 참조 관계 뱃지 레이아웃 개선 - 테이블 높이 부드러운 애니메이션 및 스크롤 기능 구현
This commit is contained in:
@@ -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-")) {
|
||||
|
||||
Reference in New Issue
Block a user