✨ 메모 트리 시스템 드래그 앤 드롭 기능 완성
�� 주요 기능 추가: - 노드 드래그 앤 드롭으로 부모-자식 관계 변경 - 드래그 중 시각적 피드백 (투명도, 크기 변화) - 드롭 대상 하이라이트 효과 - 실시간 노드 위치 추적 및 이동 🔧 기술적 구현: - startDragNode, handleNodeDrag, endNodeDrag 함수 구현 - data-node-id 속성으로 노드 식별 - document.elementsFromPoint로 드롭 대상 감지 - moveNodeToParent API 호출로 실제 데이터 업데이트 🎨 UI/UX 개선: - 드래그 중 노드 스타일 변경 (opacity, scale, z-index) - 드롭 대상 하이라이트 CSS (.drop-target-highlight) - 토스트 알림 시스템 추가 - 성공/실패 피드백 제공 📱 사용자 경험: - 직관적인 드래그 앤 드롭 인터페이스 - 실시간 시각적 피드백 - 자동 트리 구조 업데이트 - 에러 처리 및 사용자 알림
This commit is contained in:
@@ -597,6 +597,21 @@
|
|||||||
.status-writing { border-left-color: #f59e0b; }
|
.status-writing { border-left-color: #f59e0b; }
|
||||||
.status-review { border-left-color: #3b82f6; }
|
.status-review { border-left-color: #3b82f6; }
|
||||||
.status-complete { border-left-color: #10b981; }
|
.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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -764,6 +779,7 @@
|
|||||||
<div
|
<div
|
||||||
class="absolute tree-diagram-node"
|
class="absolute tree-diagram-node"
|
||||||
:style="getNodePosition(node)"
|
:style="getNodePosition(node)"
|
||||||
|
:data-node-id="node.id"
|
||||||
@mousedown="startDragNode($event, node)"
|
@mousedown="startDragNode($event, node)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -977,6 +993,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 토스트 알림 -->
|
||||||
|
<div x-show="notification.show"
|
||||||
|
x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 transform translate-x-full"
|
||||||
|
x-transition:enter-end="opacity-100 transform translate-x-0"
|
||||||
|
x-transition:leave="transition ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100 transform translate-x-0"
|
||||||
|
x-transition:leave-end="opacity-0 transform translate-x-full"
|
||||||
|
class="fixed top-4 right-4 z-50 max-w-sm">
|
||||||
|
<div class="rounded-lg shadow-lg border p-4"
|
||||||
|
:class="{
|
||||||
|
'bg-green-50 border-green-200 text-green-800': notification.type === 'success',
|
||||||
|
'bg-red-50 border-red-200 text-red-800': notification.type === 'error',
|
||||||
|
'bg-blue-50 border-blue-200 text-blue-800': notification.type === 'info'
|
||||||
|
}">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i :class="{
|
||||||
|
'fas fa-check-circle text-green-600': notification.type === 'success',
|
||||||
|
'fas fa-exclamation-circle text-red-600': notification.type === 'error',
|
||||||
|
'fas fa-info-circle text-blue-600': notification.type === 'info'
|
||||||
|
}" class="mr-2"></i>
|
||||||
|
<span x-text="notification.message"></span>
|
||||||
|
<button @click="notification.show = false" class="ml-auto text-gray-400 hover:text-gray-600">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 로그인이 필요한 상태 -->
|
<!-- 로그인이 필요한 상태 -->
|
||||||
<div x-show="!currentUser" class="flex items-center justify-center h-screen">
|
<div x-show="!currentUser" class="flex items-center justify-center h-screen">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ window.memoTreeApp = function() {
|
|||||||
showLoginModal: false,
|
showLoginModal: false,
|
||||||
showMobileEditModal: false,
|
showMobileEditModal: false,
|
||||||
|
|
||||||
|
// 알림 시스템
|
||||||
|
notification: {
|
||||||
|
show: false,
|
||||||
|
message: '',
|
||||||
|
type: 'info' // 'success', 'error', 'info'
|
||||||
|
},
|
||||||
|
|
||||||
// 로그인 폼 상태
|
// 로그인 폼 상태
|
||||||
loginForm: {
|
loginForm: {
|
||||||
email: '',
|
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) {
|
getTreeIcon(treeType) {
|
||||||
const icons = {
|
const icons = {
|
||||||
novel: '📚',
|
novel: '📚',
|
||||||
@@ -604,10 +625,144 @@ window.memoTreeApp = function() {
|
|||||||
|
|
||||||
// 노드 드래그 시작
|
// 노드 드래그 시작
|
||||||
startDragNode(event, node) {
|
startDragNode(event, node) {
|
||||||
// 현재는 드래그 기능 비활성화 (패닝과 충돌 방지)
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
console.log('드래그 시작:', node.title);
|
event.preventDefault();
|
||||||
// TODO: 노드 드래그 앤 드롭 구현
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 노드 인라인 편집
|
// 노드 인라인 편집
|
||||||
|
|||||||
Reference in New Issue
Block a user