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:
@@ -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; // 노드 클릭 시 패닝 방지
|
||||
|
||||
Reference in New Issue
Block a user