Major UI overhaul and upload system improvements

- Removed hierarchy view and integrated functionality into index.html
- Added book-based document grouping with dedicated book-documents.html page
- Implemented comprehensive multi-file upload system with drag-and-drop reordering
- Added HTML-PDF matching functionality with download capability
- Enhanced upload workflow with 3-step process (File Selection, Book Settings, Order & Match)
- Added book conflict resolution (existing book vs new edition)
- Improved document order adjustment with one-click sort options
- Added modular header component system
- Updated API connectivity for Docker environment
- Enhanced viewer.html with PDF download functionality
- Fixed browser caching issues with version management
- Improved mobile responsiveness and modern UI design
This commit is contained in:
Hyungi Ahn
2025-08-25 15:58:30 +09:00
parent f95f67364a
commit 4038040faa
21 changed files with 3875 additions and 2603 deletions

View File

@@ -3,8 +3,12 @@
*/
class DocumentServerAPI {
constructor() {
this.baseURL = '/api'; // Nginx 프록시를 통해 접근
// 도커 백엔드 API (24102 포트)
this.baseURL = 'http://localhost:24102/api';
this.token = localStorage.getItem('access_token');
console.log('🐳 API Base URL (DOCKER BACKEND):', this.baseURL);
console.log('🔧 도커 환경 설정 완료 - 버전 2025012380');
}
// 토큰 설정
@@ -184,6 +188,10 @@ class DocumentServerAPI {
return await this.uploadFile('/documents/', formData);
}
async updateDocument(documentId, updateData) {
return await this.put(`/documents/${documentId}`, updateData);
}
async deleteDocument(documentId) {
return await this.delete(`/documents/${documentId}`);
}

View File

@@ -17,26 +17,40 @@ window.authModal = () => ({
this.loginError = '';
try {
// 실제 API 호출
const response = await window.api.login(this.loginForm.email, this.loginForm.password);
console.log('🔐 로그인 시도:', this.loginForm.email);
// 토큰 저장
window.api.setToken(response.access_token);
localStorage.setItem('refresh_token', response.refresh_token);
// API 클래스의 login 메서드 사용 (이미 토큰 설정과 사용자 정보 가져오기 포함)
const result = await window.api.login(this.loginForm.email, this.loginForm.password);
// 사용자 정보 가져오기
const userResponse = await window.api.getCurrentUser();
console.log('✅ 로그인 결과:', result);
// 전역 상태 업데이트
window.dispatchEvent(new CustomEvent('auth-changed', {
detail: { isAuthenticated: true, user: userResponse }
}));
// 모달 닫기 (부모 컴포넌트의 상태 변경)
window.dispatchEvent(new CustomEvent('close-login-modal'));
this.loginForm = { email: '', password: '' };
if (result.success) {
// refresh_token 저장 (access_token은 API 클래스에서 이미 처리됨)
const loginResponse = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.loginForm)
});
const tokenData = await loginResponse.json();
localStorage.setItem('refresh_token', tokenData.refresh_token);
console.log('💾 토큰 저장 완료');
// 전역 상태 업데이트
window.dispatchEvent(new CustomEvent('auth-changed', {
detail: { isAuthenticated: true, user: result.user }
}));
// 모달 닫기
window.dispatchEvent(new CustomEvent('close-login-modal'));
this.loginForm = { email: '', password: '' };
} else {
this.loginError = result.message || '로그인에 실패했습니다';
}
} catch (error) {
console.error('❌ 로그인 오류:', error);
this.loginError = error.message || '로그인에 실패했습니다';
} finally {
this.loginLoading = false;

View File

@@ -0,0 +1,180 @@
// 서적 문서 목록 애플리케이션 컴포넌트
window.bookDocumentsApp = () => ({
// 상태 관리
documents: [],
bookInfo: {},
loading: false,
error: '',
// 인증 상태
isAuthenticated: false,
currentUser: null,
// URL 파라미터
bookId: null,
// 초기화
async init() {
console.log('🚀 Book Documents App 초기화 시작');
// URL 파라미터 파싱
this.parseUrlParams();
// 인증 상태 확인
await this.checkAuthStatus();
if (this.isAuthenticated) {
await this.loadBookDocuments();
}
// 헤더 로드
await this.loadHeader();
},
// URL 파라미터 파싱
parseUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
this.bookId = urlParams.get('bookId');
console.log('📖 서적 ID:', this.bookId);
},
// 인증 상태 확인
async checkAuthStatus() {
try {
const user = await window.api.getCurrentUser();
this.isAuthenticated = true;
this.currentUser = user;
console.log('✅ 인증됨:', user.username);
} catch (error) {
console.log('❌ 인증되지 않음');
this.isAuthenticated = false;
this.currentUser = null;
// 로그인 페이지로 리다이렉트하거나 로그인 모달 표시
window.location.href = '/login.html';
}
},
// 헤더 로드
async loadHeader() {
try {
await window.headerLoader.loadHeader();
} catch (error) {
console.error('헤더 로드 실패:', error);
}
},
// 서적 문서 목록 로드
async loadBookDocuments() {
this.loading = true;
this.error = '';
try {
// 모든 문서 가져오기
const allDocuments = await window.api.getDocuments();
if (this.bookId === 'none') {
// 서적 미분류 문서들
this.documents = allDocuments.filter(doc => !doc.book_id);
this.bookInfo = {
title: '서적 미분류',
description: '서적에 속하지 않은 문서들입니다.'
};
} else {
// 특정 서적의 문서들
this.documents = allDocuments.filter(doc => doc.book_id === this.bookId);
if (this.documents.length > 0) {
// 첫 번째 문서에서 서적 정보 추출
const firstDoc = this.documents[0];
this.bookInfo = {
id: firstDoc.book_id,
title: firstDoc.book_title,
author: firstDoc.book_author,
description: firstDoc.book_description || '서적 설명이 없습니다.'
};
} else {
// 서적 정보만 가져오기 (문서가 없는 경우)
try {
this.bookInfo = await window.api.getBook(this.bookId);
} catch (error) {
console.error('서적 정보 로드 실패:', error);
this.bookInfo = {
title: '알 수 없는 서적',
description: '서적 정보를 불러올 수 없습니다.'
};
}
}
}
console.log('📚 서적 문서 로드 완료:', this.documents.length, '개');
} catch (error) {
console.error('서적 문서 로드 실패:', error);
this.error = '문서를 불러오는데 실패했습니다: ' + error.message;
this.documents = [];
} finally {
this.loading = false;
}
},
// 문서 열기
openDocument(documentId) {
// 현재 페이지 정보를 세션 스토리지에 저장
sessionStorage.setItem('previousPage', 'book-documents.html');
// 뷰어로 이동
window.open(`/viewer.html?id=${documentId}&from=book`, '_blank');
},
// 문서 수정
editDocument(doc) {
// TODO: 문서 수정 모달 또는 페이지로 이동
console.log('문서 수정:', doc.title);
alert('문서 수정 기능은 준비 중입니다.');
},
// 문서 삭제
async deleteDocument(documentId) {
if (!confirm('이 문서를 삭제하시겠습니까?')) {
return;
}
try {
await window.api.deleteDocument(documentId);
await this.loadBookDocuments(); // 목록 새로고침
this.showNotification('문서가 삭제되었습니다', 'success');
} catch (error) {
console.error('문서 삭제 실패:', error);
this.showNotification('문서 삭제에 실패했습니다: ' + error.message, 'error');
}
},
// 뒤로가기
goBack() {
window.location.href = 'index.html';
},
// 날짜 포맷팅
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
},
// 알림 표시
showNotification(message, type = 'info') {
// TODO: 알림 시스템 구현
console.log(`${type.toUpperCase()}: ${message}`);
if (type === 'error') {
alert(message);
}
}
});
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', () => {
console.log('📄 Book Documents 페이지 로드됨');
});

View File

@@ -0,0 +1,159 @@
/**
* 공통 헤더 로더
* 모든 페이지에서 동일한 헤더를 로드하기 위한 유틸리티
*/
class HeaderLoader {
constructor() {
this.headerLoaded = false;
}
/**
* 헤더 HTML을 로드하고 삽입
*/
async loadHeader(targetSelector = '#header-container') {
if (this.headerLoaded) {
console.log('✅ 헤더가 이미 로드됨');
return;
}
try {
console.log('🔄 헤더 로딩 중...');
const response = await fetch('components/header.html?v=2025012352');
if (!response.ok) {
throw new Error(`헤더 로드 실패: ${response.status}`);
}
const headerHtml = await response.text();
// 헤더 컨테이너 찾기
const container = document.querySelector(targetSelector);
if (!container) {
throw new Error(`헤더 컨테이너를 찾을 수 없음: ${targetSelector}`);
}
// 헤더 HTML 삽입
container.innerHTML = headerHtml;
this.headerLoaded = true;
console.log('✅ 헤더 로드 완료');
// 헤더 로드 완료 이벤트 발생
document.dispatchEvent(new CustomEvent('headerLoaded'));
} catch (error) {
console.error('❌ 헤더 로드 오류:', error);
this.showFallbackHeader(targetSelector);
}
}
/**
* 헤더 로드 실패 시 폴백 헤더 표시
*/
showFallbackHeader(targetSelector) {
const container = document.querySelector(targetSelector);
if (container) {
container.innerHTML = `
<header class="bg-white border-b border-gray-200 shadow-sm">
<div class="max-w-full mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center space-x-2">
<i class="fas fa-book text-blue-600 text-xl"></i>
<h1 class="text-xl font-bold text-gray-900">Document Server</h1>
</div>
<nav class="flex space-x-6">
<a href="index.html" class="text-gray-600 hover:text-blue-600">📁 문서 관리</a>
<a href="memo-tree.html" class="text-gray-600 hover:text-blue-600">🌳 메모장</a>
</nav>
</div>
</div>
</header>
`;
}
}
/**
* 현재 페이지 정보 가져오기
*/
getCurrentPageInfo() {
const path = window.location.pathname;
const filename = path.split('/').pop().replace('.html', '') || 'index';
const pageInfo = {
filename,
isDocumentPage: ['index', 'hierarchy'].includes(filename),
isMemoPage: ['memo-tree', 'story-view'].includes(filename)
};
return pageInfo;
}
/**
* 페이지별 활성 상태 업데이트
*/
updateActiveStates() {
const pageInfo = this.getCurrentPageInfo();
// 모든 활성 클래스 제거
document.querySelectorAll('.nav-link, .nav-dropdown-item').forEach(item => {
item.classList.remove('active');
});
// 현재 페이지에 따라 활성 상태 설정
console.log('현재 페이지:', pageInfo.filename);
if (pageInfo.isDocumentPage) {
const docLink = document.getElementById('doc-nav-link');
if (docLink) {
docLink.classList.add('active');
console.log('문서 관리 메뉴 활성화');
}
}
if (pageInfo.isMemoPage) {
const memoLink = document.getElementById('memo-nav-link');
if (memoLink) {
memoLink.classList.add('active');
console.log('메모장 메뉴 활성화');
}
}
// 특정 페이지 드롭다운 아이템 활성화
const pageItemMap = {
'index': 'index-nav-item',
'hierarchy': 'hierarchy-nav-item',
'memo-tree': 'memo-tree-nav-item',
'story-view': 'story-view-nav-item'
};
const itemId = pageItemMap[pageInfo.filename];
if (itemId) {
const item = document.getElementById(itemId);
if (item) {
item.classList.add('active');
console.log(`${pageInfo.filename} 페이지 아이템 활성화`);
}
}
}
}
// 전역 인스턴스 생성
window.headerLoader = new HeaderLoader();
// DOM 로드 완료 시 자동 초기화
document.addEventListener('DOMContentLoaded', () => {
window.headerLoader.loadHeader();
});
// 헤더 로드 완료 후 활성 상태 업데이트
document.addEventListener('headerLoaded', () => {
setTimeout(() => {
window.headerLoader.updateActiveStates();
// 사용자 메뉴 초기 상태 설정 (로그아웃 상태로 시작)
if (typeof window.updateUserMenu === 'function') {
window.updateUserMenu(null);
}
}, 100);
});

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,8 @@ window.documentApp = () => ({
// 상태 관리
documents: [],
filteredDocuments: [],
groupedDocuments: [],
expandedBooks: [],
loading: false,
error: '',
@@ -17,20 +19,34 @@ window.documentApp = () => ({
availableTags: [],
// UI 상태
viewMode: 'grid', // 'grid' 또는 'list'
viewMode: 'grid', // 'grid' 또는 'books'
user: null, // currentUser의 별칭
tags: [], // availableTags의 별칭
// 모달 상태
showUploadModal: false,
// 로그인 관련 함수들
openLoginModal() {
this.showLoginModal = true;
},
// 초기화
async init() {
await this.checkAuthStatus();
if (this.isAuthenticated) {
await this.loadDocuments();
} else {
// 로그인하지 않은 경우에도 빈 배열로 초기화
this.groupedDocuments = [];
}
this.setupEventListeners();
// 커스텀 이벤트 리스너 등록
document.addEventListener('open-login-modal', () => {
console.log('📨 open-login-modal 이벤트 수신 (index.html)');
this.openLoginModal();
});
},
// 인증 상태 확인
@@ -162,6 +178,7 @@ window.documentApp = () => ({
}
this.filteredDocuments = filtered;
this.groupDocumentsByBook();
},
// 검색어 변경 시
@@ -169,6 +186,59 @@ window.documentApp = () => ({
this.filterDocuments();
},
// 필터 초기화
clearFilters() {
this.searchQuery = '';
this.selectedTag = '';
this.filterDocuments();
},
// 서적별 그룹화
groupDocumentsByBook() {
if (!this.filteredDocuments || this.filteredDocuments.length === 0) {
this.groupedDocuments = [];
return;
}
const grouped = {};
this.filteredDocuments.forEach(doc => {
const bookKey = doc.book_id || 'no-book';
if (!grouped[bookKey]) {
grouped[bookKey] = {
book: doc.book_id ? {
id: doc.book_id,
title: doc.book_title,
author: doc.book_author
} : null,
documents: []
};
}
grouped[bookKey].documents.push(doc);
});
// 배열로 변환하고 정렬 (서적 있는 것 먼저, 그 다음 서적명 순)
this.groupedDocuments = Object.values(grouped).sort((a, b) => {
if (!a.book && b.book) return 1;
if (a.book && !b.book) return -1;
if (!a.book && !b.book) return 0;
return a.book.title.localeCompare(b.book.title);
});
// 기본적으로 모든 서적을 펼친 상태로 설정
this.expandedBooks = this.groupedDocuments.map(group => group.book?.id || 'no-book');
},
// 서적 펼침/접힘 토글
toggleBookExpansion(bookId) {
const index = this.expandedBooks.indexOf(bookId);
if (index > -1) {
this.expandedBooks.splice(index, 1);
} else {
this.expandedBooks.push(bookId);
}
},
// 문서 검색 (HTML에서 사용)
searchDocuments() {
this.filterDocuments();
@@ -204,7 +274,29 @@ window.documentApp = () => ({
// 문서 보기
viewDocument(documentId) {
window.open(`/viewer.html?id=${documentId}`, '_blank');
// 현재 페이지 정보를 세션 스토리지에 저장
const currentPage = window.location.pathname.split('/').pop() || 'index.html';
sessionStorage.setItem('previousPage', currentPage);
// from 파라미터 추가
const fromParam = currentPage === 'hierarchy.html' ? 'hierarchy' : 'index';
window.open(`/viewer.html?id=${documentId}&from=${fromParam}`, '_blank');
},
// 서적의 문서들 보기
openBookDocuments(book) {
if (book && book.id) {
// 서적 ID를 URL 파라미터로 전달하여 해당 서적의 문서들만 표시
window.location.href = `book-documents.html?bookId=${book.id}`;
} else {
// 서적 미분류 문서들 보기
window.location.href = `book-documents.html?bookId=none`;
}
},
// 업로드 페이지 열기
openUploadPage() {
window.location.href = 'upload.html';
},
// 문서 열기 (HTML에서 사용)

View File

@@ -21,6 +21,7 @@ window.memoTreeApp = function() {
showNewNodeModal: false,
showTreeSettings: false,
showLoginModal: false,
showMobileEditModal: false,
// 로그인 폼 상태
loginForm: {
@@ -59,10 +60,86 @@ window.memoTreeApp = function() {
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) {
@@ -149,10 +226,14 @@ window.memoTreeApp = function() {
this.selectNode(this.treeNodes[0]);
}
// 트리 다이어그램 위치 계산
// 트리 다이어그램 위치 계산 및 중앙 정렬
this.$nextTick(() => {
setTimeout(() => {
this.calculateNodePositions();
// 위치 계산 완료 후 중앙 정렬
setTimeout(() => {
this.centerTree();
}, 50);
}, 100);
});
@@ -220,23 +301,74 @@ window.memoTreeApp = function() {
this.selectedNode = node;
// 에디터에 내용 로드
if (monacoEditor && node.content) {
// 에디터에 내용 로드 (빈 내용도 포함)
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
@@ -327,6 +459,37 @@ window.memoTreeApp = function() {
};
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) {
@@ -486,6 +649,7 @@ window.memoTreeApp = function() {
const nodeHeight = 80;
const levelHeight = 150; // 레벨 간 간격
const nodeSpacing = 50; // 노드 간 간격
const margin = 100; // 여백
// 레벨별 노드 그룹화
const levels = new Map();
@@ -493,6 +657,8 @@ window.memoTreeApp = function() {
// 루트 노드들 찾기
const rootNodes = this.treeNodes.filter(node => !node.parent_id);
if (rootNodes.length === 0) return;
// BFS로 레벨별 노드 배치
const queue = [];
rootNodes.forEach(node => {
@@ -514,11 +680,22 @@ window.memoTreeApp = function() {
});
}
// 트리 전체 크기 계산
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 = 100 + level * levelHeight; // 상단 여백 + 레벨 * 높이
const totalWidth = nodes.length * nodeWidth + (nodes.length - 1) * nodeSpacing;
const startX = (canvasWidth - totalWidth) / 2;
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);
@@ -526,6 +703,14 @@ window.memoTreeApp = function() {
});
});
// 트리 크기 저장 (centerTree에서 사용)
this.treeBounds = {
width: treeWidth + 2 * margin,
height: treeHeight + 2 * margin,
offsetX: offsetX - margin,
offsetY: offsetY - margin
};
// 연결선 다시 그리기
this.drawConnections();
},
@@ -598,9 +783,38 @@ window.memoTreeApp = function() {
// 트리 중앙 정렬
centerTree() {
this.treePanX = 0;
this.treePanY = 0;
this.treeZoom = 1;
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
});
},
// 확대
@@ -613,16 +827,6 @@ window.memoTreeApp = function() {
this.treeZoom = Math.max(this.treeZoom / 1.2, 0.3);
},
// 휠 이벤트 처리 (확대/축소)
handleWheel(event) {
event.preventDefault();
if (event.deltaY < 0) {
this.zoomIn();
} else {
this.zoomOut();
}
},
// 패닝 시작
startPan(event) {
if (event.target.closest('.tree-diagram-node')) return; // 노드 클릭 시 패닝 방지

View File

@@ -0,0 +1,333 @@
// 스토리 읽기 애플리케이션
function storyReaderApp() {
return {
// 상태 변수들
currentUser: null,
loading: true,
error: null,
// 스토리 데이터
selectedTree: null,
canonicalNodes: [],
currentChapter: null,
currentChapterIndex: 0,
// URL 파라미터
treeId: null,
nodeId: null,
chapterIndex: null,
// 편집 관련
showEditModal: false,
editingChapter: null,
editEditor: null,
saving: false,
// 로그인 관련
showLoginModal: false,
loginForm: {
email: '',
password: ''
},
loginError: '',
loginLoading: false,
// 초기화
async init() {
console.log('🚀 스토리 리더 초기화 시작');
// URL 파라미터 파싱
this.parseUrlParams();
// 사용자 인증 확인
await this.checkAuth();
if (this.currentUser) {
await this.loadStoryData();
}
},
// URL 파라미터 파싱
parseUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
this.treeId = urlParams.get('treeId');
this.nodeId = urlParams.get('nodeId');
this.chapterIndex = parseInt(urlParams.get('index')) || 0;
console.log('📖 URL 파라미터:', {
treeId: this.treeId,
nodeId: this.nodeId,
chapterIndex: this.chapterIndex
});
},
// 인증 확인
async checkAuth() {
try {
this.currentUser = await window.api.getCurrentUser();
console.log('✅ 사용자 인증됨:', this.currentUser?.email);
} catch (error) {
console.log('❌ 인증 실패:', error);
this.currentUser = null;
}
},
// 스토리 데이터 로드
async loadStoryData() {
if (!this.treeId) {
this.error = '트리 ID가 필요합니다';
this.loading = false;
return;
}
try {
this.loading = true;
this.error = null;
// 트리 정보 로드
this.selectedTree = await window.api.getMemoTree(this.treeId);
console.log('📚 트리 로드됨:', this.selectedTree.title);
// 트리의 모든 노드 로드
const allNodes = await window.api.getMemoTreeNodes(this.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);
if (this.canonicalNodes.length === 0) {
this.error = '정사로 설정된 노드가 없습니다';
this.loading = false;
return;
}
// 현재 챕터 설정
this.setCurrentChapter();
this.loading = false;
} catch (error) {
console.error('❌ 스토리 로드 실패:', error);
this.error = '스토리를 불러오는데 실패했습니다';
this.loading = false;
}
},
// 현재 챕터 설정
setCurrentChapter() {
if (this.nodeId) {
// 특정 노드 ID로 찾기
const index = this.canonicalNodes.findIndex(node => node.id === this.nodeId);
if (index !== -1) {
this.currentChapterIndex = index;
}
} else if (this.chapterIndex >= 0 && this.chapterIndex < this.canonicalNodes.length) {
// 인덱스로 설정
this.currentChapterIndex = this.chapterIndex;
} else {
// 기본값: 첫 번째 챕터
this.currentChapterIndex = 0;
}
this.currentChapter = this.canonicalNodes[this.currentChapterIndex];
console.log('📖 현재 챕터:', this.currentChapter?.title, `(${this.currentChapterIndex + 1}/${this.canonicalNodes.length})`);
},
// 계산된 속성들
get totalChapters() {
return this.canonicalNodes.length;
},
get hasPreviousChapter() {
return this.currentChapterIndex > 0;
},
get hasNextChapter() {
return this.currentChapterIndex < this.canonicalNodes.length - 1;
},
get previousChapter() {
return this.hasPreviousChapter ? this.canonicalNodes[this.currentChapterIndex - 1] : null;
},
get nextChapter() {
return this.hasNextChapter ? this.canonicalNodes[this.currentChapterIndex + 1] : null;
},
// 네비게이션 함수들
goToPreviousChapter() {
if (this.hasPreviousChapter) {
this.currentChapterIndex--;
this.currentChapter = this.canonicalNodes[this.currentChapterIndex];
this.updateUrl();
this.scrollToTop();
}
},
goToNextChapter() {
if (this.hasNextChapter) {
this.currentChapterIndex++;
this.currentChapter = this.canonicalNodes[this.currentChapterIndex];
this.updateUrl();
this.scrollToTop();
}
},
goBackToStoryView() {
window.location.href = `story-view.html?treeId=${this.treeId}`;
},
editChapter() {
if (this.currentChapter) {
this.editingChapter = { ...this.currentChapter }; // 복사본 생성
this.showEditModal = true;
console.log('✅ 편집 모달 열림 (Textarea 방식)');
}
},
// 편집 취소
cancelEdit() {
this.showEditModal = false;
this.editingChapter = null;
console.log('✅ 편집 취소됨 (Textarea 방식)');
},
// 편집 저장
async saveEdit() {
if (!this.editingChapter) {
console.warn('⚠️ 편집 중인 챕터가 없습니다');
return;
}
try {
this.saving = true;
// 단어 수 계산
const content = this.editingChapter.content || '';
let wordCount = 0;
if (content && content.trim()) {
const words = content.trim().split(/\s+/);
wordCount = words.filter(word => word.length > 0).length;
}
this.editingChapter.word_count = wordCount;
// API로 저장
const updateData = {
title: this.editingChapter.title,
content: this.editingChapter.content,
word_count: this.editingChapter.word_count
};
await window.api.updateMemoNode(this.editingChapter.id, updateData);
// 현재 챕터 업데이트
this.currentChapter.title = this.editingChapter.title;
this.currentChapter.content = this.editingChapter.content;
this.currentChapter.word_count = this.editingChapter.word_count;
// 정사 노드 목록에서도 업데이트
const nodeIndex = this.canonicalNodes.findIndex(node => node.id === this.editingChapter.id);
if (nodeIndex !== -1) {
this.canonicalNodes[nodeIndex].title = this.editingChapter.title;
this.canonicalNodes[nodeIndex].content = this.editingChapter.content;
this.canonicalNodes[nodeIndex].word_count = this.editingChapter.word_count;
}
console.log('✅ 챕터 저장 완료');
this.cancelEdit();
} catch (error) {
console.error('❌ 저장 실패:', error);
alert('저장 중 오류가 발생했습니다: ' + error.message);
} finally {
this.saving = false;
}
},
// URL 업데이트
updateUrl() {
const url = new URL(window.location);
url.searchParams.set('nodeId', this.currentChapter.id);
url.searchParams.set('index', this.currentChapterIndex.toString());
window.history.replaceState({}, '', url);
},
// 페이지 상단으로 스크롤
scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
},
// 인쇄
printChapter() {
window.print();
},
// 콘텐츠 포맷팅
formatContent(content) {
if (!content) return '';
// 마크다운 스타일 간단 변환
return content
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>')
.replace(/^/, '<p>')
.replace(/$/, '</p>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>');
},
// 노드 타입 라벨
getNodeTypeLabel(nodeType) {
const labels = {
'memo': '메모',
'folder': '폴더',
'chapter': '챕터',
'character': '캐릭터',
'plot': '플롯'
};
return labels[nodeType] || nodeType;
},
// 상태 라벨
getStatusLabel(status) {
const labels = {
'draft': '초안',
'writing': '작성중',
'review': '검토중',
'complete': '완료'
};
return labels[status] || status;
},
// 로그인 관련
openLoginModal() {
this.showLoginModal = true;
this.loginError = '';
},
async handleLogin() {
try {
this.loginLoading = true;
this.loginError = '';
const result = await window.api.login(this.loginForm.email, this.loginForm.password);
if (result.access_token) {
this.currentUser = result.user;
this.showLoginModal = false;
this.loginForm = { email: '', password: '' };
// 로그인 후 스토리 데이터 로드
await this.loadStoryData();
}
} catch (error) {
console.error('❌ 로그인 실패:', error);
this.loginError = '로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.';
} finally {
this.loginLoading = false;
}
}
};
}

View File

@@ -15,7 +15,6 @@ window.storyViewApp = function() {
canonicalNodes: [], // 정사 경로 노드들만
// UI 상태
viewMode: 'toc', // 'toc' | 'full'
showLoginModal: false,
showEditModal: false,
editingNode: null,
@@ -55,6 +54,9 @@ window.storyViewApp = function() {
// 인증된 경우 트리 목록 로드
if (this.currentUser) {
await this.loadUserTrees();
// URL 파라미터에서 treeId 확인하고 자동 선택
this.checkUrlParams();
}
},
@@ -91,6 +93,8 @@ window.storyViewApp = function() {
if (!treeId) {
this.selectedTree = null;
this.canonicalNodes = [];
// URL에서 treeId 파라미터 제거
this.updateUrl(null);
return;
}
@@ -108,6 +112,9 @@ window.storyViewApp = function() {
.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);
@@ -115,28 +122,33 @@ window.storyViewApp = function() {
}
},
// 뷰 모드 토글
toggleView() {
this.viewMode = this.viewMode === 'toc' ? 'full' : 'toc';
// 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);
}
},
// 챕터로 스크롤
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' });
}
});
// URL 업데이트
updateUrl(treeId) {
const url = new URL(window.location);
if (treeId) {
url.searchParams.set('treeId', treeId);
} else {
const element = document.getElementById(`chapter-${nodeId}`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
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;
},
// 챕터 편집 (인라인 모달)

View File

@@ -0,0 +1,589 @@
// 업로드 애플리케이션 컴포넌트
window.uploadApp = () => ({
// 상태 관리
currentStep: 1,
selectedFiles: [],
uploadedDocuments: [],
pdfFiles: [],
// 업로드 상태
uploading: false,
finalizing: false,
// 인증 상태
isAuthenticated: false,
currentUser: null,
// 서적 관련
bookSelectionMode: 'none',
bookSearchQuery: '',
searchedBooks: [],
selectedBook: null,
newBook: {
title: '',
author: '',
description: ''
},
// Sortable 인스턴스
sortableInstance: null,
// 초기화
async init() {
console.log('🚀 Upload App 초기화 시작');
// 인증 상태 확인
await this.checkAuthStatus();
if (this.isAuthenticated) {
// 헤더 로드
await this.loadHeader();
}
},
// 인증 상태 확인
async checkAuthStatus() {
try {
const user = await window.api.getCurrentUser();
this.isAuthenticated = true;
this.currentUser = user;
console.log('✅ 인증됨:', user.username);
} catch (error) {
console.log('❌ 인증되지 않음');
this.isAuthenticated = false;
this.currentUser = null;
window.location.href = '/login.html';
}
},
// 헤더 로드
async loadHeader() {
try {
await window.headerLoader.loadHeader();
} catch (error) {
console.error('헤더 로드 실패:', error);
}
},
// 드래그 오버 처리
handleDragOver(event) {
event.dataTransfer.dropEffect = 'copy';
event.target.closest('.drag-area').classList.add('drag-over');
},
// 드래그 리브 처리
handleDragLeave(event) {
event.target.closest('.drag-area').classList.remove('drag-over');
},
// 드롭 처리
handleDrop(event) {
event.target.closest('.drag-area').classList.remove('drag-over');
const files = Array.from(event.dataTransfer.files);
this.processFiles(files);
},
// 파일 선택 처리
handleFileSelect(event) {
const files = Array.from(event.target.files);
this.processFiles(files);
},
// 파일 처리
processFiles(files) {
const validFiles = files.filter(file => {
const isValid = file.type === 'text/html' ||
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.html') ||
file.name.toLowerCase().endsWith('.htm') ||
file.name.toLowerCase().endsWith('.pdf');
if (!isValid) {
console.warn('지원하지 않는 파일 형식:', file.name);
}
return isValid;
});
// 기존 파일과 중복 체크
validFiles.forEach(file => {
const isDuplicate = this.selectedFiles.some(existing =>
existing.name === file.name && existing.size === file.size
);
if (!isDuplicate) {
this.selectedFiles.push(file);
}
});
console.log('📁 선택된 파일:', this.selectedFiles.length, '개');
},
// 파일 제거
removeFile(index) {
this.selectedFiles.splice(index, 1);
},
// 파일 크기 포맷팅
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
// 다음 단계
nextStep() {
if (this.selectedFiles.length === 0) {
alert('업로드할 파일을 선택해주세요.');
return;
}
this.currentStep = 2;
},
// 이전 단계
prevStep() {
this.currentStep = Math.max(1, this.currentStep - 1);
},
// 서적 검색
async searchBooks() {
if (!this.bookSearchQuery.trim()) {
this.searchedBooks = [];
return;
}
try {
const books = await window.api.searchBooks(this.bookSearchQuery, 10);
this.searchedBooks = books;
} catch (error) {
console.error('서적 검색 실패:', error);
this.searchedBooks = [];
}
},
// 서적 선택
selectBook(book) {
this.selectedBook = book;
this.bookSearchQuery = book.title;
this.searchedBooks = [];
},
// 파일 업로드
async uploadFiles() {
if (this.selectedFiles.length === 0) {
alert('업로드할 파일이 없습니다.');
return;
}
// 서적 설정 검증
if (this.bookSelectionMode === 'new' && !this.newBook.title.trim()) {
alert('새 서적을 생성하려면 제목을 입력해주세요.');
return;
}
if (this.bookSelectionMode === 'existing' && !this.selectedBook) {
alert('기존 서적을 선택해주세요.');
return;
}
this.uploading = true;
try {
let bookId = null;
// 서적 처리
if (this.bookSelectionMode === 'new' && this.newBook.title.trim()) {
try {
const newBook = await window.api.createBook({
title: this.newBook.title,
author: this.newBook.author,
description: this.newBook.description
});
bookId = newBook.id;
console.log('📚 새 서적 생성됨:', newBook.title);
} catch (error) {
if (error.message.includes('already exists')) {
// 동일한 서적이 이미 존재하는 경우 - 선택 모달 표시
const choice = await this.showBookConflictModal();
if (choice === 'existing') {
// 기존 서적 검색해서 사용
const existingBooks = await window.api.searchBooks(this.newBook.title, 10);
const matchingBook = existingBooks.find(book =>
book.title === this.newBook.title &&
book.author === this.newBook.author
);
if (matchingBook) {
bookId = matchingBook.id;
console.log('📚 기존 서적 사용:', matchingBook.title);
} else {
throw new Error('기존 서적을 찾을 수 없습니다.');
}
} else if (choice === 'edition') {
// 에디션 정보 입력받아서 새 서적 생성
const edition = await this.getEditionInfo();
if (edition) {
const newBookWithEdition = await window.api.createBook({
title: `${this.newBook.title} (${edition})`,
author: this.newBook.author,
description: this.newBook.description
});
bookId = newBookWithEdition.id;
console.log('📚 에디션 서적 생성됨:', newBookWithEdition.title);
} else {
throw new Error('에디션 정보가 입력되지 않았습니다.');
}
} else {
throw new Error('사용자가 업로드를 취소했습니다.');
}
} else {
throw error;
}
}
} else if (this.bookSelectionMode === 'existing' && this.selectedBook) {
bookId = this.selectedBook.id;
}
// HTML과 PDF 파일 분리
const htmlFiles = this.selectedFiles.filter(file =>
file.type === 'text/html' ||
file.name.toLowerCase().endsWith('.html') ||
file.name.toLowerCase().endsWith('.htm')
);
const pdfFiles = this.selectedFiles.filter(file =>
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
);
console.log('📄 HTML 파일:', htmlFiles.length, '개');
console.log('📕 PDF 파일:', pdfFiles.length, '개');
// HTML 파일 업로드 (백엔드 API에 맞게)
const uploadPromises = htmlFiles.map(async (file, index) => {
const formData = new FormData();
formData.append('html_file', file); // 백엔드가 요구하는 필드명
formData.append('title', file.name.replace(/\.[^/.]+$/, "")); // 확장자 제거
formData.append('description', `업로드된 파일: ${file.name}`);
formData.append('language', 'ko');
formData.append('is_public', 'false');
if (bookId) {
formData.append('book_id', bookId);
}
try {
const response = await window.api.uploadDocument(formData);
console.log('✅ HTML 파일 업로드 완료:', file.name);
return response;
} catch (error) {
console.error('❌ HTML 파일 업로드 실패:', file.name, error);
throw error;
}
});
// PDF 파일은 별도로 처리 (나중에 HTML과 매칭)
const pdfUploadPromises = pdfFiles.map(async (file, index) => {
// PDF 전용 업로드 로직 (임시로 HTML 파일로 처리하지 않음)
console.log('📕 PDF 파일 대기 중:', file.name);
return {
id: `pdf-${Date.now()}-${index}`,
title: file.name.replace(/\.[^/.]+$/, ""),
original_filename: file.name,
file_type: 'pdf',
file: file // 실제 파일 객체 보관
};
});
// HTML 파일 업로드 완료 대기
const uploadedHtmlDocs = await Promise.all(uploadPromises);
// PDF 파일 처리 (실제 업로드는 하지 않고 매칭용으로만 보관)
const pdfDocs = await Promise.all(pdfUploadPromises);
// 업로드된 HTML 문서 정리
this.uploadedDocuments = uploadedHtmlDocs.map((doc, index) => ({
...doc,
display_order: index + 1,
matched_pdf_id: null,
file_type: 'html'
}));
// PDF 파일 목록 (매칭용)
this.pdfFiles = pdfDocs;
console.log('🎉 모든 파일 업로드 완료!');
console.log('📄 HTML 문서:', this.uploadedDocuments.filter(doc => doc.file_type === 'html').length, '개');
console.log('📕 PDF 문서:', this.pdfFiles.length, '개');
// 3단계로 이동
this.currentStep = 3;
// 다음 틱에서 Sortable 초기화
this.$nextTick(() => {
this.initSortable();
});
} catch (error) {
console.error('업로드 실패:', error);
alert('업로드 중 오류가 발생했습니다: ' + error.message);
} finally {
this.uploading = false;
}
},
// Sortable 초기화
initSortable() {
const sortableList = document.getElementById('sortable-list');
if (sortableList && !this.sortableInstance) {
this.sortableInstance = Sortable.create(sortableList, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
handle: '.cursor-move',
onEnd: (evt) => {
// 배열 순서 업데이트
const item = this.uploadedDocuments.splice(evt.oldIndex, 1)[0];
this.uploadedDocuments.splice(evt.newIndex, 0, item);
// display_order 업데이트
this.updateDisplayOrder();
console.log('📋 드래그로 순서 변경됨');
}
});
}
},
// display_order 업데이트
updateDisplayOrder() {
this.uploadedDocuments.forEach((doc, index) => {
doc.display_order = index + 1;
});
},
// 위로 이동
moveUp(index) {
if (index > 0) {
const item = this.uploadedDocuments.splice(index, 1)[0];
this.uploadedDocuments.splice(index - 1, 0, item);
this.updateDisplayOrder();
console.log('📋 위로 이동:', item.title);
}
},
// 아래로 이동
moveDown(index) {
if (index < this.uploadedDocuments.length - 1) {
const item = this.uploadedDocuments.splice(index, 1)[0];
this.uploadedDocuments.splice(index + 1, 0, item);
this.updateDisplayOrder();
console.log('📋 아래로 이동:', item.title);
}
},
// 이름순 정렬
autoSortByName() {
this.uploadedDocuments.sort((a, b) => {
return a.title.localeCompare(b.title, 'ko', { numeric: true });
});
this.updateDisplayOrder();
console.log('📋 이름순 정렬 완료');
},
// 순서 뒤집기
reverseOrder() {
this.uploadedDocuments.reverse();
this.updateDisplayOrder();
console.log('📋 순서 뒤집기 완료');
},
// 섞기
shuffleDocuments() {
for (let i = this.uploadedDocuments.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.uploadedDocuments[i], this.uploadedDocuments[j]] = [this.uploadedDocuments[j], this.uploadedDocuments[i]];
}
this.updateDisplayOrder();
console.log('📋 문서 섞기 완료');
},
// 최종 완료 처리
async finalizeUpload() {
this.finalizing = true;
try {
// 문서 순서 및 PDF 매칭 정보 업데이트
const updatePromises = this.uploadedDocuments.map(async (doc) => {
const updateData = {
display_order: doc.display_order
};
// PDF 매칭 정보 추가 (필요시 백엔드 API 확장)
if (doc.matched_pdf_id) {
updateData.matched_pdf_id = doc.matched_pdf_id;
}
return await window.api.updateDocument(doc.id, updateData);
});
await Promise.all(updatePromises);
console.log('🎉 업로드 완료 처리됨!');
alert('업로드가 완료되었습니다!');
// 메인 페이지로 이동
window.location.href = 'index.html';
} catch (error) {
console.error('완료 처리 실패:', error);
alert('완료 처리 중 오류가 발생했습니다: ' + error.message);
} finally {
this.finalizing = false;
}
},
// 업로드 재시작
resetUpload() {
if (confirm('업로드를 다시 시작하시겠습니까? 현재 진행 상황이 초기화됩니다.')) {
this.currentStep = 1;
this.selectedFiles = [];
this.uploadedDocuments = [];
this.pdfFiles = [];
this.bookSelectionMode = 'none';
this.selectedBook = null;
this.newBook = { title: '', author: '', description: '' };
if (this.sortableInstance) {
this.sortableInstance.destroy();
this.sortableInstance = null;
}
}
},
// 뒤로가기
goBack() {
if (this.currentStep > 1) {
if (confirm('진행 중인 업로드를 취소하고 돌아가시겠습니까?')) {
window.location.href = 'index.html';
}
} else {
window.location.href = 'index.html';
}
},
// 서적 중복 시 선택 모달
showBookConflictModal() {
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
modal.innerHTML = `
<div class="bg-white rounded-lg p-6 max-w-md mx-4">
<h3 class="text-lg font-semibold text-gray-900 mb-4">📚 서적 중복 발견</h3>
<p class="text-gray-600 mb-6">
"<strong>${this.newBook.title}</strong>"${this.newBook.author ? ` (${this.newBook.author})` : ''} 서적이 이미 존재합니다.
<br><br>어떻게 처리하시겠습니까?
</p>
<div class="space-y-3">
<button id="use-existing" class="w-full px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-left">
<div class="font-medium">🔄 기존 서적에 추가</div>
<div class="text-sm text-blue-100">기존 서적에 새 문서들을 추가합니다</div>
</button>
<button id="add-edition" class="w-full px-4 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-left">
<div class="font-medium">📖 에디션으로 구분</div>
<div class="text-sm text-green-100">에디션 정보를 입력해서 별도 서적으로 생성합니다</div>
</button>
<button id="cancel-upload" class="w-full px-4 py-3 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors text-center">
❌ 업로드 취소
</button>
</div>
</div>
`;
document.body.appendChild(modal);
// 이벤트 리스너
modal.querySelector('#use-existing').onclick = () => {
document.body.removeChild(modal);
resolve('existing');
};
modal.querySelector('#add-edition').onclick = () => {
document.body.removeChild(modal);
resolve('edition');
};
modal.querySelector('#cancel-upload').onclick = () => {
document.body.removeChild(modal);
resolve('cancel');
};
});
},
// 에디션 정보 입력
getEditionInfo() {
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
modal.innerHTML = `
<div class="bg-white rounded-lg p-6 max-w-md mx-4">
<h3 class="text-lg font-semibold text-gray-900 mb-4">📖 에디션 정보 입력</h3>
<p class="text-gray-600 mb-4">
서적을 구분할 에디션 정보를 입력해주세요.
</p>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">에디션 정보</label>
<input type="text" id="edition-input"
placeholder="예: 2nd Edition, 2024년판, Ver 2.0"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">
💡 예시: "2nd Edition", "2024년판", "개정판", "Ver 2.0"
</p>
</div>
<div class="flex space-x-3">
<button id="confirm-edition" class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
확인
</button>
<button id="cancel-edition" class="flex-1 px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors">
취소
</button>
</div>
</div>
`;
document.body.appendChild(modal);
const input = modal.querySelector('#edition-input');
input.focus();
// 이벤트 리스너
const confirm = () => {
const edition = input.value.trim();
document.body.removeChild(modal);
resolve(edition || null);
};
const cancel = () => {
document.body.removeChild(modal);
resolve(null);
};
modal.querySelector('#confirm-edition').onclick = confirm;
modal.querySelector('#cancel-edition').onclick = cancel;
// Enter 키 처리
input.onkeypress = (e) => {
if (e.key === 'Enter') {
confirm();
}
};
});
}
});
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', () => {
console.log('📄 Upload 페이지 로드됨');
});

View File

@@ -28,6 +28,9 @@ window.documentViewer = () => ({
noteSearchQuery: '',
filteredNotes: [],
// 언어 전환
isKorean: false,
// 모달
showNoteModal: false,
showBookmarkModal: false,
@@ -783,9 +786,31 @@ window.documentViewer = () => ({
});
},
// 뒤로가기
// 뒤로가기 - 문서 관리 페이지로 이동
goBack() {
window.history.back();
// 1. URL 파라미터에서 from 확인
const urlParams = new URLSearchParams(window.location.search);
const fromPage = urlParams.get('from');
// 2. 세션 스토리지에서 이전 페이지 확인
const previousPage = sessionStorage.getItem('previousPage');
// 3. referrer 확인
const referrer = document.referrer;
let targetPage = 'index.html'; // 기본값: 그리드 뷰
// 우선순위: URL 파라미터 > 세션 스토리지 > referrer
if (fromPage === 'hierarchy') {
targetPage = 'hierarchy.html';
} else if (previousPage === 'hierarchy.html') {
targetPage = 'hierarchy.html';
} else if (referrer && referrer.includes('hierarchy.html')) {
targetPage = 'hierarchy.html';
}
console.log(`🔙 뒤로가기: ${targetPage}로 이동`);
window.location.href = targetPage;
},
// 날짜 포맷팅
@@ -1025,5 +1050,79 @@ window.documentViewer = () => ({
console.error('Failed to delete highlight:', error);
alert('하이라이트 삭제에 실패했습니다');
}
},
// 언어 전환 함수
toggleLanguage() {
this.isKorean = !this.isKorean;
// 문서 내 언어별 요소 토글 (더 범용적으로)
const primaryLangElements = document.querySelectorAll('[lang="ko"], .korean, .kr, .primary-lang');
const secondaryLangElements = document.querySelectorAll('[lang="en"], .english, .en, [lang="ja"], .japanese, .jp, [lang="zh"], .chinese, .cn, .secondary-lang');
primaryLangElements.forEach(el => {
el.style.display = this.isKorean ? 'block' : 'none';
});
secondaryLangElements.forEach(el => {
el.style.display = this.isKorean ? 'none' : 'block';
});
console.log(`🌐 언어 전환됨 (Primary: ${this.isKorean ? '표시' : '숨김'})`);
},
// 매칭된 PDF 다운로드
async downloadMatchedPDF() {
if (!this.document.matched_pdf_id) {
console.warn('매칭된 PDF가 없습니다');
return;
}
try {
console.log('📕 PDF 다운로드 시작:', this.document.matched_pdf_id);
// PDF 문서 정보 가져오기
const pdfDocument = await window.api.getDocument(this.document.matched_pdf_id);
if (!pdfDocument) {
throw new Error('PDF 문서를 찾을 수 없습니다');
}
// PDF 파일 다운로드 URL 생성
const downloadUrl = `/api/documents/${this.document.matched_pdf_id}/download`;
// 인증 헤더 추가를 위해 fetch 사용
const response = await fetch(downloadUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (!response.ok) {
throw new Error('PDF 다운로드에 실패했습니다');
}
// Blob으로 변환하여 다운로드
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
// 다운로드 링크 생성 및 클릭
const link = document.createElement('a');
link.href = url;
link.download = pdfDocument.original_filename || `${pdfDocument.title}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// URL 정리
window.URL.revokeObjectURL(url);
console.log('✅ PDF 다운로드 완료');
} catch (error) {
console.error('❌ PDF 다운로드 실패:', error);
alert('PDF 다운로드에 실패했습니다: ' + error.message);
}
}
});