Files
document-server/frontend/static/js/story-view.js
Hyungi Ahn 43e7466195 스토리 뷰 페이지 헤더 z-index 충돌 문제 해결
- 메인 컨테이너 padding-top을 pt-4에서 pt-20으로 증가
- 드롭다운 z-index를 z-[60]으로 설정하여 헤더보다 높은 우선순위 부여
- 스토리 선택 드롭다운이 정상적으로 작동하도록 수정
- 디버깅용 코드 정리
2025-09-04 10:22:43 +09:00

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('📖 스토리 뷰 컴포넌트 등록 완료');