/** * 트리 구조 메모장 JavaScript */ // Monaco Editor 인스턴스 let monacoEditor = null; // 트리 메모장 Alpine.js 컴포넌트 window.memoTreeApp = function() { return { // 상태 관리 currentUser: null, userTrees: [], selectedTreeId: '', selectedTree: null, treeNodes: [], selectedNode: null, // UI 상태 showNewTreeModal: false, showNewNodeModal: false, showTreeSettings: false, showLoginModal: false, showMobileEditModal: false, // 로그인 폼 상태 loginForm: { email: '', password: '' }, loginError: '', loginLoading: false, // 폼 데이터 newTree: { title: '', description: '', tree_type: 'general' }, newNode: { title: '', node_type: 'memo', parent_id: null }, // 에디터 상태 editorContent: '', isEditorDirty: false, // 트리 상태 expandedNodes: new Set(), // 트리 다이어그램 상태 treeZoom: 1, treePanX: 0, treePanY: 0, nodePositions: new Map(), // 노드 ID -> {x, y} 위치 매핑 isDragging: false, dragNode: null, dragOffset: { x: 0, y: 0 }, // 로그인 관련 함수들 openLoginModal() { this.showLoginModal = true; }, async login() { this.loginLoading = true; this.loginError = ''; try { // 실제 API 호출 const response = await window.api.login(this.loginForm.email, this.loginForm.password); // 토큰 저장 window.api.setToken(response.access_token); localStorage.setItem('refresh_token', response.refresh_token); // 사용자 정보 가져오기 const userResponse = await window.api.getCurrentUser(); this.currentUser = userResponse; // 헤더 사용자 메뉴 업데이트 if (typeof window.updateUserMenu === 'function') { window.updateUserMenu(userResponse); } // 사용자 트리 목록 로드 await this.loadUserTrees(); // 모달 닫기 this.showLoginModal = false; this.loginForm = { email: '', password: '' }; console.log('✅ 로그인 성공'); } catch (error) { this.loginError = error.message || '로그인에 실패했습니다'; console.error('❌ 로그인 오류:', error); } finally { this.loginLoading = false; } }, async logout() { try { await window.api.logout(); } catch (error) { console.error('Logout error:', error); } finally { // 로컬 스토리지 정리 localStorage.removeItem('refresh_token'); window.api.setToken(null); // 상태 초기화 this.currentUser = null; this.userTrees = []; this.selectedTreeId = ''; this.selectedTree = null; this.treeNodes = []; this.selectedNode = null; // 헤더 사용자 메뉴 업데이트 if (typeof window.updateUserMenu === 'function') { window.updateUserMenu(null); } console.log('✅ 로그아웃 완료'); } }, // 초기화 async init() { console.log('🌳 트리 메모장 초기화 중...'); // 커스텀 이벤트 리스너 등록 document.addEventListener('open-login-modal', () => { console.log('📨 open-login-modal 이벤트 수신'); this.openLoginModal(); }); // API 객체가 로드될 때까지 대기 (더 긴 시간) let retries = 0; while ((!window.api || typeof window.api.getUserMemoTrees !== 'function') && retries < 50) { console.log(`⏳ API 객체 로딩 대기 중... (${retries + 1}/50)`); await new Promise(resolve => setTimeout(resolve, 100)); retries++; } if (!window.api || typeof window.api.getUserMemoTrees !== 'function') { console.error('❌ API 객체 또는 getUserMemoTrees 함수를 로드할 수 없습니다.'); console.log('현재 window.api:', window.api); if (window.api) { console.log('API 객체의 메서드들:', Object.getOwnPropertyNames(window.api)); } return; } try { await this.checkAuthStatus(); if (this.currentUser) { await this.loadUserTrees(); await this.initMonacoEditor(); } } catch (error) { console.error('❌ 초기화 실패:', error); } }, // 인증 상태 확인 async checkAuthStatus() { try { const user = await window.api.getCurrentUser(); this.currentUser = user; console.log('✅ 사용자 인증됨:', user.email); } catch (error) { console.log('❌ 인증되지 않음:', error.message); this.currentUser = null; // 토큰이 있지만 만료된 경우 제거 if (localStorage.getItem('access_token')) { console.log('🗑️ 만료된 토큰 제거'); localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); window.api.clearToken(); } } }, // 사용자 트리 목록 로드 async loadUserTrees() { try { console.log('📊 사용자 트리 목록 로딩...'); const trees = await window.api.getUserMemoTrees(); this.userTrees = trees || []; console.log(`✅ ${this.userTrees.length}개 트리 로드 완료`); } catch (error) { console.error('❌ 트리 목록 로드 실패:', error); this.userTrees = []; } }, // 트리 로드 async loadTree(treeId) { if (!treeId) { this.selectedTree = null; this.treeNodes = []; this.selectedNode = null; return; } try { console.log('🌳 트리 로딩:', treeId); // 트리 정보 로드 const tree = this.userTrees.find(t => t.id === treeId); this.selectedTree = tree; // 트리 노드들 로드 const nodes = await window.api.getMemoTreeNodes(treeId); this.treeNodes = nodes || []; // 첫 번째 노드 선택 (있다면) if (this.treeNodes.length > 0) { this.selectNode(this.treeNodes[0]); } // 트리 다이어그램 위치 계산 및 중앙 정렬 this.$nextTick(() => { setTimeout(() => { this.calculateNodePositions(); // 위치 계산 완료 후 중앙 정렬 setTimeout(() => { this.centerTree(); }, 50); }, 100); }); console.log(`✅ 트리 로드 완료: ${this.treeNodes.length}개 노드`); } catch (error) { console.error('❌ 트리 로드 실패:', error); alert('트리를 불러오는 중 오류가 발생했습니다.'); } }, // 트리 생성 async createTree() { try { console.log('🌳 새 트리 생성:', this.newTree); const tree = await window.api.createMemoTree(this.newTree); // 트리 목록에 추가 this.userTrees.push(tree); // 새 트리 선택 this.selectedTreeId = tree.id; await this.loadTree(tree.id); // 모달 닫기 및 폼 리셋 this.showNewTreeModal = false; this.newTree = { title: '', description: '', tree_type: 'general' }; console.log('✅ 트리 생성 완료'); } catch (error) { console.error('❌ 트리 생성 실패:', error); alert('트리 생성 중 오류가 발생했습니다.'); } }, // 루트 노드 생성 async createRootNode() { if (!this.selectedTree) return; try { const nodeData = { tree_id: this.selectedTree.id, title: '새 노드', node_type: 'memo', parent_id: null }; const node = await window.api.createMemoNode(nodeData); this.treeNodes.push(node); this.selectNode(node); console.log('✅ 루트 노드 생성 완료'); } catch (error) { console.error('❌ 루트 노드 생성 실패:', error); alert('노드 생성 중 오류가 발생했습니다.'); } }, // 노드 선택 selectNode(node) { // 이전 노드 저장 if (this.selectedNode && this.isEditorDirty) { this.saveNode(); } this.selectedNode = node; // 에디터에 내용 로드 (빈 내용도 포함) if (monacoEditor) { monacoEditor.setValue(node.content || ''); this.isEditorDirty = false; } // 모바일에서만 편집 모달 열기 (기존 동작에 추가) if (window.innerWidth < 1024) { // lg 브레이크포인트 this.showMobileEditModal = true; // 모바일 에디터 초기화 (약간의 지연 후) setTimeout(() => { this.initMobileEditor(); }, 100); } console.log('📝 노드 선택:', node.title); }, // 모바일 에디터 초기화 initMobileEditor() { if (!window.mobileMonacoEditor && this.selectedNode) { const container = document.getElementById('mobile-editor-container'); if (container) { window.mobileMonacoEditor = monaco.editor.create(container, { value: this.selectedNode.content || '', language: 'markdown', theme: 'vs', automaticLayout: true, wordWrap: 'on', minimap: { enabled: false }, scrollBeyondLastLine: false, fontSize: 14, lineNumbers: 'off', folding: false, lineDecorationsWidth: 0, lineNumbersMinChars: 0, glyphMargin: false }); // 모바일 에디터 변경 감지 window.mobileMonacoEditor.onDidChangeModelContent(() => { if (this.selectedNode) { this.selectedNode.content = window.mobileMonacoEditor.getValue(); this.isEditorDirty = true; } }); console.log('📱 모바일 에디터 초기화 완료'); } } else if (window.mobileMonacoEditor && this.selectedNode) { // 기존 에디터가 있으면 내용만 업데이트 window.mobileMonacoEditor.setValue(this.selectedNode.content || ''); } }, // 노드 저장 async saveNode() { if (!this.selectedNode) return; try { // 에디터 내용 가져오기 (데스크톱 또는 모바일) if (monacoEditor) { this.selectedNode.content = monacoEditor.getValue(); } else if (window.mobileMonacoEditor) { this.selectedNode.content = window.mobileMonacoEditor.getValue(); } if (this.selectedNode.content !== undefined) { // 단어 수 계산 (간단한 방식) const wordCount = this.selectedNode.content .replace(/\s+/g, ' ') .trim() .split(' ') .filter(word => word.length > 0).length; this.selectedNode.word_count = wordCount; } await window.api.updateMemoNode(this.selectedNode.id, this.selectedNode); this.isEditorDirty = false; console.log('✅ 노드 저장 완료'); } catch (error) { console.error('❌ 노드 저장 실패:', error); alert('노드 저장 중 오류가 발생했습니다.'); } }, // 노드 제목 저장 async saveNodeTitle() { if (!this.selectedNode) return; await this.saveNode(); }, // 노드 타입 저장 async saveNodeType() { if (!this.selectedNode) return; await this.saveNode(); }, // 노드 상태 저장 async saveNodeStatus() { if (!this.selectedNode) return; await this.saveNode(); }, // 노드 삭제 async deleteNode(nodeId) { if (!confirm('정말로 이 노드를 삭제하시겠습니까?')) return; try { await window.api.deleteMemoNode(nodeId); // 트리에서 제거 this.treeNodes = this.treeNodes.filter(node => node.id !== nodeId); // 선택된 노드였다면 선택 해제 if (this.selectedNode && this.selectedNode.id === nodeId) { this.selectedNode = null; if (monacoEditor) { monacoEditor.setValue(''); } } console.log('✅ 노드 삭제 완료'); } catch (error) { console.error('❌ 노드 삭제 실패:', error); alert('노드 삭제 중 오류가 발생했습니다.'); } }, // 자식 노드 가져오기 getChildNodes(parentId) { return this.treeNodes .filter(node => node.parent_id === parentId) .sort((a, b) => a.sort_order - b.sort_order); }, // 루트 노드들 가져오기 get rootNodes() { return this.treeNodes .filter(node => !node.parent_id) .sort((a, b) => a.sort_order - b.sort_order); }, // 노드 타입별 아이콘 가져오기 getNodeIcon(nodeType) { const icons = { folder: '📁', memo: '📝', chapter: '📖', character: '👤', plot: '📋' }; return icons[nodeType] || '📝'; }, getNodeTypeLabel(nodeType) { const labels = { 'memo': '메모', 'folder': '폴더', 'chapter': '챕터', 'character': '캐릭터', 'plot': '플롯' }; return labels[nodeType] || '메모'; }, getStatusIcon(status) { const icons = { 'draft': '📝', 'writing': '✍️', 'review': '👀', 'complete': '✅' }; return icons[status] || '📝'; }, getStatusLabel(status) { const labels = { 'draft': '초안', 'writing': '작성중', 'review': '검토중', 'complete': '완료' }; return labels[status] || '초안'; }, // 상태별 색상 클래스 가져오기 getStatusColor(status) { const colors = { draft: 'text-gray-500', writing: 'text-yellow-600', review: 'text-blue-600', complete: 'text-green-600' }; return colors[status] || 'text-gray-700'; }, // 트리 타입별 아이콘 가져오기 getTreeIcon(treeType) { const icons = { novel: '📚', research: '🔬', project: '💼', general: '📂' }; return icons[treeType] || '📂'; }, // 빠른 자식 노드 추가 async addChildNode(parentNode) { if (!this.selectedTree) return; try { const nodeData = { tree_id: this.selectedTree.id, title: '새 노드', node_type: 'memo', parent_id: parentNode.id }; const node = await window.api.createMemoNode(nodeData); this.treeNodes.push(node); // 부모 노드 펼치기 this.expandedNodes.add(parentNode.id); // 새 노드 선택 this.selectNode(node); console.log('✅ 자식 노드 생성 완료'); } catch (error) { console.error('❌ 자식 노드 생성 실패:', error); alert('노드 생성 중 오류가 발생했습니다.'); } }, // 컨텍스트 메뉴 표시 showContextMenu(event, node) { // TODO: 우클릭 컨텍스트 메뉴 구현 console.log('컨텍스트 메뉴:', node.title); }, // 노드 메뉴 표시 showNodeMenu(event, node) { // TODO: 노드 옵션 메뉴 구현 console.log('노드 메뉴:', node.title); }, // 정사 경로 토글 async toggleCanonical(node) { try { const newCanonicalState = !node.is_canonical; console.log(`🌟 정사 경로 토글: ${node.title} (${newCanonicalState ? '설정' : '해제'})`); const updatedData = { is_canonical: newCanonicalState }; const updatedNode = await window.api.updateMemoNode(node.id, updatedData); // 로컬 상태 업데이트 (Alpine.js 반응성 보장) const nodeIndex = this.treeNodes.findIndex(n => n.id === node.id); if (nodeIndex !== -1) { // 배열 전체를 새로 생성하여 Alpine.js 반응성 트리거 this.treeNodes = this.treeNodes.map(n => n.id === node.id ? { ...n, ...updatedNode } : n ); } // 선택된 노드도 업데이트 if (this.selectedNode && this.selectedNode.id === node.id) { this.selectedNode = { ...this.selectedNode, ...updatedNode }; } // 강제 리렌더링을 위한 더미 업데이트 this.treeNodes = [...this.treeNodes]; // 트리 다시 그리기 (연결선 업데이트) this.$nextTick(() => { this.calculateNodePositions(); }); console.log(`✅ 정사 경로 ${newCanonicalState ? '설정' : '해제'} 완료`); console.log('📊 업데이트된 노드:', updatedNode); console.log('🔍 is_canonical 값:', updatedNode.is_canonical); console.log('🔍 canonical_order 값:', updatedNode.canonical_order); console.log('🔄 현재 treeNodes 개수:', this.treeNodes.length); // 업데이트된 노드 찾기 const updatedNodeInArray = this.treeNodes.find(n => n.id === node.id); console.log('🔍 배열 내 업데이트된 노드:', updatedNodeInArray?.is_canonical); } catch (error) { console.error('❌ 정사 경로 토글 실패:', error); alert('정사 경로 설정 중 오류가 발생했습니다.'); } }, // 노드 드래그 시작 startDragNode(event, node) { // 현재는 드래그 기능 비활성화 (패닝과 충돌 방지) event.stopPropagation(); console.log('드래그 시작:', node.title); // TODO: 노드 드래그 앤 드롭 구현 }, // 노드 인라인 편집 editNodeInline(node) { // 더블클릭 시 노드 선택하고 에디터로 포커스 this.selectNode(node); // 에디터가 있다면 포커스 this.$nextTick(() => { const editorContainer = document.getElementById('editor-container'); if (editorContainer && this.monacoEditor) { this.monacoEditor.focus(); } }); console.log('인라인 편집:', node.title); }, // 노드 위치 계산 및 반환 getNodePosition(node) { if (!this.nodePositions.has(node.id)) { this.calculateNodePositions(); } const pos = this.nodePositions.get(node.id) || { x: 0, y: 0 }; return `left: ${pos.x}px; top: ${pos.y}px;`; }, // 트리 노드 위치 자동 계산 calculateNodePositions() { const canvas = document.getElementById('tree-canvas'); if (!canvas) return; const canvasWidth = canvas.clientWidth; const canvasHeight = canvas.clientHeight; // 노드 크기 설정 const nodeWidth = 200; const nodeHeight = 80; const levelHeight = 150; // 레벨 간 간격 const nodeSpacing = 50; // 노드 간 간격 const margin = 100; // 여백 // 레벨별 노드 그룹화 const levels = new Map(); // 루트 노드들 찾기 const rootNodes = this.treeNodes.filter(node => !node.parent_id); if (rootNodes.length === 0) return; // BFS로 레벨별 노드 배치 const queue = []; rootNodes.forEach(node => { queue.push({ node, level: 0 }); }); while (queue.length > 0) { const { node, level } = queue.shift(); if (!levels.has(level)) { levels.set(level, []); } levels.get(level).push(node); // 자식 노드들을 다음 레벨에 추가 const children = this.getChildNodes(node.id); children.forEach(child => { queue.push({ node: child, level: level + 1 }); }); } // 트리 전체 크기 계산 const maxLevel = Math.max(...levels.keys()); const maxNodesInLevel = Math.max(...Array.from(levels.values()).map(nodes => nodes.length)); const treeWidth = maxNodesInLevel * nodeWidth + (maxNodesInLevel - 1) * nodeSpacing; const treeHeight = (maxLevel + 1) * levelHeight; // 캔버스 중앙에 트리 배치하기 위한 오프셋 계산 const offsetX = Math.max(margin, (canvasWidth - treeWidth) / 2); const offsetY = Math.max(margin, (canvasHeight - treeHeight) / 2); // 각 레벨의 노드들 위치 계산 levels.forEach((nodes, level) => { const y = offsetY + level * levelHeight; const levelWidth = nodes.length * nodeWidth + (nodes.length - 1) * nodeSpacing; const startX = offsetX + (treeWidth - levelWidth) / 2; nodes.forEach((node, index) => { const x = startX + index * (nodeWidth + nodeSpacing); this.nodePositions.set(node.id, { x, y }); }); }); // 트리 크기 저장 (centerTree에서 사용) this.treeBounds = { width: treeWidth + 2 * margin, height: treeHeight + 2 * margin, offsetX: offsetX - margin, offsetY: offsetY - margin }; // 연결선 다시 그리기 this.drawConnections(); }, // SVG 연결선 그리기 drawConnections() { const svg = document.getElementById('tree-connections'); if (!svg) return; // 기존 연결선 제거 svg.innerHTML = ''; // 각 노드의 자식들과 연결선 그리기 this.treeNodes.forEach(node => { const children = this.getChildNodes(node.id); if (children.length === 0) return; const parentPos = this.nodePositions.get(node.id); if (!parentPos) return; children.forEach(child => { const childPos = this.nodePositions.get(child.id); if (!childPos) return; // 연결선 생성 const line = document.createElementNS('http://www.w3.org/2000/svg', 'path'); // 부모 노드 하단 중앙에서 시작 const startX = parentPos.x + 100; // 노드 중앙 const startY = parentPos.y + 80; // 노드 하단 // 자식 노드 상단 중앙으로 연결 const endX = childPos.x + 100; // 노드 중앙 const endY = childPos.y; // 노드 상단 // 곡선 경로 생성 (베지어 곡선) const midY = startY + (endY - startY) / 2; const path = `M ${startX} ${startY} C ${startX} ${midY} ${endX} ${midY} ${endX} ${endY}`; line.setAttribute('d', path); line.setAttribute('stroke', '#9CA3AF'); line.setAttribute('stroke-width', '2'); line.setAttribute('fill', 'none'); line.setAttribute('marker-end', 'url(#arrowhead)'); svg.appendChild(line); }); }); // 화살표 마커 추가 (한 번만) if (!svg.querySelector('#arrowhead')) { const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker'); marker.setAttribute('id', 'arrowhead'); marker.setAttribute('markerWidth', '10'); marker.setAttribute('markerHeight', '7'); marker.setAttribute('refX', '9'); marker.setAttribute('refY', '3.5'); marker.setAttribute('orient', 'auto'); const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); polygon.setAttribute('points', '0 0, 10 3.5, 0 7'); polygon.setAttribute('fill', '#9CA3AF'); marker.appendChild(polygon); defs.appendChild(marker); svg.appendChild(defs); } }, // 트리 중앙 정렬 centerTree() { const canvas = document.getElementById('tree-canvas'); if (!canvas || !this.treeBounds) { this.treePanX = 0; this.treePanY = 0; this.treeZoom = 1; return; } const canvasWidth = canvas.clientWidth; const canvasHeight = canvas.clientHeight; // 트리가 캔버스보다 큰 경우 적절한 줌 레벨 계산 const scaleX = canvasWidth / this.treeBounds.width; const scaleY = canvasHeight / this.treeBounds.height; const optimalZoom = Math.min(scaleX, scaleY, 1) * 0.9; // 90%로 여유 공간 확보 // 줌 적용 this.treeZoom = Math.max(0.1, Math.min(optimalZoom, 2)); // 중앙 정렬을 위한 팬 값 계산 const scaledTreeWidth = this.treeBounds.width * this.treeZoom; const scaledTreeHeight = this.treeBounds.height * this.treeZoom; this.treePanX = (canvasWidth - scaledTreeWidth) / 2 - this.treeBounds.offsetX * this.treeZoom; this.treePanY = (canvasHeight - scaledTreeHeight) / 2 - this.treeBounds.offsetY * this.treeZoom; console.log('🎯 트리 중앙 정렬:', { zoom: this.treeZoom, panX: this.treePanX, panY: this.treePanY, treeBounds: this.treeBounds }); }, // 확대 zoomIn() { this.treeZoom = Math.min(this.treeZoom * 1.2, 3); }, // 축소 zoomOut() { this.treeZoom = Math.max(this.treeZoom / 1.2, 0.3); }, // 패닝 시작 startPan(event) { if (event.target.closest('.tree-diagram-node')) return; // 노드 클릭 시 패닝 방지 this.isPanning = true; this.panStartX = event.clientX - this.treePanX; this.panStartY = event.clientY - this.treePanY; document.addEventListener('mousemove', this.handlePan.bind(this)); document.addEventListener('mouseup', this.stopPan.bind(this)); }, // 패닝 처리 handlePan(event) { if (!this.isPanning) return; this.treePanX = event.clientX - this.panStartX; this.treePanY = event.clientY - this.panStartY; }, // 패닝 종료 stopPan() { this.isPanning = false; document.removeEventListener('mousemove', this.handlePan); document.removeEventListener('mouseup', this.stopPan); }, // 재귀적 트리 노드 렌더링 renderTreeNodeRecursive(node, depth, isLast = false, parentPath = []) { const hasChildren = this.getChildNodes(node.id).length > 0; const isExpanded = this.expandedNodes.has(node.id); const isSelected = this.selectedNode && this.selectedNode.id === node.id; const isRoot = depth === 0; // 상태별 색상 let statusClass = 'text-gray-700'; switch(node.status) { case 'draft': statusClass = 'text-gray-500'; break; case 'writing': statusClass = 'text-yellow-600'; break; case 'review': statusClass = 'text-blue-600'; break; case 'complete': statusClass = 'text-green-600'; break; } // 트리 라인 생성 let treeLines = ''; for (let i = 0; i < depth; i++) { if (i < parentPath.length && parentPath[i]) { // 부모가 마지막이 아니면 세로선 표시 treeLines += ''; } else { // 빈 공간 treeLines += ''; } } // 현재 노드의 연결선 let nodeConnector = ''; if (!isRoot) { if (isLast) { nodeConnector = ''; // └─ } else { nodeConnector = ''; // ├─ } } let html = `
${treeLines} ${nodeConnector} ${hasChildren ? `` : '' } ${this.getNodeIcon(node.node_type)} ${node.title}
${node.word_count > 0 ? `${node.word_count}w` : '' }
`; // 자식 노드들 재귀적으로 렌더링 if (hasChildren && isExpanded) { const children = this.getChildNodes(node.id); const newParentPath = [...parentPath, !isLast]; children.forEach((child, index) => { const isChildLast = index === children.length - 1; html += this.renderTreeNodeRecursive(child, depth + 1, isChildLast, newParentPath); }); } html += '
'; return html; }, // 노드 토글 toggleNode(nodeId) { if (this.expandedNodes.has(nodeId)) { this.expandedNodes.delete(nodeId); } else { this.expandedNodes.add(nodeId); } // 트리 다시 렌더링을 위해 상태 업데이트 this.$nextTick(() => { this.rerenderTree(); }); }, // 트리 다시 렌더링 rerenderTree() { // Alpine.js의 반응성을 트리거하기 위해 배열을 새로 할당 this.treeNodes = [...this.treeNodes]; // 강제로 DOM 업데이트 this.$nextTick(() => { // 트리 컨테이너 찾아서 다시 렌더링 const treeContainer = document.querySelector('.tree-container'); if (treeContainer) { // Alpine.js가 자동으로 다시 렌더링함 } }); }, // 모두 펼치기 expandAll() { this.treeNodes.forEach(node => { if (this.getChildNodes(node.id).length > 0) { this.expandedNodes.add(node.id); } }); this.rerenderTree(); }, // 모두 접기 collapseAll() { this.expandedNodes.clear(); this.rerenderTree(); }, // Monaco Editor 초기화 async initMonacoEditor() { console.log('🎨 Monaco Editor 초기화 시작...'); // Monaco Editor 로더가 로드될 때까지 대기 let retries = 0; while (typeof require === 'undefined' && retries < 30) { console.log(`⏳ Monaco Editor 로더 대기 중... (${retries + 1}/30)`); await new Promise(resolve => setTimeout(resolve, 200)); retries++; } if (typeof require === 'undefined') { console.warn('❌ Monaco Editor 로더를 찾을 수 없습니다. 기본 textarea를 사용합니다.'); this.setupFallbackEditor(); return; } try { require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs' } }); require(['vs/editor/editor.main'], () => { // 에디터 컨테이너가 생성될 때까지 대기 const waitForEditor = () => { const editorElement = document.getElementById('monaco-editor'); if (!editorElement) { console.log('⏳ Monaco Editor 컨테이너 대기 중...'); setTimeout(waitForEditor, 100); return; } // 이미 Monaco Editor가 생성되어 있다면 제거 if (monacoEditor) { monacoEditor.dispose(); monacoEditor = null; } monacoEditor = monaco.editor.create(editorElement, { value: '', language: 'markdown', theme: 'vs-light', automaticLayout: true, wordWrap: 'on', minimap: { enabled: false }, scrollBeyondLastLine: false, fontSize: 14, lineNumbers: 'on', folding: true, renderWhitespace: 'boundary' }); // 내용 변경 감지 monacoEditor.onDidChangeModelContent(() => { this.isEditorDirty = true; }); console.log('✅ Monaco Editor 초기화 완료'); }; // 에디터 컨테이너 대기 시작 waitForEditor(); }); } catch (error) { console.error('❌ Monaco Editor 초기화 실패:', error); this.setupFallbackEditor(); } }, // 폴백 에디터 설정 (Monaco가 실패했을 때) setupFallbackEditor() { console.log('📝 폴백 textarea 에디터 설정 중...'); const editorElement = document.getElementById('monaco-editor'); if (editorElement) { editorElement.innerHTML = ` `; const textarea = document.getElementById('fallback-editor'); if (textarea) { textarea.addEventListener('input', () => { this.isEditorDirty = true; }); console.log('✅ 폴백 에디터 설정 완료'); } } }, // 로그인 모달 열기 openLoginModal() { this.showLoginModal = true; this.loginForm = { email: '', password: '' }; this.loginError = ''; }, // 로그인 처리 async handleLogin() { this.loginLoading = true; this.loginError = ''; try { const response = await window.api.login(this.loginForm.email, this.loginForm.password); if (response.success) { this.currentUser = response.user; this.showLoginModal = false; // 트리 목록 다시 로드 await this.loadUserTrees(); } else { this.loginError = response.message || '로그인에 실패했습니다.'; } } catch (error) { console.error('로그인 오류:', error); this.loginError = '로그인 중 오류가 발생했습니다.'; } finally { this.loginLoading = false; } }, // 로그아웃 async logout() { try { await window.api.logout(); this.currentUser = null; this.userTrees = []; this.selectedTree = null; this.treeNodes = []; this.selectedNode = null; console.log('✅ 로그아웃 완료'); } catch (error) { console.error('❌ 로그아웃 실패:', error); } } }; }; // 전역 인스턴스 등록 (HTML에서 접근하기 위해) window.memoTreeInstance = null; // Alpine.js 초기화 후 전역 인스턴스 설정 document.addEventListener('alpine:init', () => { // 페이지 로드 후 인스턴스 등록 setTimeout(() => { const appElement = document.querySelector('[x-data="memoTreeApp()"]'); if (appElement && appElement._x_dataStack) { window.memoTreeInstance = appElement._x_dataStack[0]; } }, 100); }); // 트리 노드 컴포넌트 window.treeNodeComponent = function(node) { return { node: node, expanded: true, get hasChildren() { return window.memoTreeInstance?.getChildNodes(this.node.id).length > 0; }, toggleExpanded() { this.expanded = !this.expanded; if (this.expanded) { window.memoTreeInstance?.expandedNodes.add(this.node.id); } else { window.memoTreeInstance?.expandedNodes.delete(this.node.id); } } }; }; console.log('🌳 트리 메모장 JavaScript 로드 완료');