ERP-node/frontend/components/screen/ScreenRelationFlow.tsx

774 lines
30 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback } from "react";
import {
ReactFlow,
Controls,
Background,
BackgroundVariant,
Node,
Edge,
useNodesState,
useEdgesState,
MarkerType,
useReactFlow,
ReactFlowProvider,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { ScreenDefinition } from "@/types/screen";
import { ScreenNode, TableNode, ScreenNodeData, TableNodeData } from "./ScreenNode";
import {
getFieldJoins,
getDataFlows,
getTableRelations,
getMultipleScreenLayoutSummary,
getScreenGroup,
getScreenSubTables,
ScreenLayoutSummary,
ScreenSubTablesData,
} from "@/lib/api/screenGroup";
import { getTableColumns, ColumnTypeInfo } from "@/lib/api/tableManagement";
// 노드 타입 등록
const nodeTypes = {
screenNode: ScreenNode,
tableNode: TableNode,
};
// 레이아웃 상수
const SCREEN_Y = 50; // 화면 노드 Y 위치 (상단)
const TABLE_Y = 520; // 메인 테이블 노드 Y 위치 (중단)
const SUB_TABLE_Y = 780; // 서브 테이블 노드 Y 위치 (하단)
const NODE_WIDTH = 260; // 노드 너비
const NODE_GAP = 40; // 노드 간격
interface ScreenRelationFlowProps {
screen: ScreenDefinition | null;
selectedGroup?: { id: number; name: string } | null;
initialFocusedScreenId?: number | null;
}
// 노드 타입 (Record<string, unknown> 확장)
type ScreenNodeType = Node<ScreenNodeData & Record<string, unknown>>;
type TableNodeType = Node<TableNodeData & Record<string, unknown>>;
type AllNodeType = ScreenNodeType | TableNodeType;
// 내부 컴포넌트 (useReactFlow 사용 가능)
function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId }: ScreenRelationFlowProps) {
const [nodes, setNodes, onNodesChange] = useNodesState<AllNodeType>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const [loading, setLoading] = useState(false);
const [tableColumns, setTableColumns] = useState<Record<string, ColumnTypeInfo[]>>({});
// 그룹 내 포커스된 화면 ID (그룹 모드에서만 사용)
const [focusedScreenId, setFocusedScreenId] = useState<number | null>(null);
// 외부에서 전달된 초기 포커스 ID 적용 (화면 이동 없이 강조만)
useEffect(() => {
if (initialFocusedScreenId !== undefined && initialFocusedScreenId !== null) {
setFocusedScreenId(initialFocusedScreenId);
}
}, [initialFocusedScreenId]);
// 화면 ID와 테이블명 매핑 (포커스 시 연결선 강조용)
const [screenTableMap, setScreenTableMap] = useState<Record<number, string>>({});
// 테이블 컬럼 정보 로드
const loadTableColumns = useCallback(
async (tableName: string): Promise<ColumnTypeInfo[]> => {
if (!tableName) return [];
if (tableColumns[tableName]) return tableColumns[tableName];
try {
const response = await getTableColumns(tableName);
if (response.success && response.data && response.data.columns) {
const columns = response.data.columns;
setTableColumns((prev) => ({ ...prev, [tableName]: columns }));
return columns;
}
} catch (error) {
console.error(`테이블 ${tableName} 컬럼 로드 실패:`, error);
}
return [];
},
[tableColumns]
);
// 그룹 변경 시 focusedScreenId 초기화
useEffect(() => {
setFocusedScreenId(null);
}, [selectedGroup?.id, screen?.screenId]);
// 데이터 로드 및 노드/엣지 생성
useEffect(() => {
// 그룹도 없고 화면도 없으면 빈 상태
if (!screen && !selectedGroup) {
setNodes([]);
setEdges([]);
return;
}
const loadRelations = async () => {
setLoading(true);
try {
let screenList: ScreenDefinition[] = [];
// ========== 그룹 선택 시: 그룹의 화면들 로드 ==========
if (selectedGroup) {
const groupRes = await getScreenGroup(selectedGroup.id);
if (groupRes.success && groupRes.data) {
const groupData = groupRes.data as any;
const groupScreens = groupData.screens || [];
// display_order 순으로 정렬
groupScreens.sort((a: any, b: any) => (a.display_order || 0) - (b.display_order || 0));
// screen_definitions 형식으로 변환 (table_name 포함)
screenList = groupScreens.map((gs: any) => ({
screenId: gs.screen_id,
screenName: gs.screen_name || `화면 ${gs.screen_id}`,
screenCode: gs.screen_code || "",
tableName: gs.table_name || "", // 테이블명 포함
companyCode: groupData.company_code,
isActive: "Y",
createdDate: new Date(),
updatedDate: new Date(),
screenRole: gs.screen_role, // screen_role 추가
displayOrder: gs.display_order, // display_order 추가
} as ScreenDefinition & { screenRole?: string; displayOrder?: number }));
}
} else if (screen) {
// 기존 방식: 선택된 화면 중심
screenList = [screen];
}
if (screenList.length === 0) {
setNodes([]);
setEdges([]);
setLoading(false);
return;
}
// 화면-테이블 매핑 저장 (포커스 시 연결선 강조용)
const newScreenTableMap: Record<number, string> = {};
screenList.forEach((scr: any) => {
if (scr.tableName) {
newScreenTableMap[scr.screenId] = scr.tableName;
}
});
setScreenTableMap(newScreenTableMap);
// 관계 데이터 로드 (첫 번째 화면 기준)
const [joinsRes, flowsRes, relationsRes] = await Promise.all([
getFieldJoins(screenList[0].screenId).catch(() => ({ success: false, data: [] })),
getDataFlows().catch(() => ({ success: false, data: [] })),
getTableRelations({ screen_id: screenList[0].screenId }).catch(() => ({ success: false, data: [] })),
]);
const joins = joinsRes.success ? joinsRes.data || [] : [];
const flows = flowsRes.success ? flowsRes.data || [] : [];
const relations = relationsRes.success ? relationsRes.data || [] : [];
// 데이터 흐름에서 연결된 화면들 추가
flows.forEach((flow: any) => {
if (flow.source_screen_id === screen.screenId && flow.target_screen_id) {
const exists = screenList.some((s) => s.screenId === flow.target_screen_id);
if (!exists) {
screenList.push({
screenId: flow.target_screen_id,
screenName: flow.target_screen_name || `화면 ${flow.target_screen_id}`,
screenCode: "",
tableName: "",
companyCode: screen.companyCode,
isActive: "Y",
createdDate: new Date(),
updatedDate: new Date(),
} as ScreenDefinition);
}
}
});
// 화면 레이아웃 요약 정보 로드
const screenIds = screenList.map((s) => s.screenId);
let layoutSummaries: Record<number, ScreenLayoutSummary> = {};
let subTablesData: Record<number, ScreenSubTablesData> = {};
try {
// 레이아웃 요약과 서브 테이블 정보 병렬 로드
const [layoutRes, subTablesRes] = await Promise.all([
getMultipleScreenLayoutSummary(screenIds),
getScreenSubTables(screenIds),
]);
if (layoutRes.success && layoutRes.data) {
// API 응답이 Record 형태 (screenId -> summary)
layoutSummaries = layoutRes.data as Record<number, ScreenLayoutSummary>;
}
if (subTablesRes.success && subTablesRes.data) {
subTablesData = subTablesRes.data as Record<number, ScreenSubTablesData>;
}
} catch (e) {
console.error("레이아웃 요약/서브 테이블 로드 실패:", e);
}
// ========== 상단: 화면 노드들 ==========
const screenNodes: ScreenNodeType[] = [];
const screenStartX = 50;
// screen_role 레이블 매핑
const getRoleLabel = (role?: string) => {
if (!role || role === "member") return "화면";
const roleMap: Record<string, string> = {
main_list: "메인 그리드",
register_form: "등록 폼",
popup: "팝업",
detail: "상세",
};
return roleMap[role] || role;
};
screenList.forEach((scr: any, idx) => {
const isMain = screen && scr.screenId === screen.screenId;
const summary = layoutSummaries[scr.screenId];
const roleLabel = getRoleLabel(scr.screenRole);
// 포커스 여부 결정 (그룹 모드 & 개별 화면 모드 모두 지원)
const isInGroup = !!selectedGroup;
let isFocused: boolean;
let isFaded: boolean;
if (isInGroup) {
// 그룹 모드: 클릭한 화면만 포커스
isFocused = focusedScreenId === scr.screenId;
isFaded = focusedScreenId !== null && !isFocused;
} else {
// 개별 화면 모드: 메인 화면(선택된 화면)만 포커스, 연결 화면은 흐리게
isFocused = isMain;
isFaded = !isMain && screenList.length > 1;
}
screenNodes.push({
id: `screen-${scr.screenId}`,
type: "screenNode",
position: { x: screenStartX + idx * (NODE_WIDTH + NODE_GAP), y: SCREEN_Y },
data: {
label: scr.screenName,
subLabel: selectedGroup ? `${roleLabel} (#${scr.displayOrder || idx + 1})` : (isMain ? "메인 화면" : "연결 화면"),
type: "screen",
isMain: selectedGroup ? idx === 0 : isMain,
tableName: scr.tableName,
layoutSummary: summary,
// 화면 포커스 관련 속성 (그룹 모드 & 개별 모드 공통)
isInGroup,
isFocused,
isFaded,
screenRole: scr.screenRole,
},
});
});
// ========== 중단: 메인 테이블 노드들 ==========
const tableNodes: TableNodeType[] = [];
const mainTableSet = new Set<string>();
const subTableSet = new Set<string>();
// 모든 화면의 메인 테이블 추가
screenList.forEach((scr) => {
if (scr.tableName) {
mainTableSet.add(scr.tableName);
}
});
// 조인된 테이블들 (screen_field_joins에서)
joins.forEach((join: any) => {
if (join.save_table) mainTableSet.add(join.save_table);
if (join.join_table) mainTableSet.add(join.join_table);
});
// 테이블 관계에서 추가
relations.forEach((rel: any) => {
if (rel.table_name) mainTableSet.add(rel.table_name);
});
// 서브 테이블 수집 (componentConfig에서 추출된 테이블들)
// 서브 테이블은 메인 테이블과 다른 테이블들
Object.values(subTablesData).forEach((screenSubData) => {
screenSubData.subTables.forEach((subTable) => {
// 메인 테이블에 없는 것만 서브 테이블로 추가
if (!mainTableSet.has(subTable.tableName)) {
subTableSet.add(subTable.tableName);
}
});
});
// 메인 테이블 노드 배치 (화면들의 중앙 아래에 배치)
const mainTableList = Array.from(mainTableSet);
// 화면 노드들의 총 너비 계산
const screenTotalWidth = screenList.length * NODE_WIDTH + (screenList.length - 1) * NODE_GAP;
const screenCenterX = screenStartX + screenTotalWidth / 2;
// 메인 테이블 노드들의 총 너비 계산
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);
for (let idx = 0; idx < mainTableList.length; idx++) {
const tableName = mainTableList[idx];
const isPrimaryTable = tableName === primaryTableName;
// 컬럼 정보 로드
let columns: ColumnTypeInfo[] = [];
try {
columns = await loadTableColumns(tableName);
} catch (e) {
// ignore
}
// 컬럼 정보를 PK/FK 표시와 함께 변환
const formattedColumns = columns.slice(0, 8).map((col) => ({
name: col.displayName || col.columnName || "",
type: col.dataType || "",
isPrimaryKey: col.isPrimaryKey || col.columnName === "id",
isForeignKey: !!col.referenceTable || (col.columnName?.includes("_id") && col.columnName !== "id"),
}));
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,
columns: formattedColumns,
},
});
}
// ========== 하단: 서브 테이블 노드들 (참조/조회용) ==========
const subTableList = Array.from(subTableSet);
if (subTableList.length > 0) {
// 서브 테이블 노드들의 총 너비 계산
const subTableTotalWidth = subTableList.length * NODE_WIDTH + (subTableList.length - 1) * NODE_GAP;
const subTableStartX = screenCenterX - subTableTotalWidth / 2;
for (let idx = 0; idx < subTableList.length; idx++) {
const tableName = subTableList[idx];
// 컬럼 정보 로드
let columns: ColumnTypeInfo[] = [];
try {
columns = await loadTableColumns(tableName);
} catch (e) {
// ignore
}
// 컬럼 정보를 PK/FK 표시와 함께 변환
const formattedColumns = columns.slice(0, 5).map((col) => ({
name: col.displayName || col.columnName || "",
type: col.dataType || "",
isPrimaryKey: col.isPrimaryKey || col.columnName === "id",
isForeignKey: !!col.referenceTable || (col.columnName?.includes("_id") && col.columnName !== "id"),
}));
// 서브 테이블의 관계 타입 결정
let relationType = "참조";
Object.values(subTablesData).forEach((screenSubData) => {
const matchedSub = screenSubData.subTables.find((st) => st.tableName === tableName);
if (matchedSub) {
if (matchedSub.relationType === "lookup") relationType = "조회";
else if (matchedSub.relationType === "source") relationType = "데이터 소스";
else if (matchedSub.relationType === "join") relationType = "조인";
}
});
tableNodes.push({
id: `subtable-${tableName}`,
type: "tableNode",
position: { x: subTableStartX + idx * (NODE_WIDTH + NODE_GAP), y: SUB_TABLE_Y },
data: {
label: tableName,
subLabel: `서브 테이블 (${relationType})`,
isMain: false,
columns: formattedColumns,
},
});
}
}
// ========== 엣지: 연결선 생성 ==========
const newEdges: Edge[] = [];
// 그룹 선택 시: 화면 간 연결선 (display_order 순)
if (selectedGroup && screenList.length > 1) {
for (let i = 0; i < screenList.length - 1; i++) {
const currentScreen = screenList[i];
const nextScreen = screenList[i + 1];
newEdges.push({
id: `edge-screen-flow-${i}`,
source: `screen-${currentScreen.screenId}`,
target: `screen-${nextScreen.screenId}`,
sourceHandle: "right",
targetHandle: "left",
type: "smoothstep",
label: `${i + 1}`,
labelStyle: { fontSize: 11, fill: "#0ea5e9", fontWeight: 600 },
labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 },
labelBgPadding: [4, 2] as [number, number],
markerEnd: { type: MarkerType.ArrowClosed, color: "#0ea5e9" },
animated: true,
style: { stroke: "#0ea5e9", strokeWidth: 2 },
});
}
}
// 각 화면 → 해당 메인 테이블 연결선 생성 (실선)
screenList.forEach((scr, idx) => {
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}`,
target: `table-${scr.tableName}`,
sourceHandle: "bottom",
targetHandle: "top",
type: "smoothstep",
animated: isMain, // 메인 화면만 애니메이션
style: {
stroke: isMain ? "#3b82f6" : "#94a3b8",
strokeWidth: isMain ? 2 : 1.5,
strokeDasharray: isMain ? undefined : "5,5", // 보조 연결은 점선
},
});
}
});
// 메인 테이블 → 서브 테이블 연결선 생성 (점선)
Object.values(subTablesData).forEach((screenSubData) => {
const mainTable = screenSubData.mainTable;
if (!mainTable || !mainTableSet.has(mainTable)) return;
screenSubData.subTables.forEach((subTable) => {
// 서브 테이블 노드가 실제로 생성되었는지 확인
if (!subTableSet.has(subTable.tableName)) return;
// 중복 엣지 방지
const edgeId = `edge-main-sub-${mainTable}-${subTable.tableName}`;
const exists = newEdges.some((e) => e.id === edgeId);
if (exists) return;
// 관계 타입에 따른 라벨
let relationLabel = "참조";
if (subTable.relationType === "lookup") relationLabel = "조회";
else if (subTable.relationType === "source") relationLabel = "데이터 소스";
else if (subTable.relationType === "join") relationLabel = "조인";
newEdges.push({
id: edgeId,
source: `table-${mainTable}`,
target: `subtable-${subTable.tableName}`,
sourceHandle: "bottom",
targetHandle: "top",
type: "smoothstep",
label: relationLabel,
labelStyle: { fontSize: 9, fill: "#f97316", fontWeight: 500 },
labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 },
labelBgPadding: [3, 2] as [number, number],
markerEnd: { type: MarkerType.ArrowClosed, color: "#f97316" },
style: {
stroke: "#f97316",
strokeWidth: 1.5,
strokeDasharray: "6,4", // 점선
},
});
});
});
// 조인 관계 엣지 (테이블 간 - 1:N 관계 표시)
joins.forEach((join: any, idx: number) => {
if (join.save_table && join.join_table && join.save_table !== join.join_table) {
newEdges.push({
id: `edge-join-${idx}`,
source: `table-${join.save_table}`,
target: `table-${join.join_table}`,
sourceHandle: "right",
targetHandle: "left",
type: "smoothstep",
label: "1:N 관계",
labelStyle: { fontSize: 10, fill: "#6366f1", fontWeight: 500 },
labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 },
labelBgPadding: [4, 2] as [number, number],
markerEnd: { type: MarkerType.ArrowClosed, color: "#6366f1" },
style: { stroke: "#6366f1", strokeWidth: 1.5, strokeDasharray: "5,5" },
});
}
});
// 테이블 관계 엣지 (추가 관계)
relations.forEach((rel: any, idx: number) => {
if (rel.table_name && rel.table_name !== screen.tableName) {
// 화면 → 연결 테이블
const edgeExists = newEdges.some(
(e) => e.source === `screen-${screen.screenId}` && e.target === `table-${rel.table_name}`
);
if (!edgeExists) {
newEdges.push({
id: `edge-rel-${idx}`,
source: `screen-${screen.screenId}`,
target: `table-${rel.table_name}`,
sourceHandle: "bottom",
targetHandle: "top",
type: "smoothstep",
label: rel.relation_type === "join" ? "조인" : rel.crud_operations || "",
labelStyle: { fontSize: 9, fill: "#10b981" },
labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 },
labelBgPadding: [3, 2] as [number, number],
style: { stroke: "#10b981", strokeWidth: 1.5 },
});
}
}
});
// 데이터 흐름 엣지 (화면 간)
flows
.filter((flow: any) => flow.source_screen_id === screen.screenId)
.forEach((flow: any, idx: number) => {
if (flow.target_screen_id) {
newEdges.push({
id: `edge-flow-${idx}`,
source: `screen-${screen.screenId}`,
target: `screen-${flow.target_screen_id}`,
sourceHandle: "right",
targetHandle: "left",
type: "smoothstep",
animated: true,
label: flow.flow_label || flow.flow_type || "이동",
labelStyle: { fontSize: 10, fill: "#8b5cf6", fontWeight: 500 },
labelBgStyle: { fill: "white", stroke: "#e2e8f0", strokeWidth: 1 },
labelBgPadding: [4, 2] as [number, number],
markerEnd: { type: MarkerType.ArrowClosed, color: "#8b5cf6" },
style: { stroke: "#8b5cf6", strokeWidth: 2 },
});
}
});
// 최종 노드 배열 합치기
const allNodes: AllNodeType[] = [...screenNodes, ...tableNodes];
// 테이블이 없으면 안내 노드 추가
if (tableNodes.length === 0) {
allNodes.push({
id: "hint-table",
type: "tableNode",
position: { x: 50, y: TABLE_Y },
data: {
label: "연결된 테이블 없음",
subLabel: "화면에 테이블을 설정하세요",
isMain: false,
columns: [],
},
});
}
setNodes(allNodes);
setEdges(newEdges);
} catch (error) {
console.error("관계 데이터 로드 실패:", error);
} finally {
setLoading(false);
}
};
loadRelations();
// focusedScreenId는 스타일링에만 영향을 미치므로 의존성에서 제외
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screen, selectedGroup, setNodes, setEdges, loadTableColumns]);
// 노드 클릭 핸들러 (그룹 모드에서 화면 포커스) - 조건부 return 전에 선언해야 함
const handleNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
// 그룹 모드가 아니면 무시
if (!selectedGroup) return;
// 화면 노드만 처리
if (node.id.startsWith("screen-")) {
const screenId = parseInt(node.id.replace("screen-", ""));
// 이미 포커스된 화면을 다시 클릭하면 포커스 해제
setFocusedScreenId((prev) => (prev === screenId ? null : screenId));
}
}, [selectedGroup]);
// 포커스에 따른 노드 스타일링 (그룹 모드에서 화면 클릭 시)
const styledNodes = React.useMemo(() => {
// 그룹 모드에서 포커스된 화면이 있을 때만 추가 스타일링
if (!selectedGroup || focusedScreenId === null) return nodes;
return nodes.map((node) => {
if (node.id.startsWith("screen-")) {
const screenId = parseInt(node.id.replace("screen-", ""));
const isFocused = screenId === focusedScreenId;
const isFaded = !isFocused;
return {
...node,
data: {
...node.data,
isFocused,
isFaded,
},
};
}
return node;
});
}, [nodes, selectedGroup, focusedScreenId]);
// 포커스에 따른 엣지 스타일링 (그룹 모드 & 개별 화면 모드)
const styledEdges = React.useMemo(() => {
// 개별 화면 모드: 메인 화면의 연결선만 강조
if (!selectedGroup && screen) {
const mainScreenId = screen.screenId;
return edges.map((edge) => {
// 화면 간 연결선
if (edge.source.startsWith("screen-") && edge.target.startsWith("screen-")) {
const sourceId = parseInt(edge.source.replace("screen-", ""));
const targetId = parseInt(edge.target.replace("screen-", ""));
const isConnected = sourceId === mainScreenId || targetId === mainScreenId;
return {
...edge,
animated: isConnected,
style: {
...edge.style,
stroke: isConnected ? "#8b5cf6" : "#d1d5db",
strokeWidth: isConnected ? 2 : 1,
opacity: isConnected ? 1 : 0.3,
},
};
}
// 화면-테이블 연결선
if (edge.source.startsWith("screen-") && edge.target.startsWith("table-")) {
const sourceId = parseInt(edge.source.replace("screen-", ""));
const isMyConnection = sourceId === mainScreenId;
return {
...edge,
animated: isMyConnection,
style: {
...edge.style,
stroke: isMyConnection ? "#3b82f6" : "#d1d5db",
strokeWidth: isMyConnection ? 2 : 1,
strokeDasharray: isMyConnection ? undefined : "5,5",
opacity: isMyConnection ? 1 : 0.3,
},
};
}
return edge;
});
}
// 그룹 모드: 포커스된 화면이 없으면 원본 반환
if (!selectedGroup || focusedScreenId === null) return edges;
return edges.map((edge) => {
// 화면 간 연결선 (1, 2, 3 라벨)
if (edge.source.startsWith("screen-") && edge.target.startsWith("screen-")) {
// 포커스된 화면과 연결된 화면 간 선만 활성화
const sourceId = parseInt(edge.source.replace("screen-", ""));
const targetId = parseInt(edge.target.replace("screen-", ""));
const isConnected = sourceId === focusedScreenId || targetId === focusedScreenId;
return {
...edge,
animated: isConnected,
style: {
...edge.style,
stroke: isConnected ? "#8b5cf6" : "#d1d5db",
strokeWidth: isConnected ? 2 : 1,
opacity: isConnected ? 1 : 0.3,
},
};
}
// 화면-테이블 연결선
if (edge.source.startsWith("screen-") && edge.target.startsWith("table-")) {
const sourceId = parseInt(edge.source.replace("screen-", ""));
const isMyConnection = sourceId === focusedScreenId;
return {
...edge,
animated: isMyConnection,
style: {
...edge.style,
stroke: isMyConnection ? "#3b82f6" : "#d1d5db",
strokeWidth: isMyConnection ? 2 : 1,
strokeDasharray: isMyConnection ? undefined : "5,5",
opacity: isMyConnection ? 1 : 0.3,
},
};
}
return edge;
});
}, [edges, selectedGroup, focusedScreenId, screen]);
// 조건부 렌더링 (모든 훅 선언 후에 위치해야 함)
if (!screen && !selectedGroup) {
return (
<div className="flex h-full items-center justify-center text-muted-foreground">
<div className="text-center">
<p className="text-sm"> </p>
<p className="text-sm"> </p>
</div>
</div>
);
}
if (loading) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-sm text-muted-foreground"> ...</div>
</div>
);
}
return (
<div className="h-full w-full">
<ReactFlow
nodes={styledNodes}
edges={styledEdges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={handleNodeClick}
nodeTypes={nodeTypes}
fitView
fitViewOptions={{ padding: 0.2 }}
minZoom={0.3}
maxZoom={1.5}
proOptions={{ hideAttribution: true }}
>
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#e2e8f0" />
<Controls position="bottom-right" />
</ReactFlow>
</div>
);
}
// 외부 래퍼 컴포넌트 (ReactFlowProvider 포함)
export function ScreenRelationFlow(props: ScreenRelationFlowProps) {
return (
<ReactFlowProvider>
<ScreenRelationFlowInner {...props} />
</ReactFlowProvider>
);
}