static 폴더 및 하위 파일 업로드

This commit is contained in:
sotam0316
2026-04-22 12:05:03 +09:00
commit 514b209a5a
203 changed files with 29494 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
import { graphHandlers } from './handlers/graph.js';
import { uiHandlers } from './handlers/ui.js';
import { clipboardHandlers } from './handlers/clipboard.js';
import { ioHandlers } from './handlers/io.js';
/**
* actionHandlers - 단축키 동작을 기능별 모듈에서 취합하여 제공하는 매니페스트 객체입니다.
*/
export const actionHandlers = {
...graphHandlers,
...uiHandlers,
...clipboardHandlers,
...ioHandlers
};
@@ -0,0 +1,190 @@
import { state } from '../../state.js';
import { logger } from '../../utils/logger.js';
const generateShortId = () => Math.random().toString(36).substring(2, 6);
export const clipboardHandlers = {
copyAttributes: () => {
if (!state.graph) return;
const selected = state.graph.getSelectedCells();
if (selected.length === 0) return;
const cell = selected[0];
const data = cell.getData() || {};
if (cell.isNode()) {
const whitelistedData = {};
const dataKeys = ['vendor', 'model', 'status', 'project', 'env', 'tags', 'color', 'asset_path'];
dataKeys.forEach(key => {
if (data[key] !== undefined) whitelistedData[key] = data[key];
});
const whitelistedAttrs = {
label: {
fill: cell.attr('label/fill'),
fontSize: cell.attr('label/fontSize'),
},
body: {
fill: cell.attr('body/fill'),
stroke: cell.attr('body/stroke'),
strokeWidth: cell.attr('body/strokeWidth'),
}
};
state.clipboardNodeData = whitelistedData;
state.clipboardNodeAttrs = whitelistedAttrs;
state.clipboardEdgeData = null; // Clear edge clipboard
logger.info(`Node attributes copied from ${cell.id}`);
} else if (cell.isEdge()) {
const edgeKeys = [
'color', 'routing', 'style', 'source_anchor', 'target_anchor',
'direction', 'is_tunnel', 'routing_offset', 'width', 'flow'
];
const edgeData = {};
edgeKeys.forEach(key => {
if (data[key] !== undefined) edgeData[key] = data[key];
});
state.clipboardEdgeData = edgeData;
state.clipboardNodeData = null; // Clear node clipboard
logger.info(`Edge attributes (Style) copied from ${cell.id}`);
}
},
pasteAttributes: async () => {
if (!state.graph) return;
const selected = state.graph.getSelectedCells();
if (selected.length === 0) return;
const hasNodeData = !!state.clipboardNodeData;
const hasEdgeData = !!state.clipboardEdgeData;
if (!hasNodeData && !hasEdgeData) return;
const { handleEdgeUpdate } = await import('../../properties_sidebar/handlers/edge.js');
state.graph.batchUpdate(async () => {
for (const cell of selected) {
if (cell.isNode() && hasNodeData) {
const currentData = cell.getData() || {};
cell.setData({ ...currentData, ...state.clipboardNodeData });
if (state.clipboardNodeAttrs) {
const attrs = state.clipboardNodeAttrs;
if (attrs.label) {
if (attrs.label.fill) cell.attr('label/fill', attrs.label.fill);
if (attrs.label.fontSize) cell.attr('label/fontSize', attrs.label.fontSize);
}
if (attrs.body) {
if (attrs.body.fill) cell.attr('body/fill', attrs.body.fill);
if (attrs.body.stroke) cell.attr('body/stroke', attrs.body.stroke);
if (attrs.body.strokeWidth) cell.attr('body/strokeWidth', attrs.body.strokeWidth);
}
}
} else if (cell.isEdge() && hasEdgeData) {
// Apply Edge Formatting
// We use handleEdgeUpdate to ensure physical routers/anchors are updated
await handleEdgeUpdate(cell, state.clipboardEdgeData);
}
}
});
logger.high(`Format Painter applied to ${selected.length} elements.`);
import('../../persistence.js').then(m => m.markDirty());
},
copyNodes: () => {
if (!state.graph) return;
const selected = state.graph.getSelectedCells();
if (selected.length === 0) return;
state.graph.copy(selected);
logger.high("Nodes copied to clipboard (X6).");
},
pasteNodes: () => {
if (!state.graph || state.graph.isClipboardEmpty()) return;
// Logical Layer Guard: Filter out nodes if target layer is logical
const activeLayer = state.layers.find(l => l.id === state.activeLayerId);
const isLogical = activeLayer && activeLayer.type === 'logical';
let pasted = state.graph.paste({ offset: 20 });
if (pasted && pasted.length > 0) {
if (isLogical) {
pasted.forEach(cell => { if (cell.isNode()) cell.remove(); });
pasted = pasted.filter(c => !c.isNode());
if (pasted.length === 0) {
import('/static/js/modules/i18n.js').then(({ t }) => {
import('../../ui/utils.js').then(m => m.showToast(t('err_logical_layer_drop'), 'warning'));
});
}
}
state.graph.batchUpdate(() => {
pasted.forEach(cell => {
if (cell.isNode()) {
const currentData = cell.getData() || {};
const currentLabel = currentData.label || cell.attr('label/text');
if (currentLabel) {
const newLabel = `${currentLabel}_${generateShortId()}`;
cell.setData({ ...currentData, label: newLabel, id: cell.id });
cell.attr('label/text', newLabel);
} else {
cell.setData({ ...currentData, id: cell.id });
}
}
});
});
state.graph.cleanSelection();
state.graph.select(pasted);
}
logger.high("Nodes pasted and randomized (Logical Filter applied).");
import('../../persistence.js').then(m => m.markDirty());
},
duplicateNodes: () => {
if (!state.graph) return;
const selected = state.graph.getSelectedCells();
if (selected.length === 0) return;
state.graph.copy(selected);
// Logical Layer Guard: Filter out nodes if target layer is logical
const activeLayer = state.layers.find(l => l.id === state.activeLayerId);
const isLogical = activeLayer && activeLayer.type === 'logical';
let cloned = state.graph.paste({ offset: 30 });
if (cloned && cloned.length > 0) {
if (isLogical) {
cloned.forEach(cell => { if (cell.isNode()) cell.remove(); });
cloned = cloned.filter(c => !c.isNode());
if (cloned.length === 0) {
import('/static/js/modules/i18n.js').then(({ t }) => {
import('../../ui/utils.js').then(m => m.showToast(t('err_logical_layer_drop'), 'warning'));
});
}
}
state.graph.batchUpdate(() => {
cloned.forEach(cell => {
if (cell.isNode()) {
const currentData = cell.getData() || {};
const currentLabel = currentData.label || cell.attr('label/text');
if (currentLabel) {
const newLabel = `${currentLabel}_${generateShortId()}`;
cell.setData({ ...currentData, label: newLabel, id: cell.id });
cell.attr('label/text', newLabel);
} else {
cell.setData({ ...currentData, id: cell.id });
}
}
});
});
state.graph.cleanSelection();
state.graph.select(cloned);
}
logger.high("Nodes duplicated and randomized (Logical Filter applied).");
import('../../persistence.js').then(m => m.markDirty());
}
};
+111
View File
@@ -0,0 +1,111 @@
import { state } from '../../state.js';
import { logger } from '../../utils/logger.js';
import { alignNodes, moveNodes, distributeNodes } from '../../graph/alignment.js';
import { handleMenuAction } from '../../ui/context_menu/handlers.js';
export const graphHandlers = {
// ... cleanup ...
deleteSelected: () => {
if (!state.graph) return;
const selected = state.graph.getSelectedCells();
if (selected.length === 0) return;
state.graph.removeCells(selected);
import('../../persistence.js').then(m => m.markDirty());
},
fitScreen: () => {
if (state.graph) {
state.graph.zoomToFit({ padding: 50 });
logger.high("Graph fit to screen (X6).");
}
},
zoomIn: () => state.graph?.zoom(0.2),
zoomOut: () => state.graph?.zoom(-0.2),
connectNodes: () => handleMenuAction('connect-solid'),
connectStraight: () => handleMenuAction('connect-straight'),
disconnectNodes: () => handleMenuAction('disconnect'),
undo: () => {
if (!state.graph) return;
try {
if (typeof state.graph.undo === 'function') {
state.graph.undo();
logger.info("Undo performed via graph.undo()");
} else {
const history = state.graph.getPlugin('history');
if (history) history.undo();
logger.info("Undo performed via history plugin");
}
} catch (e) {
logger.critical("Undo failed", e);
}
},
redo: () => {
if (!state.graph) return;
try {
if (typeof state.graph.redo === 'function') {
state.graph.redo();
logger.info("Redo performed via graph.redo()");
} else {
const history = state.graph.getPlugin('history');
if (history) history.redo();
logger.info("Redo performed via history plugin");
}
} catch (e) {
logger.critical("Redo failed", e);
}
},
arrangeLayout: () => {
logger.high("Auto-layout is currently disabled (Refactoring).");
},
bringToFront: () => {
if (!state.graph) return;
const selected = state.graph.getSelectedCells();
selected.forEach(cell => cell.toFront({ deep: true }));
import('../../persistence.js').then(m => m.markDirty());
},
sendToBack: () => {
if (!state.graph) return;
const selected = state.graph.getSelectedCells();
selected.forEach(cell => cell.toBack({ deep: true }));
import('../../persistence.js').then(m => m.markDirty());
},
toggleLock: () => {
if (!state.graph) return;
const selected = state.graph.getSelectedCells();
if (selected.length === 0) return;
selected.forEach(cell => {
const data = cell.getData() || {};
const isLocked = !data.locked;
cell.setData({ ...data, locked: isLocked });
cell.setProp('movable', !isLocked);
cell.setProp('deletable', !isLocked);
cell.setProp('rotatable', !isLocked);
if (isLocked) {
cell.addTools([{ name: 'boundary', args: { attrs: { stroke: '#ef4444', 'stroke-dasharray': '5,5' } } }]);
} else {
cell.removeTools();
}
});
import('../../persistence.js').then(m => m.markDirty());
logger.high(`Toggled lock for ${selected.length} elements.`);
},
alignTop: () => alignNodes('top'),
alignBottom: () => alignNodes('bottom'),
alignLeft: () => alignNodes('left'),
alignRight: () => alignNodes('right'),
alignMiddle: () => alignNodes('middle'),
alignCenter: () => alignNodes('center'),
distributeHorizontal: () => distributeNodes('horizontal'),
distributeVertical: () => distributeNodes('vertical'),
groupNodes: () => handleMenuAction('group'),
moveUp: () => moveNodes(0, -5),
moveDown: () => moveNodes(0, 5),
moveLeft: () => moveNodes(-5, 0),
moveRight: () => moveNodes(5, 0),
moveUpLarge: () => moveNodes(0, -(state.gridSpacing || 20)),
moveDownLarge: () => moveNodes(0, (state.gridSpacing || 20)),
moveLeftLarge: () => moveNodes(-(state.gridSpacing || 20), 0),
moveRightLarge: () => moveNodes((state.gridSpacing || 20), 0)
};
+10
View File
@@ -0,0 +1,10 @@
export const ioHandlers = {
exportJson: () => {
const exportBtn = document.getElementById('export-json');
if (exportBtn) exportBtn.click();
},
importJson: () => {
const importBtn = document.getElementById('import-json');
if (importBtn) importBtn.click();
}
};
+34
View File
@@ -0,0 +1,34 @@
import { state } from '../../state.js';
export const uiHandlers = {
toggleInventory: () => {
import('../../graph/analysis.js').then(m => m.toggleInventory());
},
toggleProperties: () => {
import('../../properties_sidebar/index.js').then(m => m.toggleSidebar());
},
toggleLayers: () => {
import('../../ui/layer_panel.js').then(m => m.toggleLayerPanel());
},
editLabel: () => {
if (!state.graph) return;
const selected = state.graph.getSelectedCells();
if (selected.length === 0) return;
import('../../properties_sidebar/index.js').then(m => {
m.toggleSidebar(true);
setTimeout(() => {
const labelInput = document.getElementById('prop-label');
if (labelInput) {
labelInput.focus();
labelInput.select();
}
}, 50);
});
},
cancel: () => {
if (!state.graph) return;
state.graph.cleanSelection();
import('../../properties_sidebar/index.js').then(m => m.toggleSidebar(false));
}
};
+94
View File
@@ -0,0 +1,94 @@
import { state } from '../state.js';
import { actionHandlers } from './handlers.js';
import { logger } from '../utils/logger.js';
/**
* initHotkeys - Initializes the keyboard shortcut system.
*/
export async function initHotkeys() {
if (window.__drawNET_Hotkeys_Initialized) {
logger.high("Hotkeys already initialized. Skipping.");
return;
}
window.__drawNET_Hotkeys_Initialized = true;
try {
const response = await fetch('/static/hotkeys.json');
const config = await response.json();
const hotkeys = config.hotkeys;
window.addEventListener('keydown', (e) => {
if (e.repeat) return;
const editableElements = ['TEXTAREA', 'INPUT'];
const isTyping = editableElements.includes(e.target.tagName) && e.target.id !== 'dark-mode-toggle';
const keyPressed = e.key.toLowerCase();
const keyCode = e.code;
for (const hk of hotkeys) {
// Match by physical code (preferred) or key string
const matchCode = hk.code && hk.code === keyCode;
const matchKey = hk.key && hk.key.toLowerCase() === keyPressed;
if (!(matchCode || matchKey)) continue;
// Match modifiers strictly
const matchCtrl = !!hk.ctrl === e.ctrlKey;
const matchAlt = !!hk.alt === e.altKey;
const matchShift = !!hk.shift === e.shiftKey;
if (matchCtrl && matchAlt && matchShift) {
const allowWhileTyping = hk.alwaysEnabled === true;
if (isTyping && !allowWhileTyping) break;
logger.info(`Hotkey Match - ${hk.action} (${hk.key})`);
if (allowWhileTyping || !isTyping) {
e.preventDefault();
}
const handler = actionHandlers[hk.action];
if (handler) {
handler();
}
return;
}
}
});
logger.high("Hotkeys module initialized (X6).");
} catch (err) {
logger.critical("Failed to load hotkeys configuration (X6).", err);
}
// Ctrl + Wheel: Zoom handling
window.addEventListener('wheel', (e) => {
if (e.ctrlKey) {
e.preventDefault();
if (!state.graph) return;
const delta = e.deltaY > 0 ? -0.1 : 0.1;
state.graph.zoom(delta, { center: { x: e.clientX, y: e.clientY } });
}
}, { passive: false });
// Global Key State Tracking (for Ctrl+Drag copy)
// Extra guard: Check e.ctrlKey/metaKey on mouse events to auto-heal mismatched state
const syncCtrlState = (e) => {
const isModifier = e.ctrlKey || e.metaKey;
if (state.isCtrlPressed !== isModifier) {
state.isCtrlPressed = isModifier;
}
};
window.addEventListener('mousedown', syncCtrlState, true);
window.addEventListener('mousemove', syncCtrlState, true);
window.addEventListener('keydown', (e) => {
if (e.key === 'Control' || e.key === 'Meta') state.isCtrlPressed = true;
}, true);
window.addEventListener('keyup', (e) => {
if (e.key === 'Control' || e.key === 'Meta') state.isCtrlPressed = false;
}, true);
window.addEventListener('blur', () => {
state.isCtrlPressed = false; // Reset on window blur to avoid stuck state
});
}