- 메인 컨테이너 padding-top을 pt-4에서 pt-20으로 증가 - 드롭다운 z-index를 z-[60]으로 설정하여 헤더보다 높은 우선순위 부여 - 스토리 선택 드롭다운이 정상적으로 작동하도록 수정 - 디버깅용 코드 정리
372 lines
13 KiB
JavaScript
372 lines
13 KiB
JavaScript
// story-view.js - 정사 경로 스토리 뷰 컴포넌트
|
|
|
|
console.log('📖 스토리 뷰 JavaScript 로드 완료');
|
|
|
|
// Alpine.js 컴포넌트
|
|
window.storyViewApp = function() {
|
|
return {
|
|
// 사용자 상태
|
|
currentUser: null,
|
|
|
|
// 트리 데이터
|
|
userTrees: [],
|
|
selectedTreeId: '',
|
|
selectedTree: null,
|
|
canonicalNodes: [], // 정사 경로 노드들만
|
|
|
|
// UI 상태
|
|
showLoginModal: false,
|
|
showEditModal: false,
|
|
editingNode: {
|
|
title: '',
|
|
content: ''
|
|
},
|
|
|
|
// 로그인 폼 상태
|
|
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();
|
|
|
|
// URL 파라미터에서 treeId 확인하고 자동 선택
|
|
this.checkUrlParams();
|
|
}
|
|
},
|
|
|
|
// 인증 상태 확인
|
|
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('📊 사용자 트리 목록 로딩...');
|
|
console.log('🔍 API 객체 확인:', window.api);
|
|
console.log('🔍 getUserMemoTrees 함수 확인:', typeof window.api?.getUserMemoTrees);
|
|
|
|
const trees = await window.api.getUserMemoTrees();
|
|
this.userTrees = trees || [];
|
|
console.log(`✅ ${this.userTrees.length}개 트리 로드 완료`);
|
|
console.log('📋 트리 목록:', this.userTrees);
|
|
|
|
// Alpine.js 반응성 업데이트를 위한 약간의 지연 후 URL 파라미터 확인
|
|
setTimeout(() => {
|
|
this.checkUrlParams();
|
|
}, 100);
|
|
} catch (error) {
|
|
console.error('❌ 트리 목록 로드 실패:', error);
|
|
console.error('❌ 에러 상세:', error.message);
|
|
console.error('❌ 에러 스택:', error.stack);
|
|
this.userTrees = [];
|
|
}
|
|
},
|
|
|
|
// 스토리 로드 (정사 경로만)
|
|
async loadStory(treeId) {
|
|
if (!treeId) {
|
|
this.selectedTree = null;
|
|
this.canonicalNodes = [];
|
|
// URL에서 treeId 파라미터 제거
|
|
this.updateUrl(null);
|
|
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));
|
|
|
|
// URL 업데이트
|
|
this.updateUrl(treeId);
|
|
|
|
console.log(`✅ 스토리 로드 완료: ${this.canonicalNodes.length}개 정사 노드`);
|
|
} catch (error) {
|
|
console.error('❌ 스토리 로드 실패:', error);
|
|
alert('스토리를 불러오는 중 오류가 발생했습니다.');
|
|
}
|
|
},
|
|
|
|
// URL 파라미터 확인 및 처리
|
|
checkUrlParams() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const treeId = urlParams.get('treeId');
|
|
|
|
if (treeId && this.userTrees.some(tree => tree.id === treeId)) {
|
|
console.log('📖 URL에서 트리 ID 감지:', treeId);
|
|
this.selectedTreeId = treeId;
|
|
this.loadStory(treeId);
|
|
}
|
|
},
|
|
|
|
// URL 업데이트
|
|
updateUrl(treeId) {
|
|
const url = new URL(window.location);
|
|
if (treeId) {
|
|
url.searchParams.set('treeId', treeId);
|
|
} else {
|
|
url.searchParams.delete('treeId');
|
|
}
|
|
window.history.replaceState({}, '', url);
|
|
},
|
|
|
|
// 스토리 리더로 이동
|
|
openStoryReader(nodeId, index) {
|
|
const url = `story-reader.html?treeId=${this.selectedTreeId}&nodeId=${nodeId}&index=${index}`;
|
|
window.location.href = url;
|
|
},
|
|
|
|
// 챕터 편집 (인라인 모달)
|
|
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, '</p><p>')
|
|
.replace(/\n/g, '<br>')
|
|
.replace(/^/, '<p>')
|
|
.replace(/$/, '</p>');
|
|
},
|
|
|
|
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('📖 스토리 뷰 컴포넌트 등록 완료');
|