// story-view.js - 정사 경로 스토리 뷰 컴포넌트 console.log('📖 스토리 뷰 JavaScript 로드 완료'); // Alpine.js 컴포넌트 window.storyViewApp = function() { return { // 사용자 상태 currentUser: null, // 트리 데이터 userTrees: [], selectedTreeId: '', selectedTree: null, canonicalNodes: [], // 정사 경로 노드들만 // UI 상태 viewMode: 'toc', // 'toc' | 'full' showLoginModal: false, showEditModal: false, editingNode: null, // 로그인 폼 상태 loginForm: { email: '', password: '' }, loginError: '', loginLoading: false, // 계산된 속성들 get totalWords() { return this.canonicalNodes.reduce((sum, node) => sum + (node.word_count || 0), 0); }, // 초기화 async init() { console.log('📖 스토리 뷰 초기화 중...'); // API 로드 대기 let retryCount = 0; while (!window.api && retryCount < 50) { await new Promise(resolve => setTimeout(resolve, 100)); retryCount++; } if (!window.api) { console.error('❌ API가 로드되지 않았습니다.'); return; } // 사용자 인증 상태 확인 await this.checkAuthStatus(); // 인증된 경우 트리 목록 로드 if (this.currentUser) { await this.loadUserTrees(); } }, // 인증 상태 확인 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; // 만료된 토큰 정리 localStorage.removeItem('token'); } }, // 사용자 트리 목록 로드 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 loadStory(treeId) { if (!treeId) { this.selectedTree = null; this.canonicalNodes = []; return; } try { console.log('📖 스토리 로딩:', treeId); // 트리 정보 로드 this.selectedTree = await window.api.getMemoTree(treeId); // 모든 노드 로드 const allNodes = await window.api.getMemoTreeNodes(treeId); // 정사 경로 노드들만 필터링하고 순서대로 정렬 this.canonicalNodes = allNodes .filter(node => node.is_canonical) .sort((a, b) => (a.canonical_order || 0) - (b.canonical_order || 0)); console.log(`✅ 스토리 로드 완료: ${this.canonicalNodes.length}개 정사 노드`); } catch (error) { console.error('❌ 스토리 로드 실패:', error); alert('스토리를 불러오는 중 오류가 발생했습니다.'); } }, // 뷰 모드 토글 toggleView() { this.viewMode = this.viewMode === 'toc' ? 'full' : 'toc'; }, // 챕터로 스크롤 scrollToChapter(nodeId) { if (this.viewMode === 'toc') { this.viewMode = 'full'; // DOM 업데이트 대기 후 스크롤 this.$nextTick(() => { const element = document.getElementById(`chapter-${nodeId}`); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); } else { const element = document.getElementById(`chapter-${nodeId}`); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } }, // 챕터 편집 (인라인 모달) editChapter(node) { this.editingNode = { ...node }; // 복사본 생성 this.showEditModal = true; }, // 편집 취소 cancelEdit() { this.showEditModal = false; this.editingNode = null; }, // 편집 저장 async saveEdit() { if (!this.editingNode) return; try { console.log('💾 챕터 저장 중:', this.editingNode.title); const updatedNode = await window.api.updateMemoNode(this.editingNode.id, { title: this.editingNode.title, content: this.editingNode.content }); // 로컬 상태 업데이트 const nodeIndex = this.canonicalNodes.findIndex(n => n.id === this.editingNode.id); if (nodeIndex !== -1) { this.canonicalNodes = this.canonicalNodes.map(n => n.id === this.editingNode.id ? { ...n, ...updatedNode } : n ); } this.showEditModal = false; this.editingNode = null; console.log('✅ 챕터 저장 완료'); } catch (error) { console.error('❌ 챕터 저장 실패:', error); alert('챕터 저장 중 오류가 발생했습니다.'); } }, // 스토리 내보내기 async exportStory() { if (!this.selectedTree || this.canonicalNodes.length === 0) { alert('내보낼 스토리가 없습니다.'); return; } try { // 텍스트 형태로 스토리 생성 let storyText = `${this.selectedTree.title}\n`; storyText += `${'='.repeat(this.selectedTree.title.length)}\n\n`; if (this.selectedTree.description) { storyText += `${this.selectedTree.description}\n\n`; } storyText += `작성일: ${this.formatDate(this.selectedTree.created_at)}\n`; storyText += `수정일: ${this.formatDate(this.selectedTree.updated_at)}\n`; storyText += `총 ${this.canonicalNodes.length}개 챕터, ${this.totalWords}단어\n\n`; storyText += `${'='.repeat(50)}\n\n`; this.canonicalNodes.forEach((node, index) => { storyText += `${index + 1}. ${node.title}\n`; storyText += `${'-'.repeat(node.title.length + 3)}\n\n`; if (node.content) { // HTML 태그 제거 const plainText = node.content.replace(/<[^>]*>/g, ''); storyText += `${plainText}\n\n`; } else { storyText += `[이 챕터는 아직 내용이 없습니다]\n\n`; } storyText += `${'─'.repeat(30)}\n\n`; }); // 파일 다운로드 const blob = new Blob([storyText], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${this.selectedTree.title}_정사스토리.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); console.log('✅ 스토리 내보내기 완료'); } catch (error) { console.error('❌ 스토리 내보내기 실패:', error); alert('스토리 내보내기 중 오류가 발생했습니다.'); } }, // 스토리 인쇄 printStory() { if (!this.selectedTree || this.canonicalNodes.length === 0) { alert('인쇄할 스토리가 없습니다.'); return; } // 전체 뷰로 변경 후 인쇄 if (this.viewMode === 'toc') { this.viewMode = 'full'; this.$nextTick(() => { window.print(); }); } else { window.print(); } }, // 유틸리티 함수들 formatDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric' }); }, formatContent(content) { if (!content) return ''; // 간단한 마크다운 스타일 변환 return content .replace(/\n\n/g, '
')
.replace(/\n/g, '
')
.replace(/^/, '
') .replace(/$/, '
'); }, getNodeTypeLabel(nodeType) { const labels = { 'folder': '📁 폴더', 'memo': '📝 메모', 'chapter': '📖 챕터', 'character': '👤 인물', 'plot': '📋 플롯' }; return labels[nodeType] || '📝 메모'; }, getStatusLabel(status) { const labels = { 'draft': '📝 초안', 'writing': '✍️ 작성중', 'review': '👀 검토중', 'complete': '✅ 완료' }; return labels[status] || '📝 초안'; }, // 로그인 관련 함수들 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.canonicalNodes = []; console.log('✅ 로그아웃 완료'); } catch (error) { console.error('❌ 로그아웃 실패:', error); } } }; }; console.log('📖 스토리 뷰 컴포넌트 등록 완료');