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

@@ -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; // 노드 클릭 시 패닝 방지