✨ 새로운 기능: - 사용자별 세분화된 권한 체크 (can_manage_books, can_manage_notes, can_manage_novels) - 페이지별 권한 가드 시스템 추가 (permission-guard.js) - 헤더 메뉴 권한별 표시/숨김 기능 🔧 백엔드 개선: - 모든 문서 관련 API에서 can_manage_books 권한 체크 추가 - documents.py: 개별 문서 조회, PDF 조회 권한 로직 수정 - highlights.py: 하이라이트 생성/조회 권한 체크 개선 - bookmarks.py: 북마크 생성/조회 권한 체크 개선 - document_links.py: 문서 링크 관련 권한 체크 개선 🎨 프론트엔드 개선: - header-loader.js: updateMenuPermissions 함수 추가로 권한별 메뉴 제어 - permission-guard.js: 페이지 접근 권한 체크 및 리다이렉트 처리 - 권한 없는 페이지 접근 시 메인 페이지로 안전한 리다이렉트 - 헤더 사용자 정보 상태 보존 로직 추가 🛡️ 보안 강화: - 403 Forbidden 에러 해결 - 권한 없는 사용자의 무단 페이지 접근 차단 - 문서 관리 권한이 있는 사용자는 모든 문서 공유 가능 📱 사용자 경험 개선: - 권한에 따른 메뉴 자동 표시/숨김 - 로그인 상태 유지 개선 - 권한 없는 기능 접근 시 친화적인 알림 및 리다이렉트
405 lines
17 KiB
JavaScript
405 lines
17 KiB
JavaScript
/**
|
|
* 공통 헤더 로더
|
|
* 모든 페이지에서 동일한 헤더를 로드하기 위한 유틸리티
|
|
*/
|
|
|
|
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.initializeHeaderFunctions();
|
|
|
|
this.headerLoaded = true;
|
|
console.log('✅ 헤더 로드 완료');
|
|
|
|
// 헤더 로드 완료 이벤트 발생
|
|
document.dispatchEvent(new CustomEvent('headerLoaded'));
|
|
|
|
} catch (error) {
|
|
console.error('❌ 헤더 로드 오류:', error);
|
|
this.showFallbackHeader(targetSelector);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 헤더 로드 후 필요한 함수들 초기화
|
|
*/
|
|
initializeHeaderFunctions() {
|
|
console.log('🔧 헤더 함수들 초기화 중...');
|
|
|
|
// handleLogin 함수 정의
|
|
window.handleLogin = () => {
|
|
console.log('🔐 handleLogin 호출됨 - 로그인 페이지로 이동');
|
|
const currentUrl = encodeURIComponent(window.location.href);
|
|
window.location.href = `login.html?redirect=${currentUrl}`;
|
|
};
|
|
|
|
// handleLogout 함수 정의
|
|
window.handleLogout = async () => {
|
|
try {
|
|
console.log('🔄 로그아웃 시작...');
|
|
console.log('🔍 window.api 존재 여부:', !!window.api);
|
|
console.log('🔍 logout 함수 존재 여부:', typeof logout);
|
|
|
|
// 각 페이지의 로그아웃 함수가 있으면 호출
|
|
if (typeof logout === 'function') {
|
|
await logout();
|
|
} else {
|
|
console.log('🔄 직접 로그아웃 처리 시작...');
|
|
|
|
// API 로그아웃 시도
|
|
if (window.api && typeof window.api.logout === 'function') {
|
|
console.log('🌐 API 로그아웃 호출...');
|
|
try {
|
|
await window.api.logout();
|
|
console.log('✅ API 로그아웃 성공');
|
|
} catch (apiError) {
|
|
console.log('⚠️ API 로그아웃 실패:', apiError);
|
|
}
|
|
} else {
|
|
console.log('⚠️ window.api.logout 함수를 찾을 수 없음');
|
|
}
|
|
|
|
// 로컬 스토리지 정리 (항상 실행)
|
|
console.log('🧹 로컬 스토리지 정리...');
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
localStorage.removeItem('user_info');
|
|
|
|
console.log('✅ 로그아웃 완료');
|
|
}
|
|
|
|
// 로그인 페이지로 리다이렉트
|
|
const currentUrl = encodeURIComponent(window.location.pathname + window.location.search);
|
|
window.location.href = `login.html?redirect=${currentUrl}`;
|
|
|
|
} catch (error) {
|
|
console.error('❌ 로그아웃 실패:', error);
|
|
|
|
// 에러가 발생해도 로컬 데이터는 정리하고 로그인 페이지로 이동
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
localStorage.removeItem('user_info');
|
|
|
|
window.location.href = 'login.html';
|
|
}
|
|
};
|
|
|
|
console.log('✅ 헤더 함수들 초기화 완료');
|
|
}
|
|
|
|
/**
|
|
* 사용자 권한에 따른 메뉴 표시/숨김
|
|
*/
|
|
updateMenuPermissions(user) {
|
|
// 메뉴 요소들 가져오기
|
|
const menuItems = {
|
|
// 문서 관리 관련
|
|
'pdf-manager-nav-item': user.can_manage_books || user.is_admin,
|
|
'book-documents-nav-item': user.can_manage_books || user.is_admin,
|
|
'book-editor-nav-item': user.can_manage_books || user.is_admin,
|
|
|
|
// 노트 관리 관련
|
|
'notes-list-nav-item': user.can_manage_notes || user.is_admin,
|
|
'notebooks-nav-item': user.can_manage_notes || user.is_admin,
|
|
'note-editor-nav-item': user.can_manage_notes || user.is_admin,
|
|
|
|
// 소설 관리 관련
|
|
'story-view-nav-item': user.can_manage_novels || user.is_admin,
|
|
'story-reader-nav-item': user.can_manage_novels || user.is_admin,
|
|
'memo-tree-nav-item': user.can_manage_novels || user.is_admin,
|
|
|
|
// 할일 관리 - 노트 관리 권한 필요 (올바른 ID 사용)
|
|
'todos-nav-link': user.can_manage_notes || user.is_admin,
|
|
|
|
// 검색은 모든 사용자 허용
|
|
'search-nav-link': true,
|
|
|
|
// 메인 페이지는 모든 사용자 허용 (문서 보기만)
|
|
'index-nav-item': true
|
|
};
|
|
|
|
// 각 메뉴 아이템의 표시/숨김 처리
|
|
Object.entries(menuItems).forEach(([itemId, hasPermission]) => {
|
|
const menuItem = document.getElementById(itemId);
|
|
if (menuItem) {
|
|
if (hasPermission) {
|
|
menuItem.classList.remove('hidden');
|
|
console.log(`✅ ${itemId} 메뉴 표시`);
|
|
} else {
|
|
menuItem.classList.add('hidden');
|
|
console.log(`❌ ${itemId} 메뉴 숨김`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 드롭다운 메뉴의 링크들도 체크
|
|
const dropdownLinks = document.querySelectorAll('.user-dropdown a, .user-dropdown button');
|
|
dropdownLinks.forEach(link => {
|
|
const href = link.getAttribute('href') || '';
|
|
let hasPermission = true;
|
|
|
|
if (href.includes('pdf-manager') || href.includes('book-')) {
|
|
hasPermission = user.can_manage_books || user.is_admin;
|
|
} else if (href.includes('note') || href.includes('notebook')) {
|
|
hasPermission = user.can_manage_notes || user.is_admin;
|
|
} else if (href.includes('story') || href.includes('memo-tree')) {
|
|
hasPermission = user.can_manage_novels || user.is_admin;
|
|
}
|
|
|
|
if (hasPermission) {
|
|
link.classList.remove('hidden');
|
|
} else {
|
|
link.classList.add('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 헤더 로드 실패 시 폴백 헤더 표시
|
|
*/
|
|
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',
|
|
'search': 'search-nav-link',
|
|
'notes': 'notes-nav-link',
|
|
'notebooks': 'notebooks-nav-item',
|
|
'note-editor': 'note-editor-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();
|
|
|
|
// 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;
|
|
|
|
// 사용자별 메뉴 권한 체크
|
|
console.log('🔍 사용자 권한 확인:', {
|
|
role: user.role,
|
|
is_admin: user.is_admin,
|
|
can_manage_books: user.can_manage_books,
|
|
can_manage_notes: user.can_manage_notes,
|
|
can_manage_novels: user.can_manage_novels
|
|
});
|
|
|
|
// 개별 메뉴 권한 체크
|
|
window.headerLoader.updateMenuPermissions(user);
|
|
|
|
// 관리자 메뉴 표시/숨김 (전체 관리자만)
|
|
if (adminMenuSection) {
|
|
if (user.role === 'root' || user.role === 'admin' || user.is_admin) {
|
|
console.log('✅ 관리자 메뉴 표시');
|
|
adminMenuSection.classList.remove('hidden');
|
|
} else {
|
|
console.log('❌ 관리자 메뉴 숨김');
|
|
adminMenuSection.classList.add('hidden');
|
|
}
|
|
} else {
|
|
console.log('❌ adminMenuSection 요소를 찾을 수 없음');
|
|
}
|
|
} 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);
|
|
});
|