Files
drawNET/static/js/modules/graph/io/json_handler.js
T

154 lines
6.0 KiB
JavaScript

/**
* graph/io/json_handler.js - Project serialization and restoration with auto-repair logic.
*/
import { state } from '../../state.js';
import { logger } from '../../utils/logger.js';
/**
* getProjectData - 프로젝트 전체를 JSON으로 직렬화 (graph.toJSON 기반)
*/
export function getProjectData() {
if (!state.graph) return null;
const graphJson = state.graph.toJSON();
const viewport = state.graph.translate();
return {
header: {
app: "drawNET Premium",
version: "3.0.0",
export_time: new Date().toISOString(),
projectName: state.projectName || "Untitled_Project"
},
graphJson: graphJson,
viewport: {
zoom: state.graph.zoom(),
tx: viewport.tx,
ty: viewport.ty
},
settings: {
gridSpacing: state.gridSpacing,
gridStyle: state.gridStyle,
isSnapEnabled: state.isSnapEnabled,
canvasSize: state.canvasSize,
theme: document.documentElement.getAttribute('data-theme')
},
layers: state.layers,
activeLayerId: state.activeLayerId
};
}
/**
* restoreProjectData - JSON 프로젝트 데이터로 그래프 복원
*/
export function restoreProjectData(data) {
if (!state.graph || !data) return false;
try {
if (data.graphJson) {
state.graph.fromJSON(data.graphJson);
// Migrate old data to new schema (Add defaults if missing)
state.graph.getCells().forEach(cell => {
const cellData = cell.getData() || {};
let updated = false;
// 1. Recover missing position/size for nodes from data.pos
if (cell.isNode()) {
const pos = cell.getPosition();
if ((!pos || (pos.x === 0 && pos.y === 0)) && cellData.pos) {
cell.setPosition(cellData.pos.x, cellData.pos.y);
logger.info(`Recovered position for ${cell.id} from data.pos`);
updated = true;
}
const size = cell.getSize();
if (!size || (size.width === 0 && size.height === 0)) {
if (cellData.is_group) cell.setSize(200, 200);
else cell.setSize(60, 60);
updated = true;
}
}
// 2. Ensure description exists for all objects
if (cellData.description === undefined) {
cellData.description = "";
updated = true;
}
// 3. Ensure routing_offset exists for edges
if (cell.isEdge()) {
if (cellData.routing_offset === undefined) {
cellData.routing_offset = 20;
updated = true;
}
// [6개월 뒤의 나를 위한 메모]
// 하위 호환성 유지 및 데이터 무결성 강화를 위한 자가 치유(Self-healing) 로직.
// 기존 도면을 불러올 때 누락된 크로스 레이어 정보를 노드 데이터를 참조하여 실시간 복구함.
if (cellData.source_layer === undefined || cellData.target_layer === undefined) {
const srcNode = state.graph.getCellById(cellData.source);
const dstNode = state.graph.getCellById(cellData.target);
// Default to current edge's layer if node is missing (safe fallback)
const srcLayer = srcNode?.getData()?.layerId || cellData.layerId || 'l1';
const dstLayer = dstNode?.getData()?.layerId || cellData.layerId || 'l1';
cellData.source_layer = srcLayer;
cellData.target_layer = dstLayer;
cellData.is_cross_layer = (srcLayer !== dstLayer);
updated = true;
logger.info(`Auto-repaired layer metadata for edge ${cell.id}`);
}
}
// 4. Ensure label consistency (data vs attrs)
const pureLabel = cellData.label || "";
const visualLabel = cell.attr('label/text');
if (visualLabel !== pureLabel) {
cell.attr('label/text', pureLabel);
updated = true;
}
if (updated) {
cell.setData(cellData, { silent: true });
}
});
}
const vp = data.viewport || {};
if (vp.zoom !== undefined) state.graph.zoomTo(vp.zoom);
if (vp.tx !== undefined) state.graph.translate(vp.tx, vp.ty || 0);
const settings = data.settings || {};
if (settings.gridSpacing) {
window.dispatchEvent(new CustomEvent('gridSpacingChanged', { detail: { spacing: settings.gridSpacing } }));
}
if (settings.gridStyle) {
window.dispatchEvent(new CustomEvent('gridChanged', { detail: { style: settings.gridStyle } }));
}
if (settings.theme) {
document.documentElement.setAttribute('data-theme', settings.theme);
}
// Restore Layers
if (data.layers && Array.isArray(data.layers)) {
state.layers = data.layers;
state.activeLayerId = data.activeLayerId || data.layers[0]?.id;
}
// Restore Metadata
if (data.header?.projectName) {
state.projectName = data.header.projectName;
const titleEl = document.getElementById('project-title');
if (titleEl) titleEl.innerText = state.projectName;
}
state.graph.trigger('project:restored');
return true;
} catch (e) {
logger.critical("Failed to restore project data.", e);
return false;
}
}