From b279f8d58ddca14a3dc62c9a460845fe3b1d7716 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Wed, 7 Jan 2026 14:50:03 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=99=94=EB=A9=B4=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20?= =?UTF-8?q?=EC=84=9C=EB=B8=8C=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면 선택 시 그룹을 재설정하지 않도록 로직 개선하여 데이터 재로드 방지 - 테이블 노드 데이터 구조에 필드 매핑 정보 추가 - 서브 테이블과 조인 관계를 시각화하기 위한 컬럼 강조 및 스타일링 개선 - 화면 관계 흐름에서 서브 테이블 연결선 강조 기능 추가 - 사용 컬럼 및 조인 컬럼 정보를 화면별로 매핑하여 관리 --- .../admin/screenMng/screenMngList/page.tsx | 7 +- frontend/components/screen/ScreenNode.tsx | 181 ++++- .../components/screen/ScreenRelationFlow.tsx | 744 ++++++++++++++++-- frontend/lib/api/screenGroup.ts | 15 +- 4 files changed, 861 insertions(+), 86 deletions(-) diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 3e895ff8..1157810d 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -160,8 +160,11 @@ export default function ScreenManagementPage() { setFocusedScreenIdInGroup(null); // 포커스 초기화 }} onScreenSelectInGroup={(group, screenId) => { - // 그룹 내 화면 클릭 시: 그룹 선택 + 해당 화면 포커스 - setSelectedGroup(group); + // 그룹 내 화면 클릭 시: 해당 화면 포커스 + // 이미 같은 그룹이 선택된 상태라면 그룹을 다시 설정하지 않음 (데이터 재로드 방지) + if (selectedGroup?.id !== group.id) { + setSelectedGroup(group); + } setSelectedScreen(null); setFocusedScreenIdInGroup(screenId); }} diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx index 07ddf8f0..97326bde 100644 --- a/frontend/components/screen/ScreenNode.tsx +++ b/frontend/components/screen/ScreenNode.tsx @@ -33,17 +33,33 @@ export interface ScreenNodeData { screenRole?: string; // 화면 역할 (메인그리드, 등록폼 등) } +// 필드 매핑 정보 (조인 관계 표시용) +export interface FieldMappingDisplay { + sourceField: string; // 메인 테이블 컬럼 (예: manager_id) + targetField: string; // 서브 테이블 컬럼 (예: user_id) + sourceDisplayName?: string; // 메인 테이블 한글 컬럼명 (예: 담당자) + targetDisplayName?: string; // 서브 테이블 한글 컬럼명 (예: 사용자ID) +} + // 테이블 노드 데이터 인터페이스 export interface TableNodeData { label: string; subLabel?: string; isMain?: boolean; + isFocused?: boolean; // 포커스된 테이블인지 + isFaded?: boolean; // 흑백 처리할지 columns?: Array<{ - name: string; + name: string; // 표시용 이름 (한글명) + originalName?: string; // 원본 컬럼명 (영문, 필터링용) type: string; isPrimaryKey?: boolean; isForeignKey?: boolean; }>; + // 포커스 시 강조할 컬럼 정보 + highlightedColumns?: string[]; // 화면에서 사용하는 컬럼 (영문명) + joinColumns?: string[]; // 조인에 사용되는 컬럼 + // 필드 매핑 정보 (조인 관계 표시용) + fieldMappings?: FieldMappingDisplay[]; // 서브 테이블일 때 조인 관계 표시 } // ========== 유틸리티 함수 ========== @@ -404,13 +420,60 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: // ========== 테이블 노드 (하단) - 컬럼 목록 표시 (컴팩트) ========== export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { - const { label, subLabel, isMain, columns } = data; - // 최대 5개 컬럼만 표시 - const displayColumns = columns?.slice(0, 5) || []; - const remainingCount = (columns?.length || 0) - 5; + const { label, subLabel, isMain, isFocused, isFaded, columns, highlightedColumns, joinColumns, fieldMappings } = data; + + // 강조할 컬럼 세트 (영문 컬럼명 기준) + const highlightSet = new Set(highlightedColumns || []); + const joinSet = new Set(joinColumns || []); + + // 필드 매핑 맵 생성 (targetField → { sourceField, sourceDisplayName }) + // 서브 테이블에서 targetField가 어떤 메인 테이블 컬럼(sourceField)과 연결되는지 + const fieldMappingMap = new Map(); + if (fieldMappings) { + fieldMappings.forEach(mapping => { + fieldMappingMap.set(mapping.targetField, { + sourceField: mapping.sourceField, + // 한글명이 있으면 한글명, 없으면 영문명 사용 + sourceDisplayName: mapping.sourceDisplayName || mapping.sourceField, + }); + }); + } + + // 포커스 모드: 사용 컬럼만 필터링하여 표시 + // originalName (영문) 또는 name으로 매칭 시도 + const potentialFilteredColumns = columns?.filter(col => { + const colOriginal = col.originalName || col.name; + return highlightSet.has(colOriginal) || joinSet.has(colOriginal); + }) || []; + const hasActiveColumns = potentialFilteredColumns.length > 0; + + // 표시할 컬럼: + // - 포커스 시 (활성 컬럼 있음): 필터된 컬럼만 표시 + // - 비포커스 시: 최대 8개만 표시 + const MAX_DEFAULT_COLUMNS = 8; + const allColumns = columns || []; + const displayColumns = hasActiveColumns + ? potentialFilteredColumns + : allColumns.slice(0, MAX_DEFAULT_COLUMNS); + const remainingCount = hasActiveColumns + ? 0 + : Math.max(0, allColumns.length - MAX_DEFAULT_COLUMNS); + const totalCount = allColumns.length; return ( -
+
{/* Handles */} = ({ data }) => { /> {/* 헤더 (초록색, 컴팩트) */} -
+
{label}
{subLabel &&
{subLabel}
}
+ {hasActiveColumns && ( + + {displayColumns.length}개 활성 + + )}
- {/* 컬럼 목록 (컴팩트) */} -
+ {/* 컬럼 목록 - 컴팩트하게 (스크롤 가능) */} +
{displayColumns.length > 0 ? (
- {displayColumns.map((col, idx) => ( -
- {/* PK/FK 아이콘 */} - {col.isPrimaryKey && } - {col.isForeignKey && !col.isPrimaryKey && } - {!col.isPrimaryKey && !col.isForeignKey &&
} + {displayColumns.map((col, idx) => { + const colOriginal = col.originalName || col.name; + const isJoinColumn = joinSet.has(colOriginal); + const isHighlighted = highlightSet.has(colOriginal); + + return ( +
+ {/* PK/FK/조인 아이콘 */} + {isJoinColumn && } + {!isJoinColumn && col.isPrimaryKey && } + {!isJoinColumn && col.isForeignKey && !col.isPrimaryKey && } + {!isJoinColumn && !col.isPrimaryKey && !col.isForeignKey &&
} - {/* 컬럼명 */} - - {col.name} - + {/* 컬럼명 */} + + {col.name} + - {/* 타입 */} - {col.type} -
- ))} + {/* 역할 태그 + 참조 관계 표시 */} + {isJoinColumn && ( + <> + {/* 참조 관계 표시: ← 한글 컬럼명 (또는 영문) */} + {fieldMappingMap.has(colOriginal) && ( + + ← {fieldMappingMap.get(colOriginal)?.sourceDisplayName} + + )} + 조인 + + )} + {isHighlighted && !isJoinColumn && ( + 사용 + )} + {/* 타입 */} + {col.type} +
+ ); + })} + {/* 더 많은 컬럼이 있을 경우 표시 */} {remainingCount > 0 && ( -
+ {remainingCount}개 더
+
+ + {remainingCount}개 더 +
)}
) : ( @@ -486,9 +595,25 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
PostgreSQL {columns && ( - {columns.length}개 컬럼 + + {hasActiveColumns ? `${displayColumns.length}/${totalCount}` : totalCount}개 컬럼 + )}
+ + {/* CSS 애니메이션 정의 */} +
); }; diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index da677eb0..e8d9d259 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -38,8 +38,8 @@ const nodeTypes = { // 레이아웃 상수 const SCREEN_Y = 50; // 화면 노드 Y 위치 (상단) -const TABLE_Y = 520; // 메인 테이블 노드 Y 위치 (중단) -const SUB_TABLE_Y = 780; // 서브 테이블 노드 Y 위치 (하단) +const TABLE_Y = 420; // 메인 테이블 노드 Y 위치 (중단) - 위로 이동 +const SUB_TABLE_Y = 680; // 서브 테이블 노드 Y 위치 (하단) - 위로 이동 const NODE_WIDTH = 260; // 노드 너비 const NODE_GAP = 40; // 노드 간격 @@ -61,19 +61,42 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId const [loading, setLoading] = useState(false); const [tableColumns, setTableColumns] = useState>({}); + // ReactFlow 인스턴스 (fitView 제어용) + const reactFlowInstance = useReactFlow(); + + // 데이터 로드 버전 (초기 로드 시에만 fitView 호출) + const [dataLoadVersion, setDataLoadVersion] = useState(0); + + // 뷰 준비 상태 (fitView 완료 후 true로 설정하여 깜빡임 방지) + const [isViewReady, setIsViewReady] = useState(false); + // 그룹 내 포커스된 화면 ID (그룹 모드에서만 사용) const [focusedScreenId, setFocusedScreenId] = useState(null); + // 그룹 또는 화면이 변경될 때 포커스 초기화 + useEffect(() => { + setFocusedScreenId(null); + }, [selectedGroup?.id, screen?.screenId]); + // 외부에서 전달된 초기 포커스 ID 적용 (화면 이동 없이 강조만) useEffect(() => { - if (initialFocusedScreenId !== undefined && initialFocusedScreenId !== null) { + if (initialFocusedScreenId !== undefined) { setFocusedScreenId(initialFocusedScreenId); } }, [initialFocusedScreenId]); // 화면 ID와 테이블명 매핑 (포커스 시 연결선 강조용) const [screenTableMap, setScreenTableMap] = useState>({}); + + // 화면 ID별 서브 테이블 매핑 (포커스 시 서브 테이블 연결선 강조용) + const [screenSubTableMap, setScreenSubTableMap] = useState>({}); + + // 서브 테이블 데이터 저장 (조인 컬럼 정보 포함) + const [subTablesDataMap, setSubTablesDataMap] = useState>({}); + + // 화면별 사용 컬럼 매핑 (화면 ID -> 테이블명 -> 사용 컬럼들) + const [screenUsedColumnsMap, setScreenUsedColumnsMap] = useState>>({}); // 테이블 컬럼 정보 로드 const loadTableColumns = useCallback( @@ -95,11 +118,8 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId }, [tableColumns] ); - - // 그룹 변경 시 focusedScreenId 초기화 - useEffect(() => { - setFocusedScreenId(null); - }, [selectedGroup?.id, screen?.screenId]); + + // 중복 useEffect 제거됨 (위에서 이미 선언) // 데이터 로드 및 노드/엣지 생성 useEffect(() => { @@ -112,6 +132,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId const loadRelations = async () => { setLoading(true); + setIsViewReady(false); // 뷰 준비 상태 초기화 try { let screenList: ScreenDefinition[] = []; @@ -209,10 +230,57 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId if (subTablesRes.success && subTablesRes.data) { subTablesData = subTablesRes.data as Record; + // 서브 테이블 데이터 저장 (조인 컬럼 정보 포함) + setSubTablesDataMap(subTablesData); } } catch (e) { console.error("레이아웃 요약/서브 테이블 로드 실패:", e); } + + // 화면별 사용 컬럼 정보 추출 (layoutSummaries에서) + const usedColumnsMap: Record> = {}; + screenList.forEach((screenItem) => { + const layout = layoutSummaries[screenItem.screenId]; + if (layout && layout.layoutItems) { + const mainTable = screenItem.tableName; + if (mainTable) { + // layoutItems에서 사용 컬럼과 조인 컬럼 추출 + const allUsedColumns: string[] = []; + const allJoinColumns: string[] = []; + + layout.layoutItems.forEach((item) => { + // usedColumns 배열에서 추출 (columns_config에서 가져온 컬럼명) + if (item.usedColumns && Array.isArray(item.usedColumns)) { + item.usedColumns.forEach((col) => { + if (col && !allUsedColumns.includes(col)) { + allUsedColumns.push(col); + } + }); + } + // joinColumns 배열에서 추출 (isEntityJoin = true인 컬럼) + if (item.joinColumns && Array.isArray(item.joinColumns)) { + item.joinColumns.forEach((col) => { + if (col && !allJoinColumns.includes(col)) { + allJoinColumns.push(col); + } + }); + } + // 하위 호환성: bindField도 사용 컬럼에 추가 + if (item.bindField && !allUsedColumns.includes(item.bindField)) { + allUsedColumns.push(item.bindField); + } + }); + + if (!usedColumnsMap[screenItem.screenId]) { + usedColumnsMap[screenItem.screenId] = {}; + } + // 사용 컬럼과 조인 컬럼을 별도 키로 저장 + usedColumnsMap[screenItem.screenId][mainTable] = allUsedColumns; + usedColumnsMap[screenItem.screenId][`${mainTable}__join`] = allJoinColumns; + } + } + }); + setScreenUsedColumnsMap(usedColumnsMap); // ========== 상단: 화면 노드들 ========== const screenNodes: ScreenNodeType[] = []; @@ -295,14 +363,28 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // 서브 테이블 수집 (componentConfig에서 추출된 테이블들) // 서브 테이블은 메인 테이블과 다른 테이블들 - Object.values(subTablesData).forEach((screenSubData) => { + // 화면별 서브 테이블 매핑도 함께 구축 + const newScreenSubTableMap: Record = {}; + + Object.entries(subTablesData).forEach(([screenIdStr, screenSubData]) => { + const screenId = parseInt(screenIdStr); + const subTableNames: string[] = []; + screenSubData.subTables.forEach((subTable) => { // 메인 테이블에 없는 것만 서브 테이블로 추가 if (!mainTableSet.has(subTable.tableName)) { subTableSet.add(subTable.tableName); + subTableNames.push(subTable.tableName); } }); + + if (subTableNames.length > 0) { + newScreenSubTableMap[screenId] = subTableNames; + } }); + + // 화면별 서브 테이블 매핑 저장 + setScreenSubTableMap(newScreenSubTableMap); // 메인 테이블 노드 배치 (화면들의 중앙 아래에 배치) const mainTableList = Array.from(mainTableSet); @@ -315,12 +397,20 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId const mainTableTotalWidth = mainTableList.length * NODE_WIDTH + (mainTableList.length - 1) * NODE_GAP; const mainTableStartX = screenCenterX - mainTableTotalWidth / 2; - // 첫 번째 화면의 테이블 또는 선택된 화면의 테이블 - const primaryTableName = screen?.tableName || (screenList.length > 0 ? screenList[0].tableName : null); + // 각 테이블이 어떤 화면들의 메인 테이블인지 매핑 + const tableToScreensMap = new Map(); + screenList.forEach((scr: any) => { + if (scr.tableName) { + const screens = tableToScreensMap.get(scr.tableName) || []; + screens.push(scr.screenName); + tableToScreensMap.set(scr.tableName, screens); + } + }); for (let idx = 0; idx < mainTableList.length; idx++) { const tableName = mainTableList[idx]; - const isPrimaryTable = tableName === primaryTableName; + // mainTableSet에 있는 테이블은 모두 해당 화면의 "메인 테이블" + const linkedScreens = tableToScreensMap.get(tableName) || []; // 컬럼 정보 로드 let columns: ColumnTypeInfo[] = []; @@ -330,22 +420,28 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // ignore } - // 컬럼 정보를 PK/FK 표시와 함께 변환 - const formattedColumns = columns.slice(0, 8).map((col) => ({ + // 컬럼 정보를 PK/FK 표시와 함께 변환 (전체 컬럼 저장, 표시는 TableNode에서 제한) + const formattedColumns = columns.map((col) => ({ name: col.displayName || col.columnName || "", + originalName: col.columnName || "", // 영문 컬럼명 (필터링용) type: col.dataType || "", isPrimaryKey: col.isPrimaryKey || col.columnName === "id", isForeignKey: !!col.referenceTable || (col.columnName?.includes("_id") && col.columnName !== "id"), })); + // 여러 화면이 같은 테이블 사용하면 "공통 메인 테이블", 아니면 "메인 테이블" + const subLabel = linkedScreens.length > 1 + ? `메인 테이블 (${linkedScreens.length}개 화면)` + : "메인 테이블"; + tableNodes.push({ id: `table-${tableName}`, type: "tableNode", position: { x: mainTableStartX + idx * (NODE_WIDTH + NODE_GAP), y: TABLE_Y }, data: { label: tableName, - subLabel: isPrimaryTable ? "메인 테이블" : "조인 테이블", - isMain: isPrimaryTable, + subLabel: subLabel, + isMain: true, // mainTableSet의 모든 테이블은 메인 columns: formattedColumns, }, }); @@ -370,9 +466,10 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // ignore } - // 컬럼 정보를 PK/FK 표시와 함께 변환 - const formattedColumns = columns.slice(0, 5).map((col) => ({ + // 컬럼 정보를 PK/FK 표시와 함께 변환 (전체 컬럼 저장, 표시는 TableNode에서 제한) + const formattedColumns = columns.map((col) => ({ name: col.displayName || col.columnName || "", + originalName: col.columnName || "", // 영문 컬럼명 (필터링용) type: col.dataType || "", isPrimaryKey: col.isPrimaryKey || col.columnName === "id", isForeignKey: !!col.referenceTable || (col.columnName?.includes("_id") && col.columnName !== "id"), @@ -398,6 +495,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId subLabel: `서브 테이블 (${relationType})`, isMain: false, columns: formattedColumns, + isFaded: true, // 기본적으로 흐리게 표시 (포커스 시에만 활성화) }, }); } @@ -431,9 +529,9 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId } // 각 화면 → 해당 메인 테이블 연결선 생성 (실선) - screenList.forEach((scr, idx) => { + // 모든 화면-테이블 연결은 동일한 스타일 (각 화면의 메인 테이블이므로) + screenList.forEach((scr) => { if (scr.tableName && mainTableSet.has(scr.tableName)) { - const isMain = screen ? scr.screenId === screen.screenId : idx === 0; newEdges.push({ id: `edge-screen-table-${scr.screenId}`, source: `screen-${scr.screenId}`, @@ -441,18 +539,19 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId sourceHandle: "bottom", targetHandle: "top", type: "smoothstep", - animated: isMain, // 메인 화면만 애니메이션 + animated: true, // 모든 메인 테이블 연결은 애니메이션 style: { - stroke: isMain ? "#3b82f6" : "#94a3b8", - strokeWidth: isMain ? 2 : 1.5, - strokeDasharray: isMain ? undefined : "5,5", // 보조 연결은 점선 + stroke: "#3b82f6", + strokeWidth: 2, }, }); } }); - // 메인 테이블 → 서브 테이블 연결선 생성 (점선) - Object.values(subTablesData).forEach((screenSubData) => { + // 메인 테이블 → 서브 테이블 연결선 생성 (점선 + 애니메이션) + // 화면별 서브 테이블 연결을 추적하기 위해 screenId 정보도 엣지 ID에 포함 + Object.entries(subTablesData).forEach(([screenIdStr, screenSubData]) => { + const sourceScreenId = parseInt(screenIdStr); const mainTable = screenSubData.mainTable; if (!mainTable || !mainTableSet.has(mainTable)) return; @@ -460,8 +559,8 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // 서브 테이블 노드가 실제로 생성되었는지 확인 if (!subTableSet.has(subTable.tableName)) return; - // 중복 엣지 방지 - const edgeId = `edge-main-sub-${mainTable}-${subTable.tableName}`; + // 화면별로 고유한 엣지 ID (같은 서브 테이블이라도 다른 화면에서 사용하면 별도 엣지) + const edgeId = `edge-main-sub-${sourceScreenId}-${mainTable}-${subTable.tableName}`; const exists = newEdges.some((e) => e.id === edgeId); if (exists) return; @@ -479,15 +578,19 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId targetHandle: "top", type: "smoothstep", label: relationLabel, - labelStyle: { fontSize: 9, fill: "#f97316", fontWeight: 500 }, + labelStyle: { fontSize: 9, fill: "#94a3b8", fontWeight: 500 }, // 기본 흐린 색상 labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 }, labelBgPadding: [3, 2] as [number, number], - markerEnd: { type: MarkerType.ArrowClosed, color: "#f97316" }, + markerEnd: { type: MarkerType.ArrowClosed, color: "#94a3b8" }, // 기본 흐린 색상 + animated: false, // 기본: 애니메이션 비활성화 (포커스 시에만 활성화) style: { - stroke: "#f97316", - strokeWidth: 1.5, + stroke: "#94a3b8", // 기본 흐린 색상 + strokeWidth: 1, strokeDasharray: "6,4", // 점선 + opacity: 0.5, // 기본 투명도 }, + // 화면 ID 정보를 data에 저장 (styledEdges에서 활용) + data: { sourceScreenId }, }); }); }); @@ -580,6 +683,9 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId setNodes(allNodes); setEdges(newEdges); + + // 데이터 로드 완료 후 버전 증가 (fitView 트리거용) + setDataLoadVersion((prev) => prev + 1); } catch (error) { console.error("관계 데이터 로드 실패:", error); } finally { @@ -591,6 +697,20 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // focusedScreenId는 스타일링에만 영향을 미치므로 의존성에서 제외 // eslint-disable-next-line react-hooks/exhaustive-deps }, [screen, selectedGroup, setNodes, setEdges, loadTableColumns]); + + // 데이터 로드 완료 시 fitView 호출 (초기 로드 시에만) + useEffect(() => { + if (dataLoadVersion > 0 && nodes.length > 0) { + // setTimeout으로 노드 렌더링 완료 후 fitView 호출 + const timer = setTimeout(() => { + // duration: 0으로 설정하여 애니메이션 없이 즉시 이동 + reactFlowInstance.fitView({ padding: 0.2, duration: 0 }); + // fitView 완료 후 뷰 표시 + setIsViewReady(true); + }, 50); + return () => clearTimeout(timer); + } + }, [dataLoadVersion, reactFlowInstance, nodes.length]); // 노드 클릭 핸들러 (그룹 모드에서 화면 포커스) - 조건부 return 전에 선언해야 함 const handleNodeClick = useCallback((_event: React.MouseEvent, node: Node) => { @@ -607,11 +727,91 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // 포커스에 따른 노드 스타일링 (그룹 모드에서 화면 클릭 시) const styledNodes = React.useMemo(() => { - // 그룹 모드에서 포커스된 화면이 있을 때만 추가 스타일링 - if (!selectedGroup || focusedScreenId === null) return nodes; + // 그룹 모드가 아니면 원본 반환 + if (!selectedGroup) return nodes; + + // 포커스된 화면의 서브 테이블 목록 (포커스가 없으면 빈 배열) + const focusedSubTables = focusedScreenId !== null + ? (screenSubTableMap[focusedScreenId] || []) + : []; + + // 포커스된 화면의 서브 테이블 데이터 (조인 컬럼 정보 포함) + const focusedSubTablesData = focusedScreenId !== null + ? subTablesDataMap[focusedScreenId] + : null; + + // 포커스된 화면의 사용 컬럼 정보 + const focusedUsedColumns = focusedScreenId !== null + ? screenUsedColumnsMap[focusedScreenId] + : null; + + // 연관 테이블 정보 추출 (parentDataMapping, rightPanelRelation 등으로 연결된 테이블) + // { tableName: { columns: [sourceField1, sourceField2], displayNames: [displayName1, displayName2] } } + const relatedTablesMap: Record = {}; + if (focusedSubTablesData) { + focusedSubTablesData.subTables.forEach((subTable) => { + // parentDataMapping, rightPanelRelation 타입의 연결에서 sourceTable 추출 + if ((subTable.relationType === 'parentMapping' || subTable.relationType === 'rightPanelRelation') + && subTable.fieldMappings) { + subTable.fieldMappings.forEach((mapping) => { + // sourceTable이 명시되어 있으면 그 테이블에 sourceField 추가 + // sourceTable이 없으면 subTable.tableName을 sourceTable로 간주하지 않음 + // mapping에서 sourceTable 정보가 필요함 - 현재는 tableName에서 추론 + // parentDataMapping의 경우: tableName이 targetTable이고, sourceTable은 별도 정보 필요 + + // 임시: subTables의 tableName과 다른 메인 테이블들을 확인 + // fieldMappings에서 sourceField가 다른 테이블에 있다면 그 테이블이 연관 테이블 + }); + } + }); + } + + // 모든 화면의 메인 테이블과 그에 연결된 조인 정보 매핑 + // 포커스된 화면이 다른 메인 테이블을 참조하는 경우 해당 테이블도 강조 + const relatedMainTables: Record = {}; + if (focusedSubTablesData) { + // screenTableMap에서 다른 화면들의 메인 테이블 확인 + Object.entries(screenTableMap).forEach(([screenIdStr, mainTableName]) => { + const screenId = parseInt(screenIdStr); + if (screenId === focusedScreenId) return; // 자신의 메인 테이블은 제외 + + // 포커스된 화면의 subTables 중 이 메인 테이블을 참조하는지 확인 + focusedSubTablesData.subTables.forEach((subTable) => { + if (subTable.fieldMappings) { + subTable.fieldMappings.forEach((mapping: any) => { + // mapping에 sourceTable 정보가 있는 경우 (parentDataMapping에서 설정) + if (mapping.sourceTable && mapping.sourceTable === mainTableName) { + if (!relatedMainTables[mainTableName]) { + relatedMainTables[mainTableName] = { columns: [], displayNames: [] }; + } + if (mapping.sourceField && !relatedMainTables[mainTableName].columns.includes(mapping.sourceField)) { + relatedMainTables[mainTableName].columns.push(mapping.sourceField); + relatedMainTables[mainTableName].displayNames.push(mapping.sourceDisplayName || mapping.sourceField); + } + } + }); + } + }); + }); + } + + console.log('[DEBUG] relatedMainTables:', relatedMainTables); return nodes.map((node) => { + // 화면 노드 스타일링 (포커스가 있을 때만) if (node.id.startsWith("screen-")) { + if (focusedScreenId === null) { + // 포커스 없음: 모든 화면 정상 표시 + return { + ...node, + data: { + ...node.data, + isFocused: false, + isFaded: false, + }, + }; + } + const screenId = parseInt(node.id.replace("screen-", "")); const isFocused = screenId === focusedScreenId; const isFaded = !isFocused; @@ -625,9 +825,325 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId }, }; } + + // 메인 테이블 노드 스타일링 + if (node.id.startsWith("table-")) { + const tableName = node.id.replace("table-", ""); + + // 포커스된 화면의 메인 테이블인지 확인 + const isFocusedTable = focusedScreenId !== null && screenTableMap[focusedScreenId] === tableName; + + // 연관 테이블인지 확인 (다른 화면의 메인 테이블이지만 포커스된 화면에서 참조하는 테이블) + const isRelatedTable = relatedMainTables[tableName] !== undefined; + const relatedTableInfo = relatedMainTables[tableName]; + + // 조인 컬럼 추출 + // 1. columns_config에서 isEntityJoin=true인 컬럼 (__join 키) + // 2. 서브 테이블 연결 시 fieldMappings에서 메인테이블 컬럼 추출 + let joinColumns: string[] = [...(focusedUsedColumns?.[`${tableName}__join`] || [])]; + + // 서브 테이블 연결 정보에서도 추가 (포커스된 화면의 메인 테이블인 경우) + // relationType에 따라 다름: + // - reference, source: sourceField가 메인테이블 컬럼 (예: manager_id -> user_id, material -> material) + // - parentMapping, rightPanelRelation: targetField가 메인테이블 컬럼 + // - lookup 등: targetField가 메인테이블 컬럼 + console.log('[DEBUG] joinColumns before subTable processing:', { + tableName, + focusedMainTable: focusedSubTablesData?.mainTable, + subTables: focusedSubTablesData?.subTables?.map((st: any) => ({ + tableName: st.tableName, + relationType: st.relationType, + fieldMappings: st.fieldMappings + })) + }); + if (focusedSubTablesData && focusedSubTablesData.mainTable === tableName) { + // 디버그: subTables 처리 전 로그 + if (tableName === 'customer_item_mapping') { + console.log('[DEBUG] Processing subTables for mainTable:', { + mainTable: tableName, + subTablesCount: focusedSubTablesData.subTables.length, + subTables: focusedSubTablesData.subTables.map(st => ({ + tableName: st.tableName, + relationType: st.relationType, + fieldMappingsCount: st.fieldMappings?.length || 0, + fieldMappings: st.fieldMappings?.map(fm => ({ + sourceField: fm.sourceField, + targetField: fm.targetField, + })), + })), + }); + } + focusedSubTablesData.subTables.forEach((subTable, stIdx) => { + // 각 서브테이블 디버그 로그 + if (tableName === 'customer_item_mapping') { + console.log(`[DEBUG] SubTable ${stIdx}:`, { + tableName: subTable.tableName, + relationType: subTable.relationType, + hasFieldMappings: !!subTable.fieldMappings, + fieldMappingsCount: subTable.fieldMappings?.length || 0, + }); + } + if (subTable.fieldMappings) { + subTable.fieldMappings.forEach((mapping, mIdx) => { + // 각 매핑 디버그 로그 + if (tableName === 'customer_item_mapping') { + console.log(`[DEBUG] SubTable ${stIdx} Mapping ${mIdx}:`, { + sourceField: mapping.sourceField, + targetField: mapping.targetField, + relationType: subTable.relationType, + }); + } + // sourceTable이 있으면 parentDataMapping/rightPanelRelation에서 추가된 것이므로 + // relationType과 관계없이 targetField가 메인테이블 컬럼 + const hasSourceTable = 'sourceTable' in mapping && mapping.sourceTable; + + if (hasSourceTable) { + // parentDataMapping/rightPanelRelation: targetField가 메인테이블 컬럼 + if (tableName === 'customer_item_mapping') { + console.log('[DEBUG] Adding targetField to joinColumns (has sourceTable):', { + subTableName: subTable.tableName, + relationType: subTable.relationType, + sourceTable: mapping.sourceTable, + targetField: mapping.targetField, + alreadyIncludes: joinColumns.includes(mapping.targetField), + }); + } + if (mapping.targetField && !joinColumns.includes(mapping.targetField)) { + joinColumns.push(mapping.targetField); + } + } else if (subTable.relationType === 'reference' || subTable.relationType === 'source') { + // reference, source (sourceTable 없는 경우): sourceField가 메인테이블 컬럼 + if (mapping.sourceField && !joinColumns.includes(mapping.sourceField)) { + joinColumns.push(mapping.sourceField); + } + } else if (subTable.relationType === 'parentMapping' || subTable.relationType === 'rightPanelRelation') { + // parentMapping, rightPanelRelation: targetField가 메인테이블 컬럼 + if (tableName === 'customer_item_mapping') { + console.log('[DEBUG] Adding targetField to joinColumns (parentMapping):', { + subTableName: subTable.tableName, + relationType: subTable.relationType, + targetField: mapping.targetField, + alreadyIncludes: joinColumns.includes(mapping.targetField), + }); + } + if (mapping.targetField && !joinColumns.includes(mapping.targetField)) { + joinColumns.push(mapping.targetField); + } + } else { + // lookup 등: targetField가 메인테이블 컬럼 + if (tableName === 'customer_item_mapping') { + console.log('[DEBUG] Adding targetField to joinColumns (else branch):', { + subTableName: subTable.tableName, + relationType: subTable.relationType, + targetField: mapping.targetField, + alreadyIncludes: joinColumns.includes(mapping.targetField), + }); + } + if (mapping.targetField && !joinColumns.includes(mapping.targetField)) { + joinColumns.push(mapping.targetField); + } + } + }); + } + }); + } + + // 연관 테이블인 경우: 참조되는 컬럼을 조인 컬럼으로 추가 + if (isRelatedTable && relatedTableInfo) { + relatedTableInfo.columns.forEach((col) => { + if (!joinColumns.includes(col)) { + joinColumns.push(col); + } + }); + } + + // 사용 컬럼 추출 (조인 컬럼 제외) + const allUsedColumns = focusedUsedColumns?.[tableName] || []; + const highlightedColumns = allUsedColumns.filter(col => !joinColumns.includes(col)); + + // 테이블 활성화 여부: 포커스된 화면의 메인 테이블 OR 연관 테이블 + const isActiveTable = isFocusedTable || isRelatedTable; + + // 메인테이블용 fieldMappings 생성 (조인 컬럼 옆에 연관 테이블 컬럼명 표시) + // 예: 거래처 ID ← 거래처 코드 (메인테이블 컬럼 ← 연관테이블 컬럼) + let mainTableFieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> = []; + if (isFocusedTable && focusedSubTablesData) { + focusedSubTablesData.subTables.forEach((subTable) => { + if (subTable.fieldMappings) { + subTable.fieldMappings.forEach((mapping) => { + // 메인테이블에서는: + // - sourceField/targetField 중 메인테이블 컬럼이 targetField (표시할 컬럼) + // - 연관테이블 컬럼이 sourceField (← 뒤에 표시할 참조 컬럼) + if (subTable.relationType === 'source' && mapping.sourceTable) { + // parentDataMapping 스타일: targetField = 메인테이블, sourceField = 연관테이블 + mainTableFieldMappings.push({ + sourceField: mapping.sourceField, // 연관 테이블 컬럼 + targetField: mapping.targetField, // 메인 테이블 컬럼 + sourceDisplayName: mapping.sourceDisplayName || mapping.sourceField, + targetDisplayName: mapping.targetDisplayName || mapping.targetField, + }); + } else if (subTable.relationType === 'parentMapping' || subTable.relationType === 'rightPanelRelation') { + mainTableFieldMappings.push({ + sourceField: mapping.sourceField, + targetField: mapping.targetField, + sourceDisplayName: mapping.sourceDisplayName || mapping.sourceField, + targetDisplayName: mapping.targetDisplayName || mapping.targetField, + }); + } else if (subTable.relationType === 'reference' || subTable.relationType === 'source') { + // reference/source: sourceField = 메인테이블, targetField = 서브테이블 + // 메인테이블 표시: sourceField ← targetDisplayName + mainTableFieldMappings.push({ + sourceField: mapping.targetField, // 연관 테이블 컬럼 (표시용) + targetField: mapping.sourceField, // 메인 테이블 컬럼 + sourceDisplayName: mapping.targetDisplayName || mapping.targetField, + targetDisplayName: mapping.sourceDisplayName || mapping.sourceField, + }); + } else { + // lookup: targetField = 메인테이블, sourceField = 서브테이블 + mainTableFieldMappings.push({ + sourceField: mapping.sourceField, + targetField: mapping.targetField, + sourceDisplayName: mapping.sourceDisplayName || mapping.sourceField, + targetDisplayName: mapping.targetDisplayName || mapping.targetField, + }); + } + }); + } + }); + } + + // 연관 테이블용 fieldMappings 생성 + let relatedTableFieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> = []; + if (isRelatedTable && relatedTableInfo && focusedSubTablesData) { + focusedSubTablesData.subTables.forEach((subTable) => { + if (subTable.fieldMappings) { + subTable.fieldMappings.forEach((mapping) => { + if (mapping.sourceTable === tableName) { + // 이 테이블이 sourceTable인 경우: sourceField가 이 테이블의 컬럼 + relatedTableFieldMappings.push({ + sourceField: mapping.targetField, // 메인 테이블 컬럼 (참조 표시용) + targetField: mapping.sourceField, // 연관 테이블 컬럼 (표시할 컬럼) + sourceDisplayName: mapping.targetDisplayName || mapping.targetField, + targetDisplayName: mapping.sourceDisplayName || mapping.sourceField, + }); + } + }); + } + }); + } + + return { + ...node, + data: { + ...node.data, + isFocused: isFocusedTable, + isRelated: isRelatedTable, + isFaded: focusedScreenId !== null && !isActiveTable, + highlightedColumns: isActiveTable ? highlightedColumns : [], + joinColumns: isActiveTable ? joinColumns : [], + fieldMappings: isFocusedTable ? mainTableFieldMappings : (isRelatedTable ? relatedTableFieldMappings : []), + }, + }; + } + + // 서브 테이블 노드 스타일링 + // 기본: 흐리게, 포커스된 화면의 서브 테이블만 활성화 + if (node.id.startsWith("subtable-")) { + const subTableName = node.id.replace("subtable-", ""); + const isActiveSubTable = focusedSubTables.includes(subTableName); + + // 조인 컬럼 추출 (서브 테이블 측의 컬럼) + // relationType에 따라 다름: + // - reference: targetField가 서브테이블 컬럼 (예: manager_id -> user_id) + // - lookup 등: sourceField가 서브테이블 컬럼 + let subTableJoinColumns: string[] = []; + if (isActiveSubTable && focusedSubTablesData) { + const subTableInfo = focusedSubTablesData.subTables.find(st => st.tableName === subTableName); + if (subTableInfo?.fieldMappings) { + subTableInfo.fieldMappings.forEach((mapping) => { + // reference, source, parentMapping, rightPanelRelation 타입: targetField가 서브테이블의 컬럼 (조인 키) + // lookup 타입: sourceField가 서브테이블의 컬럼 + if (subTableInfo.relationType === 'reference' || subTableInfo.relationType === 'source' || + subTableInfo.relationType === 'parentMapping' || subTableInfo.relationType === 'rightPanelRelation') { + // reference, source, parentMapping, rightPanelRelation: 메인테이블 컬럼(sourceField) -> 서브테이블 컬럼(targetField) + if (mapping.targetField && !subTableJoinColumns.includes(mapping.targetField)) { + subTableJoinColumns.push(mapping.targetField); + } + } else { + // lookup 등: sourceField가 서브테이블 컬럼 + if (mapping.sourceField && !subTableJoinColumns.includes(mapping.sourceField)) { + subTableJoinColumns.push(mapping.sourceField); + } + } + }); + } + + // 디버깅 로그 + console.log(`서브테이블 ${subTableName} (${subTableInfo?.relationType}):`, { + fieldMappings: subTableInfo?.fieldMappings, + extractedJoinColumns: subTableJoinColumns + }); + } + + // 서브 테이블의 highlightedColumns도 추가 (화면에서 서브테이블 컬럼을 직접 사용하는 경우) + // joinColumns와 별개로 표시 (조인 키 외 사용 컬럼) + const subTableHighlightedColumns: string[] = []; + + // 필드 매핑 정보 추출 (조인 관계 표시용) + // reference, source, parentMapping, rightPanelRelation 타입: sourceField = 메인테이블 컬럼, targetField = 서브테이블 컬럼 + // lookup 타입: sourceField = 서브테이블 컬럼, targetField = 메인테이블 컬럼 (swap 필요) + let displayFieldMappings: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }> = []; + if (isActiveSubTable && focusedSubTablesData) { + const subTableInfo = focusedSubTablesData.subTables.find(st => st.tableName === subTableName); + if (subTableInfo?.fieldMappings) { + displayFieldMappings = subTableInfo.fieldMappings.map((mapping) => { + if (subTableInfo.relationType === 'reference' || subTableInfo.relationType === 'source' || + subTableInfo.relationType === 'parentMapping' || subTableInfo.relationType === 'rightPanelRelation') { + // reference, source, parentMapping, rightPanelRelation: 백엔드에서 sourceField = 메인테이블, targetField = 서브테이블 + // 표시: 서브테이블 컬럼(targetField) ← 메인테이블 한글명(sourceDisplayName) + return { + sourceField: mapping.sourceField, // 메인 테이블 컬럼 (참조) + targetField: mapping.targetField, // 서브 테이블 컬럼 (표시) + sourceDisplayName: mapping.sourceDisplayName || mapping.sourceField, // 메인 테이블 한글명 + targetDisplayName: mapping.targetDisplayName || mapping.targetField, + }; + } else { + // lookup 등: 백엔드 fieldMappings가 reference/source와 반대 + // 백엔드: sourceField = 서브테이블 컬럼, targetField = 메인테이블 컬럼 + // 프론트엔드 표시: 서브테이블 컬럼(sourceField) ← 메인테이블 컬럼(targetField) + // 그래서 swap 필요! + return { + sourceField: mapping.targetField, // 메인 테이블 컬럼 (참조) + targetField: mapping.sourceField, // 서브 테이블 컬럼 (표시) + sourceDisplayName: mapping.targetDisplayName || mapping.targetField, // 메인 테이블 한글명 + targetDisplayName: mapping.sourceDisplayName || mapping.sourceField, // 서브 테이블 한글명 + }; + } + }); + } + } + + return { + ...node, + style: { + ...node.style, + opacity: isActiveSubTable ? 1 : 0.3, + filter: isActiveSubTable ? "none" : "grayscale(80%)", + }, + data: { + ...node.data, + isFocused: isActiveSubTable, + isFaded: !isActiveSubTable, + highlightedColumns: isActiveSubTable ? subTableHighlightedColumns : [], + joinColumns: isActiveSubTable ? subTableJoinColumns : [], + fieldMappings: isActiveSubTable ? displayFieldMappings : [], + }, + }; + } + return node; }); - }, [nodes, selectedGroup, focusedScreenId]); + }, [nodes, selectedGroup, focusedScreenId, screenSubTableMap, subTablesDataMap, screenUsedColumnsMap, screenTableMap]); // 포커스에 따른 엣지 스타일링 (그룹 모드 & 개별 화면 모드) const styledEdges = React.useMemo(() => { @@ -676,12 +1192,67 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId }); } - // 그룹 모드: 포커스된 화면이 없으면 원본 반환 - if (!selectedGroup || focusedScreenId === null) return edges; + // 그룹 모드가 아니면 원본 반환 + if (!selectedGroup) return edges; - return edges.map((edge) => { + // 연관 테이블 간 조인 엣지 생성 (parentDataMapping, rightPanelRelation) + const joinEdges: Edge[] = []; + + if (focusedScreenId !== null) { + const focusedSubTablesData = subTablesDataMap[focusedScreenId]; + const focusedMainTable = screenTableMap[focusedScreenId]; + + + if (focusedSubTablesData) { + focusedSubTablesData.subTables.forEach((subTable) => { + // fieldMappings에 sourceTable이 있는 경우 처리 (parentMapping, rightPanelRelation 등) + if (subTable.fieldMappings) { + + subTable.fieldMappings.forEach((mapping: any, idx: number) => { + const sourceTable = mapping.sourceTable; + if (!sourceTable) return; + + // 연관 테이블 → 포커싱된 화면의 메인 테이블로 연결 + // sourceTable(연관) → focusedMainTable(메인) + const edgeId = `edge-join-relation-${focusedScreenId}-${sourceTable}-${focusedMainTable}-${idx}`; + + // 이미 존재하는 엣지인지 확인 + if (joinEdges.some(e => e.id === edgeId)) return; + + // 라벨 제거 - 조인 정보는 테이블 노드 내부에서 컬럼 옆에 표시 + joinEdges.push({ + id: edgeId, + source: `table-${sourceTable}`, + target: `table-${focusedMainTable}`, + type: 'smoothstep', + animated: true, + style: { + stroke: '#ea580c', + strokeWidth: 2, + strokeDasharray: '8,4', + }, + markerEnd: { + type: MarkerType.ArrowClosed, + color: '#ea580c', + width: 15, + height: 15, + }, + }); + }); + } + }); + } + } + + // 기존 엣지 스타일링 + 조인 엣지 추가 + const styledOriginalEdges = edges.map((edge) => { // 화면 간 연결선 (1, 2, 3 라벨) if (edge.source.startsWith("screen-") && edge.target.startsWith("screen-")) { + // 포커스가 없으면 모든 화면 간 연결선 정상 표시 + if (focusedScreenId === null) { + return edge; // 원본 그대로 + } + // 포커스된 화면과 연결된 화면 간 선만 활성화 const sourceId = parseInt(edge.source.replace("screen-", "")); const targetId = parseInt(edge.target.replace("screen-", "")); @@ -701,6 +1272,11 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // 화면-테이블 연결선 if (edge.source.startsWith("screen-") && edge.target.startsWith("table-")) { + // 포커스가 없으면 모든 화면-테이블 연결선 정상 표시 + if (focusedScreenId === null) { + return edge; // 원본 그대로 + } + const sourceId = parseInt(edge.source.replace("screen-", "")); const isMyConnection = sourceId === focusedScreenId; @@ -717,9 +1293,66 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId }; } + // 메인 테이블 → 서브 테이블 연결선 + // 기본: 흐리게 처리, 포커스된 화면의 서브 테이블만 강조 + if (edge.source.startsWith("table-") && edge.target.startsWith("subtable-")) { + // 포커스가 없으면 모든 서브 테이블 연결선 흐리게 (기본 상태) + if (focusedScreenId === null) { + return { + ...edge, + animated: false, + style: { + ...edge.style, + stroke: "#d1d5db", + strokeWidth: 1, + strokeDasharray: "6,4", + opacity: 0.3, + }, + labelStyle: { + ...edge.labelStyle, + opacity: 0.3, + }, + }; + } + + // 엣지 ID에서 화면 ID 추출: edge-main-sub-{screenId}-{mainTable}-{subTable} + const idParts = edge.id.split("-"); + // edge-main-sub-1413-sales_order_mng-customer_mng 형식 + const edgeScreenId = idParts.length >= 4 ? parseInt(idParts[3]) : null; + + // 포커스된 화면의 서브 테이블 연결인지 확인 + const isMySubTable = edgeScreenId === focusedScreenId; + + // 대체 방법: screenSubTableMap 사용 + const focusedSubTables = focusedScreenId ? screenSubTableMap[focusedScreenId] || [] : []; + const subTableName = edge.target.replace("subtable-", ""); + const isMySubTableByMap = focusedSubTables.includes(subTableName); + + const isActive = isMySubTable || isMySubTableByMap; + + return { + ...edge, + animated: isActive, // 활성화된 것만 애니메이션 + style: { + ...edge.style, + stroke: isActive ? "#f97316" : "#d1d5db", + strokeWidth: isActive ? 2 : 1, + strokeDasharray: "6,4", // 항상 점선 + opacity: isActive ? 1 : 0.2, + }, + labelStyle: { + ...edge.labelStyle, + opacity: isActive ? 1 : 0.3, + }, + }; + } + return edge; }); - }, [edges, selectedGroup, focusedScreenId, screen]); + + // 기존 엣지 + 조인 관계 엣지 합치기 + return [...styledOriginalEdges, ...joinEdges]; + }, [edges, selectedGroup, focusedScreenId, screen, screenSubTableMap, subTablesDataMap, screenTableMap]); // 조건부 렌더링 (모든 훅 선언 후에 위치해야 함) if (!screen && !selectedGroup) { @@ -743,22 +1376,23 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId return (
- - - - + {/* isViewReady가 false면 숨김 처리하여 깜빡임 방지 */} +
+ + + + +
); } diff --git a/frontend/lib/api/screenGroup.ts b/frontend/lib/api/screenGroup.ts index 2ecd0fb0..89f5591d 100644 --- a/frontend/lib/api/screenGroup.ts +++ b/frontend/lib/api/screenGroup.ts @@ -353,6 +353,9 @@ export interface LayoutItem { componentKind: string; // 정확한 컴포넌트 종류 (table-list, button-primary 등) widgetType: string; // 일반적인 위젯 타입 (button, text 등) label?: string; + bindField?: string; // 바인딩된 필드명 (컬럼명) + usedColumns?: string[]; // 이 컴포넌트에서 사용하는 컬럼 목록 + joinColumns?: string[]; // 이 컴포넌트에서 조인 컬럼 목록 (isEntityJoin=true) } export interface ScreenLayoutSummary { @@ -388,11 +391,21 @@ export async function getMultipleScreenLayoutSummary( } } +// 필드 매핑 정보 타입 +export interface FieldMappingInfo { + sourceTable?: string; // 연관 테이블명 (parentDataMapping에서 사용) + sourceField: string; + targetField: string; + sourceDisplayName?: string; // 메인 테이블 한글 컬럼명 + targetDisplayName?: string; // 서브 테이블 한글 컬럼명 +} + // 서브 테이블 정보 타입 export interface SubTableInfo { tableName: string; componentType: string; - relationType: 'lookup' | 'source' | 'join'; + relationType: 'lookup' | 'source' | 'join' | 'reference' | 'parentMapping' | 'rightPanelRelation'; + fieldMappings?: FieldMappingInfo[]; } export interface ScreenSubTablesData {