+
⠿
${esc(node.mark||'·')}
${esc(node.content||'이름 없음')}
${col?'
📥':''}
@@ -831,6 +839,7 @@ function renderCanvas(){
+
@@ -861,6 +870,117 @@ function deleteFromCanvas(nid){
deleteNode(nid);
}
+// ── 노드 트리 직접 조작 ────────────────────────────────
+function removeNodeFromTree(nid){
+ const ti=S.nodes.findIndex(n=>n.id===nid);
+ if(ti!==-1) return S.nodes.splice(ti,1)[0];
+ for(const n of Object.values(nodeMap)){
+ const vi=n.values.findIndex(v=>v.type==='node'&&v.node.id===nid);
+ if(vi!==-1) return n.values.splice(vi,1)[0].node;
+ }
+ return null;
+}
+function insertAsChildOf(node, parentNid){
+ const p=nodeMap[parentNid]; if(!p) return;
+ p.values.push(mkNodeVal(node));
+}
+function insertBeforeNode(node, targetNid){
+ const ti=S.nodes.findIndex(n=>n.id===targetNid);
+ if(ti!==-1){ S.nodes.splice(ti,0,node); return; }
+ for(const n of Object.values(nodeMap)){
+ const vi=n.values.findIndex(v=>v.type==='node'&&v.node.id===targetNid);
+ if(vi!==-1){ n.values.splice(vi,0,mkNodeVal(node)); return; }
+ }
+}
+function insertAfterNode(node, targetNid){
+ const ti=S.nodes.findIndex(n=>n.id===targetNid);
+ if(ti!==-1){ S.nodes.splice(ti+1,0,node); return; }
+ for(const n of Object.values(nodeMap)){
+ const vi=n.values.findIndex(v=>v.type==='node'&&v.node.id===targetNid);
+ if(vi!==-1){ n.values.splice(vi+1,0,mkNodeVal(node)); return; }
+ }
+}
+
+// ── 노드 복사 ──────────────────────────────────────────
+function deepCloneNode(n){
+ function clone(node){
+ const c=mkNode(node.mark,node.content);
+ c.hidden=node.hidden??false;
+ c.nameNewline=node.nameNewline??true;
+ c.settings=JSON.parse(JSON.stringify(node.settings||{}));
+ for(const v of node.values||[]){
+ if(v.type==='node') c.values.push(mkNodeVal(clone(v.node)));
+ else{ const sv=mkScalarVal(v.type); sv.data=v.data??''; c.values.push(sv); }
+ }
+ return c;
+ }
+ return clone(n);
+}
+function copyNode(nid){
+ rebuildMaps();
+ const n=nodeMap[nid]; if(!n) return;
+ const cloned=deepCloneNode(n);
+ insertAfterNode(cloned,nid);
+ selectedNodeId=cloned.id;
+ refresh();
+}
+
+// ── 드래그로 이동 ──────────────────────────────────────
+function startDrag(e, nid){
+ if(e.button!==0) return;
+ e.preventDefault(); e.stopPropagation();
+ const srcEl=document.querySelector(`.cnode[data-nid="${nid}"]`); if(!srcEl) return;
+ const rect=srcEl.getBoundingClientRect();
+
+ const ghost=srcEl.cloneNode(true);
+ ghost.style.cssText=`width:${rect.width}px;position:fixed;left:${rect.left}px;top:${rect.top}px;pointer-events:none;z-index:9999;opacity:.85;transform:rotate(1.5deg) scale(1.04);box-shadow:0 12px 40px rgba(0,0,0,.25);transition:none`;
+ ghost.classList.remove('selected','dragging');
+ document.body.appendChild(ghost);
+ srcEl.classList.add('dragging');
+
+ const state={ nid, ghost, ox:e.clientX-rect.left, oy:e.clientY-rect.top, drop:null };
+
+ function onMove(e){
+ ghost.style.left=(e.clientX-state.ox)+'px';
+ ghost.style.top =(e.clientY-state.oy)+'px';
+ document.querySelectorAll('.cnode.drop-child,.cnode.drop-before,.cnode.drop-after')
+ .forEach(el=>el.classList.remove('drop-child','drop-before','drop-after'));
+ state.drop=null;
+ for(const el of document.querySelectorAll('.cnode:not(.dragging)')){
+ const r=el.getBoundingClientRect();
+ if(e.clientX>=r.left&&e.clientX<=r.right&&e.clientY>=r.top&&e.clientY<=r.bottom){
+ const rel=(e.clientY-r.top)/r.height;
+ const mode=rel<0.3?'before':rel>0.7?'after':'child';
+ el.classList.add('drop-'+mode);
+ state.drop={nid:el.dataset.nid, mode};
+ break;
+ }
+ }
+ }
+
+ function onUp(){
+ ghost.remove();
+ document.querySelectorAll('.cnode').forEach(el=>el.classList.remove('dragging','drop-child','drop-before','drop-after'));
+ document.removeEventListener('mousemove',onMove);
+ document.removeEventListener('mouseup',onUp);
+ if(state.drop&&state.drop.nid!==state.nid){
+ rebuildMaps();
+ const node=removeNodeFromTree(state.nid);
+ if(node){
+ rebuildMaps();
+ if(state.drop.mode==='child') insertAsChildOf(node, state.drop.nid);
+ else if(state.drop.mode==='before') insertBeforeNode(node, state.drop.nid);
+ else insertAfterNode(node, state.drop.nid);
+ selectedNodeId=state.nid;
+ refresh();
+ }
+ }
+ }
+
+ document.addEventListener('mousemove',onMove);
+ document.addEventListener('mouseup',onUp);
+}
+
function renderDetail(){
const panel=document.getElementById('detailPanel'); if(!panel) return;
if(!selectedNodeId||!nodeMap[selectedNodeId]){
@@ -883,6 +1003,7 @@ function renderDetail(){
+