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
@@ -0,0 +1,95 @@
import { state } from '../../state.js';
import { logger } from '../../utils/logger.js';
import { t } from '../../i18n.js';
import { getX6NodeConfig } from '../styles.js';
/**
* initDropHandling - Sets up drag and drop from the asset library to the graph
*/
export function initDropHandling(container) {
container.addEventListener('dragover', (e) => e.preventDefault());
container.addEventListener('drop', (e) => {
e.preventDefault();
const assetId = e.dataTransfer.getData('assetId');
const asset = state.assetMap[assetId];
const pos = state.graph.clientToLocal(e.clientX, e.clientY);
logger.info(`Drop Event at {${pos.x}, ${pos.y}}`, {
assetId: assetId,
found: !!asset,
id: asset?.id,
path: asset?.path,
type: asset?.type,
rawAsset: asset
});
if (!asset) {
logger.high(`Asset [${assetId}] not found in map`);
return;
}
// Logical Layer Guard: Block dropping new nodes
const activeLayer = state.layers.find(l => l.id === state.activeLayerId);
if (activeLayer && activeLayer.type === 'logical') {
import('../../ui/utils.js').then(m => m.showToast(t('err_logical_layer_drop'), 'warning'));
return;
}
const isPrimitive = ['rect', 'rounded-rect', 'circle', 'text-box', 'label'].includes(asset.type);
const prefix = asset.id === 'fixed-group' ? 'group_' : (asset.type === 'blank' || asset.type === 'dot' ? 'p_' : (isPrimitive ? 'shp_' : 'node_'));
const id = prefix + Math.random().toString(36).substr(2, 4);
// Use translate keys or asset label first, or fallback to generic name
let initialLabel = asset.label || t(asset.id) || (asset.id === 'fixed-group' ? 'New Group' : 'New ' + (asset.type || 'Object'));
if (asset.id === 'fixed-group') initialLabel = t('group') || 'New Group';
// Find if dropped over a group (prefer the smallest/innermost one)
const candidates = state.graph.getNodes().filter(n => {
if (!n.getData()?.is_group) return false;
const bbox = n.getBBox();
return bbox.containsPoint(pos);
});
const parent = candidates.length > 0
? candidates.reduce((smallest, current) => {
const smallestBBox = smallest.getBBox();
const currentBBox = current.getBBox();
const currentArea = currentBBox.width * currentBBox.height;
const smallestArea = smallestBBox.width * smallestBBox.height;
if (currentArea < smallestArea) return current;
if (currentArea > smallestArea) return smallest;
// If areas are equal, pick the one that is a descendant of the other
if (current.getAncestors().some(a => a.id === smallest.id)) return current;
return smallest;
})
: undefined;
// 2. Add to X6 immediately
const config = getX6NodeConfig({
id: id,
label: initialLabel,
type: asset.type || asset.category || asset.label || 'Unknown',
assetId: assetId, // Store unique composite ID for lookup
assetPath: asset.id === 'fixed-group' ? undefined : asset.path,
is_group: asset.id === 'fixed-group' || asset.is_group === true,
pos: { x: pos.x, y: pos.y },
parent: parent ? parent.id : undefined,
layerId: state.activeLayerId,
description: "",
asset_tag: "",
tags: []
});
const newNode = state.graph.addNode(config);
if (parent) {
parent.addChild(newNode);
}
// Ensure the new node follows the active layer's interaction rules immediately
import('../layers.js').then(m => m.applyLayerFilters());
});
}
@@ -0,0 +1,47 @@
/**
* graph/interactions/edges.js - Handles edge connection logic and metadata assignment.
*/
import { state } from '../../state.js';
/**
* initEdgeHandling - Sets up edge connection events
*/
export function initEdgeHandling() {
state.graph.on('edge:connected', ({ edge }) => {
const sourceId = edge.getSourceCellId();
const targetId = edge.getTargetCellId();
if (!sourceId || !targetId) return;
// Assign to current active layer and apply standard styling
import('/static/js/modules/graph/styles.js').then(({ getX6EdgeConfig }) => {
// [6개월 뒤의 나를 위한 메모]
// 단순 유추가 아닌 명시적 기록을 통해, 도면 전체를 전송하지 않고도 연결선 단독으로
// 크로스 레이어 여부를 파악할수 있게 하여 외부 도구/AI 연동 안정성을 높임.
const sourceNode = state.graph.getCellById(sourceId);
const targetNode = state.graph.getCellById(targetId);
const sourceLayer = sourceNode?.getData()?.layerId || state.activeLayerId;
const targetLayer = targetNode?.getData()?.layerId || state.activeLayerId;
const isCrossLayer = (sourceLayer !== targetLayer);
const edgeData = {
source: sourceId,
target: targetId,
layerId: state.activeLayerId,
source_layer: sourceLayer,
target_layer: targetLayer,
is_cross_layer: isCrossLayer,
...edge.getData()
};
const config = getX6EdgeConfig(edgeData);
// Apply standardized config to the new edge
edge.setData(edgeData, { silent: true });
if (config.router) edge.setRouter(config.router);
else edge.setRouter(null);
if (config.connector) edge.setConnector(config.connector);
edge.setAttrs(config.attrs);
});
});
}
@@ -0,0 +1,23 @@
import { state } from '../../state.js';
import { initDropHandling } from './drop.js';
import { initEdgeHandling } from './edges.js';
import { initSnapping } from './snapping.js';
import { initSpecialShortcuts } from './shortcuts.js';
import { logger } from '../../utils/logger.js';
/**
* initInteractions - Entry point for all graph interactions
*/
export function initInteractions() {
if (!state.graph) return;
const container = state.graph.container;
// 1. Initialize Sub-modules
initDropHandling(container);
initEdgeHandling();
initSnapping();
initSpecialShortcuts();
logger.info("Graph interactions initialized via modules.");
}
@@ -0,0 +1,125 @@
import { state } from '../../state.js';
import { logger } from '../../utils/logger.js';
/**
* initSpecialShortcuts - Sets up specialized mouse/keyboard combined gestures
*/
export function initSpecialShortcuts() {
if (!state.graph) return;
let dragStartData = null;
// Ctrl + Drag to Copy: Improved Ghost Logic
state.graph.on('node:mousedown', ({ e, node }) => {
// Support both Ctrl (Windows) and Cmd (Mac)
const isModifier = e.ctrlKey || e.metaKey;
if (isModifier && e.button === 0) {
const selected = state.graph.getSelectedCells();
const targets = selected.includes(node) ? selected : [node];
// 1. Initial State for tracking
const mouseStart = state.graph.clientToLocal(e.clientX, e.clientY);
dragStartData = {
targets: targets,
mouseStart: mouseStart,
initialNodesPos: targets.map(t => ({ ...t.position() })),
ghost: null
};
const onMouseMove = (moveEvt) => {
if (!dragStartData) return;
const mouseNow = state.graph.clientToLocal(moveEvt.clientX, moveEvt.clientY);
const dx = mouseNow.x - dragStartData.mouseStart.x;
const dy = mouseNow.y - dragStartData.mouseStart.y;
// Create Ghost on movement
if (!dragStartData.ghost && (Math.abs(dx) > 2 || Math.abs(dy) > 2)) {
const cellsBBox = state.graph.getCellsBBox(dragStartData.targets);
dragStartData.ghost = state.graph.addNode({
shape: 'rect',
x: cellsBBox.x,
y: cellsBBox.y,
width: cellsBBox.width,
height: cellsBBox.height,
attrs: {
body: {
fill: 'rgba(59, 130, 246, 0.1)',
stroke: '#3b82f6',
strokeWidth: 2,
strokeDasharray: '5,5',
pointerEvents: 'none'
}
},
zIndex: 1000
});
dragStartData.initialGhostPos = { x: cellsBBox.x, y: cellsBBox.y };
}
if (dragStartData.ghost) {
dragStartData.ghost.position(
dragStartData.initialGhostPos.x + dx,
dragStartData.initialGhostPos.y + dy
);
}
};
const onMouseUp = (upEvt) => {
cleanup(upEvt.button === 0);
};
const onKeyDown = (keyEvt) => {
if (keyEvt.key === 'Escape') {
cleanup(false); // Cancel on ESC
}
};
function cleanup(doPaste) {
if (dragStartData) {
if (doPaste && dragStartData.ghost) {
const finalPos = dragStartData.ghost.position();
const dx = finalPos.x - dragStartData.initialGhostPos.x;
const dy = finalPos.y - dragStartData.initialGhostPos.y;
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
state.graph.copy(dragStartData.targets, { deep: true });
let clones = state.graph.paste({ offset: { dx, dy }, select: true });
// Logical Layer Guard: Filter out nodes if target layer is logical
const activeLayer = state.layers.find(l => l.id === state.activeLayerId);
if (activeLayer && activeLayer.type === 'logical') {
clones.forEach(clone => {
if (clone.isNode()) clone.remove();
});
clones = clones.filter(c => !c.isNode());
if (clones.length === 0) {
import('/static/js/modules/ui/utils.js').then(m => m.showToast(t('err_logical_layer_drop'), 'warning'));
}
}
clones.forEach(clone => {
if (clone.isNode()) {
const d = clone.getData() || {};
clone.setData({ ...d, id: clone.id }, { silent: true });
}
});
import('/static/js/modules/persistence.js').then(m => m.markDirty());
}
}
if (dragStartData.ghost) dragStartData.ghost.remove();
}
dragStartData = null;
window.removeEventListener('mousemove', onMouseMove, true);
window.removeEventListener('mouseup', onMouseUp, true);
window.removeEventListener('keydown', onKeyDown, true);
}
window.addEventListener('mousemove', onMouseMove, true);
window.addEventListener('mouseup', onMouseUp, true);
window.addEventListener('keydown', onKeyDown, true);
}
});
logger.info("initSpecialShortcuts (Ctrl+Drag with ESC Failsafe) initialized.");
}
@@ -0,0 +1,48 @@
import { state } from '../../state.js';
/**
* initSnapping - Sets up snapping and grouping interactions during movement
*/
export function initSnapping() {
// 1. Rack Snapping Logic
state.graph.on('node:moving', ({ node }) => {
const parent = node.getParent();
if (parent && parent.shape === 'drawnet-rack') {
const rackBBox = parent.getBBox();
const nodeSize = node.size();
const pos = node.getPosition();
const headerHeight = 30;
const rackBodyHeight = rackBBox.height - headerHeight;
const unitCount = parent.getData()?.slots || 42;
const unitHeight = rackBodyHeight / unitCount;
const centerX = rackBBox.x + (rackBBox.width - nodeSize.width) / 2;
const relativeY = pos.y - rackBBox.y - headerHeight;
const snapY = rackBBox.y + headerHeight + Math.round(relativeY / unitHeight) * unitHeight;
node.setPosition(centerX, snapY, { silent: true });
}
});
// 2. Parent-Child Relationship Update on Move End
state.graph.on('node:moved', ({ node }) => {
if (node.getData()?.is_group) return;
let parent = node.getParent();
if (!parent) {
const pos = node.getPosition();
const center = { x: pos.x + node.size().width / 2, y: pos.y + node.size().height / 2 };
parent = state.graph.getNodes().find(n => {
if (n.id === node.id || !n.getData()?.is_group) return false;
const bbox = n.getBBox();
return bbox.containsPoint(center);
});
}
if (parent && node.getParent()?.id !== parent.id) {
parent.addChild(node);
}
});
}