diff --git a/frontend/memo-tree.html b/frontend/memo-tree.html index 1b0db82..25da939 100644 --- a/frontend/memo-tree.html +++ b/frontend/memo-tree.html @@ -597,6 +597,21 @@ .status-writing { border-left-color: #f59e0b; } .status-review { border-left-color: #3b82f6; } .status-complete { border-left-color: #10b981; } + + /* 드래그 앤 드롭 스타일 */ + .drop-target-highlight { + background: rgba(59, 130, 246, 0.1) !important; + border: 2px dashed #3b82f6 !important; + transform: scale(1.05) !important; + box-shadow: 0 8px 25px rgba(59, 130, 246, 0.3) !important; + } + + .dragging { + opacity: 0.7; + transform: scale(1.05); + z-index: 1000; + cursor: grabbing !important; + } @@ -764,6 +779,7 @@
+ +
+
+
+ + + +
+
+
+
diff --git a/frontend/static/js/memo-tree.js b/frontend/static/js/memo-tree.js index a35e8b2..c7c6558 100644 --- a/frontend/static/js/memo-tree.js +++ b/frontend/static/js/memo-tree.js @@ -23,6 +23,13 @@ window.memoTreeApp = function() { showLoginModal: false, showMobileEditModal: false, + // 알림 시스템 + notification: { + show: false, + message: '', + type: 'info' // 'success', 'error', 'info' + }, + // 로그인 폼 상태 loginForm: { email: '', @@ -503,6 +510,20 @@ window.memoTreeApp = function() { }, // 트리 타입별 아이콘 가져오기 + // 알림 표시 + showNotification(message, type = 'info') { + this.notification = { + show: true, + message: message, + type: type + }; + + // 3초 후 자동으로 숨김 + setTimeout(() => { + this.notification.show = false; + }, 3000); + }, + getTreeIcon(treeType) { const icons = { novel: '📚', @@ -604,10 +625,144 @@ window.memoTreeApp = function() { // 노드 드래그 시작 startDragNode(event, node) { - // 현재는 드래그 기능 비활성화 (패닝과 충돌 방지) event.stopPropagation(); - console.log('드래그 시작:', node.title); - // TODO: 노드 드래그 앤 드롭 구현 + event.preventDefault(); + + console.log('🎯 노드 드래그 시작:', node.title); + + this.isDragging = true; + this.dragNode = node; + + // 드래그 시작 위치 계산 + const rect = event.target.getBoundingClientRect(); + this.dragOffset = { + x: event.clientX - rect.left, + y: event.clientY - rect.top + }; + + // 드래그 중인 노드 스타일 적용 + event.target.style.opacity = '0.7'; + event.target.style.transform = 'scale(1.05)'; + event.target.style.zIndex = '1000'; + + // 이벤트 리스너 등록 + document.addEventListener('mousemove', this.handleNodeDrag.bind(this)); + document.addEventListener('mouseup', this.endNodeDrag.bind(this)); + }, + + // 노드 드래그 처리 + handleNodeDrag(event) { + if (!this.isDragging || !this.dragNode) return; + + event.preventDefault(); + + // 드래그 중인 노드 위치 업데이트 + const dragElement = document.querySelector(`[data-node-id="${this.dragNode.id}"]`); + if (dragElement) { + const newX = event.clientX - this.dragOffset.x; + const newY = event.clientY - this.dragOffset.y; + + dragElement.style.position = 'fixed'; + dragElement.style.left = `${newX}px`; + dragElement.style.top = `${newY}px`; + dragElement.style.pointerEvents = 'none'; + } + + // 드롭 대상 하이라이트 + this.highlightDropTarget(event); + }, + + // 드롭 대상 하이라이트 + highlightDropTarget(event) { + // 모든 노드에서 하이라이트 제거 + document.querySelectorAll('.tree-diagram-node').forEach(el => { + el.classList.remove('drop-target-highlight'); + }); + + // 현재 마우스 위치의 노드 찾기 + const elements = document.elementsFromPoint(event.clientX, event.clientY); + const targetNode = elements.find(el => + el.classList.contains('tree-diagram-node') && + el.getAttribute('data-node-id') !== this.dragNode.id + ); + + if (targetNode) { + targetNode.classList.add('drop-target-highlight'); + } + }, + + // 노드 드래그 종료 + endNodeDrag(event) { + if (!this.isDragging || !this.dragNode) return; + + console.log('🎯 노드 드래그 종료'); + + // 드롭 대상 찾기 + const elements = document.elementsFromPoint(event.clientX, event.clientY); + const targetElement = elements.find(el => + el.classList.contains('tree-diagram-node') && + el.getAttribute('data-node-id') !== this.dragNode.id + ); + + let targetNodeId = null; + if (targetElement) { + targetNodeId = targetElement.getAttribute('data-node-id'); + } + + // 드래그 중인 노드 스타일 복원 + const dragElement = document.querySelector(`[data-node-id="${this.dragNode.id}"]`); + if (dragElement) { + dragElement.style.opacity = ''; + dragElement.style.transform = ''; + dragElement.style.zIndex = ''; + dragElement.style.position = ''; + dragElement.style.left = ''; + dragElement.style.top = ''; + dragElement.style.pointerEvents = ''; + } + + // 모든 하이라이트 제거 + document.querySelectorAll('.tree-diagram-node').forEach(el => { + el.classList.remove('drop-target-highlight'); + }); + + // 실제 노드 이동 처리 + if (targetNodeId && targetNodeId !== this.dragNode.id) { + this.moveNodeToParent(this.dragNode.id, targetNodeId); + } + + // 상태 초기화 + this.isDragging = false; + this.dragNode = null; + this.dragOffset = { x: 0, y: 0 }; + + // 이벤트 리스너 제거 + document.removeEventListener('mousemove', this.handleNodeDrag); + document.removeEventListener('mouseup', this.endNodeDrag); + }, + + // 노드를 다른 부모로 이동 + async moveNodeToParent(nodeId, newParentId) { + try { + console.log(`📦 노드 이동: ${nodeId} -> 부모: ${newParentId}`); + + const moveData = { + parent_id: newParentId, + sort_order: 0 // 새 부모의 첫 번째 자식으로 + }; + + await window.api.moveMemoNode(nodeId, moveData); + + // 트리 다시 로드 + await this.loadTreeNodes(); + + console.log('✅ 노드 이동 완료'); + this.showNotification('노드가 이동되었습니다.', 'success'); + + } catch (error) { + console.error('❌ 노드 이동 실패:', error); + this.showNotification('노드 이동에 실패했습니다.', 'error'); + } }, // 노드 인라인 편집