From aebce091a9aa9957f56f7c3c31f456514a4f1ecb Mon Sep 17 00:00:00 2001 From: hyungi Date: Fri, 5 Sep 2025 12:36:05 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ 새로운 기능: - 사용자별 세분화된 권한 체크 (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 에러 해결 - 권한 없는 사용자의 무단 페이지 접근 차단 - 문서 관리 권한이 있는 사용자는 모든 문서 공유 가능 📱 사용자 경험 개선: - 권한에 따른 메뉴 자동 표시/숨김 - 로그인 상태 유지 개선 - 권한 없는 기능 접근 시 친화적인 알림 및 리다이렉트 --- backend/src/api/routes/bookmarks.py | 8 +- backend/src/api/routes/document_links.py | 30 ++--- backend/src/api/routes/documents.py | 18 +-- backend/src/api/routes/highlights.py | 4 +- frontend/components/header.html | 55 +++++++- frontend/pdf-manager.html | 1 + frontend/static/js/book-documents.js | 16 +++ frontend/static/js/book-editor.js | 16 +++ frontend/static/js/header-loader.js | 143 ++++++++++++++++++++- frontend/static/js/pdf-manager.js | 16 +++ frontend/static/js/permission-guard.js | 155 +++++++++++++++++++++++ frontend/todos.html | 2 + 12 files changed, 426 insertions(+), 38 deletions(-) create mode 100644 frontend/static/js/permission-guard.js diff --git a/backend/src/api/routes/bookmarks.py b/backend/src/api/routes/bookmarks.py index bd6e8c8..39541e5 100644 --- a/backend/src/api/routes/bookmarks.py +++ b/backend/src/api/routes/bookmarks.py @@ -75,8 +75,8 @@ async def create_bookmark( detail="Document not found" ) - # 문서 접근 권한 확인 - if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin: + # 문서 접근 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능) + if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions to access this document" @@ -155,8 +155,8 @@ async def get_document_bookmarks( detail="Document not found" ) - # 문서 접근 권한 확인 - if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin: + # 문서 접근 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능) + if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions to access this document" diff --git a/backend/src/api/routes/document_links.py b/backend/src/api/routes/document_links.py index bec63ed..e4c75a4 100644 --- a/backend/src/api/routes/document_links.py +++ b/backend/src/api/routes/document_links.py @@ -109,8 +109,8 @@ async def create_document_link( detail="Source document not found" ) - # 권한 확인 - if not source_doc.is_public and source_doc.uploaded_by != current_user.id and not current_user.is_admin: + # 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능) + if not (current_user.is_admin or current_user.can_manage_books) and not source_doc.is_public and source_doc.uploaded_by != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to source document" @@ -146,7 +146,7 @@ async def create_document_link( # 대상 문서/노트 권한 확인 if target_doc: - if not target_doc.is_public and target_doc.uploaded_by != current_user.id and not current_user.is_admin: + if not (current_user.is_admin or current_user.can_manage_books) and not target_doc.is_public and target_doc.uploaded_by != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to target document" @@ -235,8 +235,8 @@ async def get_document_links( detail="Document not found" ) - # 권한 확인 - if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin: + # 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능) + if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" @@ -345,8 +345,8 @@ async def get_linkable_documents( and_( Document.html_path.isnot(None), # HTML 문서만 Document.id != document_id, # 자기 자신 제외 - # 권한 확인: 공개 문서이거나 본인이 업로드한 문서 - (Document.is_public == True) | (Document.uploaded_by == current_user.id) | (current_user.is_admin == True) + # 권한 확인: 공개 문서이거나 본인이 업로드한 문서이거나 문서 관리 권한이 있음 + (Document.is_public == True) | (Document.uploaded_by == current_user.id) | (current_user.is_admin == True) | (current_user.can_manage_books == True) ) ).order_by( # 같은 서적 우선, 그 다음 정렬 순서 @@ -522,8 +522,8 @@ async def get_document_backlinks( print(f"✅ 문서 찾음: {document.title}") - # 권한 확인 - if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin: + # 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능) + if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" @@ -541,8 +541,8 @@ async def get_document_backlinks( ).outerjoin(Book, Document.book_id == Book.id).where( and_( DocumentLink.target_document_id == document_id, - # 권한 확인: 공개 문서이거나 본인이 업로드한 문서 - (Document.is_public == True) | (Document.uploaded_by == current_user.id) | (current_user.is_admin == True) + # 권한 확인: 공개 문서이거나 본인이 업로드한 문서이거나 문서 관리 권한이 있음 + (Document.is_public == True) | (Document.uploaded_by == current_user.id) | (current_user.is_admin == True) | (current_user.can_manage_books == True) ) ).order_by(DocumentLink.created_at.desc()) @@ -650,8 +650,8 @@ async def get_document_link_fragments( detail="Document not found" ) - # 권한 확인 - if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin: + # 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능) + if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" @@ -663,8 +663,8 @@ async def get_document_link_fragments( ).where( and_( DocumentLink.source_document_id == document_id, - # 권한 확인: 공개 문서이거나 본인이 업로드한 문서 - (Document.is_public == True) | (Document.uploaded_by == current_user.id) | (current_user.is_admin == True) + # 권한 확인: 공개 문서이거나 본인이 업로드한 문서이거나 문서 관리 권한이 있음 + (Document.is_public == True) | (Document.uploaded_by == current_user.id) | (current_user.is_admin == True) | (current_user.can_manage_books == True) ) ).order_by(DocumentLink.start_offset.asc()) diff --git a/backend/src/api/routes/documents.py b/backend/src/api/routes/documents.py index 103a6e1..eb67c17 100644 --- a/backend/src/api/routes/documents.py +++ b/backend/src/api/routes/documents.py @@ -98,8 +98,8 @@ async def list_documents( selectinload(Document.category) # 소분류 정보 추가 ) - # 권한 필터링 (관리자가 아니면 공개 문서 + 자신이 업로드한 문서만) - if not current_user.is_admin: + # 권한 필터링 (관리자 또는 문서 관리 권한이 있으면 모든 문서, 아니면 공개 문서 + 자신이 업로드한 문서만) + if not (current_user.is_admin or current_user.can_manage_books): query = query.where( or_( Document.is_public == True, @@ -175,8 +175,8 @@ async def list_all_documents( selectinload(Document.category) # 소분류 정보 추가 ) - # 권한 필터링 (관리자가 아니면 공개 문서 + 자신이 업로드한 문서만) - if not current_user.is_admin: + # 권한 필터링 (관리자 또는 문서 관리 권한이 있으면 모든 문서, 아니면 공개 문서 + 자신이 업로드한 문서만) + if not (current_user.is_admin or current_user.can_manage_books): query = query.where( or_( Document.is_public == True, @@ -514,8 +514,8 @@ async def get_document( detail="Document not found" ) - # 권한 확인 - if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin: + # 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능) + if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions" @@ -616,9 +616,9 @@ async def get_document_pdf( print(f"🔐 문서 권한: is_public={document.is_public}, uploaded_by={document.uploaded_by}") print(f"👤 사용자 권한: is_admin={current_user.is_admin}, user_id={current_user.id}") - # 권한 확인 - if not current_user.is_admin and not document.is_public and document.uploaded_by != current_user.id: - print(f"❌ 접근 권한 없음 - 관리자: {current_user.is_admin}, 공개: {document.is_public}, 소유자: {document.uploaded_by == current_user.id}") + # 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능) + if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id: + print(f"❌ 접근 권한 없음 - 관리자: {current_user.is_admin}, 문서권한: {current_user.can_manage_books}, 공개: {document.is_public}, 소유자: {document.uploaded_by == current_user.id}") raise HTTPException(status_code=403, detail="Access denied") # PDF 파일 확인 diff --git a/backend/src/api/routes/highlights.py b/backend/src/api/routes/highlights.py index 11db165..9c66d0b 100644 --- a/backend/src/api/routes/highlights.py +++ b/backend/src/api/routes/highlights.py @@ -84,8 +84,8 @@ async def create_highlight( detail="Document not found" ) - # 문서 접근 권한 확인 - if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin: + # 문서 접근 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능) + if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions to access this document" diff --git a/frontend/components/header.html b/frontend/components/header.html index 5fe279f..58309c1 100644 --- a/frontend/components/header.html +++ b/frontend/components/header.html @@ -213,7 +213,7 @@
- @@ -404,12 +404,53 @@ window.location.href = `login.html?redirect=${currentUrl}`; }; - window.handleLogout = () => { - // 각 페이지의 로그아웃 함수 호출 - if (typeof logout === 'function') { - logout(); - } else { - console.log('로그아웃 함수를 찾을 수 없습니다.'); + 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'; } }; diff --git a/frontend/pdf-manager.html b/frontend/pdf-manager.html index 8161e55..225bcd1 100644 --- a/frontend/pdf-manager.html +++ b/frontend/pdf-manager.html @@ -441,6 +441,7 @@ + diff --git a/frontend/static/js/book-documents.js b/frontend/static/js/book-documents.js index 0f7baf5..9713a64 100644 --- a/frontend/static/js/book-documents.js +++ b/frontend/static/js/book-documents.js @@ -277,6 +277,22 @@ window.bookDocumentsApp = () => ({ }, // 날짜 포맷팅 + // 로그아웃 + async logout() { + try { + await window.api.logout(); + } catch (error) { + console.error('Logout error:', error); + } finally { + // 로컬 스토리지 정리 + localStorage.removeItem('access_token'); + localStorage.removeItem('refresh_token'); + localStorage.removeItem('user_info'); + + console.log('✅ 로그아웃 완료'); + } + }, + formatDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); diff --git a/frontend/static/js/book-editor.js b/frontend/static/js/book-editor.js index 1ebfcef..ac44a50 100644 --- a/frontend/static/js/book-editor.js +++ b/frontend/static/js/book-editor.js @@ -398,6 +398,22 @@ window.bookEditorApp = () => ({ } }, + // 로그아웃 + async logout() { + try { + await window.api.logout(); + } catch (error) { + console.error('Logout error:', error); + } finally { + // 로컬 스토리지 정리 + localStorage.removeItem('access_token'); + localStorage.removeItem('refresh_token'); + localStorage.removeItem('user_info'); + + console.log('✅ 로그아웃 완료'); + } + }, + // 뒤로가기 goBack() { window.location.href = `book-documents.html?bookId=${this.bookId}`; diff --git a/frontend/static/js/header-loader.js b/frontend/static/js/header-loader.js index 6a2704c..3e5975e 100644 --- a/frontend/static/js/header-loader.js +++ b/frontend/static/js/header-loader.js @@ -36,6 +36,9 @@ class HeaderLoader { // 헤더 HTML 삽입 container.innerHTML = headerHtml; + // 헤더 로드 후 필요한 함수들 정의 + this.initializeHeaderFunctions(); + this.headerLoaded = true; console.log('✅ 헤더 로드 완료'); @@ -48,6 +51,140 @@ class HeaderLoader { } } + /** + * 헤더 로드 후 필요한 함수들 초기화 + */ + 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'); + } + }); + } + /** * 헤더 로드 실패 시 폴백 헤더 표시 */ @@ -194,7 +331,7 @@ document.addEventListener('headerLoaded', () => { if (dropdownUserEmail) dropdownUserEmail.textContent = user.email || ''; if (dropdownUserRole) dropdownUserRole.textContent = roleText; - // 관리자 메뉴 표시/숨김 + // 사용자별 메뉴 권한 체크 console.log('🔍 사용자 권한 확인:', { role: user.role, is_admin: user.is_admin, @@ -203,6 +340,10 @@ document.addEventListener('headerLoaded', () => { 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('✅ 관리자 메뉴 표시'); diff --git a/frontend/static/js/pdf-manager.js b/frontend/static/js/pdf-manager.js index bd4a0e7..e4b1c16 100644 --- a/frontend/static/js/pdf-manager.js +++ b/frontend/static/js/pdf-manager.js @@ -332,6 +332,22 @@ window.pdfManagerApp = () => ({ day: 'numeric' }); }, + + // 로그아웃 + async logout() { + try { + await window.api.logout(); + } catch (error) { + console.error('Logout error:', error); + } finally { + // 로컬 스토리지 정리 + localStorage.removeItem('access_token'); + localStorage.removeItem('refresh_token'); + localStorage.removeItem('user_info'); + + console.log('✅ 로그아웃 완료'); + } + }, // 알림 표시 showNotification(message, type = 'info') { diff --git a/frontend/static/js/permission-guard.js b/frontend/static/js/permission-guard.js new file mode 100644 index 0000000..471ae9a --- /dev/null +++ b/frontend/static/js/permission-guard.js @@ -0,0 +1,155 @@ +/** + * 페이지별 권한 체크 가드 + */ +class PermissionGuard { + constructor() { + this.pagePermissions = { + // 문서 관리 관련 + 'pdf-manager.html': (user) => user.can_manage_books || user.is_admin, + 'book-documents.html': (user) => user.can_manage_books || user.is_admin, + 'book-editor.html': (user) => user.can_manage_books || user.is_admin, + + // 노트 관리 관련 + 'notes.html': (user) => user.can_manage_notes || user.is_admin, + 'notebooks.html': (user) => user.can_manage_notes || user.is_admin, + 'note-editor.html': (user) => user.can_manage_notes || user.is_admin, + 'todos.html': (user) => user.can_manage_notes || user.is_admin, + + // 소설 관리 관련 + 'story-view.html': (user) => user.can_manage_novels || user.is_admin, + 'story-reader.html': (user) => user.can_manage_novels || user.is_admin, + 'memo-tree.html': (user) => user.can_manage_novels || user.is_admin, + + // 관리자 전용 + 'user-management.html': (user) => user.is_admin, + 'system-settings.html': (user) => user.is_admin, + 'backup-restore.html': (user) => user.is_admin, + 'logs.html': (user) => user.is_admin, + 'setup.html': (user) => user.is_admin, + + // 모든 사용자 허용 + 'index.html': () => true, + 'search.html': () => true, + 'upload.html': () => true, + 'viewer.html': () => true, + 'login.html': () => true, + 'profile.html': () => true + }; + } + + /** + * 현재 페이지의 권한을 체크합니다 + */ + async checkCurrentPagePermission() { + try { + // 현재 페이지 파일명 추출 + const currentPage = window.location.pathname.split('/').pop() || 'index.html'; + console.log(`🔐 권한 체크 시작 - 페이지: ${currentPage}`); + + // 권한 체크 함수 가져오기 + const permissionCheck = this.pagePermissions[currentPage]; + + // 권한 체크가 정의되지 않은 페이지는 허용 + if (!permissionCheck) { + console.log(`✅ 권한 체크 없음 - 페이지 허용: ${currentPage}`); + return true; + } + + // 사용자 정보 가져오기 + const token = localStorage.getItem('access_token'); + if (!token) { + console.log(`❌ 토큰 없음 - 로그인 페이지로 이동`); + this.redirectToLogin(); + return false; + } + + // 먼저 기존 사용자 정보가 있는지 확인 (헤더 상태 보존) + let user = window.currentUser; + + // 사용자 정보가 없으면 API로 조회 + if (!user) { + const response = await fetch('/api/auth/me', { + headers: { 'Authorization': `Bearer ${token}` } + }); + + if (!response.ok) { + console.log(`❌ 사용자 정보 조회 실패 - 로그인 페이지로 이동`); + this.redirectToLogin(); + return false; + } + + user = await response.json(); + // 전역 사용자 정보 저장 (헤더에서 사용) + window.currentUser = user; + } + + console.log(`👤 사용자 정보: ${user.email}, 권한: 관리자=${user.is_admin}, 문서=${user.can_manage_books}, 노트=${user.can_manage_notes}, 소설=${user.can_manage_novels}`); + + // 권한 체크 실행 + const hasPermission = permissionCheck(user); + + if (hasPermission) { + console.log(`✅ 권한 확인 - 페이지 접근 허용: ${currentPage}`); + window.permissionGuardExecuted = true; + return true; + } else { + console.log(`❌ 권한 없음 - 메인 페이지로 이동: ${currentPage}`); + window.permissionGuardExecuted = true; + this.redirectToMain(); + return false; + } + + } catch (error) { + console.error(`🚫 권한 체크 오류:`, error); + this.redirectToLogin(); + return false; + } + } + + /** + * 로그인 페이지로 리다이렉트 + */ + redirectToLogin() { + const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); + window.location.href = `login.html?redirect=${currentUrl}`; + } + + /** + * 메인 페이지로 리다이렉트 + */ + redirectToMain() { + // 사용자에게 알림 표시 + if (typeof alert !== 'undefined') { + alert('이 페이지에 접근할 권한이 없습니다. 메인 페이지로 이동합니다.'); + } + window.location.href = 'index.html'; + } +} + +// 전역 인스턴스 생성 +window.permissionGuard = new PermissionGuard(); + +// 헤더 로드 완료 후 권한 체크 실행 (헤더 상태 보존) +document.addEventListener('headerLoaded', () => { + console.log('🔐 헤더 로드 완료 후 권한 체크 시작'); + // 로그인 페이지는 권한 체크 제외 + const currentPage = window.location.pathname.split('/').pop() || 'index.html'; + if (currentPage !== 'login.html') { + // 약간의 지연을 두어 헤더 초기화 완료 대기 + setTimeout(() => { + window.permissionGuard.checkCurrentPagePermission(); + }, 100); + } +}); + +// 백업: DOM 로드 완료 시 권한 체크 실행 (헤더 이벤트가 없는 경우) +document.addEventListener('DOMContentLoaded', () => { + // 헤더 로드 이벤트를 기다리되, 3초 후에는 강제 실행 + setTimeout(() => { + const currentPage = window.location.pathname.split('/').pop() || 'index.html'; + if (currentPage !== 'login.html' && !window.permissionGuardExecuted) { + console.log('🔐 백업 권한 체크 실행'); + window.permissionGuard.checkCurrentPagePermission(); + } + }, 3000); +}); diff --git a/frontend/todos.html b/frontend/todos.html index 95bc096..9e3d442 100644 --- a/frontend/todos.html +++ b/frontend/todos.html @@ -664,6 +664,8 @@ + +