Fix: 업로드 및 API 연결 문제 해결

- FastAPI 라우터에서 슬래시 문제로 인한 307 리다이렉트 수정
- Nginx 프록시 설정에서 경로 중복 문제 해결
- 계정 관리 시스템 구현 (로그인, 사용자 관리, 권한 설정)
- 노트북 연결 기능 수정 (notebook_id 필드 추가)
- 메모 트리 UI 개선 (수평 레이아웃, 드래그 기능 제거)
- 헤더 UI 개선 및 고정 위치 설정
- 백업/복원 스크립트 추가
- PDF 미리보기 토큰 인증 지원
This commit is contained in:
Hyungi Ahn
2025-09-03 15:58:10 +09:00
parent d4b10b16b1
commit 6e01dbdeb3
47 changed files with 3672 additions and 398 deletions

View File

@@ -1,6 +1,7 @@
/* 메인 스타일 */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding-top: 4rem; /* 고정 헤더를 위한 패딩 */
}
/* 알림 애니메이션 */

View File

@@ -0,0 +1,41 @@
# 로그인 페이지 이미지
이 폴더에는 로그인 페이지에서 사용되는 배경 이미지를 저장합니다.
## 필요한 이미지 파일
### 배경 이미지
- `login-bg.jpg` - 전체 페이지 배경 이미지 (권장 크기: 1920x1080px 이상)
## 이미지 사양
- **형식**: JPG, PNG 지원
- **품질**: 웹 최적화된 고품질 이미지
- **용량**: 1MB 이하 권장
- **비율**: 16:9 또는 16:10 비율 권장
- **색상**: 어두운 톤 또는 블러 처리된 이미지 권장 (텍스트 가독성을 위해)
## 폴백 동작
배경 이미지 파일이 없는 경우:
- 파란색-보라색 그라디언트 배경으로 자동 폴백
## 사용 예시
```
static/images/
└── login-bg.jpg (전체 배경)
```
## 배경 이미지 선택 가이드
- **문서/도서관 테마**: 책장, 도서관, 서재 등
- **기술/현대적 테마**: 추상적 패턴, 기하학적 형태
- **자연 테마**: 차분한 풍경, 블러 처리된 자연 이미지
- **미니멀 테마**: 단순한 패턴, 텍스처
## 변경 사항 (v2.0)
- 갤러리 액자 기능 제거
- 중앙 집중형 로그인 레이아웃으로 변경
- 배경 이미지만 사용하는 심플한 디자인

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 KiB

View File

@@ -201,7 +201,7 @@ class DocumentServerAPI {
}
async getCurrentUser() {
return await this.get('/auth/me');
return await this.get('/users/me');
}
async refreshToken(refreshToken) {

View File

@@ -0,0 +1,92 @@
/**
* 인증 가드 - 모든 보호된 페이지에서 사용
* 로그인하지 않은 사용자를 자동으로 로그인 페이지로 리다이렉트
*/
(function() {
'use strict';
// 인증이 필요하지 않은 페이지들
const PUBLIC_PAGES = [
'login.html',
'setup.html'
];
// 현재 페이지가 공개 페이지인지 확인
function isPublicPage() {
const currentPath = window.location.pathname;
return PUBLIC_PAGES.some(page => currentPath.includes(page));
}
// 로그인 페이지로 리다이렉트
function redirectToLogin() {
const currentUrl = encodeURIComponent(window.location.href);
console.log('🔐 인증되지 않은 접근. 로그인 페이지로 이동합니다.');
window.location.href = `login.html?redirect=${currentUrl}`;
}
// 인증 체크 함수
async function checkAuthentication() {
// 공개 페이지는 체크하지 않음
if (isPublicPage()) {
return;
}
const token = localStorage.getItem('access_token');
// 토큰이 없으면 즉시 리다이렉트
if (!token) {
redirectToLogin();
return;
}
try {
// 토큰 유효성 검사
const response = await fetch('/api/auth/me', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
console.log('🔐 토큰이 유효하지 않습니다. 상태:', response.status);
localStorage.removeItem('access_token');
redirectToLogin();
return;
}
// 인증 성공
const user = await response.json();
console.log('✅ 인증 성공:', user.email);
// 전역 사용자 정보 설정
window.currentUser = user;
// 헤더 사용자 메뉴 업데이트
if (typeof window.updateUserMenu === 'function') {
window.updateUserMenu(user);
}
} catch (error) {
console.error('🔐 인증 확인 중 오류:', error);
localStorage.removeItem('access_token');
redirectToLogin();
}
}
// DOM 로드 완료 전에 인증 체크 실행
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', checkAuthentication);
} else {
checkAuthentication();
}
// 전역 함수로 노출
window.authGuard = {
checkAuthentication,
redirectToLogin,
isPublicPage
};
})();

View File

@@ -155,9 +155,97 @@ document.addEventListener('headerLoaded', () => {
setTimeout(() => {
window.headerLoader.updateActiveStates();
// 사용자 메뉴 초기 상태 설정 (로그아웃 상태로 시작)
if (typeof window.updateUserMenu === 'function') {
window.updateUserMenu(null);
// updateUserMenu 함수 정의 (헤더 로더에서 직접 정의)
if (typeof window.updateUserMenu === 'undefined') {
window.updateUserMenu = (user) => {
console.log('🔄 updateUserMenu 호출됨:', user);
const loggedInMenu = document.getElementById('logged-in-menu');
const loginButton = document.getElementById('login-button');
const adminMenuSection = document.getElementById('admin-menu-section');
// 사용자 정보 요소들
const userName = document.getElementById('user-name');
const userRole = document.getElementById('user-role');
const dropdownUserName = document.getElementById('dropdown-user-name');
const dropdownUserEmail = document.getElementById('dropdown-user-email');
const dropdownUserRole = document.getElementById('dropdown-user-role');
if (user) {
// 로그인된 상태
console.log('✅ 사용자 로그인 상태 - UI 업데이트');
if (loggedInMenu) {
loggedInMenu.classList.remove('hidden');
console.log('✅ 로그인 메뉴 표시');
}
if (loginButton) {
loginButton.classList.add('hidden');
console.log('✅ 로그인 버튼 숨김');
}
// 사용자 정보 업데이트
const displayName = user.full_name || user.email || 'User';
const roleText = user.role === 'root' ? '시스템 관리자' :
user.role === 'admin' ? '관리자' : '사용자';
if (userName) userName.textContent = displayName;
if (userRole) userRole.textContent = roleText;
if (dropdownUserName) dropdownUserName.textContent = displayName;
if (dropdownUserEmail) dropdownUserEmail.textContent = user.email || '';
if (dropdownUserRole) dropdownUserRole.textContent = roleText;
// 관리자 메뉴 표시/숨김
if (adminMenuSection) {
if (user.role === 'root' || user.role === 'admin' || user.is_admin) {
adminMenuSection.classList.remove('hidden');
} else {
adminMenuSection.classList.add('hidden');
}
}
} else {
// 로그아웃된 상태
console.log('❌ 로그아웃 상태');
if (loggedInMenu) loggedInMenu.classList.add('hidden');
if (loginButton) loginButton.classList.remove('hidden');
if (adminMenuSection) adminMenuSection.classList.add('hidden');
}
};
console.log('✅ updateUserMenu 함수 정의 완료');
}
// 사용자 메뉴 상태 설정 (현재 로그인 상태 확인)
setTimeout(() => {
// 전역 사용자 정보가 있으면 사용, 없으면 토큰으로 확인
if (window.currentUser) {
window.updateUserMenu(window.currentUser);
} else {
// 토큰이 있으면 사용자 정보 다시 가져오기
const token = localStorage.getItem('access_token');
if (token) {
fetch('/api/auth/me', {
headers: { 'Authorization': `Bearer ${token}` }
})
.then(response => response.ok ? response.json() : null)
.then(user => {
if (user) {
window.currentUser = user;
window.updateUserMenu(user);
} else {
window.updateUserMenu(null);
}
})
.catch(() => window.updateUserMenu(null));
} else {
window.updateUserMenu(null);
}
}
}, 200);
// 전역 함수들이 정의되지 않은 경우 빈 함수로 초기화
if (typeof window.handleLanguageChange === 'undefined') {
window.handleLanguageChange = function(lang) {
console.log('언어 변경 함수가 아직 로드되지 않았습니다:', lang);
};
}
}, 100);
});

View File

@@ -65,6 +65,15 @@ window.documentApp = () => ({
this.isAuthenticated = false;
this.currentUser = null;
localStorage.removeItem('access_token');
// 로그인 페이지로 리다이렉트 (setup.html 제외)
if (!window.location.pathname.includes('setup.html') &&
!window.location.pathname.includes('login.html')) {
const currentUrl = encodeURIComponent(window.location.href);
window.location.href = `login.html?redirect=${currentUrl}`;
return;
}
this.syncUIState(); // UI 상태 동기화
}
},

View File

@@ -63,9 +63,6 @@ window.memoTreeApp = function() {
treePanX: 0,
treePanY: 0,
nodePositions: new Map(), // 노드 ID -> {x, y} 위치 매핑
isDragging: false,
dragNode: null,
dragOffset: { x: 0, y: 0 },
// 로그인 관련 함수들
openLoginModal() {
@@ -290,7 +287,15 @@ window.memoTreeApp = function() {
const node = await window.api.createMemoNode(nodeData);
this.treeNodes.push(node);
this.selectNode(node);
// 노드 위치 재계산 (새 노드 추가 후)
this.$nextTick(() => {
this.calculateNodePositions();
// 위치 계산 완료 후 새 노드 선택
setTimeout(() => {
this.selectNode(node);
}, 50);
});
console.log('✅ 루트 노드 생성 완료');
} catch (error) {
@@ -301,6 +306,11 @@ window.memoTreeApp = function() {
// 노드 선택
selectNode(node) {
// 현재 팬 값 저장 (위치 변경 방지)
const currentPanX = this.treePanX;
const currentPanY = this.treePanY;
const currentZoom = this.treeZoom;
// 이전 노드 저장
if (this.selectedNode && this.isEditorDirty) {
this.saveNode();
@@ -323,6 +333,11 @@ window.memoTreeApp = function() {
}, 100);
}
// 팬 값 복원 (위치 변경 방지)
this.treePanX = currentPanX;
this.treePanY = currentPanY;
this.treeZoom = currentZoom;
console.log('📝 노드 선택:', node.title);
},
@@ -552,8 +567,14 @@ window.memoTreeApp = function() {
// 부모 노드 펼치기
this.expandedNodes.add(parentNode.id);
// 새 노드 선택
this.selectNode(node);
// 노드 위치 재계산 (새 노드 추가 후)
this.$nextTick(() => {
this.calculateNodePositions();
// 위치 계산 완료 후 새 노드 선택
setTimeout(() => {
this.selectNode(node);
}, 50);
});
console.log('✅ 자식 노드 생성 완료');
} catch (error) {
@@ -623,123 +644,9 @@ window.memoTreeApp = function() {
}
},
// 노드 드래그 시작
startDragNode(event, node) {
event.stopPropagation();
event.preventDefault();
console.log('🎯 노드 드래그 시작:', node.title);
this.isDragging = true;
this.dragNode = node;
// 드래그 시작 위치 계산
const rect = event.target.getBoundingClientRect();
this.dragOffset = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
// 드래그 중인 노드 스타일 적용
event.target.style.opacity = '0.7';
event.target.style.transform = 'scale(1.05)';
event.target.style.zIndex = '1000';
// 이벤트 리스너 등록
document.addEventListener('mousemove', this.handleNodeDrag.bind(this));
document.addEventListener('mouseup', this.endNodeDrag.bind(this));
},
// 노드 드래그 처리
handleNodeDrag(event) {
if (!this.isDragging || !this.dragNode) return;
event.preventDefault();
// 드래그 중인 노드 위치 업데이트
const dragElement = document.querySelector(`[data-node-id="${this.dragNode.id}"]`);
if (dragElement) {
const newX = event.clientX - this.dragOffset.x;
const newY = event.clientY - this.dragOffset.y;
dragElement.style.position = 'fixed';
dragElement.style.left = `${newX}px`;
dragElement.style.top = `${newY}px`;
dragElement.style.pointerEvents = 'none';
}
// 드롭 대상 하이라이트
this.highlightDropTarget(event);
},
// 드롭 대상 하이라이트
highlightDropTarget(event) {
// 모든 노드에서 하이라이트 제거
document.querySelectorAll('.tree-diagram-node').forEach(el => {
el.classList.remove('drop-target-highlight');
});
// 현재 마우스 위치의 노드 찾기
const elements = document.elementsFromPoint(event.clientX, event.clientY);
const targetNode = elements.find(el =>
el.classList.contains('tree-diagram-node') &&
el.getAttribute('data-node-id') !== this.dragNode.id
);
if (targetNode) {
targetNode.classList.add('drop-target-highlight');
}
},
// 노드 드래그 종료
endNodeDrag(event) {
if (!this.isDragging || !this.dragNode) return;
console.log('🎯 노드 드래그 종료');
// 드롭 대상 찾기
const elements = document.elementsFromPoint(event.clientX, event.clientY);
const targetElement = elements.find(el =>
el.classList.contains('tree-diagram-node') &&
el.getAttribute('data-node-id') !== this.dragNode.id
);
let targetNodeId = null;
if (targetElement) {
targetNodeId = targetElement.getAttribute('data-node-id');
}
// 드래그 중인 노드 스타일 복원
const dragElement = document.querySelector(`[data-node-id="${this.dragNode.id}"]`);
if (dragElement) {
dragElement.style.opacity = '';
dragElement.style.transform = '';
dragElement.style.zIndex = '';
dragElement.style.position = '';
dragElement.style.left = '';
dragElement.style.top = '';
dragElement.style.pointerEvents = '';
}
// 모든 하이라이트 제거
document.querySelectorAll('.tree-diagram-node').forEach(el => {
el.classList.remove('drop-target-highlight');
});
// 실제 노드 이동 처리
if (targetNodeId && targetNodeId !== this.dragNode.id) {
this.moveNodeToParent(this.dragNode.id, targetNodeId);
}
// 상태 초기화
this.isDragging = false;
this.dragNode = null;
this.dragOffset = { x: 0, y: 0 };
// 이벤트 리스너 제거
document.removeEventListener('mousemove', this.handleNodeDrag);
document.removeEventListener('mouseup', this.endNodeDrag);
},
// 노드를 다른 부모로 이동
async moveNodeToParent(nodeId, newParentId) {
@@ -783,15 +690,12 @@ window.memoTreeApp = function() {
// 노드 위치 계산 및 반환
getNodePosition(node) {
if (!this.nodePositions.has(node.id)) {
this.calculateNodePositions();
}
// 위치가 없으면 기본 위치 반환 (전체 재계산 방지)
const pos = this.nodePositions.get(node.id) || { x: 0, y: 0 };
return `left: ${pos.x}px; top: ${pos.y}px;`;
},
// 트리 노드 위치 자동 계산
// 트리 노드 위치 자동 계산 (가로 방향: 왼쪽에서 오른쪽)
calculateNodePositions() {
const canvas = document.getElementById('tree-canvas');
if (!canvas) return;
@@ -802,11 +706,11 @@ window.memoTreeApp = function() {
// 노드 크기 설정
const nodeWidth = 200;
const nodeHeight = 80;
const levelHeight = 150; // 레벨 간 간격
const nodeSpacing = 50; // 노드 간 간격
const levelWidth = 250; // 레벨 간 가로 간격 (왼쪽에서 오른쪽)
const nodeSpacing = 100; // 노드 간 세로 간격
const margin = 100; // 여백
// 레벨별 노드 그룹화
// 레벨별 노드 그룹화 (가로 방향)
const levels = new Map();
// 루트 노드들 찾기
@@ -814,7 +718,7 @@ window.memoTreeApp = function() {
if (rootNodes.length === 0) return;
// BFS로 레벨별 노드 배치
// BFS로 레벨별 노드 배치 (가로 방향)
const queue = [];
rootNodes.forEach(node => {
queue.push({ node, level: 0 });
@@ -835,25 +739,25 @@ 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 treeWidth = (maxLevel + 1) * levelWidth; // 가로 방향 전체 너비
const treeHeight = maxNodesInLevel * nodeHeight + (maxNodesInLevel - 1) * nodeSpacing; // 세로 방향 전체 높이
// 캔버스 중앙에 트리 배치하기 위한 오프셋 계산
const offsetX = Math.max(margin, (canvasWidth - treeWidth) / 2);
const offsetY = Math.max(margin, (canvasHeight - treeHeight) / 2);
// 각 레벨의 노드들 위치 계산
// 각 레벨의 노드들 위치 계산 (가로 방향)
levels.forEach((nodes, level) => {
const y = offsetY + level * levelHeight;
const levelWidth = nodes.length * nodeWidth + (nodes.length - 1) * nodeSpacing;
const startX = offsetX + (treeWidth - levelWidth) / 2;
const x = offsetX + level * levelWidth; // 가로 위치 (왼쪽에서 오른쪽)
const levelHeight = nodes.length * nodeHeight + (nodes.length - 1) * nodeSpacing;
const startY = offsetY + (treeHeight - levelHeight) / 2; // 세로 중앙 정렬
nodes.forEach((node, index) => {
const x = startX + index * (nodeWidth + nodeSpacing);
const y = startY + index * (nodeHeight + nodeSpacing);
this.nodePositions.set(node.id, { x, y });
});
});
@@ -893,17 +797,17 @@ window.memoTreeApp = function() {
// 연결선 생성
const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
// 부모 노드 하단 중앙에서 시작
const startX = parentPos.x + 100; // 노드 중앙
const startY = parentPos.y + 80; // 노드 하단
// 부모 노드 오른쪽 중앙에서 시작 (가로 방향)
const startX = parentPos.x + 200; // 노드 오른쪽 끝
const startY = parentPos.y + 40; // 노드 세로 중앙
// 자식 노드 상단 중앙으로 연결
const endX = childPos.x + 100; // 노드 중앙
const endY = childPos.y; // 노드 상단
// 자식 노드 왼쪽 중앙으로 연결 (가로 방향)
const endX = childPos.x; // 노드 왼쪽 끝
const endY = childPos.y + 40; // 노드 세로 중앙
// 곡선 경로 생성 (베지어 곡선)
const midY = startY + (endY - startY) / 2;
const path = `M ${startX} ${startY} C ${startX} ${midY} ${endX} ${midY} ${endX} ${endY}`;
// 곡선 경로 생성 (베지어 곡선, 가로 방향)
const midX = startX + (endX - startX) / 2;
const path = `M ${startX} ${startY} C ${midX} ${startY} ${midX} ${endY} ${endX} ${endY}`;
line.setAttribute('d', path);
line.setAttribute('stroke', '#9CA3AF');

View File

@@ -15,6 +15,7 @@ window.searchApp = function() {
// 필터링
typeFilter: '', // '', 'document', 'note', 'memo', 'highlight'
fileTypeFilter: '', // '', 'PDF', 'HTML'
sortBy: 'relevance', // 'relevance', 'date_desc', 'date_asc', 'title'
// 검색 디바운스
@@ -157,9 +158,43 @@ window.searchApp = function() {
applyFilters() {
let results = [...this.searchResults];
// 중복 ID 제거 (같은 문서의 document와 document_content가 중복될 수 있음)
const uniqueResults = [];
const seenIds = new Set();
results.forEach(result => {
const uniqueKey = `${result.type}-${result.id}`;
if (!seenIds.has(uniqueKey)) {
seenIds.add(uniqueKey);
uniqueResults.push({
...result,
unique_id: uniqueKey // Alpine.js x-for 키로 사용
});
}
});
results = uniqueResults;
// 타입 필터
if (this.typeFilter) {
results = results.filter(result => result.type === this.typeFilter);
results = results.filter(result => {
// 문서 타입은 document와 document_content 모두 포함
if (this.typeFilter === 'document') {
return result.type === 'document' || result.type === 'document_content';
}
// 하이라이트 타입은 highlight와 highlight_note 모두 포함
if (this.typeFilter === 'highlight') {
return result.type === 'highlight' || result.type === 'highlight_note';
}
return result.type === this.typeFilter;
});
}
// 파일 타입 필터
if (this.fileTypeFilter) {
results = results.filter(result => {
return result.highlight_info?.file_type === this.fileTypeFilter;
});
}
// 정렬
@@ -179,7 +214,7 @@ window.searchApp = function() {
});
this.filteredResults = results;
console.log('🔧 필터 적용 완료:', this.filteredResults.length, '개 결과');
console.log('🔧 필터 적용 완료:', this.filteredResults.length, '개 결과 (타입:', this.typeFilter, ', 파일타입:', this.fileTypeFilter, ')');
},
// URL 업데이트
@@ -339,22 +374,23 @@ window.searchApp = function() {
this.pdfLoaded = false;
try {
// PDF 파일 존재 여부 먼저 확인
const response = await fetch(`/api/documents/${documentId}/pdf`, {
method: 'HEAD',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
// PDF 파일 src 직접 설정 (HEAD 요청 대신)
const token = localStorage.getItem('access_token');
console.log('🔍 토큰 디버깅:', {
token: token,
tokenType: typeof token,
tokenLength: token ? token.length : 0,
isNull: token === null,
isStringNull: token === 'null',
localStorage: Object.keys(localStorage)
});
if (response.ok) {
// PDF 파일이 존재하면 src 설정
const token = localStorage.getItem('token');
this.pdfSrc = `/api/documents/${documentId}/pdf?_token=${encodeURIComponent(token)}`;
console.log('PDF 미리보기 준비 완료:', this.pdfSrc);
} else {
throw new Error(`PDF 파일을 찾을 수 없습니다 (${response.status})`);
if (!token || token === 'null' || token === null) {
console.error('❌ 토큰 문제:', token);
throw new Error('인증 토큰이 없습니다. 다시 로그인해주세요.');
}
this.pdfSrc = `/api/documents/${documentId}/pdf?_token=${encodeURIComponent(token)}`;
console.log('✅ PDF 미리보기 준비 완료:', this.pdfSrc);
} catch (error) {
console.error('PDF 미리보기 로드 실패:', error);
this.pdfError = true;
@@ -370,6 +406,50 @@ window.searchApp = function() {
this.pdfLoading = false;
},
// PDF에서 검색어 찾기 (브라우저 내장 검색 활용)
searchInPdf() {
if (this.searchQuery && this.pdfLoaded) {
// iframe 내에서 검색 실행 (Ctrl+F 시뮬레이션)
const iframe = document.querySelector('#pdf-preview-iframe');
if (iframe && iframe.contentWindow) {
try {
iframe.contentWindow.focus();
// 브라우저 검색 창 열기 시도
if (iframe.contentWindow.find) {
iframe.contentWindow.find(this.searchQuery);
} else {
// 대안: 사용자에게 수동 검색 안내
this.showNotification(`PDF에서 "${this.searchQuery}"를 찾으려면 Ctrl+F를 눌러주세요.`, 'info');
}
} catch (e) {
// 보안상 직접 접근이 안 되는 경우, 사용자에게 안내
this.showNotification(`PDF에서 "${this.searchQuery}"를 찾으려면 Ctrl+F를 눌러주세요.`, 'info');
}
}
}
},
// 알림 표시 (간단한 토스트)
showNotification(message, type = 'info') {
// 간단한 알림 구현 (실제로는 더 정교한 토스트 시스템을 사용할 수 있음)
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 px-4 py-2 rounded-lg text-white z-50 ${
type === 'info' ? 'bg-blue-500' :
type === 'success' ? 'bg-green-500' :
type === 'error' ? 'bg-red-500' : 'bg-gray-500'
}`;
notification.textContent = message;
document.body.appendChild(notification);
// 3초 후 자동 제거
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
},
// HTML 미리보기 로드
async loadHtmlPreview(documentId) {
this.htmlLoading = true;
@@ -385,7 +465,12 @@ window.searchApp = function() {
const iframe = document.getElementById('htmlPreviewFrame');
if (iframe) {
// iframe src를 직접 설정 (인증 헤더 포함)
const token = localStorage.getItem('token');
const token = localStorage.getItem('access_token');
console.log('🔍 HTML 미리보기 토큰:', token ? '있음' : '없음', token);
if (!token || token === 'null' || token === null) {
console.error('❌ HTML 미리보기 토큰 문제:', token);
throw new Error('인증 토큰이 없습니다.');
}
iframe.src = `/api/documents/${documentId}/content?_token=${encodeURIComponent(token)}`;
// iframe 로드 완료 후 검색어 하이라이트